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

dsavransky / EXOSIMS / 19649142370

24 Nov 2025 09:01PM UTC coverage: 65.627% (+0.02%) from 65.607%
19649142370

Pull #446

github

web-flow
Merge ed4071a9f into e3fee7dce
Pull Request #446: [pre-commit.ci] pre-commit autoupdate

9787 of 14913 relevant lines covered (65.63%)

0.66 hits per line

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

55.26
/EXOSIMS/SurveySimulation/coroOnlyScheduler.py
1
from EXOSIMS.Prototypes.SurveySimulation import SurveySimulation
1✔
2
import astropy.units as u
1✔
3
import astropy.constants as const
1✔
4
from astropy.time import Time
1✔
5
import numpy as np
1✔
6
import time
1✔
7
import copy
1✔
8
from EXOSIMS.util.deltaMag import deltaMag
1✔
9

10

11
class coroOnlyScheduler(SurveySimulation):
1✔
12
    """coroOnlyScheduler - Coronograph Only Scheduler
13

14
    This scheduler inherits directly from the prototype SurveySimulation module.
15

16
    The coronlyScheduler operates using only a coronograph. The scheduler makes
17
    detections until stars can be promoted into a characterization list, at which
18
    point they are charcterized.
19

20
    Args:
21
        revisit_wait (float):
22
            Wait time threshold for star revisits. The value given is the fraction of a
23
            characterized planet's period that must be waited before scheduling a
24
            revisit.
25
        revisit_weight (float):
26
            Weight used to increase preference for coronograph revisits.
27
        n_det_remove (integer):
28
            Number of failed detections before a star is removed from the target list.
29
        n_det_min (integer):
30
            Minimum number of detections required for promotion to char target.
31
        max_successful_chars (integer):
32
            Maximum number of successful characterizions before star is taken off
33
            target list.
34
        max_successful_dets (integer):
35
            Maximum number of successful detections before star is taken off target
36
            list.
37
        lum_exp (int):
38
            The exponent to use for luminosity weighting on coronograph targets.
39
        promote_by_time (bool):
40
            Only promote stars that have had detections that span longer than half
41
            a period.
42
        detMargin (float):
43
            Acts in the same way a charMargin. Adds a multiplyer to the calculated
44
            detection time.
45
        **specs:
46
            user specified values
47

48
    """
49

50
    def __init__(
1✔
51
        self,
52
        revisit_wait=0.5,
53
        revisit_weight=1.0,
54
        n_det_remove=3,
55
        n_det_min=3,
56
        max_successful_chars=1,
57
        max_successful_dets=4,
58
        lum_exp=1,
59
        promote_by_time=False,
60
        detMargin=0.0,
61
        **specs,
62
    ):
63

64
        SurveySimulation.__init__(self, **specs)
1✔
65

66
        TL = self.TargetList
1✔
67
        OS = self.OpticalSystem
1✔
68
        SU = self.SimulatedUniverse
1✔
69

70
        # Add to outspec
71
        self._outspec["revisit_wait"] = revisit_wait
1✔
72
        self._outspec["revisit_weight"] = revisit_weight
1✔
73
        self._outspec["n_det_remove"] = n_det_remove
1✔
74
        self._outspec["max_successful_chars"] = max_successful_chars
1✔
75
        self._outspec["lum_exp"] = lum_exp
1✔
76
        self._outspec["n_det_min"] = n_det_min
1✔
77

78
        self.FA_status = np.zeros(TL.nStars, dtype=bool)  # False Alarm status array
1✔
79
        # The exponent to use for luminosity weighting on coronograph targets
80
        self.lum_exp = lum_exp
1✔
81

82
        self.sInd_charcounts = {}  # Number of characterizations by star index
1✔
83
        self.sInd_detcounts = np.zeros(
1✔
84
            TL.nStars, dtype=int
85
        )  # Number of detections by star index
86
        self.sInd_dettimes = {}
1✔
87
        # Minimum number of visits with no detections required to filter off star
88
        self.n_det_remove = n_det_remove
1✔
89
        # Minimum number of detections required for promotio
90
        self.n_det_min = n_det_min
1✔
91
        # Maximum allowed number of successful chars of deep dive targets before
92
        # removal from target list
93
        self.max_successful_chars = max_successful_chars
1✔
94
        self.max_successful_dets = max_successful_dets
1✔
95
        self.char_starRevisit = np.array([])  # Array of star revisit times
1✔
96
        # The number of times each star was visited by the occulter
97
        self.char_starVisits = np.zeros(TL.nStars, dtype=int)
1✔
98
        self.promote_by_time = promote_by_time
1✔
99
        self.detMargin = detMargin
1✔
100

101
        # self.revisit_wait = revisit_wait * u.d
102
        EEID = 1 * u.AU * np.sqrt(TL.L)
1✔
103
        mu = const.G * (TL.MsTrue)
1✔
104
        T = (2.0 * np.pi * np.sqrt(EEID**3 / mu)).to(u.d)
1✔
105
        self.revisit_wait = revisit_wait * T
1✔
106

107
        self.revisit_weight = revisit_weight
1✔
108
        self.no_dets = np.ones(self.TargetList.nStars, dtype=bool)
1✔
109
        # list of stars promoted from the detection list to the characterization list
110
        self.promoted_stars = self.known_rocky
1✔
111
        # list of stars that have been removed from the occ_sInd list
112
        self.ignore_stars = []
1✔
113
        self.t_char_earths = np.array([])  # corresponding integration times for earths
1✔
114

115
        allModes = OS.observingModes
1✔
116
        num_char_modes = len(
1✔
117
            list(filter(lambda mode: "spec" in mode["inst"]["name"], allModes))
118
        )
119
        self.fullSpectra = np.zeros((num_char_modes, SU.nPlans), dtype=int)
1✔
120
        self.partialSpectra = np.zeros((num_char_modes, SU.nPlans), dtype=int)
1✔
121

122
        # Promote all stars assuming they have known earths
123
        char_sInds_with_earths = []
1✔
124
        if TL.earths_only:
1✔
125

126
            OS = self.OpticalSystem
×
127
            TL = self.TargetList
×
128
            SU = self.SimulatedUniverse
×
129
            # char_modes = list(
130
            #    filter(lambda mode: "spec" in mode["inst"]["name"], OS.observingModes)
131
            # )
132

133
            # check for earths around the available stars
134
            for sInd in np.arange(TL.nStars):
×
135
                pInds = np.where(SU.plan2star == sInd)[0]
×
136
                pinds_earthlike = self.is_earthlike(pInds, sInd)
×
137
                if np.any(pinds_earthlike):
×
138
                    self.known_earths = np.union1d(
×
139
                        self.known_earths, pInds[pinds_earthlike]
140
                    ).astype(int)
141
                    char_sInds_with_earths.append(sInd)
×
142
            self.promoted_stars = np.union1d(
×
143
                self.promoted_stars, char_sInds_with_earths
144
            ).astype(int)
145

146
    def initializeStorageArrays(self):
1✔
147
        """
148
        Initialize all storage arrays based on # of stars and targets
149
        """
150

151
        self.DRM = []
1✔
152
        OS = self.OpticalSystem
1✔
153
        SU = self.SimulatedUniverse
1✔
154
        allModes = OS.observingModes
1✔
155
        num_char_modes = len(
1✔
156
            list(filter(lambda mode: "spec" in mode["inst"]["name"], allModes))
157
        )
158
        self.fullSpectra = np.zeros((num_char_modes, SU.nPlans), dtype=int)
1✔
159
        self.partialSpectra = np.zeros((num_char_modes, SU.nPlans), dtype=int)
1✔
160
        self.propagTimes = np.zeros(self.TargetList.nStars) << u.d
1✔
161
        self.lastObsTimes = np.zeros(self.TargetList.nStars) << u.d
1✔
162
        self.starVisits = np.zeros(
1✔
163
            self.TargetList.nStars, dtype=int
164
        )  # contains the number of times each star was visited
165
        self.starRevisit = np.array([])
1✔
166
        self.starExtended = np.array([], dtype=int)
1✔
167
        self.lastDetected = np.empty((self.TargetList.nStars, 4), dtype=object)
1✔
168

169
    def run_sim(self):
1✔
170
        """Performs the survey simulation"""
171

172
        OS = self.OpticalSystem
1✔
173
        TL = self.TargetList
1✔
174
        SU = self.SimulatedUniverse
1✔
175
        Obs = self.Observatory
1✔
176
        TK = self.TimeKeeping
1✔
177
        Comp = self.Completeness
1✔
178

179
        # choose observing modes selected for detection (default marked with a flag)
180
        allModes = OS.observingModes
1✔
181
        det_modes = list(
1✔
182
            filter(lambda mode: "imag" in mode["inst"]["name"], OS.observingModes)
183
        )
184
        base_det_mode = list(
1✔
185
            filter(lambda mode: mode["detectionMode"], OS.observingModes)
186
        )[0]
187
        # and for characterization (default is first spectro/IFS mode)
188
        spectroModes = list(
1✔
189
            filter(lambda mode: "spec" in mode["inst"]["name"], allModes)
190
        )
191
        if np.any(spectroModes):
1✔
192
            char_modes = spectroModes
1✔
193
        # if no spectro mode, default char mode is first observing mode
194
        else:
195
            char_modes = [allModes[0]]
×
196

197
        # begin Survey, and loop until mission is finished
198
        log_begin = "OB%s: survey beginning." % (TK.OBnumber)
1✔
199
        self.logger.info(log_begin)
1✔
200
        self.vprint(log_begin)
1✔
201
        t0 = time.time()
1✔
202
        sInd = None
1✔
203
        ObsNum = 0
1✔
204
        while not TK.mission_is_over(OS, Obs, det_modes[0]):
1✔
205

206
            # acquire the NEXT TARGET star index and create DRM
207
            old_sInd = sInd  # used to save sInd if returned sInd is None
1✔
208
            DRM, sInd, det_intTime, waitTime, det_mode = self.next_target(
1✔
209
                sInd, det_modes, char_modes
210
            )
211

212
            if sInd is not None:
1✔
213

214
                # beginning of observation, start to populate DRM
215
                pInds = np.where(SU.plan2star == sInd)[0]
1✔
216
                log_obs = (
1✔
217
                    "  Observation #%s, star ind %s (of %s) with %s planet(s), "
218
                    + "mission time at Obs start: %s, exoplanetObsTime: %s"
219
                ) % (
220
                    ObsNum,
221
                    sInd,
222
                    TL.nStars,
223
                    len(pInds),
224
                    TK.currentTimeNorm.to("day").copy().round(2),
225
                    TK.exoplanetObsTime.to("day").copy().round(2),
226
                )
227
                self.logger.info(log_obs)
1✔
228
                self.vprint(log_obs)
1✔
229

230
                FA = False
1✔
231
                if sInd not in self.promoted_stars:
1✔
232
                    ObsNum += (
1✔
233
                        1  # we're making an observation so increment observation number
234
                    )
235
                    pInds = np.where(SU.plan2star == sInd)[0]
1✔
236
                    DRM["star_ind"] = sInd
1✔
237
                    DRM["star_name"] = TL.Name[sInd]
1✔
238
                    DRM["arrival_time"] = TK.currentTimeNorm.to("day").copy()
1✔
239
                    DRM["OB_nb"] = TK.OBnumber
1✔
240
                    DRM["ObsNum"] = ObsNum
1✔
241
                    DRM["plan_inds"] = pInds.astype(int)
1✔
242

243
                    # update visited list for selected star
244
                    self.starVisits[sInd] += 1
1✔
245
                    # PERFORM DETECTION and populate revisit list attribute
246
                    (
1✔
247
                        detected,
248
                        det_fZ,
249
                        det_JEZ,
250
                        det_systemParams,
251
                        det_SNR,
252
                        FA,
253
                    ) = self.observation_detection(sInd, det_intTime.copy(), det_mode)
254

255
                    if np.any(detected > 0):
1✔
256
                        self.sInd_detcounts[sInd] += 1
1✔
257
                        self.sInd_dettimes[sInd] = (
1✔
258
                            self.sInd_dettimes.get(sInd) or []
259
                        ) + [TK.currentTimeNorm.copy().to("day")]
260
                        self.vprint("  Det. results are: %s" % (detected))
1✔
261

262
                    if (
1✔
263
                        np.any(self.is_earthlike(pInds.astype(int), sInd))
264
                        and self.sInd_detcounts[sInd] >= self.n_det_min
265
                    ):
266
                        good_2_promote = False
×
267
                        if not self.promote_by_time:
×
268
                            good_2_promote = True
×
269
                        else:
270
                            sp = SU.s[pInds]
×
271
                            Ms = TL.MsTrue[sInd]
×
272
                            Mp = SU.Mp[pInds]
×
273
                            mu = const.G * (Mp + Ms)
×
274
                            T = (2.0 * np.pi * np.sqrt(sp**3 / mu)).to("d")
×
275
                            # star must have detections that span longer than half a
276
                            # period and be in the habitable zone
277
                            # and have a smaller radius that a sub-neptune
278
                            if np.any(
×
279
                                (
280
                                    T / 2.0
281
                                    < (
282
                                        self.sInd_dettimes[sInd][-1]
283
                                        - self.sInd_dettimes[sInd][0]
284
                                    )
285
                                )
286
                            ):
287
                                good_2_promote = True
×
288
                        if sInd not in self.promoted_stars and good_2_promote:
×
289
                            self.promoted_stars = np.union1d(
×
290
                                self.promoted_stars, sInd
291
                            ).astype(int)
292
                            self.known_earths = np.union1d(
×
293
                                self.known_earths,
294
                                pInds[self.is_earthlike(pInds.astype(int), sInd)],
295
                            ).astype(int)
296

297
                    # populate the DRM with detection results
298
                    DRM["det_time"] = det_intTime.to("day")
1✔
299
                    DRM["det_status"] = detected
1✔
300
                    DRM["det_SNR"] = det_SNR
1✔
301
                    DRM["det_fZ"] = det_fZ.to("1/arcsec2")
1✔
302
                    if np.any(pInds):
1✔
303
                        DRM["det_JEZ"] = det_JEZ
×
304
                        DRM["det_dMag"] = SU.dMag[pInds].tolist()
×
305
                        DRM["det_WA"] = SU.WA[pInds].to("mas").value.tolist()
×
306
                    DRM["det_params"] = det_systemParams
1✔
307
                    DRM["det_mode"] = dict(det_mode)
1✔
308

309
                    if det_intTime is not None:
1✔
310
                        det_comp = Comp.comp_per_intTime(
1✔
311
                            det_intTime,
312
                            TL,
313
                            sInd,
314
                            det_fZ,
315
                            TL.JEZ0[det_mode["hex"]][sInd],
316
                            TL.int_WA[sInd],
317
                            det_mode,
318
                        )[0]
319
                        DRM["det_comp"] = det_comp
1✔
320
                    else:
321
                        DRM["det_comp"] = 0.0
×
322
                    del DRM["det_mode"]["inst"], DRM["det_mode"]["syst"]
1✔
323
                    # append result values to self.DRM
324
                    self.DRM.append(DRM)
1✔
325
                    # handle case of inf OBs and missionPortion < 1
326
                    if np.isinf(TK.OBduration) and (TK.missionPortion < 1.0):
1✔
327
                        self.arbitrary_time_advancement(
1✔
328
                            TK.currentTimeNorm.to("day").copy() - DRM["arrival_time"]
329
                        )
330
                else:
331
                    self.char_starVisits[sInd] += 1
×
332
                    # PERFORM CHARACTERIZATION and populate spectra list attribute
333
                    do_char = True
×
334
                    for mode_index, char_mode in enumerate(char_modes):
×
335
                        (
×
336
                            characterized,
337
                            char_fZ,
338
                            char_JEZ,
339
                            char_systemParams,
340
                            char_SNR,
341
                            char_intTime,
342
                        ) = self.test_observation_characterization(
343
                            sInd, char_mode, mode_index
344
                        )
345
                        if char_intTime is None:
×
346
                            char_intTime = self.zero_d
×
347
                        if char_intTime == self.zero_d:
×
348
                            do_char = False
×
349
                            TK.advanceToAbsTime(TK.currentTimeAbs.copy() + 0.5 * u.d)
×
350

351
                    if do_char is True:
×
352
                        # we're making an observation so increment observation number
353
                        ObsNum += 1
×
354
                        pInds = np.where(SU.plan2star == sInd)[0]
×
355
                        DRM["star_ind"] = sInd
×
356
                        DRM["star_name"] = TL.Name[sInd]
×
357
                        DRM["arrival_time"] = TK.currentTimeNorm.to("day").copy()
×
358
                        DRM["OB_nb"] = TK.OBnumber
×
359
                        DRM["ObsNum"] = ObsNum
×
360
                        DRM["plan_inds"] = pInds.astype(int)
×
361
                        DRM["char_info"] = []
×
362
                        for mode_index, char_mode in enumerate(char_modes):
×
363
                            char_data = {}
×
364
                            if char_mode["SNR"] not in [0, np.inf]:
×
365
                                (
×
366
                                    characterized,
367
                                    char_fZ,
368
                                    char_JEZ,
369
                                    char_systemParams,
370
                                    char_SNR,
371
                                    char_intTime,
372
                                ) = self.observation_characterization(
373
                                    sInd, char_mode, mode_index
374
                                )
375
                                if np.any(characterized):
×
376
                                    self.vprint(
×
377
                                        "  Char. results are: %s" % (characterized.T)
378
                                    )
379
                            else:
380
                                char_intTime = None
×
381
                                lenChar = len(pInds) + 1 if FA else len(pInds)
×
382
                                characterized = np.zeros(lenChar, dtype=float)
×
383
                                char_SNR = np.zeros(lenChar, dtype=float)
×
384
                                char_fZ = 0.0 / u.arcsec**2
×
385
                                char_systemParams = SU.dump_system_params(sInd)
×
386
                            assert char_intTime != 0, "Integration time can't be 0."
×
387

388
                            # populate the DRM with characterization results
389
                            char_data["char_time"] = (
×
390
                                char_intTime.to("day")
391
                                if char_intTime is not None
392
                                else self.zero_d
393
                            )
394
                            char_data["char_status"] = (
×
395
                                characterized[:-1] if FA else characterized
396
                            )
397
                            char_data["char_SNR"] = char_SNR[:-1] if FA else char_SNR
×
398
                            char_data["char_fZ"] = char_fZ.to("1/arcsec2")
×
399
                            char_data["char_params"] = char_systemParams
×
400

401
                            if char_intTime is not None and np.any(characterized):
×
402
                                char_comp = Comp.comp_per_intTime(
×
403
                                    char_intTime,
404
                                    TL,
405
                                    sInd,
406
                                    char_fZ,
407
                                    TL.JEZ0[det_mode["hex"]][sInd],
408
                                    TL.int_WA[sInd],
409
                                    char_mode,
410
                                )[0]
411
                                DRM["char_comp"] = char_comp
×
412
                            else:
413
                                DRM["char_comp"] = 0.0
×
414
                            # populate the DRM with FA results
415
                            char_data["FA_det_status"] = int(FA)
×
416
                            char_data["FA_char_status"] = characterized[-1] if FA else 0
×
417
                            char_data["FA_char_SNR"] = char_SNR[-1] if FA else 0.0
×
418
                            char_data["FA_char_JEZ"] = (
×
419
                                self.lastDetected[sInd, 1][-1] / u.arcsec**2
420
                                if FA
421
                                else 0.0 * self.JEZ_unit
422
                            )
423
                            char_data["FA_char_dMag"] = (
×
424
                                self.lastDetected[sInd, 2][-1] if FA else 0.0
425
                            )
426
                            char_data["FA_char_WA"] = (
×
427
                                self.lastDetected[sInd, 3][-1] * u.arcsec
428
                                if FA
429
                                else self.zero_arcsec
430
                            )
431

432
                            # populate the DRM with observation modes
433
                            char_data["char_mode"] = dict(char_mode)
×
434
                            del (
×
435
                                char_data["char_mode"]["inst"],
436
                                char_data["char_mode"]["syst"],
437
                            )
438

439
                            char_data["exoplanetObsTime"] = TK.exoplanetObsTime.copy()
×
440
                            DRM["char_info"].append(char_data)
×
441

442
                        # do not revisit partial char if lucky_planets
443
                        if SU.lucky_planets:
×
444
                            self.char_starVisits[sInd] = self.nVisitsMax
×
445

446
                        # append result values to self.DRM
447
                        self.DRM.append(DRM)
×
448

449
                        # handle case of inf OBs and missionPortion < 1
450
                        if np.isinf(TK.OBduration) and (TK.missionPortion < 1.0):
×
451
                            self.arbitrary_time_advancement(
×
452
                                TK.currentTimeNorm.to("day").copy()
453
                                - DRM["arrival_time"]
454
                            )
455

456
            else:  # sInd == None
457
                sInd = old_sInd  # Retain the last observed star
×
458
                if (
×
459
                    TK.currentTimeNorm.copy() >= TK.OBendTimes[TK.OBnumber]
460
                ):  # currentTime is at end of OB
461
                    # Conditional Advance To Start of Next OB
462
                    if not TK.mission_is_over(
×
463
                        OS, Obs, det_mode
464
                    ):  # as long as the mission is not over
465
                        TK.advancetToStartOfNextOB()  # Advance To Start of Next OB
×
466
                elif waitTime is not None:
×
467
                    # CASE 1: Advance specific wait time
468
                    _ = TK.advanceToAbsTime(TK.currentTimeAbs.copy() + waitTime)
×
469
                    self.vprint("waitTime is not None")
×
470
                else:
471
                    startTimes = (
×
472
                        TK.currentTimeAbs.copy() + np.zeros(TL.nStars) * u.d
473
                    )  # Start Times of Observations
474
                    observableTimes = Obs.calculate_observableTimes(
×
475
                        TL,
476
                        np.arange(TL.nStars),
477
                        startTimes,
478
                        self.koMaps,
479
                        self.koTimes,
480
                        base_det_mode,
481
                    )[0]
482
                    # CASE 2 If There are no observable targets for the rest
483
                    # of the mission
484
                    if (
×
485
                        observableTimes[
486
                            (
487
                                TK.missionFinishAbs.copy().value * u.d
488
                                > observableTimes.value * u.d
489
                            )
490
                            * (
491
                                observableTimes.value * u.d
492
                                >= TK.currentTimeAbs.copy().value * u.d
493
                            )
494
                        ].shape[0]
495
                    ) == 0:
496
                        self.vprint(
×
497
                            (
498
                                "No Observable Targets for Remainder of mission at "
499
                                "currentTimeNorm = {}"
500
                            ).format(TK.currentTimeNorm.copy())
501
                        )
502
                        # Manually advancing time to mission end
503
                        TK.currentTimeNorm = TK.missionLife
×
504
                        TK.currentTimeAbs = TK.missionFinishAbs
×
505
                    else:
506
                        # CASE 3 nominal wait time if at least 1 target is still in
507
                        # list and observable
508
                        # TODO: ADD ADVANCE TO WHEN FZMIN OCURS
509
                        inds1 = np.arange(TL.nStars)[
×
510
                            observableTimes.value * u.d
511
                            > TK.currentTimeAbs.copy().value * u.d
512
                        ]
513
                        # apply intTime filter
514
                        inds2 = np.intersect1d(self.intTimeFilterInds, inds1)
×
515
                        # apply revisit Filter #NOTE this means stars you added to
516
                        # the revisit list
517
                        inds3 = self.revisitFilter(
×
518
                            inds2, TK.currentTimeNorm.copy() + self.dt_max.to(u.d)
519
                        )
520
                        self.vprint(
×
521
                            "Filtering %d stars from advanceToAbsTime"
522
                            % (TL.nStars - len(inds3))
523
                        )
524
                        oTnowToEnd = observableTimes[inds3]
×
525
                        # there is at least one observableTime between now and the
526
                        # end of the mission
527
                        if not oTnowToEnd.value.shape[0] == 0:
×
528
                            tAbs = np.min(oTnowToEnd)  # advance to that observable time
×
529
                        else:
530
                            tAbs = (
×
531
                                TK.missionStart + TK.missionLife
532
                            )  # advance to end of mission
533
                        tmpcurrentTimeNorm = TK.currentTimeNorm.copy()
×
534
                        # Advance Time to this time OR start of next OB following
535
                        # this time
536
                        _ = TK.advanceToAbsTime(tAbs)
×
537
                        self.vprint(
×
538
                            (
539
                                "No Observable Targets a currentTimeNorm = {:.2f} "
540
                                "Advanced To currentTimeNorm= {:.2f}"
541
                            ).format(
542
                                tmpcurrentTimeNorm.to("day"),
543
                                TK.currentTimeNorm.to("day"),
544
                            )
545
                        )
546
        else:  # TK.mission_is_over()
547
            dtsim = (time.time() - t0) * u.s
1✔
548
            log_end = (
1✔
549
                "Mission complete: no more time available.\n"
550
                + "Simulation duration: %s.\n" % dtsim.astype("int")
551
                + "Results stored in SurveySimulation.DRM (Design Reference Mission)."
552
            )
553
            self.logger.info(log_end)
1✔
554
            self.vprint(log_end)
1✔
555

556
    def next_target(self, old_sInd, det_modes, char_modes):
1✔
557
        """Finds index of next target star and calculates its integration time.
558

559
        This method chooses the next target star index based on which
560
        stars are available, their integration time, and maximum completeness.
561
        Returns None if no target could be found.
562

563
        Args:
564
            old_sInd (integer):
565
                Index of the previous target star
566
            mode (dict):
567
                Selected observing mode for detection
568

569
        Returns:
570
            tuple:
571
                DRM (dict):
572
                    Design Reference Mission, contains the results of one complete
573
                    observation (detection and characterization)
574
                sInd (integer):
575
                    Index of next target star. Defaults to None.
576
                intTime (astropy Quantity):
577
                    Selected star integration time for detection in units of day.
578
                    Defaults to None.
579
                waitTime (astropy Quantity):
580
                    a strategically advantageous amount of time to wait in the case
581
                    of an occulter for slew times
582

583
        """
584
        OS = self.OpticalSystem
1✔
585
        ZL = self.ZodiacalLight
1✔
586
        TL = self.TargetList
1✔
587
        Obs = self.Observatory
1✔
588
        TK = self.TimeKeeping
1✔
589
        SU = self.SimulatedUniverse
1✔
590

591
        # create DRM
592
        DRM = {}
1✔
593

594
        # allocate settling time + overhead time
595
        tmpCurrentTimeAbs = (
1✔
596
            TK.currentTimeAbs.copy() + Obs.settlingTime + det_modes[0]["syst"]["ohTime"]
597
        )
598
        tmpCurrentTimeNorm = (
1✔
599
            TK.currentTimeNorm.copy()
600
            + Obs.settlingTime
601
            + det_modes[0]["syst"]["ohTime"]
602
        )
603

604
        # create appropriate koMap
605
        koMap = self.koMaps[det_modes[0]["syst"]["name"]]
1✔
606
        char_koMap = self.koMaps[char_modes[0]["syst"]["name"]]
1✔
607

608
        # look for available targets
609
        # 1. initialize arrays
610
        slewTimes = np.zeros(TL.nStars) << u.d
1✔
611
        # fZs = np.zeros(TL.nStars) / u.arcsec**2.0
612
        # dV = np.zeros(TL.nStars) * u.m / u.s
613
        intTimes = np.zeros(TL.nStars) << u.d
1✔
614
        char_intTimes = np.zeros(TL.nStars) << u.d
1✔
615
        char_intTimes_no_oh_d = np.zeros(TL.nStars)
1✔
616
        # obsTimes = np.zeros([2, TL.nStars]) * u.d
617
        char_tovisit = np.zeros(TL.nStars, dtype=bool)
1✔
618
        sInds = np.arange(TL.nStars)
1✔
619

620
        # 2. find spacecraft orbital START positions (if occulter, positions
621
        # differ for each star) and filter out unavailable targets
622
        # sd = None
623

624
        # 2.1 filter out totTimes > integration cutoff
625
        if len(sInds) > 0:
1✔
626
            char_sInds = np.intersect1d(sInds, self.promoted_stars).astype(int)
1✔
627
            sInds = np.intersect1d(self.intTimeFilterInds, sInds).astype(int)
1✔
628

629
        # start times, including slew times
630
        # startTimes = tmpCurrentTimeAbs.copy() + slewTimes
631
        startTimes = Time(
1✔
632
            np.full(TL.nStars, tmpCurrentTimeAbs.mjd), format="mjd", scale="tai"
633
        )
634
        # startTimesNorm = tmpCurrentTimeNorm.copy() + slewTimes
635
        startTimesNorm = np.full(TL.nStars, tmpCurrentTimeNorm.to_value(u.d)) << u.d
1✔
636
        startTimes_mjd = startTimes.mjd
1✔
637

638
        rounded_start_times = np.round(startTimes_mjd)
1✔
639
        # 2.5 Filter stars not observable at startTimes
640
        koTimeInds = np.searchsorted(self.koTimes_mjd, rounded_start_times[sInds])
1✔
641
        mask_valid = koTimeInds < len(self.koTimes_mjd)
1✔
642
        valid_matches = np.zeros_like(koTimeInds, dtype=bool)
1✔
643
        valid_matches[mask_valid] = (
1✔
644
            self.koTimes_mjd[koTimeInds[mask_valid]]
645
            == rounded_start_times[sInds[mask_valid]]
646
        )
647
        if np.any(valid_matches):
1✔
648
            sInds = sInds[valid_matches]
1✔
649
            koTimeInds = koTimeInds[valid_matches]
1✔
650
            tmpIndsbool = koMap[sInds, koTimeInds].astype(bool)
1✔
651
            sInds = sInds[tmpIndsbool]
1✔
652
        else:
653
            sInds = np.asarray([], dtype=int)
×
654

655
        # Get char stars observable at startTimes
656
        koTimeInds = np.searchsorted(self.koTimes_mjd, rounded_start_times[char_sInds])
1✔
657
        mask_valid = koTimeInds < len(self.koTimes_mjd)
1✔
658
        valid_matches = np.zeros_like(koTimeInds, dtype=bool)
1✔
659
        valid_matches[mask_valid] = (
1✔
660
            self.koTimes_mjd[koTimeInds[mask_valid]]
661
            == rounded_start_times[char_sInds[mask_valid]]
662
        )
663
        if np.any(valid_matches):
1✔
664
            tmpIndsbool = char_koMap[char_sInds, koTimeInds].astype(bool)
×
665
            char_sInds = char_sInds[tmpIndsbool]
×
666
        else:
667
            char_sInds = np.asarray([], dtype=int)
1✔
668

669
        # 3. filter out all previously (more-)visited targets, unless in
670
        if len(sInds) > 0:
1✔
671
            sInds = self.revisitFilter(sInds, tmpCurrentTimeNorm)
1✔
672

673
        # revisit list, with time after start
674
        if np.any(char_sInds):
1✔
675

676
            char_tovisit[char_sInds] = (
×
677
                self.char_starVisits[char_sInds] < self.nVisitsMax
678
            )
679
            if self.char_starRevisit.size != 0:
×
680
                dt_rev = TK.currentTimeNorm.copy() - self.char_starRevisit[:, 1] * u.day
×
681
                ind_rev = [
×
682
                    int(x)
683
                    for x in self.char_starRevisit[dt_rev > self.zero_d, 0]
684
                    if x in char_sInds
685
                ]
686
                char_tovisit[ind_rev] = self.char_starVisits[ind_rev] < self.nVisitsMax
×
687
            char_sInds = np.where(char_tovisit)[0]
×
688

689
        # 4.1 calculate integration times for ALL preselected targets
690
        (
1✔
691
            maxIntTimeOBendTime,
692
            maxIntTimeExoplanetObsTime,
693
            maxIntTimeMissionLife,
694
        ) = TK.get_ObsDetectionMaxIntTime(Obs, det_modes[0])
695
        maxIntTime = min(
1✔
696
            maxIntTimeOBendTime,
697
            maxIntTimeExoplanetObsTime,
698
            maxIntTimeMissionLife,
699
            OS.intCutoff,
700
        )  # Maximum intTime allowed
701

702
        if len(sInds) > 0:
1✔
703
            intTimes[sInds] = self.calc_targ_intTime(
1✔
704
                sInds, startTimes[sInds], det_modes[0]
705
            ) * (1 + self.detMargin)
706
            sInds = sInds[
1✔
707
                (intTimes[sInds] <= maxIntTime)
708
            ]  # Filters targets exceeding end of OB
709
            endTimes_mjd = startTimes_mjd + intTimes.to_value(u.d)
1✔
710
            endTimes_rounded = np.round(endTimes_mjd)
1✔
711

712
            if maxIntTime.value <= 0:
1✔
713
                sInds = np.asarray([], dtype=int)
×
714

715
        if len(char_sInds) > 0:
1✔
716
            for char_mode in char_modes:
×
717
                (
×
718
                    maxIntTimeOBendTime,
719
                    maxIntTimeExoplanetObsTime,
720
                    maxIntTimeMissionLife,
721
                ) = TK.get_ObsDetectionMaxIntTime(Obs, char_mode)
722
                char_maxIntTime = min(
×
723
                    maxIntTimeOBendTime,
724
                    maxIntTimeExoplanetObsTime,
725
                    maxIntTimeMissionLife,
726
                    OS.intCutoff,
727
                )  # Maximum intTime allowed
728

729
                char_mode_intTimes = np.zeros(TL.nStars) << u.d
×
730
                char_mode_intTimes[char_sInds] = self.calc_targ_intTime(
×
731
                    char_sInds, startTimes[char_sInds], char_mode
732
                ) * (1 + self.charMargin)
733
                char_mode_intTimes[np.isnan(char_mode_intTimes)] = 0 * u.d
×
734

735
                # Adjust integration time for stars with known earths around them
736
                for char_star in char_sInds:
×
737
                    char_earths = np.intersect1d(
×
738
                        np.where(SU.plan2star == char_star)[0], self.known_earths
739
                    ).astype(int)
740
                    if np.any(char_earths):
×
741
                        fZ = ZL.fZ(
×
742
                            Obs,
743
                            TL,
744
                            np.array([char_star], ndmin=1),
745
                            startTimes[char_star].reshape(1),
746
                            char_mode,
747
                        )
748
                        JEZ = TL.JEZ0[char_mode["hex"]][char_star]
×
749
                        if SU.lucky_planets:
×
750
                            phi = (1 / np.pi) * np.ones(len(SU.d))
×
751
                            dMag = deltaMag(SU.p, SU.Rp, SU.d, phi)[
×
752
                                char_earths
753
                            ]  # delta magnitude
754
                            WA = np.arctan(SU.a / TL.dist[SU.plan2star]).to("arcsec")[
×
755
                                char_earths
756
                            ]  # working angle
757
                        else:
758
                            dMag = SU.dMag[char_earths]
×
759
                            WA = SU.WA[char_earths]
×
760

761
                        if np.all((WA < char_mode["IWA"]) | (WA > char_mode["OWA"])):
×
762
                            char_mode_intTimes[char_star] = self.zero_d
×
763
                        else:
764
                            earthlike_inttimes = OS.calc_intTime(
×
765
                                TL, char_star, fZ, JEZ, dMag, WA, char_mode
766
                            ) * (1 + self.charMargin)
767
                            earthlike_inttimes[~np.isfinite(earthlike_inttimes)] = (
×
768
                                self.zero_d
769
                            )
770
                            earthlike_inttime = earthlike_inttimes[
×
771
                                (earthlike_inttimes < char_maxIntTime)
772
                            ]
773
                            if len(earthlike_inttime) > 0:
×
774
                                char_mode_intTimes[char_star] = np.max(
×
775
                                    earthlike_inttime
776
                                )
777
                char_intTimes_no_oh_d += char_mode_intTimes.to_value(u.d)
×
778
                char_intTimes += char_mode_intTimes + char_mode["syst"]["ohTime"]
×
779
            char_endTimes_mjd = (
×
780
                startTimes_mjd
781
                + (char_intTimes.to_value(u.d) * char_mode["timeMultiplier"])
782
                + Obs.settlingTime.to_value(u.d)
783
            )
784
            char_endTimes_rounded = np.round(char_endTimes_mjd)
×
785

786
            # Filters with an inttime of 0
787
            char_sInds = char_sInds[char_intTimes_no_oh_d[char_sInds] > 0]
×
788

789
            if char_maxIntTime.value <= 0:
×
790
                char_sInds = np.asarray([], dtype=int)
×
791

792
        # 5 remove char targets on ignore_stars list
793
        sInds = np.setdiff1d(
1✔
794
            sInds, np.intersect1d(sInds, self.promoted_stars).astype(int)
795
        )
796
        char_sInds = np.setdiff1d(
1✔
797
            char_sInds, np.intersect1d(char_sInds, self.ignore_stars)
798
        )
799

800
        # 6.2 Filter off coronograph stars with too many visits and no detections
801
        no_dets = np.logical_and(
1✔
802
            (self.starVisits[sInds] > self.n_det_remove),
803
            (self.sInd_detcounts[sInds] == 0),
804
        )
805
        sInds = sInds[np.where(np.invert(no_dets))[0]]
1✔
806

807
        max_dets = np.where(self.sInd_detcounts[sInds] < self.max_successful_dets)[0]
1✔
808
        sInds = sInds[max_dets]
1✔
809

810
        # 5.1 TODO Add filter to filter out stars entering and exiting keepout
811
        # between startTimes and endTimes
812

813
        # 5.2 find spacecraft orbital END positions (for each candidate target),
814
        # and filter out unavailable targets
815
        if len(sInds) > 0 and Obs.checkKeepoutEnd:
1✔
816
            end_times = endTimes_rounded[sInds]
1✔
817
            # This finds where each end_time would be inserted to maintain order
818
            koTimeInds = np.searchsorted(self.koTimes_mjd, end_times)
1✔
819
            # Check that the found indices actually match the end_times
820
            mask_valid = koTimeInds < len(self.koTimes_mjd)
1✔
821
            valid_matches = np.zeros_like(koTimeInds, dtype=bool)
1✔
822
            valid_matches[mask_valid] = (
1✔
823
                self.koTimes_mjd[koTimeInds[mask_valid]] == end_times[mask_valid]
824
            )
825
            # Handle invalid matches
826
            if np.any(valid_matches):
1✔
827
                # Remove invalid matches from the koTimeInds list
828
                koTimeInds = koTimeInds[valid_matches]
1✔
829
                sInds = sInds[valid_matches]
1✔
830
                tmpIndsbool = koMap[sInds, koTimeInds].astype(bool)
1✔
831

832
                if np.any(tmpIndsbool):
1✔
833
                    sInds = sInds[tmpIndsbool]
1✔
834
                else:
835
                    sInds = np.asarray([], dtype=int)
×
836
            else:
837
                # If no star has a matching koTimeInd, set sInds to empty
838
                sInds = np.asarray([], dtype=int)
×
839

840
        if len(char_sInds) > 0 and Obs.checkKeepoutEnd:
1✔
841
            end_times = char_endTimes_rounded[char_sInds]
×
842

843
            # Use searchsorted to find indices in koTimes_mjd
844
            koTimeInds = np.searchsorted(self.koTimes_mjd, end_times)
×
845

846
            # Check if the found indices actually match the end_times
847
            mask_valid = koTimeInds < len(self.koTimes_mjd)
×
848
            valid_matches = np.zeros_like(koTimeInds, dtype=bool)
×
849
            valid_matches[mask_valid] = (
×
850
                self.koTimes_mjd[koTimeInds[mask_valid]] == end_times[mask_valid]
851
            )
852

853
            tmpIndsbool = np.zeros(len(char_sInds), dtype=bool)
×
854

855
            if np.any(valid_matches):
×
856
                # Retrieve observable status for valid matches
857
                tmpIndsbool[valid_matches] = char_koMap[
×
858
                    char_sInds[valid_matches], koTimeInds[valid_matches]
859
                ].astype(bool)
860

861
            # Filter char_sInds based on the boolean mask
862
            if np.any(tmpIndsbool):
×
863
                char_sInds = char_sInds[tmpIndsbool]
×
864
            else:
865
                char_sInds = np.asarray([], dtype=int)
×
866

867
        # 6. choose best target from remaining
868
        if len(sInds) > 0:
1✔
869
            # choose sInd of next target
870
            if np.any(char_sInds):
1✔
871
                sInd, waitTime = self.choose_next_target(
×
872
                    old_sInd, char_sInds, slewTimes, char_intTimes[char_sInds]
873
                )
874
                # store selected star integration time
875
                intTime = char_intTimes[sInd]
×
876
            else:
877
                sInd, waitTime = self.choose_next_target(
1✔
878
                    old_sInd, sInds, slewTimes, intTimes[sInds]
879
                )
880
                # store selected star integration time
881
                intTime = intTimes[sInd]
1✔
882

883
            # Should Choose Next Target decide there are no stars it wishes to
884
            # observe at this time.
885
            if (sInd is None) and (waitTime is not None):
1✔
886
                self.vprint(
×
887
                    (
888
                        "There are no stars Choose Next Target would like to Observe. "
889
                        "Waiting {}"
890
                    ).format(waitTime)
891
                )
892
                return DRM, None, None, waitTime, det_modes[0]
×
893
            elif (sInd is None) and (waitTime is None):
1✔
894
                self.vprint(
×
895
                    (
896
                        "There are no stars Choose Next Target would like to Observe "
897
                        "and waitTime is None"
898
                    )
899
                )
900
                return DRM, None, None, waitTime, det_modes[0]
×
901

902
            # Perform dual band detections if necessary
903
            if (len(det_modes) > 1) and (
1✔
904
                TL.int_WA[sInd] > det_modes[1]["IWA"]
905
                and TL.int_WA[sInd] < det_modes[1]["OWA"]
906
            ):
907
                # Only make a deep copy when we need to modify det_mode
908
                # to create a combined mode
909
                det_mode = copy.deepcopy(det_modes[0])
1✔
910
                modifying_det_mode = True
1✔
911
                det_mode["BW"] = det_mode["BW"] + det_modes[1]["BW"]
1✔
912
                det_mode["inst"]["sread"] = (
1✔
913
                    det_mode["inst"]["sread"] + det_modes[1]["inst"]["sread"]
914
                )
915
                det_mode["inst"]["idark"] = (
1✔
916
                    det_mode["inst"]["idark"] + det_modes[1]["inst"]["idark"]
917
                )
918
                det_mode["inst"]["CIC"] = (
1✔
919
                    det_mode["inst"]["CIC"] + det_modes[1]["inst"]["CIC"]
920
                )
921
                det_mode["syst"]["optics"] = np.mean(
1✔
922
                    (det_mode["syst"]["optics"], det_modes[1]["syst"]["optics"])
923
                )
924
                det_mode["instName"] = "combined"
1✔
925
            else:
926
                # In the case where we don't need to modify the det_mode, we
927
                # can just use the original det_mode
928
                det_mode = det_modes[0]
×
929

930
            intTime = self.calc_targ_intTime(
1✔
931
                np.array([sInd]), startTimes[sInd].reshape(1), det_mode
932
            )[0] * (1 + self.detMargin)
933

934
            if intTime > maxIntTime and maxIntTime > 0 * u.d:
1✔
935
                intTime = maxIntTime
×
936

937
        # if no observable target, advanceTime to next Observable Target
938
        else:
939
            self.vprint(
×
940
                "No Observable Targets at currentTimeNorm= "
941
                + str(TK.currentTimeNorm.copy())
942
            )
943
            return DRM, None, None, None, det_modes[0]
×
944

945
        # store normalized start time for future completeness update
946
        self.lastObsTimes[sInd] = startTimesNorm[sInd]
1✔
947

948
        return DRM, sInd, intTime, waitTime, det_modes[0]
1✔
949

950
    def choose_next_target(self, old_sInd, sInds, slewTimes, t_dets):
1✔
951
        """Choose next telescope target based on star completeness and integration time.
952

953
        Args:
954
            old_sInd (integer):
955
                Index of the previous target star
956
            sInds (integer array):
957
                Indices of available targets
958
            t_dets (astropy Quantity array):
959
                Integration times for detection in units of day
960

961
        Returns:
962
            sInd (integer):
963
                Index of next target star
964

965
        """
966

967
        Comp = self.Completeness
1✔
968
        TL = self.TargetList
1✔
969
        TK = self.TimeKeeping
1✔
970

971
        # reshape sInds
972
        sInds = np.array(sInds, ndmin=1)
1✔
973

974
        # 1/ Choose next telescope target
975
        comps = Comp.completeness_update(
1✔
976
            TL, sInds, self.starVisits[sInds], TK.currentTimeNorm.copy()
977
        )
978

979
        # add weight for star revisits
980
        ind_rev = []
1✔
981
        if self.starRevisit.size != 0:
1✔
982
            dt_rev = self.starRevisit[:, 1] - TK.currentTimeNorm.to_value(u.d)
1✔
983
            # Vectorized version
984
            candidates = self.starRevisit[dt_rev < 0, 0].astype(int)
1✔
985
            mask = np.isin(candidates, sInds)
1✔
986
            ind_rev = candidates[mask]
1✔
987

988
        _starVisits = self.starVisits[sInds]
1✔
989
        f2_uv = np.where(
1✔
990
            (_starVisits > 0) & (_starVisits < self.nVisitsMax),
991
            _starVisits,
992
            0,
993
        ) * (1 - (np.isin(sInds, ind_rev, invert=True)))
994

995
        l_extreme = max(
1✔
996
            [
997
                np.abs(np.log10(np.min(TL.L[sInds]))),
998
                np.abs(np.log10(np.max(TL.L[sInds]))),
999
            ]
1000
        )
1001
        if l_extreme == 0.0:
1✔
1002
            l_weight = 1
1✔
1003
        else:
1004
            l_weight = 1 - np.abs(np.log10(TL.L[sInds]) / l_extreme) ** self.lum_exp
×
1005

1006
        t_dets_val = t_dets.to_value(u.d)
1✔
1007
        t_weight = t_dets_val / np.max(t_dets_val)
1✔
1008
        weights = (
1✔
1009
            (comps + self.revisit_weight * f2_uv / float(self.nVisitsMax)) / t_weight
1010
        ) * l_weight
1011

1012
        sInd = np.random.choice(sInds[weights == max(weights)])
1✔
1013

1014
        return sInd, slewTimes[sInd]
1✔
1015

1016
    def observation_characterization(self, sInd, mode, mode_index):
1✔
1017
        """Finds if characterizations are possible and relevant information
1018

1019
        Args:
1020
            sInd (integer):
1021
                Integer index of the star of interest
1022
            mode (dict):
1023
                Selected observing mode for characterization
1024

1025
        Returns:
1026
            characterized (integer list):
1027
                Characterization status for each planet orbiting the observed
1028
                target star including False Alarm if any, where 1 is full spectrum,
1029
                -1 partial spectrum, and 0 not characterized
1030
            fZ (astropy Quantity):
1031
                Surface brightness of local zodiacal light in units of 1/arcsec2
1032
            JEZ (astropy Quantity):
1033
                Intensity of exo-zodiacal light in units of ph/s/m2/arcsec2
1034
            systemParams (dict):
1035
                Dictionary of time-dependant planet properties averaged over the
1036
                duration of the integration
1037
            SNR (float ndarray):
1038
                Characterization signal-to-noise ratio of the observable planets.
1039
                Defaults to None.
1040
            intTime (astropy Quantity):
1041
                Selected star characterization time in units of day. Defaults to None.
1042

1043
        """
1044

1045
        OS = self.OpticalSystem
1✔
1046
        ZL = self.ZodiacalLight
1✔
1047
        TL = self.TargetList
1✔
1048
        SU = self.SimulatedUniverse
1✔
1049
        Obs = self.Observatory
1✔
1050
        TK = self.TimeKeeping
1✔
1051

1052
        # find indices of planets around the target
1053
        pInds = np.where(SU.plan2star == sInd)[0]
1✔
1054
        JEZs = SU.scale_JEZ(sInd, mode)
1✔
1055
        dMags = SU.dMag[pInds]
1✔
1056
        if SU.lucky_planets:
1✔
1057
            # used in the "partial char" check below
1058
            WAs = np.arctan(SU.a[pInds] / TL.dist[sInd]).to("arcsec").value
×
1059
        else:
1060
            WAs = SU.WA[pInds].to("arcsec").value
1✔
1061

1062
        # get the detected status, and check if there was a FA
1063
        # det = self.lastDetected[sInd,0]
1064
        det = np.ones(pInds.size, dtype=bool)
1✔
1065
        FA = len(det) == len(pInds) + 1
1✔
1066
        if FA:
1✔
1067
            pIndsDet = np.append(pInds, -1)[det]
×
1068
        else:
1069
            pIndsDet = pInds[det]
1✔
1070

1071
        # initialize outputs, and check if there's anything (planet or FA)
1072
        # to characterize
1073
        characterized = np.zeros(len(det), dtype=int)
1✔
1074
        fZ = 0.0 << self.inv_arcsec2
1✔
1075
        JEZ = 0.0 << self.JEZ_unit
1✔
1076
        systemParams = SU.dump_system_params(
1✔
1077
            sInd
1078
        )  # write current system params by default
1079
        SNR = np.zeros(len(det))
1✔
1080
        intTime = None
1✔
1081
        if len(det) == 0:  # nothing to characterize
1✔
1082
            return characterized, fZ, JEZ, systemParams, SNR, intTime
×
1083

1084
        # look for last detected planets that have not been fully characterized
1085
        if not (FA):  # only true planets, no FA
1✔
1086
            tochar = self.fullSpectra[mode_index][pIndsDet] == 0
1✔
1087
        else:  # mix of planets and a FA
1088
            truePlans = pIndsDet[:-1]
×
1089
            tochar = np.append((self.fullSpectra[mode_index][truePlans] == 0), True)
×
1090

1091
        # 1/ find spacecraft orbital START position including overhead time,
1092
        # and check keepout angle
1093
        if np.any(tochar):
1✔
1094
            # start times
1095
            startTime = (
1✔
1096
                TK.currentTimeAbs.copy() + mode["syst"]["ohTime"] + Obs.settlingTime
1097
            )
1098
            startTimeNorm = (
1✔
1099
                TK.currentTimeNorm.copy() + mode["syst"]["ohTime"] + Obs.settlingTime
1100
            )
1101
            # planets to characterize
1102
            koTimeInd = np.where(np.round(startTime.value) - self.koTimes.value == 0)[
1✔
1103
                0
1104
            ][
1105
                0
1106
            ]  # find indice where koTime is startTime[0]
1107
            # wherever koMap is 1, the target is observable
1108
            koMap = self.koMaps[mode["syst"]["name"]]
1✔
1109
            tochar[tochar] = koMap[sInd][koTimeInd]
1✔
1110

1111
        # 2/ if any planet to characterize, find the characterization times
1112
        if np.any(tochar):
1✔
1113
            # propagate the whole system to match up with current time
1114
            # calculate characterization times at the detected JEZ, dMag, and WA
1115
            pinds_earthlike = np.logical_and(
1✔
1116
                np.array([(p in self.known_earths) for p in pIndsDet]), tochar
1117
            )
1118

1119
            fZ = ZL.fZ(Obs, TL, np.array([sInd], ndmin=1), startTime.reshape(1), mode)
1✔
1120
            WAp = TL.int_WA[sInd] * np.ones(len(tochar))
1✔
1121
            dMag = TL.int_dMag[sInd] * np.ones(len(tochar))
1✔
1122

1123
            # if lucky_planets, use lucky planet params for dMag and WA
1124
            if SU.lucky_planets:
1✔
1125
                phi = (1 / np.pi) * np.ones(len(SU.d))
×
1126
                e_dMag = deltaMag(SU.p, SU.Rp, SU.d, phi)  # delta magnitude
×
1127
                e_WA = np.arctan(SU.a / TL.dist[SU.plan2star]).to(
×
1128
                    "arcsec"
1129
                )  # working angle
1130
            else:
1131
                e_dMag = SU.dMag
1✔
1132
                e_WA = SU.WA
1✔
1133

1134
            WAp[((pinds_earthlike) & (tochar))] = e_WA[pIndsDet[pinds_earthlike]]
1✔
1135
            dMag[((pinds_earthlike) & (tochar))] = e_dMag[pIndsDet[pinds_earthlike]]
1✔
1136

1137
            intTimes = np.zeros(len(tochar)) * u.day
1✔
1138
            intTimes[tochar] = OS.calc_intTime(
1✔
1139
                TL, sInd, fZ, JEZs[tochar], dMag[tochar], WAp[tochar], mode
1140
            )
1141
            intTimes[~np.isfinite(intTimes)] = 0 * u.d
1✔
1142

1143
            # add a predetermined margin to the integration times
1144
            intTimes = intTimes * (1 + self.charMargin)
1✔
1145
            # apply time multiplier
1146
            totTimes = intTimes * (mode["timeMultiplier"])
1✔
1147
            # end times
1148
            endTimes = startTime + totTimes
1✔
1149
            endTimesNorm = startTimeNorm + totTimes
1✔
1150
            # planets to characterize
1151
            tochar = (
1✔
1152
                (totTimes > 0)
1153
                & (totTimes <= OS.intCutoff)
1154
                & (endTimesNorm <= TK.OBendTimes[TK.OBnumber])
1155
            )
1156

1157
        # 3/ is target still observable at the end of any char time?
1158
        if np.any(tochar) and Obs.checkKeepoutEnd:
1✔
1159
            koTimeInds = np.zeros(len(endTimes.value[tochar]), dtype=int)
1✔
1160

1161
            # find index in koMap where each endTime is closest to koTimes
1162
            for t, endTime in enumerate(endTimes.value[tochar]):
1✔
1163
                if endTime > self.koTimes.value[-1]:
1✔
1164
                    # case where endTime exceeds largest koTimes element
1165
                    endTimeInBounds = np.where(
×
1166
                        np.floor(endTime) - self.koTimes.value == 0
1167
                    )[0]
1168
                    koTimeInds[t] = (
×
1169
                        endTimeInBounds[0] if endTimeInBounds.size != 0 else -1
1170
                    )
1171
                else:
1172
                    koTimeInds[t] = np.where(
1✔
1173
                        np.round(endTime) - self.koTimes.value == 0
1174
                    )[0][
1175
                        0
1176
                    ]  # find indice where koTime is endTimes[0]
1177
            tochar[tochar] = [koMap[sInd][koT] if koT >= 0 else 0 for koT in koTimeInds]
1✔
1178

1179
        # 4/ if yes, perform the characterization for the maximum char time
1180
        if np.any(tochar):
1✔
1181
            # Save Current Time before attempting time allocation
1182
            currentTimeNorm = TK.currentTimeNorm.copy()
1✔
1183
            currentTimeAbs = TK.currentTimeAbs.copy()
1✔
1184

1185
            if np.any(np.logical_and(pinds_earthlike, tochar)):
1✔
1186
                intTime = np.max(intTimes[np.logical_and(pinds_earthlike, tochar)])
×
1187
            else:
1188
                intTime = np.max(intTimes[tochar])
1✔
1189
            extraTime = intTime * (mode["timeMultiplier"] - 1.0)  # calculates extraTime
1✔
1190
            success = TK.allocate_time(
1✔
1191
                intTime + extraTime + mode["syst"]["ohTime"] + Obs.settlingTime, True
1192
            )  # allocates time
1193
            if not (success):  # Time was not successfully allocated
1✔
1194
                char_intTime = None
×
1195
                lenChar = len(pInds) + 1 if FA else len(pInds)
×
1196
                characterized = np.zeros(lenChar, dtype=int)
×
1197
                char_SNR = np.zeros(lenChar, dtype=float)
×
1198
                char_fZ = 0.0 << self.inv_arcsec2
×
1199
                char_JEZ = 0.0 << self.JEZ_unit
×
1200
                char_systemParams = SU.dump_system_params(sInd)
×
1201

1202
                # finally, populate the revisit list (NOTE: sInd becomes a float)
1203
                t_rev = TK.currentTimeNorm.copy() + self.revisit_wait[sInd]
×
1204
                revisit = np.array([sInd, t_rev.to("day").value])
×
1205
                if self.char_starRevisit.size == 0:
×
1206
                    self.char_starRevisit = np.array([revisit])
×
1207
                else:
1208
                    revInd = np.where(self.char_starRevisit[:, 0] == sInd)[0]
×
1209
                    if revInd.size == 0:
×
1210
                        self.char_starRevisit = np.vstack(
×
1211
                            (self.char_starRevisit, revisit)
1212
                        )
1213
                    else:
1214
                        self.char_starRevisit[revInd, 1] = revisit[1]
×
1215
                return (
×
1216
                    characterized,
1217
                    char_fZ,
1218
                    char_JEZ,
1219
                    char_systemParams,
1220
                    char_SNR,
1221
                    char_intTime,
1222
                )
1223

1224
            pIndsChar = pIndsDet[tochar]
1✔
1225
            log_char = "   - Charact. planet(s) %s (%s/%s detected)" % (
1✔
1226
                pIndsChar,
1227
                len(pIndsChar),
1228
                len(pIndsDet),
1229
            )
1230
            self.logger.info(log_char)
1✔
1231
            self.vprint(log_char)
1✔
1232

1233
            # SNR CALCULATION:
1234
            # first, calculate SNR for observable planets (without false alarm)
1235
            planinds = pIndsChar[:-1] if pIndsChar[-1] == -1 else pIndsChar
1✔
1236
            SNRplans = np.zeros(len(planinds))
1✔
1237
            if len(planinds) > 0:
1✔
1238
                # initialize arrays for SNR integration
1239
                fZs = np.zeros(self.ntFlux) << self.inv_arcsec2
1✔
1240
                JEZs = np.zeros((self.ntFlux, len(planinds))) << self.JEZ_unit
1✔
1241
                systemParamss = np.empty(self.ntFlux, dtype="object")
1✔
1242
                Ss = np.zeros((self.ntFlux, len(planinds)))
1✔
1243
                Ns = np.zeros((self.ntFlux, len(planinds)))
1✔
1244
                # integrate the signal (planet flux) and noise
1245
                dt = intTime / float(self.ntFlux)
1✔
1246
                timePlus = (
1✔
1247
                    Obs.settlingTime.copy() + mode["syst"]["ohTime"].copy()
1248
                )  # accounts for the time since the current time
1249
                for i in range(self.ntFlux):
1✔
1250
                    # calculate signal and noise (electron count rates)
1251
                    if SU.lucky_planets:
1✔
1252
                        fZs[i] = ZL.fZ(
×
1253
                            Obs,
1254
                            TL,
1255
                            np.array([sInd], ndmin=1),
1256
                            currentTimeAbs.reshape(1),
1257
                            mode,
1258
                        )[0]
1259
                        Ss[i, :], Ns[i, :] = self.calc_signal_noise(
×
1260
                            sInd, planinds, dt, mode, fZ=fZs[i]
1261
                        )
1262
                    # allocate first half of dt
1263
                    timePlus += dt / 2.0
1✔
1264
                    # calculate current zodiacal light brightness
1265
                    fZs[i] = ZL.fZ(
1✔
1266
                        Obs,
1267
                        TL,
1268
                        np.array([sInd], ndmin=1),
1269
                        (currentTimeAbs + timePlus).reshape(1),
1270
                        mode,
1271
                    )[0]
1272
                    # propagate the system to match up with current time
1273
                    SU.propag_system(
1✔
1274
                        sInd, currentTimeNorm + timePlus - self.propagTimes[sInd]
1275
                    )
1276
                    self.propagTimes[sInd] = currentTimeNorm + timePlus
1✔
1277
                    # Calculate the exozodi intensity
1278
                    JEZs[i] = SU.scale_JEZ(sInd, mode, pInds=planinds)
1✔
1279
                    # save planet parameters
1280
                    systemParamss[i] = SU.dump_system_params(sInd)
1✔
1281
                    # calculate signal and noise (electron count rates)
1282
                    if not SU.lucky_planets:
1✔
1283
                        Ss[i, :], Ns[i, :] = self.calc_signal_noise(
1✔
1284
                            sInd, planinds, dt, mode, fZ=fZs[i], JEZ=JEZs[i]
1285
                        )
1286
                    # allocate second half of dt
1287
                    timePlus += dt / 2.0
1✔
1288

1289
                # average output parameters
1290
                fZ = np.mean(fZs)
1✔
1291
                JEZ = np.mean(JEZs, axis=0)
1✔
1292
                systemParams = {
1✔
1293
                    key: sum([systemParamss[x][key] for x in range(self.ntFlux)])
1294
                    / float(self.ntFlux)
1295
                    for key in sorted(systemParamss[0])
1296
                }
1297
                # calculate planets SNR
1298
                S = Ss.sum(0)
1✔
1299
                N = Ns.sum(0)
1✔
1300
                SNRplans[N > 0] = S[N > 0] / N[N > 0]
1✔
1301
                # allocate extra time for timeMultiplier
1302
            # if only a FA, just save zodiacal brightness in the middle of the
1303
            # integration
1304
            else:
1305
                # totTime = intTime * (mode["timeMultiplier"])
1306
                fZ = ZL.fZ(
×
1307
                    Obs,
1308
                    TL,
1309
                    np.array([sInd], ndmin=1),
1310
                    TK.currentTimeAbs.copy().reshape(1),
1311
                    mode,
1312
                )[0]
1313
                # Use the default star value if no planets
1314
                JEZ = TL.JEZ0[mode["hex"]][sInd]
×
1315

1316
            # calculate the false alarm SNR (if any)
1317
            SNRfa = []
1✔
1318
            if pIndsChar[-1] == -1:
1✔
1319
                JEZ = JEZs[-1]
×
1320
                dMag = dMags[-1]
×
1321
                WA = WAs[-1] << u.arcsec
×
1322
                C_p, C_b, C_sp = OS.Cp_Cb_Csp(TL, sInd, fZ, JEZ, dMag, WA, mode)
×
1323
                S = (C_p * intTime).decompose().value
×
1324
                N = np.sqrt((C_b * intTime + (C_sp * intTime) ** 2).decompose().value)
×
1325
                SNRfa = S / N if N > 0 else 0.0
×
1326

1327
            # save all SNRs (planets and FA) to one array
1328
            SNRinds = np.where(det)[0][tochar]
1✔
1329
            SNR[SNRinds] = np.append(SNRplans, SNRfa)
1✔
1330

1331
            # now, store characterization status: 1 for full spectrum,
1332
            # -1 for partial spectrum, 0 for not characterized
1333
            char = SNR >= mode["SNR"]
1✔
1334
            # initialize with full spectra
1335
            characterized = char.astype(int)
1✔
1336
            WAchar = WAs[char] * u.arcsec
1✔
1337
            # find the current WAs of characterized planets
1338
            if SU.lucky_planets:
1✔
1339
                # keep original WAs (note, the dump_system_params() above, whence comes
1340
                # systemParams, does not understand lucky_planets)
1341
                pass
×
1342
            else:
1343
                WAs = systemParams["WA"]
1✔
1344
            if FA:
1✔
1345
                WAs = np.append(WAs, WAs[-1] * u.arcsec)
×
1346
            # check for partial spectra
1347
            IWA_max = mode["IWA"] * (1.0 + mode["BW"] / 2.0)
1✔
1348
            OWA_min = mode["OWA"] * (1.0 - mode["BW"] / 2.0)
1✔
1349
            char[char] = (WAchar < IWA_max) | (WAchar > OWA_min)
1✔
1350
            characterized[char] = -1
1✔
1351
            all_full = np.copy(characterized)
1✔
1352
            all_full[char] = 0
1✔
1353
            if sInd not in self.sInd_charcounts.keys():
1✔
1354
                self.sInd_charcounts[sInd] = all_full
1✔
1355
            else:
1356
                self.sInd_charcounts[sInd] = self.sInd_charcounts[sInd] + all_full
×
1357

1358
            # encode results in spectra lists (only for planets, not FA)
1359
            charplans = characterized[:-1] if FA else characterized
1✔
1360
            self.fullSpectra[mode_index][pInds[charplans == 1]] += 1
1✔
1361
            self.partialSpectra[mode_index][pInds[charplans == -1]] += 1
1✔
1362

1363
        # in both cases (detection or false alarm), schedule a revisit
1364
        smin = np.min(SU.s[pInds[det]])
1✔
1365
        Ms = TL.MsTrue[sInd]
1✔
1366

1367
        # if target in promoted_stars list, schedule revisit based off of
1368
        # semi-major axis
1369
        if sInd in self.promoted_stars:
1✔
1370
            sp = np.min(SU.a[pInds[det]]).to("AU")
×
1371
            if np.any(det):
×
1372
                pInd_smin = pInds[det][np.argmin(SU.a[pInds[det]])]
×
1373
                Mp = SU.Mp[pInd_smin]
×
1374
            else:
1375
                Mp = SU.Mp.mean()
×
1376
            mu = const.G * (Mp + Ms)
×
1377
            T = 2.0 * np.pi * np.sqrt(sp**3 / mu)
×
1378
            t_rev = TK.currentTimeNorm.copy() + T / 3.0
×
1379
        # otherwise schedule revisit based off of seperation
1380
        elif smin is not None:
1✔
1381
            sp = smin
1✔
1382
            if np.any(det):
1✔
1383
                pInd_smin = pInds[det][np.argmin(SU.s[pInds[det]])]
1✔
1384
                Mp = SU.Mp[pInd_smin]
1✔
1385
            else:
1386
                Mp = SU.Mp.mean()
×
1387
            mu = const.G * (Mp + Ms)
1✔
1388
            T = 2.0 * np.pi * np.sqrt(sp**3 / mu)
1✔
1389
            t_rev = TK.currentTimeNorm.copy() + T / 2.0
1✔
1390
        # otherwise, revisit based on average of population semi-major axis and mass
1391
        else:
1392
            sp = SU.s.mean()
×
1393
            Mp = SU.Mp.mean()
×
1394
            mu = const.G * (Mp + Ms)
×
1395
            T = 2.0 * np.pi * np.sqrt(sp**3 / mu)
×
1396
            t_rev = TK.currentTimeNorm.copy() + 0.75 * T
×
1397

1398
        # finally, populate the revisit list (NOTE: sInd becomes a float)
1399
        revisit = np.array([sInd, t_rev.to("day").value])
1✔
1400
        if self.char_starRevisit.size == 0:
1✔
1401
            self.char_starRevisit = np.array([revisit])
1✔
1402
        else:
1403
            revInd = np.where(self.char_starRevisit[:, 0] == sInd)[0]
×
1404
            if revInd.size == 0:
×
1405
                self.char_starRevisit = np.vstack((self.char_starRevisit, revisit))
×
1406
            else:
1407
                self.char_starRevisit[revInd, 1] = revisit[1]
×
1408

1409
        # add stars to filter list
1410
        if np.any(characterized.astype(int) == 1):
1✔
1411
            if np.any(self.sInd_charcounts[sInd] >= self.max_successful_chars):
1✔
1412
                self.ignore_stars = np.union1d(self.ignore_stars, [sInd]).astype(int)
1✔
1413

1414
        return characterized.astype(int), fZ, JEZ, systemParams, SNR, intTime
1✔
1415

1416
    def test_observation_characterization(self, sInd, mode, mode_index):
1✔
1417
        """Finds if characterizations are possible and relevant information
1418

1419
        Args:
1420
            sInd (integer):
1421
                Integer index of the star of interest
1422
            mode (dict):
1423
                Selected observing mode for characterization
1424

1425
        Returns:
1426
            characterized (integer list):
1427
                Characterization status for each planet orbiting the observed
1428
                target star including False Alarm if any, where 1 is full spectrum,
1429
                -1 partial spectrum, and 0 not characterized
1430
            fZ (astropy Quantity):
1431
                Surface brightness of local zodiacal light in units of 1/arcsec2
1432
            JEZ (astropy Quantity):
1433
                Intensity of exo-zodiacal light in units of ph/s/m2/arcsec2
1434
            systemParams (dict):
1435
                Dictionary of time-dependant planet properties averaged over the
1436
                duration of the integration
1437
            SNR (float ndarray):
1438
                Characterization signal-to-noise ratio of the observable planets.
1439
                Defaults to None.
1440
            intTime (astropy Quantity):
1441
                Selected star characterization time in units of day. Defaults to None.
1442

1443
        """
1444

1445
        OS = self.OpticalSystem
×
1446
        ZL = self.ZodiacalLight
×
1447
        TL = self.TargetList
×
1448
        SU = self.SimulatedUniverse
×
1449
        Obs = self.Observatory
×
1450
        TK = self.TimeKeeping
×
1451

1452
        # find indices of planets around the target
1453
        pInds = np.where(SU.plan2star == sInd)[0]
×
1454
        JEZs = SU.scale_JEZ(sInd, mode)
×
1455
        dMags = SU.dMag[pInds]
×
1456
        # WAs = SU.WA[pInds].to("arcsec").value
1457

1458
        # get the detected status, and check if there was a FA
1459
        # det = self.lastDetected[sInd,0]
1460
        det = np.ones(pInds.size, dtype=bool)
×
1461
        FA = len(det) == len(pInds) + 1
×
1462
        if FA:
×
1463
            pIndsDet = np.append(pInds, -1)[det]
×
1464
        else:
1465
            pIndsDet = pInds[det]
×
1466

1467
        # initialize outputs, and check if there's anything (planet or FA)
1468
        # to characterize
1469
        characterized = np.zeros(len(det), dtype=int)
×
1470
        fZ = 0.0 << self.inv_arcsec2
×
1471
        JEZ = 0.0 << self.JEZ_unit
×
1472
        systemParams = SU.dump_system_params(
×
1473
            sInd
1474
        )  # write current system params by default
1475
        SNR = np.zeros(len(det))
×
1476
        intTime = None
×
1477
        if len(det) == 0:  # nothing to characterize
×
1478
            return characterized, fZ, JEZ, systemParams, SNR, intTime
×
1479

1480
        # look for last detected planets that have not been fully characterized
1481
        if not (FA):  # only true planets, no FA
×
1482
            tochar = self.fullSpectra[mode_index][pIndsDet] == 0
×
1483
        else:  # mix of planets and a FA
1484
            truePlans = pIndsDet[:-1]
×
1485
            tochar = np.append((self.fullSpectra[mode_index][truePlans] == 0), True)
×
1486

1487
        # 1/ find spacecraft orbital START position including overhead time,
1488
        # and check keepout angle
1489
        if np.any(tochar):
×
1490
            # start times
1491
            startTime = (
×
1492
                TK.currentTimeAbs.copy() + mode["syst"]["ohTime"] + Obs.settlingTime
1493
            )
1494
            startTimeNorm = (
×
1495
                TK.currentTimeNorm.copy() + mode["syst"]["ohTime"] + Obs.settlingTime
1496
            )
1497
            # planets to characterize
1498
            koTimeInd = np.where(np.round(startTime.value) - self.koTimes.value == 0)[
×
1499
                0
1500
            ][
1501
                0
1502
            ]  # find indice where koTime is startTime[0]
1503
            # wherever koMap is 1, the target is observable
1504
            koMap = self.koMaps[mode["syst"]["name"]]
×
1505
            tochar[tochar] = koMap[sInd][koTimeInd]
×
1506

1507
        # 2/ if any planet to characterize, find the characterization times
1508
        if np.any(tochar):
×
1509
            # propagate the whole system to match up with current time
1510
            # calculate characterization times at the detected JEZ, dMag, and WA
1511
            pinds_earthlike = np.logical_and(
×
1512
                np.array([(p in self.known_earths) for p in pIndsDet]), tochar
1513
            )
1514

1515
            fZ = ZL.fZ(Obs, TL, np.array([sInd], ndmin=1), startTime.reshape(1), mode)
×
1516
            JEZ = SU.scale_JEZ(sInd, mode)
×
1517
            dMag = dMags[tochar]
×
1518
            WAp = TL.int_WA[sInd] * np.ones(len(tochar))
×
1519
            dMag = TL.int_dMag[sInd] * np.ones(len(tochar))
×
1520

1521
            # if lucky_planets, use lucky planet params for dMag and WA
1522
            if SU.lucky_planets:
×
1523
                phi = (1 / np.pi) * np.ones(len(SU.d))
×
1524
                e_dMag = deltaMag(SU.p, SU.Rp, SU.d, phi)  # delta magnitude
×
1525
                e_WA = np.arctan(SU.a / TL.dist[SU.plan2star]).to(
×
1526
                    "arcsec"
1527
                )  # working angle
1528
            else:
1529
                e_dMag = SU.dMag
×
1530
                e_WA = SU.WA
×
1531

1532
            WAp[((pinds_earthlike) & (tochar))] = e_WA[pIndsDet[pinds_earthlike]]
×
1533
            dMag[((pinds_earthlike) & (tochar))] = e_dMag[pIndsDet[pinds_earthlike]]
×
1534

1535
            intTimes = np.zeros(len(tochar)) << u.d
×
1536
            intTimes[tochar] = OS.calc_intTime(
×
1537
                TL, sInd, fZ, JEZ[tochar], dMag[tochar], WAp[tochar], mode
1538
            )
1539
            intTimes[~np.isfinite(intTimes)] = self.zero_d
×
1540

1541
            # add a predetermined margin to the integration times
1542
            intTimes = intTimes * (1 + self.charMargin)
×
1543
            # apply time multiplier
1544
            totTimes = intTimes * (mode["timeMultiplier"])
×
1545
            # end times
1546
            endTimes = startTime + totTimes
×
1547
            endTimesNorm = startTimeNorm + totTimes
×
1548
            # planets to characterize
1549
            tochar = (
×
1550
                (totTimes > 0)
1551
                & (totTimes <= OS.intCutoff)
1552
                & (endTimesNorm <= TK.OBendTimes[TK.OBnumber])
1553
            )
1554

1555
        # 3/ is target still observable at the end of any char time?
1556
        if np.any(tochar) and Obs.checkKeepoutEnd:
×
1557
            koTimeInds = np.zeros(len(endTimes.value[tochar]), dtype=int)
×
1558

1559
            # find index in koMap where each endTime is closest to koTimes
1560
            for t, endTime in enumerate(endTimes.value[tochar]):
×
1561
                if endTime > self.koTimes.value[-1]:
×
1562
                    # case where endTime exceeds largest koTimes element
1563
                    endTimeInBounds = np.where(
×
1564
                        np.floor(endTime) - self.koTimes.value == 0
1565
                    )[0]
1566
                    koTimeInds[t] = (
×
1567
                        endTimeInBounds[0] if endTimeInBounds.size != 0 else -1
1568
                    )
1569
                else:
1570
                    koTimeInds[t] = np.where(
×
1571
                        np.round(endTime) - self.koTimes.value == 0
1572
                    )[0][
1573
                        0
1574
                    ]  # find indice where koTime is endTimes[0]
1575
            tochar[tochar] = [koMap[sInd][koT] if koT >= 0 else 0 for koT in koTimeInds]
×
1576

1577
        # 4/ if yes, perform the characterization for the maximum char time
1578
        if np.any(tochar):
×
1579
            if np.any(np.logical_and(pinds_earthlike, tochar)):
×
1580
                intTime = np.max(intTimes[np.logical_and(pinds_earthlike, tochar)])
×
1581
            else:
1582
                intTime = np.max(intTimes[tochar])
×
1583
            extraTime = intTime * (mode["timeMultiplier"] - 1.0)  # calculates extraTime
×
1584

1585
            dt = intTime + extraTime + mode["syst"]["ohTime"] + Obs.settlingTime
×
1586
            if (
×
1587
                (dt.value <= 0 or dt.value == np.inf)
1588
                or (TK.currentTimeNorm.copy() + dt > TK.missionLife.to("day"))
1589
                or (TK.currentTimeNorm.copy() + dt > TK.OBendTimes[TK.OBnumber])
1590
            ):
1591
                success = (
×
1592
                    False  # The temporal block to allocate is not positive nonzero
1593
                )
1594
            else:
1595
                success = True
×
1596

1597
            # success = TK.allocate_time(intTime + extraTime + mode['syst']['ohTime']
1598
            #                               + Obs.settlingTime, True)#allocates time
1599
            if not (success):  # Time was not successfully allocated
×
1600
                char_intTime = None
×
1601
                lenChar = len(pInds) + 1 if FA else len(pInds)
×
1602
                characterized = np.zeros(lenChar, dtype=float)
×
1603
                char_SNR = np.zeros(lenChar, dtype=float)
×
1604
                char_fZ = 0.0 << self.inv_arcsec2
×
1605
                char_JEZ = 0.0 << self.JEZ_unit
×
1606
                char_systemParams = SU.dump_system_params(sInd)
×
1607

1608
                return (
×
1609
                    characterized,
1610
                    char_fZ,
1611
                    char_JEZ,
1612
                    char_systemParams,
1613
                    char_SNR,
1614
                    char_intTime,
1615
                )
1616

1617
            # pIndsChar = pIndsDet[tochar]
1618

1619
        return characterized.astype(int), fZ, JEZ, systemParams, SNR, intTime
×
1620

1621
    def scheduleRevisit(self, sInd, smin, det, pInds):
1✔
1622
        """A Helper Method for scheduling revisits after observation detection
1623
        Args:
1624
            sInd - sInd of the star just detected
1625
            smin - minimum separation of the planet to star of planet just detected
1626
            det -
1627
            pInds - Indices of planets around target star
1628
        Return:
1629
            updates self.starRevisit attribute
1630
        """
1631

1632
        TK = self.TimeKeeping
1✔
1633

1634
        t_rev = TK.currentTimeNorm.copy() + self.revisit_wait[sInd]
1✔
1635
        # finally, populate the revisit list (NOTE: sInd becomes a float)
1636
        revisit = np.array([sInd, t_rev.to_value(u.d)])
1✔
1637
        if self.starRevisit.size == 0:  # If starRevisit has nothing in it
1✔
1638
            self.starRevisit = np.array([revisit])  # initialize sterRevisit
1✔
1639
        else:
1640
            revInd = np.where(self.starRevisit[:, 0] == sInd)[
1✔
1641
                0
1642
            ]  # indices of the first column of the starRevisit list containing sInd
1643
            if revInd.size == 0:
1✔
1644
                self.starRevisit = np.vstack((self.starRevisit, revisit))
1✔
1645
            else:
1646
                self.starRevisit[revInd, 1] = revisit[1]  # over
×
1647

1648
    def revisitFilter(self, sInds, tmpCurrentTimeNorm):
1✔
1649
        """Helper method for Overloading Revisit Filtering
1650

1651
        Args:
1652
            sInds - indices of stars still in observation list
1653
            tmpCurrentTimeNorm (MJD) - the simulation time after overhead was
1654
            added in MJD form
1655

1656
        Returns:
1657
            ~numpy.ndarray(int):
1658
                sInds - indices of stars still in observation list
1659
        """
1660
        tovisit = np.zeros(
1✔
1661
            self.TargetList.nStars, dtype=bool
1662
        )  # tovisit is a boolean array containing the
1663
        if len(sInds) > 0:  # so long as there is at least 1 star left in sInds
1✔
1664
            tovisit[sInds] = (self.starVisits[sInds] == min(self.starVisits[sInds])) & (
1✔
1665
                self.starVisits[sInds] < self.nVisitsMax
1666
            )  # Checks that no star has exceeded the number of revisits
1667
            if (
1✔
1668
                self.starRevisit.size != 0
1669
            ):  # There is at least one revisit planned in starRevisit
1670
                dt_rev = (
1✔
1671
                    self.starRevisit[:, 1] * u.day - tmpCurrentTimeNorm
1672
                )  # absolute temporal spacing between revisit and now.
1673

1674
                # return indices of all revisits within a threshold dt_max of
1675
                # revisit day and indices of all revisits with no detections
1676
                # past the revisit time
1677
                ind_rev2 = [
1✔
1678
                    int(x)
1679
                    for x in self.starRevisit[dt_rev < self.zero_d, 0]
1680
                    if (x in sInds)
1681
                ]
1682
                tovisit[ind_rev2] = self.starVisits[ind_rev2] < self.nVisitsMax
1✔
1683
            sInds = np.where(tovisit)[0]
1✔
1684

1685
        return sInds
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

© 2026 Coveralls, Inc