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

dsavransky / EXOSIMS / 20249289169

15 Dec 2025 10:16PM UTC coverage: 65.689% (-0.08%) from 65.768%
20249289169

Pull #451

github

web-flow
Merge f255f83fe into 9dd00555b
Pull Request #451: Improve angular diameter filtering in TargetList

1 of 15 new or added lines in 2 files covered. (6.67%)

15 existing lines in 5 files now uncovered.

9827 of 14960 relevant lines covered (65.69%)

0.66 hits per line

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

78.37
/EXOSIMS/SurveySimulation/linearJScheduler_DDPC.py
1
# -*- coding: utf-8 -*-
2
from EXOSIMS.SurveySimulation.linearJScheduler import linearJScheduler
1✔
3
import logging
1✔
4
import numpy as np
1✔
5
import astropy.units as u
1✔
6
import time
1✔
7
import copy
1✔
8
from EXOSIMS.util._numpy_compat import copy_if_needed
1✔
9

10
Logger = logging.getLogger(__name__)
1✔
11

12

13
class linearJScheduler_DDPC(linearJScheduler):
1✔
14
    """linearJScheduler_DDPC - linearJScheduler Dual Detection Parallel
15
    Charachterization
16

17
    This scheduler inherits from the LJS, but is capable of taking in two detection
18
    modes and two chracterization modes. Detections can then be performed using a
19
    dual-band mode, while characterizations are performed in parallel.
20
    """
21

22
    def __init__(self, revisit_weight=1.0, **specs):
1✔
23
        linearJScheduler.__init__(self, **specs)
1✔
24

25
        self._outspec["revisit_weight"] = revisit_weight
1✔
26

27
        OS = self.OpticalSystem
1✔
28
        SU = self.SimulatedUniverse
1✔
29

30
        allModes = OS.observingModes
1✔
31
        num_char_modes = len(
1✔
32
            list(filter(lambda mode: "spec" in mode["inst"]["name"], allModes))
33
        )
34
        self.fullSpectra = np.zeros((num_char_modes, SU.nPlans), dtype=int)
1✔
35
        self.partialSpectra = np.zeros((num_char_modes, SU.nPlans), dtype=int)
1✔
36

37
        self.revisit_weight = revisit_weight
1✔
38

39
    def run_sim(self):
1✔
40
        """Performs the survey simulation"""
41

42
        OS = self.OpticalSystem
1✔
43
        TL = self.TargetList
1✔
44
        SU = self.SimulatedUniverse
1✔
45
        Obs = self.Observatory
1✔
46
        TK = self.TimeKeeping
1✔
47

48
        # TODO: start using this self.currentSep
49
        # set occulter separation if haveOcculter
50
        if OS.haveOcculter:
1✔
51
            self.currentSep = Obs.occulterSep
×
52

53
        # choose observing modes selected for detection (default marked with a flag)
54
        allModes = OS.observingModes
1✔
55
        det_modes = list(filter(lambda mode: "imag" in mode["inst"]["name"], allModes))
1✔
56
        base_det_mode = list(
1✔
57
            filter(lambda mode: mode["detectionMode"], OS.observingModes)
58
        )[0]
59
        # and for characterization (default is first spectro/IFS mode)
60
        spectroModes = list(
1✔
61
            filter(lambda mode: "spec" in mode["inst"]["name"], allModes)
62
        )
63
        if np.any(spectroModes):
1✔
64
            char_modes = spectroModes
1✔
65
        # if no spectro mode, default char mode is first observing mode
66
        else:
67
            char_modes = [allModes[0]]
×
68

69
        # begin Survey, and loop until mission is finished
70
        log_begin = "OB%s: survey beginning." % (TK.OBnumber + 1)
1✔
71
        self.logger.info(log_begin)
1✔
72
        self.vprint(log_begin)
1✔
73
        t0 = time.time()
1✔
74
        sInd = None
1✔
75
        ObsNum = 0
1✔
76
        while not TK.mission_is_over(OS, Obs, det_modes[0]):
1✔
77
            # acquire the NEXT TARGET star index and create DRM
78
            old_sInd = sInd  # used to save sInd if returned sInd is None
1✔
79
            DRM, sInd, det_intTime, waitTime, det_mode = self.next_target(
1✔
80
                sInd, det_modes
81
            )
82

83
            if sInd is not None:
1✔
84
                ObsNum += 1
1✔
85

86
                if OS.haveOcculter:
1✔
87
                    # advance to start of observation
88
                    # (add slew time for selected target)
89
                    _ = TK.advanceToAbsTime(TK.currentTimeAbs.copy() + waitTime)
×
90

91
                # beginning of observation, start to populate DRM
92
                DRM["star_ind"] = sInd
1✔
93
                DRM["star_name"] = TL.Name[sInd]
1✔
94
                DRM["arrival_time"] = TK.currentTimeNorm.copy().to("day")
1✔
95
                DRM["OB_nb"] = TK.OBnumber
1✔
96
                DRM["ObsNum"] = ObsNum
1✔
97
                pInds = np.where(SU.plan2star == sInd)[0]
1✔
98
                DRM["plan_inds"] = pInds.astype(int)
1✔
99
                log_obs = (
1✔
100
                    "  Observation #%s, star ind %s (of %s) with %s planet(s), "
101
                    + "mission time at Obs start: %s"
102
                ) % (
103
                    ObsNum,
104
                    sInd,
105
                    TL.nStars,
106
                    len(pInds),
107
                    TK.currentTimeNorm.to("day").copy().round(2),
108
                )
109
                self.logger.info(log_obs)
1✔
110
                self.vprint(log_obs)
1✔
111

112
                # PERFORM DETECTION and populate revisit list attribute
113
                DRM["det_info"] = []
1✔
114
                (
1✔
115
                    detected,
116
                    det_fZ,
117
                    det_JEZ,
118
                    det_systemParams,
119
                    det_SNR,
120
                    FA,
121
                ) = self.observation_detection(sInd, det_intTime, det_mode)
122
                # update the occulter wet mass
123
                if OS.haveOcculter:
1✔
124
                    DRM = self.update_occulter_mass(DRM, sInd, det_intTime, "det")
×
125
                det_data = {}
1✔
126
                det_data["det_status"] = detected
1✔
127
                det_data["det_SNR"] = det_SNR
1✔
128
                det_data["det_fZ"] = det_fZ.to("1/arcsec2")
1✔
129
                det_data["det_params"] = det_systemParams
1✔
130
                det_data["det_mode"] = dict(det_mode)
1✔
131
                det_data["det_time"] = det_intTime.to("day")
1✔
132
                del det_data["det_mode"]["inst"], det_data["det_mode"]["syst"]
1✔
133
                DRM["det_info"].append(det_data)
1✔
134

135
                # PERFORM CHARACTERIZATION and populate spectra list attribute
136
                DRM["char_info"] = []
1✔
137
                if char_modes[0]["SNR"] not in [0, np.inf]:
1✔
138
                    (
1✔
139
                        characterized,
140
                        char_fZ,
141
                        char_JEZ,
142
                        char_systemParams,
143
                        char_SNR,
144
                        char_intTime,
145
                    ) = self.observation_characterization(sInd, char_modes)
146
                else:
147
                    char_intTime = None
×
148
                    lenChar = len(pInds) + 1 if True in FA else len(pInds)
×
149
                    characterized = np.zeros((lenChar, len(char_modes)), dtype=float)
×
150
                    char_SNR = np.zeros((lenChar, len(char_modes)), dtype=float)
×
151
                    char_fZ = np.array([0.0 / u.arcsec**2, 0.0 / u.arcsec**2])
×
152
                    char_systemParams = SU.dump_system_params(sInd)
×
153

154
                for mode_index, char_mode in enumerate(char_modes):
1✔
155
                    char_data = {}
1✔
156
                    assert char_intTime != 0, "Integration time can't be 0."
1✔
157
                    # update the occulter wet mass
158
                    if OS.haveOcculter and char_intTime is not None:
1✔
159
                        char_data = self.update_occulter_mass(
×
160
                            char_data, sInd, char_intTime, "char"
161
                        )
162
                    if np.any(characterized):
1✔
163
                        self.vprint(
1✔
164
                            "  Char. results are: {}".format(
165
                                characterized[:-1, mode_index]
166
                            )
167
                        )
168
                    # populate the DRM with characterization results
169
                    char_data["char_time"] = (
1✔
170
                        char_intTime.to("day")
171
                        if char_intTime is not None
172
                        else 0.0 * u.day
173
                    )
174
                    char_data["char_status"] = (
1✔
175
                        characterized[:-1, mode_index]
176
                        if FA
177
                        else characterized[:, mode_index]
178
                    )
179
                    char_data["char_SNR"] = (
1✔
180
                        char_SNR[:-1, mode_index] if FA else char_SNR[:, mode_index]
181
                    )
182
                    char_data["char_fZ"] = char_fZ[mode_index].to("1/arcsec2")
1✔
183
                    char_data["char_params"] = char_systemParams
1✔
184
                    # populate the DRM with FA results
185
                    char_data["FA_det_status"] = int(FA)
1✔
186
                    char_data["FA_char_status"] = (
1✔
187
                        characterized[-1, mode_index] if FA else 0
188
                    )
189
                    char_data["FA_char_SNR"] = char_SNR[-1] if FA else 0.0
1✔
190
                    char_data["FA_char_JEZ"] = (
1✔
191
                        self.lastDetected[sInd, 1][-1]
192
                        if FA
193
                        else 0.0 * u.ph / u.s / u.m**2 / u.arcsec**2
194
                    )
195
                    char_data["FA_char_dMag"] = (
1✔
196
                        self.lastDetected[sInd, 2][-1] if FA else 0.0
197
                    )
198
                    char_data["FA_char_WA"] = (
1✔
199
                        self.lastDetected[sInd, 3][-1] * u.arcsec
200
                        if FA
201
                        else 0.0 * u.arcsec
202
                    )
203

204
                    # populate the DRM with observation modes
205
                    char_data["char_mode"] = dict(char_mode)
1✔
206
                    del char_data["char_mode"]["inst"], char_data["char_mode"]["syst"]
1✔
207
                    DRM["char_info"].append(char_data)
1✔
208

209
                DRM["exoplanetObsTime"] = TK.exoplanetObsTime.copy()
1✔
210

211
                # append result values to self.DRM
212
                self.DRM.append(DRM)
1✔
213

214
            else:  # sInd == None
215
                sInd = old_sInd  # Retain the last observed star
×
216
                if (
×
217
                    TK.currentTimeNorm.copy() >= TK.OBendTimes[TK.OBnumber]
218
                ):  # currentTime is at end of OB
219
                    # Conditional Advance To Start of Next OB
220
                    if not TK.mission_is_over(
×
221
                        OS, Obs, det_mode
222
                    ):  # as long as the mission is not over
223
                        TK.advancetToStartOfNextOB()  # Advance To Start of Next OB
×
224
                elif waitTime is not None:
×
225
                    # CASE 1: Advance specific wait time
226
                    _ = TK.advanceToAbsTime(TK.currentTimeAbs.copy() + waitTime)
×
227
                    self.vprint("waitTime is not None")
×
228
                else:
229
                    startTimes = (
×
230
                        TK.currentTimeAbs.copy() + np.zeros(TL.nStars) * u.d
231
                    )  # Start Times of Observations
232
                    observableTimes = Obs.calculate_observableTimes(
×
233
                        TL,
234
                        np.arange(TL.nStars),
235
                        startTimes,
236
                        self.koMaps,
237
                        self.koTimes,
238
                        base_det_mode,
239
                    )[0]
240
                    # CASE 2 If There are no observable targets for the rest of
241
                    # the mission
242
                    if (
×
243
                        observableTimes[
244
                            (
245
                                TK.missionFinishAbs.copy().value * u.d
246
                                > observableTimes.value * u.d
247
                            )
248
                            * (
249
                                observableTimes.value * u.d
250
                                >= TK.currentTimeAbs.copy().value * u.d
251
                            )
252
                        ].shape[0]
253
                    ) == 0:
254
                        self.vprint(
×
255
                            (
256
                                "No Observable Targets for Remainder of mission at "
257
                                "currentTimeNorm = {}"
258
                            ).format(TK.currentTimeNorm.copy())
259
                        )
260
                        # Manually advancing time to mission end
261
                        TK.currentTimeNorm = TK.missionLife
×
262
                        TK.currentTimeAbs = TK.missionFinishAbs
×
263
                    else:
264
                        # CASE 3 nominal wait time if at least 1 target is still in
265
                        # list and observable
266
                        # TODO: ADD ADVANCE TO WHEN FZMIN OCURS
267
                        inds1 = np.arange(TL.nStars)[
×
268
                            observableTimes.value * u.d
269
                            > TK.currentTimeAbs.copy().value * u.d
270
                        ]
271
                        # apply intTime filter
272
                        inds2 = np.intersect1d(self.intTimeFilterInds, inds1)
×
273
                        # apply revisit Filter #NOTE this means stars you added to the
274
                        # revisit list
275
                        inds3 = self.revisitFilter(
×
276
                            inds2, TK.currentTimeNorm.copy() + self.dt_max.to(u.d)
277
                        )
278
                        self.vprint(
×
279
                            "Filtering %d stars from advanceToAbsTime"
280
                            % (TL.nStars - len(inds3))
281
                        )
282
                        oTnowToEnd = observableTimes[inds3]
×
283
                        # there is at least one observableTime between now and the end
284
                        # of the mission
285
                        if not oTnowToEnd.value.shape[0] == 0:
×
286
                            # advance to that observable time
287
                            tAbs = np.min(oTnowToEnd)
×
288
                        else:
289
                            tAbs = (
×
290
                                TK.missionStart + TK.missionLife
291
                            )  # advance to end of mission
292
                        tmpcurrentTimeNorm = TK.currentTimeNorm.copy()
×
293
                        # Advance Time to this time OR start of next OB following
294
                        # this time
295
                        _ = TK.advanceToAbsTime(tAbs)
×
296
                        self.vprint(
×
297
                            (
298
                                "No Observable Targets a currentTimeNorm = {:.2f}"
299
                                "Advanced To currentTimeNorm = {:.2f}"
300
                            ).format(
301
                                tmpcurrentTimeNorm.to("day").value,
302
                                TK.currentTimeNorm.to("day").value,
303
                            )
304
                        )
305
        else:  # TK.mission_is_over()
306
            dtsim = (time.time() - t0) * u.s
1✔
307
            log_end = (
1✔
308
                "Mission complete: no more time available.\n"
309
                + "Simulation duration: %s.\n" % dtsim.astype("int")
310
                + "Results stored in SurveySimulation.DRM (Design Reference Mission)."
311
            )
312
            self.logger.info(log_end)
1✔
313
            self.vprint(log_end)
1✔
314

315
    def next_target(self, old_sInd, modes):
1✔
316
        """Finds index of next target star and calculates its integration time.
317

318
        This method chooses the next target star index based on which
319
        stars are available, their integration time, and maximum completeness.
320
        Returns None if no target could be found.
321

322
        Args:
323
            old_sInd (integer):
324
                Index of the previous target star
325
            modes (dict):
326
                Selected observing modes for detection
327

328
        Returns:
329
            tuple:
330
                DRM (dict):
331
                    Design Reference Mission, contains the results of one complete
332
                    observation (detection and characterization)
333
                sInd (integer):
334
                    Index of next target star. Defaults to None.
335
                intTime (astropy Quantity):
336
                    Selected star integration time for detection in units of day.
337
                    Defaults to None.
338
                waitTime (astropy Quantity):
339
                    a strategically advantageous amount of time to wait in the case of
340
                    an occulter for slew times
341
                det_mode (dict):
342
                    Selected detection mode
343

344
        """
345

346
        OS = self.OpticalSystem
1✔
347
        TL = self.TargetList
1✔
348
        Obs = self.Observatory
1✔
349
        TK = self.TimeKeeping
1✔
350

351
        # create DRM
352
        DRM = {}
1✔
353

354
        # selecting appropriate koMap
355
        koMap = self.koMaps[modes[0]["syst"]["name"]]
1✔
356

357
        # allocate settling time + overhead time
358
        tmpCurrentTimeAbs = (
1✔
359
            TK.currentTimeAbs.copy() + Obs.settlingTime + modes[0]["syst"]["ohTime"]
360
        )
361
        tmpCurrentTimeNorm = (
1✔
362
            TK.currentTimeNorm.copy() + Obs.settlingTime + modes[0]["syst"]["ohTime"]
363
        )
364

365
        # look for available targets
366
        # 1. initialize arrays
367
        slewTimes = np.zeros(TL.nStars) * u.d
1✔
368
        # fZs = np.zeros(TL.nStars) / u.arcsec**2
369
        dV = np.zeros(TL.nStars) * u.m / u.s
1✔
370
        intTimes = np.zeros(TL.nStars) * u.d
1✔
371
        obsTimes = np.zeros([2, TL.nStars]) * u.d
1✔
372
        sInds = np.arange(TL.nStars)
1✔
373

374
        # 2. find spacecraft orbital START positions (if occulter, positions
375
        # differ for each star) and filter out unavailable targets
376
        sd = None
1✔
377
        if OS.haveOcculter:
1✔
378
            sd = Obs.star_angularSep(TL, old_sInd, sInds, tmpCurrentTimeAbs)
×
379
            obsTimes = Obs.calculate_observableTimes(
×
380
                TL, sInds, tmpCurrentTimeAbs, self.koMaps, self.koTimes, modes[0]
381
            )
382
            slewTimes = Obs.calculate_slewTimes(
×
383
                TL, old_sInd, sInds, sd, obsTimes, tmpCurrentTimeAbs
384
            )
385

386
        # 2.1 filter out totTimes > integration cutoff
387
        if len(sInds.tolist()) > 0:
1✔
388
            sInds = np.intersect1d(self.intTimeFilterInds, sInds)
1✔
389

390
        # start times, including slew times
391
        startTimes = tmpCurrentTimeAbs.copy() + slewTimes
1✔
392
        startTimesNorm = tmpCurrentTimeNorm.copy() + slewTimes
1✔
393

394
        # 2.5 Filter stars not observable at startTimes
395
        try:
1✔
396
            # find indice where koTime is startTime[0]
397
            koTimeInd = np.where(
1✔
398
                np.round(startTimes[0].value) - self.koTimes.value == 0
399
            )[0][0]
400
            # filters inds by koMap #verified against v1.35
401
            sInds = sInds[
1✔
402
                np.where(np.transpose(koMap)[koTimeInd].astype(bool)[sInds])[0]
403
            ]
404
        except:  # noqa: E722 If there are no target stars to observe
×
405
            sInds = np.asarray([], dtype=int)
×
406

407
        # 3. filter out all previously (more-)visited targets, unless in
408
        if len(sInds.tolist()) > 0:
1✔
409
            sInds = self.revisitFilter(sInds, tmpCurrentTimeNorm)
1✔
410

411
        # 4.1 calculate integration times for ALL preselected targets
412
        (
1✔
413
            maxIntTimeOBendTime,
414
            maxIntTimeExoplanetObsTime,
415
            maxIntTimeMissionLife,
416
        ) = TK.get_ObsDetectionMaxIntTime(Obs, modes[0])
417
        maxIntTime = min(
1✔
418
            maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife
419
        )  # Maximum intTime allowed
420

421
        if len(sInds.tolist()) > 0:
1✔
422
            intTimes[sInds] = self.calc_targ_intTime(sInds, startTimes[sInds], modes[0])
1✔
423
            sInds = sInds[
1✔
424
                np.where(intTimes[sInds] <= maxIntTime)
425
            ]  # Filters targets exceeding end of OB
426
            endTimes = startTimes + intTimes
1✔
427

428
            if maxIntTime.value <= 0:
1✔
429
                sInds = np.asarray([], dtype=int)
×
430

431
        # 5.1 TODO Add filter to filter out stars entering and exiting keepout between
432
        # startTimes and endTimes
433

434
        # 5.2 find spacecraft orbital END positions (for each candidate target),
435
        # and filter out unavailable targets
436
        if len(sInds.tolist()) > 0 and Obs.checkKeepoutEnd:
1✔
437
            # endTimes may exist past koTimes so we have an exception to hand this case
438
            try:
1✔
439
                koTimeInd = np.where(
1✔
440
                    np.round(endTimes[0].value) - self.koTimes.value == 0
441
                )[0][
442
                    0
443
                ]  # koTimeInd[0][0]  # find indice where koTime is endTime[0]
444
                sInds = sInds[
1✔
445
                    np.where(np.transpose(koMap)[koTimeInd].astype(bool)[sInds])[0]
446
                ]  # filters inds by koMap #verified against v1.35
447
            except:  # noqa: E722
×
448
                sInds = np.asarray([], dtype=int)
×
449

450
        # 6. choose best target from remaining
451
        if len(sInds.tolist()) > 0:
1✔
452
            # choose sInd of next target
453
            sInd, waitTime = self.choose_next_target(
1✔
454
                old_sInd, sInds, slewTimes, intTimes[sInds]
455
            )
456
            # Should Choose Next Target decide there are no stars it wishes to
457
            # observe at this time.
458
            if (sInd is None) and (waitTime is not None):
1✔
459
                self.vprint(
×
460
                    (
461
                        "There are no stars Choose Next Target would like to Observe. "
462
                        "Waiting {}"
463
                    ).format(waitTime)
464
                )
465
                return DRM, None, None, waitTime, None
×
466
            elif (sInd is None) and (waitTime is None):
1✔
467
                self.vprint(
×
468
                    (
469
                        "There are no stars Choose Next Target would like to Observe "
470
                        "and waitTime is None"
471
                    )
472
                )
473
                return DRM, None, None, waitTime, None
×
474
            # store selected star integration time
475
            det_mode = copy.deepcopy(modes[0])
1✔
476
            if TL.int_WA[sInd] > modes[1]["IWA"] and TL.int_WA[sInd] < modes[1]["OWA"]:
1✔
477
                det_mode["BW"] = det_mode["BW"] + modes[1]["BW"]
1✔
478
                det_mode["OWA"] = modes[1]["OWA"]
1✔
479
                det_mode["inst"]["sread"] = (
1✔
480
                    det_mode["inst"]["sread"] + modes[1]["inst"]["sread"]
481
                )
482
                det_mode["inst"]["idark"] = (
1✔
483
                    det_mode["inst"]["idark"] + modes[1]["inst"]["idark"]
484
                )
485
                det_mode["inst"]["CIC"] = (
1✔
486
                    det_mode["inst"]["CIC"] + modes[1]["inst"]["CIC"]
487
                )
488
                det_mode["syst"]["optics"] = np.mean(
1✔
489
                    (det_mode["syst"]["optics"], modes[1]["syst"]["optics"])
490
                )
491
                det_mode["instName"] = "combined"
1✔
492
                intTime = self.calc_targ_intTime(
1✔
493
                    np.array([sInd]), startTimes[sInd], det_mode
494
                )[0]
495
            else:
496
                intTime = intTimes[sInd]
×
497

498
        # if no observable target, advanceTime to next Observable Target
499
        else:
500
            self.vprint(
×
501
                "No Observable Targets at currentTimeNorm= "
502
                + str(TK.currentTimeNorm.copy())
503
            )
504
            return DRM, None, None, None, None
×
505

506
        # update visited list for selected star
507
        self.starVisits[sInd] += 1
1✔
508
        # store normalized start time for future completeness update
509
        self.lastObsTimes[sInd] = startTimesNorm[sInd]
1✔
510

511
        # populate DRM with occulter related values
512
        if OS.haveOcculter:
1✔
513
            DRM = Obs.log_occulterResults(
×
514
                DRM, slewTimes[sInd], sInd, sd[sInd], dV[sInd]
515
            )
516
            return DRM, sInd, intTime, waitTime, det_mode
×
517

518
        return DRM, sInd, intTime, waitTime, det_mode
1✔
519

520
    def choose_next_target(self, old_sInd, sInds, slewTimes, intTimes):
1✔
521
        """Choose next target based on truncated depth first search
522
        of linear cost function.
523

524
        Args:
525
            old_sInd (integer):
526
                Index of the previous target star
527
            sInds (integer array):
528
                Indices of available targets
529
            slewTimes (astropy quantity array):
530
                slew times to all stars (must be indexed by sInds)
531
            intTimes (astropy Quantity array):
532
                Integration times for detection in units of day
533

534
        Returns:
535
            sInd (integer):
536
                Index of next target star
537

538
        """
539

540
        Comp = self.Completeness
1✔
541
        TL = self.TargetList
1✔
542
        TK = self.TimeKeeping
1✔
543
        OS = self.OpticalSystem
1✔
544
        Obs = self.Observatory
1✔
545
        allModes = OS.observingModes
1✔
546

547
        # cast sInds to array
548
        sInds = np.array(sInds, ndmin=1, copy=copy_if_needed)
1✔
549
        known_sInds = np.intersect1d(sInds, self.known_rocky)
1✔
550

551
        if OS.haveOcculter:
1✔
552
            # current star has to be in the adjmat
553
            if (old_sInd is not None) and (old_sInd not in sInds):
1✔
554
                sInds = np.append(sInds, old_sInd)
1✔
555

556
            # calculate dt since previous observation
557
            dt = TK.currentTimeNorm.copy() + slewTimes[sInds] - self.lastObsTimes[sInds]
1✔
558
            # get dynamic completeness values
559
            comps = Comp.completeness_update(TL, sInds, self.starVisits[sInds], dt)
1✔
560
            for idx, sInd in enumerate(sInds):
1✔
561
                if sInd in known_sInds:
1✔
562
                    comps[idx] = 1.0
×
563

564
            # if first target, or if only 1 available target,
565
            # choose highest available completeness
566
            nStars = len(sInds)
1✔
567
            if (old_sInd is None) or (nStars == 1):
1✔
568
                sInd = np.random.choice(sInds[comps == max(comps)])
1✔
569
                return sInd, slewTimes[sInd]
1✔
570

571
            # define adjacency matrix
572
            A = np.zeros((nStars, nStars))
1✔
573

574
            # only consider slew distance when there's an occulter
575
            if OS.haveOcculter:
1✔
576
                r_ts = TL.starprop(sInds, TK.currentTimeAbs)
1✔
577
                u_ts = (
1✔
578
                    r_ts.to("AU").value.T / np.linalg.norm(r_ts.to("AU").value, axis=1)
579
                ).T
580
                angdists = np.arccos(np.clip(np.dot(u_ts, u_ts.T), -1, 1))
1✔
581
                A[np.ones((nStars), dtype=bool)] = angdists
1✔
582
                A = self.coeffs[0] * (A) / np.pi
1✔
583

584
            # add factor due to completeness
585
            A = A + self.coeffs[1] * (1 - comps)
1✔
586

587
            # add factor for unvisited ramp for known stars
588
            if np.any(known_sInds):
1✔
589
                # add factor for least visited known stars
590
                f_uv = np.zeros(nStars)
×
591
                u1 = np.in1d(sInds, known_sInds)
×
592
                u2 = self.starVisits[sInds] == min(self.starVisits[known_sInds])
×
593
                unvisited = np.logical_and(u1, u2)
×
594
                f_uv[unvisited] = (
×
595
                    float(TK.currentTimeNorm.copy() / TK.missionLife.copy()) ** 2
596
                )
597
                A = A - self.coeffs[2] * f_uv
×
598

599
                # add factor for unvisited known stars
600
                no_visits = np.zeros(nStars)
×
601
                u2 = self.starVisits[sInds] == 0
×
602
                unvisited = np.logical_and(u1, u2)
×
603
                no_visits[unvisited] = 1.0
×
604
                A = A - self.coeffs[3] * no_visits
×
605

606
            # add factor due to unvisited ramp
607
            f_uv = np.zeros(nStars)
1✔
608
            unvisited = self.starVisits[sInds] == 0
1✔
609
            f_uv[unvisited] = (
1✔
610
                float(TK.currentTimeNorm.copy() / TK.missionLife.copy()) ** 2
611
            )
612
            A = A - self.coeffs[4] * f_uv
1✔
613

614
            # add factor due to revisited ramp
615
            # f2_uv = np.where(self.starVisits[sInds] > 0, 1, 0) *\
616
            #         (1 - (np.in1d(sInds, self.starRevisit[:,0],invert=True)))
617
            if self.starRevisit.size != 0:
1✔
618
                f2_uv = 1 - (np.in1d(sInds, self.starRevisit[:, 0]))
1✔
619
                A = A + self.coeffs[5] * f2_uv
1✔
620

621
            # kill diagonal
622
            A = A + np.diag(np.ones(nStars) * np.inf)
1✔
623

624
            # take two traversal steps
625
            step1 = np.tile(A[sInds == old_sInd, :], (nStars, 1)).flatten("F")
1✔
626
            step2 = A[np.array(np.ones((nStars, nStars)), dtype=bool)]
1✔
627
            tmp = np.nanargmin(step1 + step2)
1✔
628
            sInd = sInds[int(np.floor(tmp / float(nStars)))]
1✔
629

630
        else:
631
            nStars = len(sInds)
1✔
632

633
            # 1/ Choose next telescope target
634
            comps = Comp.completeness_update(
1✔
635
                TL, sInds, self.starVisits[sInds], TK.currentTimeNorm.copy()
636
            )
637

638
            # add weight for star revisits
639
            ind_rev = []
1✔
640
            if self.starRevisit.size != 0:
1✔
641
                dt_rev = self.starRevisit[:, 1] * u.day - TK.currentTimeNorm.copy()
1✔
642
                ind_rev = [
1✔
643
                    int(x) for x in self.starRevisit[dt_rev < 0, 0] if x in sInds
644
                ]
645

646
            f2_uv = np.where(
1✔
647
                (self.starVisits[sInds] > 0)
648
                & (self.starVisits[sInds] < self.nVisitsMax),
649
                self.starVisits[sInds],
650
                0,
651
            ) * (1 - (np.in1d(sInds, ind_rev, invert=True)))
652

653
            weights = (
1✔
654
                comps + self.revisit_weight * f2_uv / float(self.nVisitsMax)
655
            ) / intTimes
656

657
            sInd = np.random.choice(sInds[weights == max(weights)])
1✔
658

659
        waitTime = slewTimes[sInd]
1✔
660
        # Check if exoplanetObsTime would be exceeded
661
        mode = list(filter(lambda mode: mode["detectionMode"], allModes))[0]
1✔
662
        (
1✔
663
            maxIntTimeOBendTime,
664
            maxIntTimeExoplanetObsTime,
665
            maxIntTimeMissionLife,
666
        ) = TK.get_ObsDetectionMaxIntTime(Obs, mode)
667
        maxIntTime = min(
1✔
668
            maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife
669
        )  # Maximum intTime allowed
670
        intTimes2 = self.calc_targ_intTime(
1✔
671
            np.array([sInd]), TK.currentTimeAbs.copy(), mode
672
        )
673
        if (
1✔
674
            intTimes2 > maxIntTime
675
        ):  # check if max allowed integration time would be exceeded
676
            self.vprint("max allowed integration time would be exceeded")
×
677
            sInd = None
×
678
            waitTime = 1.0 * u.d
×
679

680
        return sInd, waitTime
1✔
681

682
    def observation_characterization(self, sInd, modes):
1✔
683
        """Finds if characterizations are possible and relevant information
684

685
        Args:
686
            sInd (integer):
687
                Integer index of the star of interest
688
            modes (dict):
689
                Selected observing modes for characterization
690

691
        Returns:
692
            characterized (integer list):
693
                Characterization status for each planet orbiting the observed
694
                target star including False Alarm if any, where 1 is full spectrum,
695
                -1 partial spectrum, and 0 not characterized
696
            fZ (astropy Quantity):
697
                Surface brightness of local zodiacal light in units of 1/arcsec2
698
            JEZ (astropy Quantity):
699
                Intensity of exo-zodiacal light in units of ph/s/m2/arcsec2
700
            systemParams (dict):
701
                Dictionary of time-dependant planet properties averaged over the
702
                duration of the integration
703
            SNR (float ndarray):
704
                Characterization signal-to-noise ratio of the observable planets.
705
                Defaults to None.
706
            intTime (astropy Quantity):
707
                Selected star characterization time in units of day. Defaults to None.
708

709
        """
710

711
        OS = self.OpticalSystem
1✔
712
        ZL = self.ZodiacalLight
1✔
713
        TL = self.TargetList
1✔
714
        SU = self.SimulatedUniverse
1✔
715
        Obs = self.Observatory
1✔
716
        TK = self.TimeKeeping
1✔
717

718
        nmodes = len(modes)
1✔
719

720
        # selecting appropriate koMap
721
        koMap = self.koMaps[modes[0]["syst"]["name"]]
1✔
722

723
        # find indices of planets around the target
724
        pInds = np.where(SU.plan2star == sInd)[0]
1✔
725

726
        # get the detected status, and check if there was a FA
727
        det = self.lastDetected[sInd, 0]
1✔
728

729
        pIndsDet = []
1✔
730
        tochars = []
1✔
731
        intTimes_all = []
1✔
732
        FA = len(det) == len(pInds) + 1
1✔
733
        is_earthlike = []
1✔
734

735
        # initialize outputs, and check if there's anything (planet or FA) to
736
        # characterize
737
        characterizeds = np.zeros((det.size, len(modes)), dtype=int)
1✔
738
        fZ = 0.0 / u.arcsec**2 * np.ones(nmodes)
1✔
739
        JEZ = 0.0 * u.ph / u.s / u.m**2 / u.arcsec**2
1✔
740
        systemParams = SU.dump_system_params(
1✔
741
            sInd
742
        )  # write current system params by default
743
        SNR = np.zeros((len(det), len(modes)))
1✔
744
        intTime = None
1✔
745
        if det.size == 0:  # nothing to characterize
1✔
746
            return characterizeds, fZ, JEZ, systemParams, SNR, intTime
1✔
747

748
        # look for last detected planets that have not been fully characterized
749
        for m_i, mode in enumerate(modes):
1✔
750
            if FA is True:
1✔
751
                pIndsDet.append(np.append(pInds, -1)[det])
×
752
            else:
753
                pIndsDet.append(pInds[det])
1✔
754

755
            # look for last detected planets that have not been fully characterized
756
            if not (FA):  # only true planets, no FA
1✔
757
                tochar = self.fullSpectra[m_i][pIndsDet[m_i]] == 0
1✔
758
            else:  # mix of planets and a FA
759
                truePlans = pIndsDet[m_i][:-1]
×
760
                tochar = np.append((self.fullSpectra[m_i][truePlans] == 0), True)
×
761

762
            # 1/ find spacecraft orbital START position including overhead time,
763
            # and check keepout angle
764
            if np.any(tochar):
1✔
765
                # start times
766
                startTime = (
1✔
767
                    TK.currentTimeAbs.copy() + mode["syst"]["ohTime"] + Obs.settlingTime
768
                )
769
                startTimeNorm = (
1✔
770
                    TK.currentTimeNorm.copy()
771
                    + mode["syst"]["ohTime"]
772
                    + Obs.settlingTime
773
                )
774
                # planets to characterize
775
                koTimeInd = np.where(
1✔
776
                    np.round(startTime.value) - self.koTimes.value == 0
777
                )[0][
778
                    0
779
                ]  # find indice where koTime is startTime[0]
780
                # wherever koMap is 1, the target is observable
781
                tochar[tochar] = koMap[sInd][koTimeInd]
1✔
782

783
            # 2/ if any planet to characterize, find the characterization times
784
            # at the detected JEZ, dMag, and WA
785
            is_earthlike.append(
1✔
786
                np.logical_and(
787
                    np.array([(p in self.earth_candidates) for p in pIndsDet[m_i]]),
788
                    tochar,
789
                )
790
            )
791
            if np.any(tochar):
1✔
792
                fZ[m_i] = ZL.fZ(Obs, TL, sInd, startTime, mode)
1✔
793
                JEZ = self.lastDetected[sInd, 1][det][tochar]
1✔
794
                dMag = self.lastDetected[sInd, 2][det][tochar]
1✔
795
                WA = self.lastDetected[sInd, 3][det][tochar] * u.arcsec
1✔
796
                WA[is_earthlike[m_i][tochar]] = SU.WA[pIndsDet[m_i][is_earthlike[m_i]]]
1✔
797
                dMag[is_earthlike[m_i][tochar]] = SU.dMag[
1✔
798
                    pIndsDet[m_i][is_earthlike[m_i]]
799
                ]
800

801
                intTimes = np.zeros(len(tochar)) * u.day
1✔
802
                intTimes[tochar] = OS.calc_intTime(
1✔
803
                    TL, sInd, fZ[m_i], JEZ, dMag, WA, mode
804
                )
805
                intTimes[~np.isfinite(intTimes)] = 0 * u.d
1✔
806

807
                # add a predetermined margin to the integration times
808
                intTimes = intTimes * (1 + self.charMargin)
1✔
809
                # apply time multiplier
810
                totTimes = intTimes * (mode["timeMultiplier"])
1✔
811
                # end times
812
                endTimes = startTime + totTimes
1✔
813
                endTimesNorm = startTimeNorm + totTimes
1✔
814
                # planets to characterize
815
                tochar = (
1✔
816
                    (totTimes > 0)
817
                    & (totTimes <= OS.intCutoff)
818
                    & (endTimesNorm <= TK.OBendTimes[TK.OBnumber])
819
                )
820

821
                # 3/ is target still observable at the end of any char time?
822
                if np.any(tochar) and Obs.checkKeepoutEnd:
1✔
823
                    koTimeInds = np.zeros(len(endTimes.value[tochar]), dtype=int)
1✔
824
                    # find index in koMap where each endTime is closest to koTimes
825
                    for t, endTime in enumerate(endTimes.value[tochar]):
1✔
826
                        if endTime > self.koTimes.value[-1]:
1✔
827
                            # case where endTime exceeds largest koTimes element
828
                            endTimeInBounds = np.where(
×
829
                                np.floor(endTime) - self.koTimes.value == 0
830
                            )[0]
831
                            koTimeInds[t] = (
×
832
                                endTimeInBounds[0] if endTimeInBounds.size != 0 else -1
833
                            )
834
                        else:
835
                            koTimeInds[t] = np.where(
1✔
836
                                np.round(endTime) - self.koTimes.value == 0
837
                            )[0][
838
                                0
839
                            ]  # find indice where koTime is endTimes[0]
840
                    tochar[tochar] = [
1✔
841
                        koMap[sInd][koT] if koT >= 0 else 0 for koT in koTimeInds
842
                    ]
843

844
                tochars.append(tochar)
1✔
845
                intTimes_all.append(intTimes)
1✔
846
            else:
UNCOV
847
                tochar[tochar] = False
×
UNCOV
848
                tochars.append(tochar)
×
UNCOV
849
                intTimes_all.append(np.zeros(len(tochar)) * u.day)
×
850

851
        # 4/ if yes, allocate the overhead time, and perform the characterization
852
        # for the maximum char time
853
        if np.any(tochars):
1✔
854
            pIndsChar = []
1✔
855
            for m_i, mode in enumerate(modes):
1✔
856
                if len(pIndsDet[m_i]) > 0 and np.any(tochars[m_i]):
1✔
857
                    if (
1✔
858
                        intTime is None
859
                        or np.max(intTimes_all[m_i][tochars[m_i]]) > intTime
860
                    ):
861
                        # Allocate Time
862
                        if np.any(np.logical_and(is_earthlike[m_i], tochars[m_i])):
1✔
863
                            intTime = np.max(
×
864
                                intTimes_all[m_i][
865
                                    np.logical_and(is_earthlike[m_i], tochars[m_i])
866
                                ]
867
                            )
868
                        else:
869
                            intTime = np.max(intTimes_all[m_i][tochars[m_i]])
1✔
870
                    pIndsChar.append(pIndsDet[m_i][tochars[m_i]])
1✔
871
                    log_char = "   - Charact. planet inds %s (%s/%s detected)" % (
1✔
872
                        pIndsChar[m_i],
873
                        len(pIndsChar[m_i]),
874
                        len(pIndsDet[m_i]),
875
                    )
876
                    self.logger.info(log_char)
1✔
877
                    self.vprint(log_char)
1✔
878
                else:
879
                    pIndsChar.append([])
×
880

881
            if intTime is not None:
1✔
882
                extraTime = intTime * (
1✔
883
                    modes[0]["timeMultiplier"] - 1.0
884
                )  # calculates extraTime
885
                success = TK.allocate_time(
1✔
886
                    intTime + extraTime + modes[0]["syst"]["ohTime"] + Obs.settlingTime,
887
                    True,
888
                )  # allocates time
889
                if not (success):  # Time was not successfully allocated
1✔
UNCOV
890
                    return (characterizeds, fZ, JEZ, systemParams, SNR, None)
×
891

892
            # SNR CALCULATION:
893
            # first, calculate SNR for observable planets (without false alarm)
894
            if len(pIndsChar[0]) > 0:
1✔
895
                planinds = pIndsChar[0][:-1] if pIndsChar[0][-1] == -1 else pIndsChar[0]
1✔
896
            else:
897
                planinds = []
×
898
            if len(pIndsChar[1]) > 0:
1✔
899
                planinds2 = (
1✔
900
                    pIndsChar[1][:-1] if pIndsChar[1][-1] == -1 else pIndsChar[1]
901
                )
902
            else:
903
                planinds2 = []
×
904
            SNRplans = np.zeros((len(planinds)))
1✔
905
            SNRplans2 = np.zeros((len(planinds2)))
1✔
906
            if len(planinds) > 0 and len(planinds2) > 0:
1✔
907
                # initialize arrays for SNR integration
908
                fZs = np.zeros((self.ntFlux, nmodes)) / u.arcsec**2
1✔
909
                systemParamss = np.empty(self.ntFlux, dtype="object")
1✔
910
                Ss = np.zeros((self.ntFlux, len(planinds)))
1✔
911
                Ns = np.zeros((self.ntFlux, len(planinds)))
1✔
912
                Ss2 = np.zeros((self.ntFlux, len(planinds2)))
1✔
913
                Ns2 = np.zeros((self.ntFlux, len(planinds2)))
1✔
914
                # integrate the signal (planet flux) and noise
915
                dt = intTime / self.ntFlux
1✔
916
                timePlus = (
1✔
917
                    Obs.settlingTime.copy() + modes[0]["syst"]["ohTime"].copy()
918
                )  # accounts for the time since the current time
919
                for i in range(self.ntFlux):
1✔
920
                    # allocate first half of dt
921
                    timePlus += dt / 2.0
1✔
922
                    fZs[i, 0] = ZL.fZ(
1✔
923
                        Obs, TL, sInd, TK.currentTimeAbs.copy() + timePlus, modes[0]
924
                    )[0]
925
                    fZs[i, 1] = ZL.fZ(
1✔
926
                        Obs, TL, sInd, TK.currentTimeAbs.copy() + timePlus, modes[1]
927
                    )[0]
928
                    SU.propag_system(
1✔
929
                        sInd,
930
                        TK.currentTimeNorm.copy() + timePlus - self.propagTimes[sInd],
931
                    )
932
                    self.propagTimes[sInd] = TK.currentTimeNorm.copy() + timePlus
1✔
933
                    systemParamss[i] = SU.dump_system_params(sInd)
1✔
934
                    Ss[i, :], Ns[i, :] = self.calc_signal_noise(
1✔
935
                        sInd, planinds, dt, modes[0], fZ=fZs[i, 0]
936
                    )
937
                    Ss2[i, :], Ns2[i, :] = self.calc_signal_noise(
1✔
938
                        sInd, planinds2, dt, modes[1], fZ=fZs[i, 1]
939
                    )
940

941
                    # allocate second half of dt
942
                    timePlus += dt / 2.0
1✔
943

944
                # average output parameters
945
                systemParams = {
1✔
946
                    key: sum([systemParamss[x][key] for x in range(self.ntFlux)])
947
                    / float(self.ntFlux)
948
                    for key in sorted(systemParamss[0])
949
                }
950
                for m_i, mode in enumerate(modes):
1✔
951
                    fZ[m_i] = np.mean(fZs[:, m_i])
1✔
952
                # calculate planets SNR
953
                S = Ss.sum(0)
1✔
954
                N = Ns.sum(0)
1✔
955
                S2 = Ss2.sum(0)
1✔
956
                N2 = Ns2.sum(0)
1✔
957
                SNRplans[N > 0] = S[N > 0] / N[N > 0]
1✔
958
                SNRplans2[N2 > 0] = S2[N2 > 0] / N2[N2 > 0]
1✔
959
                # allocate extra time for timeMultiplier
960
                extraTime = intTime * (mode["timeMultiplier"] - 1)
1✔
961
                TK.allocate_time(extraTime)
1✔
962

963
            # if only a FA, just save zodiacal brightness in the middle of the
964
            # integration
965
            else:
966
                totTime = intTime * (mode["timeMultiplier"])
×
967
                TK.allocate_time(totTime / 2.0)
×
968
                for m_i, mode in enumerate(modes):
×
969
                    fZ[m_i] = ZL.fZ(Obs, TL, sInd, TK.currentTimeAbs.copy(), mode)[0]
×
970
                TK.allocate_time(totTime / 2.0)
×
971

972
            # calculate the false alarm SNR (if any)
973
            for m_i, mode in enumerate(modes):
1✔
974
                if len(pIndsChar[m_i]) > 0:
1✔
975
                    SNRfa = []
1✔
976
                    if pIndsChar[m_i][-1] == -1:
1✔
977
                        JEZ = self.lastDetected[sInd, 1][-1]
×
978
                        dMag = self.lastDetected[sInd, 2][-1]
×
979
                        WA = self.lastDetected[sInd, 3][-1] * u.arcsec
×
980
                        C_p, C_b, C_sp = OS.Cp_Cb_Csp(
×
981
                            TL, sInd, fZ[m_i], JEZ, dMag, WA, mode
982
                        )
983
                        S = (C_p * intTime).decompose().value
×
984
                        N = np.sqrt(
×
985
                            (C_b * intTime + (C_sp * intTime) ** 2).decompose().value
986
                        )
987
                        SNRfa.append([S / N if N > 0 else 0.0])
×
988

989
                    # save all SNRs (planets and FA) to one array
990
                    SNRinds = np.where(det)[0][tochars[m_i]]
1✔
991
                    if m_i == 0:
1✔
992
                        SNR[SNRinds, 0] = np.append(SNRplans[:], SNRfa)
1✔
993
                    else:
994
                        SNR[SNRinds, 1] = np.append(SNRplans2[:], SNRfa)
1✔
995

996
                    # now, store characterization status: 1 for full spectrum,
997
                    # -1 for partial spectrum, 0 for not characterized
998
                    char = SNR[:, m_i] >= mode["SNR"]
1✔
999
                    # initialize with full spectra
1000
                    characterized = char.astype(int)
1✔
1001
                    WAchar = self.lastDetected[sInd, 3][char] * u.arcsec
1✔
1002
                    # find the current WAs of characterized planets
1003
                    WAs = systemParams["WA"]
1✔
1004
                    if FA:
1✔
1005
                        WAs = np.append(WAs, self.lastDetected[sInd, 3][-1] * u.arcsec)
×
1006
                    # check for partial spectra
1007
                    IWA_max = mode["IWA"] * (1 + mode["BW"] / 2.0)
1✔
1008
                    OWA_min = mode["OWA"] * (1 - mode["BW"] / 2.0)
1✔
1009
                    char[char] = (WAchar < IWA_max) | (WAchar > OWA_min)
1✔
1010
                    characterized[char] = -1
1✔
1011
                    # encode results in spectra lists (only for planets, not FA)
1012
                    charplans = characterized[:-1] if FA else characterized
1✔
1013
                    self.fullSpectra[m_i][pInds[charplans == 1]] += 1
1✔
1014
                    self.partialSpectra[m_i][pInds[charplans == -1]] += 1
1✔
1015
                    characterizeds[:, m_i] = characterized.astype(int)
1✔
1016

1017
        return characterizeds, fZ, JEZ, systemParams, SNR, intTime
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