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

dsavransky / EXOSIMS / 14561753842

20 Apr 2025 05:25PM UTC coverage: 65.777% (+0.07%) from 65.708%
14561753842

push

github

web-flow
Merge pull request #408 from dsavransky/performance_optimization

Performance optimizations.

604 of 731 new or added lines in 15 files covered. (82.63%)

22 existing lines in 9 files now uncovered.

9681 of 14718 relevant lines covered (65.78%)

0.66 hits per line

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

94.06
/EXOSIMS/Prototypes/TimeKeeping.py
1
from EXOSIMS.util.vprint import vprint
1✔
2
from EXOSIMS.util.get_dirs import get_cache_dir
1✔
3
import numpy as np
1✔
4
import astropy.units as u
1✔
5
from astropy.time import Time
1✔
6
import os
1✔
7
import csv
1✔
8

9

10
class TimeKeeping(object):
1✔
11
    """:ref:`TimeKeeping` Prototype
12

13
    This class keeps track of the current mission elapsed time
14
    for exoplanet mission simulation.  It is initialized with a
15
    mission duration, and throughout the simulation, it allocates
16
    temporal intervals for observations.  Eventually, all available
17
    time has been allocated, and the mission is over.
18
    Time is allocated in contiguous windows of size "duration".  If a
19
    requested interval does not fit in the current window, we move to
20
    the next one.
21

22
    Args:
23
        missionStart (float):
24
            Mission start date in MJD. Defaults to 60634 (11-20-2024)
25
        missionLife (float):
26
            Mission duration (in years). Defaults to 0.1
27
        missionPortion (float):
28
            Fraction of mission devoted to exoplanet imaging science.
29
            Must be between 0 and 1. Defaults to 1
30
        OBduration (float):
31
            Observing block length (in days). If infinite, do not
32
            define observing blocks. Defaults to np.inf
33
        missionSchedule (str, optional):
34
            Full path to mission schedule file stored on disk.
35
            Defaults None.
36
        cachedir (str, optional):
37
            Full path to cachedir.
38
            If None (default) use default (see :ref:`EXOSIMSCACHE`)
39
        **specs:
40
            :ref:`sec:inputspec`
41

42
    Attributes:
43
        _outspec (dict):
44
            :ref:`sec:outspec`
45
        cachedir (str):
46
            Path to the EXOSIMS cache directory (see :ref:`EXOSIMSCACHE`)
47
        currentTimeAbs (astropy.time.core.Time):
48
            Current absolute mission time in MJD
49
        currentTimeNorm (astropy.units.quantity.Quantity):
50
            Current mission time minus mission start time.
51
        exoplanetObsTime (astropy.units.quantity.Quantity):
52
            How much time has been used so far on exoplanet science.
53
        missionFinishAbs (astropy.time.core.Time):
54
            Mission end time in MJD
55
        missionLife (astropy.units.quantity.Quantity):
56
            Total mission duration
57
        missionPortion (float):
58
            Fraction of mission devoted to exoplanet science
59
        missionStart (astropy.time.core.Time):
60
            Start time of mission in MJD
61
        OBduration (astropy.units.quantity.Quantity):
62
            Observing block length
63
        OBendTimes (astropy.units.quantity.Quantity):
64
            Array containing the normalized end times of each observing block
65
            throughout the mission
66
        OBnumber (int):
67
            Index of the current observing block (OB). Each
68
            observing block has a duration, a start time, an end time, and can
69
            host one or multiple observations
70
        OBstartTimes (astropy.units.quantity.Quantity):
71
            Array containing the normalized start times of each observing block
72
            throughout the mission
73
    """
74

75
    _modtype = "TimeKeeping"
1✔
76

77
    def __init__(
1✔
78
        self,
79
        missionStart=60634,
80
        missionLife=0.1,
81
        missionPortion=1,
82
        OBduration=np.inf,
83
        missionSchedule=None,
84
        cachedir=None,
85
        **specs,
86
    ):
87
        # start the outspec
88
        self._outspec = {}
1✔
89

90
        # get cache directory
91
        self.cachedir = get_cache_dir(cachedir)
1✔
92
        self._outspec["cachedir"] = self.cachedir
1✔
93
        specs["cachedir"] = self.cachedir
1✔
94

95
        # load the vprint function (same line in all prototype module constructors)
96
        self.vprint = vprint(specs.get("verbose", True))
1✔
97

98
        # illegal value checks
99
        assert missionLife >= 0, "Need missionLife >= 0, got %f" % missionLife
1✔
100
        # arithmetic on missionPortion fails if it is outside the legal range
101
        assert missionPortion > 0 and missionPortion <= 1, (
1✔
102
            "Require missionPortion in the interval [0,1], got %f" % missionPortion
103
        )
104
        # OBduration must be positive nonzero
105
        assert OBduration * u.d > 0 * u.d, (
1✔
106
            "Required OBduration positive nonzero, got %f" % OBduration
107
        )
108

109
        # set up state variables
110
        # tai scale specified because the default, utc, requires accounting for leap
111
        # seconds, causing warnings from astropy.time when time-deltas are added
112
        # Absolute mission start time:
113
        self.missionStart = Time(float(missionStart), format="mjd", scale="tai")
1✔
114
        # Fraction of missionLife allowed for exoplanet science
115
        self.missionPortion = float(missionPortion)
1✔
116
        # Total mission duration
117
        self.missionLife = float(missionLife) * u.year
1✔
118

119
        self.missionLife_d = self.missionLife.to_value(u.d)
1✔
120

121
        self.allocated_time_d = self.missionLife_d * self.missionPortion
1✔
122

123
        # populate outspec
124
        for att in self.__dict__:
1✔
125
            if att not in ["vprint", "_outspec"]:
1✔
126
                dat = self.__dict__[att]
1✔
127
                self._outspec[att] = (
1✔
128
                    dat.value if isinstance(dat, (u.Quantity, Time)) else dat
129
                )
130

131
        # Absolute mission end time
132
        self.missionFinishAbs = self.missionStart + self.missionLife.to("day")
1✔
133

134
        # initialize values updated by various class methods
135
        # the current mission elapsed time (0 at mission start)
136
        self.currentTimeNorm = 0.0 * u.day
1✔
137
        # current absolute mission time (equals missionStart at mission start)
138
        self.currentTimeAbs = self.missionStart
1✔
139

140
        # initialize observing block times arrays.
141
        # An Observing Block is a segment of time over which observations may take place
142
        self.init_OB(str(missionSchedule), OBduration * u.d)
1✔
143
        self._outspec["missionSchedule"] = missionSchedule
1✔
144
        self._outspec["OBduration"] = OBduration
1✔
145

146
        # initialize time spend using instrument
147
        self.exoplanetObsTime = 0 * u.day
1✔
148

149
    def __str__(self):
1✔
150
        r"""String representation of the TimeKeeping object.
151

152
        When the command 'print' is used on the TimeKeeping object, this
153
        method prints the values contained in the object."""
154

155
        for att in self.__dict__:
1✔
156
            print("%s: %r" % (att, getattr(self, att)))
1✔
157

158
        return (
1✔
159
            "TimeKeeping instance at %.6f days" % self.currentTimeNorm.to("day").value
160
        )
161

162
    def init_OB(self, missionSchedule, OBduration):
1✔
163
        """
164
        Initializes mission Observing Blocks from file or missionDuration, missionLife,
165
        and missionPortion. Updates attributes OBstartTimes, OBendTimes, and OBnumber
166

167
        Args:
168
            missionSchedule (str):
169
                A string containing the missionSchedule file (or "None").
170

171
            OBduration (~astropy.units.Quantity):
172
                Observing block length
173

174
        Returns:
175
            None
176

177
        """
178
        if not missionSchedule == "None":  # If the missionSchedule is specified
1✔
179
            tmpOBtimes = list()
1✔
180
            schedulefname = str(
1✔
181
                os.path.dirname(__file__) + "/../Scripts/" + missionSchedule
182
            )  # .csv file in EXOSIMS/Scripts folder
183
            if not os.path.isfile(schedulefname):
1✔
184
                # Check if scriptNames in ScriptsPath
185
                ScriptsPath = str(os.path.dirname(__file__) + "/../../../Scripts/")
×
186
                makeSimilar_TemplateFolder = ""
×
187
                dirsFolderDown = [
×
188
                    x[0].split("/")[-1] for x in os.walk(ScriptsPath)
189
                ]  # Get all directories in ScriptsPath
190
                for tmpFolder in dirsFolderDown:
×
191
                    if (
×
192
                        os.path.isfile(ScriptsPath + tmpFolder + "/" + missionSchedule)
193
                        and not tmpFolder == ""
194
                    ):  # We found the Scripts folder containing scriptfile
195
                        makeSimilar_TemplateFolder = (
×
196
                            tmpFolder + "/"
197
                        )  # We found the file!!!
198
                        break
×
199
                schedulefname = str(
×
200
                    ScriptsPath + makeSimilar_TemplateFolder + missionSchedule
201
                )  # .csv file in EXOSIMS/Scripts folder
202

203
            if os.path.isfile(
1✔
204
                schedulefname
205
            ):  # Check if a mission schedule is manually specified
206
                self.vprint("Loading Manual Schedule from %s" % missionSchedule)
1✔
207
                with open(schedulefname, "r") as f:  # load csv file
1✔
208
                    lines = csv.reader(f, delimiter=",")
1✔
209
                    self.vprint("The manual Schedule is:")
1✔
210
                    for line in lines:
1✔
211
                        tmpOBtimes.append(line)
1✔
212
                        self.vprint(line)
1✔
213
                self.OBstartTimes = (
1✔
214
                    np.asarray([float(item[0]) for item in tmpOBtimes]) * u.d
215
                )
216
                self.OBendTimes = (
1✔
217
                    np.asarray([float(item[1]) for item in tmpOBtimes]) * u.d
218
                )
219
        # Automatically construct OB from OBduration, missionLife, and missionPortion
220
        else:
221
            if OBduration == np.inf * u.d:  # There is 1 OB spanning the mission
1✔
222
                self.OBstartTimes = np.asarray([0]) * u.d
1✔
223
                self.OBendTimes = np.asarray([self.missionLife.to("day").value]) * u.d
1✔
224
            else:  # OB
225
                startToStart = OBduration / self.missionPortion
1✔
226
                numBlocks = np.ceil(
1✔
227
                    self.missionLife.to("day") / startToStart
228
                )  # This is the number of Observing Blocks
229
                self.OBstartTimes = np.arange(numBlocks) * startToStart
1✔
230
                self.OBendTimes = self.OBstartTimes + OBduration
1✔
231
                if self.OBendTimes[-1] > self.missionLife.to(
1✔
232
                    "day"
233
                ):  # If the end of the last observing block exceeds the end of mission
234
                    self.OBendTimes[-1] = self.missionLife.to(
1✔
235
                        "day"
236
                    ).copy()  # Set end of last OB to end of mission
237
        self.OBduration = OBduration
1✔
238
        self.OBnumber = 0
1✔
239
        self.vprint("OBendTimes is: " + str(self.OBendTimes))
1✔
240

241
    def mission_is_over(self, OS, Obs, mode):
1✔
242
        r"""Are the mission time, or any other mission resources, exhausted?
243

244
        Args:
245
            OS (:ref:`OpticalSystem`):
246
                Optical System object
247
            Obs (:ref:`Observatory`):
248
                Observatory object
249
            mode (dict):
250
                Selected observing mode for detection (uses only overhead time)
251

252
        Returns:
253
            bool:
254
                True if the mission time or fuel are used up, else False.
255
        """
256

257
        # let's be optimistic and assume we still have time
258
        is_over = False
1✔
259

260
        # if we've exceeded total mission time (or overhead on the next observation
261
        # will make us exceed total time, we're done)
262
        if self.currentTimeNorm + Obs.settlingTime + mode["syst"][
1✔
263
            "ohTime"
264
        ] >= self.missionLife.to("day"):
265
            self.vprint(
1✔
266
                "missionLife would be exceeded at %s"
267
                % self.currentTimeNorm.to("day").round(2)
268
            )
269
            is_over = True
1✔
270

271
        # if we've used up our time allocation (or overhead on the next observation
272
        # will make us exceed it, we're done)
273
        if (
1✔
274
            self.exoplanetObsTime.to("day") + Obs.settlingTime + mode["syst"]["ohTime"]
275
            >= self.missionLife.to("day") * self.missionPortion
276
        ):
277
            self.vprint(
1✔
278
                (
279
                    "exoplanetObstime ({:.2f}) would exceed "
280
                    "(missionPortion*missionLife) = {:.2f}) at currentTimeNorm = {}"
281
                ).format(
282
                    self.exoplanetObsTime,
283
                    self.missionPortion * self.missionLife.to("day"),
284
                    self.currentTimeNorm.to("day").round(2),
285
                )
286
            )
287
            is_over = True
1✔
288

289
        # if overheads will put us past the end of the final observing block, we're done
290
        if (
1✔
291
            self.currentTimeNorm + Obs.settlingTime + mode["syst"]["ohTime"]
292
            >= self.OBendTimes[-1]
293
        ):
294
            self.vprint(
1✔
295
                (
296
                    "Last Observing Block (# {}, end time: {:.2f}) would be "
297
                    "exceeded at currentTimeNorm {}"
298
                ).format(
299
                    self.OBnumber,
300
                    self.OBendTimes[-1],
301
                    self.currentTimeNorm.to("day").round(2),
302
                )
303
            )
304
            is_over = True
1✔
305

306
        # and now, all the fuel stuff
307
        if OS.haveOcculter:
1✔
308
            # handle case of separate fuel tanks for slew and sk:
309
            if Obs.twotanks:
1✔
310
                if Obs.skMass <= 0 * u.kg:
1✔
311
                    self.vprint(
1✔
312
                        "Stationkeeping fuel exhausted at currentTimeNorm %s"
313
                        % (self.currentTimeNorm.to("day").round(2))
314
                    )
315
                    # see if we can refuel
316
                    if not (Obs.refuel_tank(self, tank="sk")):
1✔
317
                        is_over = True
1✔
318

319
                if Obs.slewMass <= 0 * u.kg:
1✔
320
                    self.vprint(
1✔
321
                        "Slew fuel exhausted at currentTimeNorm %s"
322
                        % (self.currentTimeNorm.to("day").round(2))
323
                    )
324
                    # see if we can refuel
325
                    if not (Obs.refuel_tank(self, tank="slew")):
1✔
326
                        is_over = True
1✔
327

328
            # now consider case of only one tank
329
            else:
330
                if Obs.scMass <= Obs.dryMass:
1✔
331
                    self.vprint(
1✔
332
                        "Fuel exhausted at currentTimeNorm %s"
333
                        % (self.currentTimeNorm.to("day").round(2))
334
                    )
335
                    # see if we can refuel
336
                    if not (Obs.refuel_tank(self)):
1✔
337
                        is_over = True
1✔
338

339
        return is_over
1✔
340

341
    def allocate_time(self, dt, addExoplanetObsTime=True):
1✔
342
        r"""Allocate a temporal block of width dt
343

344
        Advance the mission time by dt units. Updates attributes currentTimeNorm and
345
        currentTimeAbs
346

347
        Args:
348
            dt (~astropy.units.Quantity):
349
                Temporal block allocated in units of days
350
            addExoplanetObsTime (bool):
351
                Indicates the allocated time is for the primary instrument (True)
352
                or some other instrument (False)
353
                By default this function assumes all allocated time is attributed to
354
                the primary instrument (is True)
355

356
        Returns:
357
            bool:
358
                a flag indicating the time allocation was successful or not successful
359
        """
360

361
        # Check dt validity
362
        if dt.value <= 0 or dt.value == np.inf:
1✔
363
            self.vprint("dt must be positive and nonzero (got {})".format(dt))
1✔
364
            return False  # The temporal block to allocate is not positive nonzero
1✔
365

366
        # Check dt exceeds mission life
367
        if self.currentTimeNorm + dt > self.missionLife.to("day"):
1✔
368
            self.vprint(
1✔
369
                (
370
                    "Allocating dt = {} at curremtTimeNorm = {} would exceed "
371
                    "missionLife = {}"
372
                ).format(
373
                    dt,
374
                    self.currentTimeNorm,
375
                    self.missionLife.to("day"),
376
                )
377
            )
378
            return False  # The time to allocate would exceed the missionLife
1✔
379

380
        # Check dt exceeds current OB
381
        if self.currentTimeNorm + dt > self.OBendTimes[self.OBnumber]:
1✔
382
            self.vprint(
1✔
383
                (
384
                    "Allocating dt = {} at currentTimeNorm = {} would exceed the end "
385
                    "of observing block #{} with end time {}"
386
                ).format(
387
                    dt,
388
                    self.currentTimeNorm,
389
                    self.OBnumber,
390
                    self.OBendTimes[self.OBnumber],
391
                )
392
            )
393
            return False
1✔
394

395
        # Check exceeds allowed instrument Time
396
        if addExoplanetObsTime:
1✔
397
            if (
1✔
398
                self.exoplanetObsTime + dt
399
                > self.missionLife.to("day") * self.missionPortion
400
            ):
401
                self.vprint(
1✔
402
                    (
403
                        "Allocating dt = {} with current exoplanetObsTime = {} would "
404
                        "exceed (missionPortion*missionLife) = {:.2f}"
405
                    ).format(
406
                        dt,
407
                        self.exoplanetObsTime,
408
                        self.missionLife.to("day") * self.missionPortion,
409
                    )
410
                )
411
                return False
1✔
412

413
            self.currentTimeAbs += dt
1✔
414
            self.currentTimeNorm += dt
1✔
415
            self.exoplanetObsTime += dt
1✔
416
            return True
1✔
417
        else:  # Time will not be counted against exoplanetObstime
418
            self.currentTimeAbs += dt
1✔
419
            self.currentTimeNorm += dt
1✔
420
            return True
1✔
421

422
    def advancetToStartOfNextOB(self):
1✔
423
        """Advances to Start of Next Observation Block
424
        This method is called in the allocate_time() method of the TimeKeeping
425
        class object, when the allocated time requires moving outside of the current OB.
426
        If no OB duration was specified, a new Observing Block is created for
427
        each observation in the SurveySimulation module. Updates attributes OBnumber,
428
        currentTimeNorm and currentTimeAbs.
429

430
        """
431
        self.OBnumber += 1  # increase the observation block number
1✔
432
        self.currentTimeNorm = self.OBstartTimes[
1✔
433
            self.OBnumber
434
        ]  # update currentTimeNorm
435
        self.currentTimeAbs = (
1✔
436
            self.OBstartTimes[self.OBnumber] + self.missionStart
437
        )  # update currentTimeAbs
438

439
        # begin Survey, and loop until mission is finished
440
        log_begin = "OB%s:" % (
1✔
441
            self.OBnumber
442
        )  # prints because this is the beginning of the nesxt observation block
443
        self.vprint(log_begin)
1✔
444
        self.vprint(
1✔
445
            "Advanced currentTimeNorm to beginning of next OB %.2fd"
446
            % (self.currentTimeNorm.to("day").value)
447
        )
448

449
    def advanceToAbsTime(self, tAbs, addExoplanetObsTime=True):
1✔
450
        """Advances the current mission time to tAbs.
451
        Updates attributes currentTimeNorma dn currentTimeAbs
452

453
        Args:
454
            tAbs (~astropy.time.Time):
455
                The absolute mission time to advance currentTimeAbs to.
456
                MUST HAVE scale='tai'
457
            addExoplanetObsTime (bool):
458
                A flag indicating whether to add advanced time to exoplanetObsTime or
459
                not
460

461
        Returns:
462
            bool:
463
                A bool indicating whether the operation was successful or not
464
        """
465

466
        # Checks on tAbs validity
467
        if tAbs <= self.currentTimeAbs:
1✔
468
            self.vprint(
1✔
469
                "The time to advance to "
470
                + str(tAbs)
471
                + " is not after "
472
                + str(self.currentTimeAbs)
473
            )
474
            return False
1✔
475

476
        # Use 2 and Use 4
477
        if tAbs >= self.missionFinishAbs:  #
1✔
478
            tmpcurrentTimeNorm = self.currentTimeNorm.copy()
1✔
479
            t_added = (tAbs - self.currentTimeAbs).value * u.d
1✔
480
            self.currentTimeNorm = (tAbs - self.missionStart).value * u.d
1✔
481
            self.currentTimeAbs = tAbs
1✔
482
            if addExoplanetObsTime:  # Count time towards exoplanetObs Time
1✔
483
                if (self.exoplanetObsTime + t_added) > (
1✔
484
                    self.missionLife.to("day") * self.missionPortion
485
                ):
486
                    self.vprint(
1✔
487
                        (
488
                            "Adding {} to current exoplanetObsTime ({:0.2f}) would "
489
                            "exceed (missionLife*missionPortion) = {:0.2f}"
490
                        ).format(
491
                            t_added.to("day"),
492
                            self.exoplanetObsTime.to("day"),
493
                            self.missionLife.to("day") * self.missionPortion,
494
                        )
495
                    )
496
                    self.exoplanetObsTime = (
1✔
497
                        self.missionLife.to("day") * self.missionPortion
498
                    )
499
                    return False
1✔
500
                self.exoplanetObsTime += (
1✔
501
                    self.missionLife.to("day") - tmpcurrentTimeNorm
502
                )  # Advances exoplanet time to end of mission time
503
            else:
504
                self.exoplanetObsTime += 0 * u.d
×
505
            return True
1✔
506

507
        # Use 1 and Use 3
508
        if (
1✔
509
            tAbs <= self.OBendTimes[self.OBnumber] + self.missionStart
510
        ):  # The time to advance to does not leave the current OB
511
            t_added = (tAbs - self.currentTimeAbs).value * u.d
1✔
512
            self.currentTimeNorm = (tAbs - self.missionStart).to("day")
1✔
513
            self.currentTimeAbs = tAbs
1✔
514
            if addExoplanetObsTime:  # count time towards exoplanet Obs Time
1✔
515
                if (self.exoplanetObsTime + t_added) > (
1✔
516
                    self.missionLife.to("day") * self.missionPortion
517
                ):
518
                    self.vprint(
1✔
519
                        (
520
                            "Adding {} to current exoplanetObsTime ({:0.2f}) would "
521
                            "exceed (missionLife*missionPortion) = {:0.2f}"
522
                        ).format(
523
                            t_added.to("day"),
524
                            self.exoplanetObsTime.to("day"),
525
                            self.missionLife.to("day") * self.missionPortion,
526
                        )
527
                    )
528
                    self.exoplanetObsTime = (
1✔
529
                        self.missionLife.to("day") * self.missionPortion
530
                    )
531
                    return False
1✔
532
                else:
533
                    self.exoplanetObsTime += t_added
1✔
534
                    return True
1✔
535
            else:  # addExoplanetObsTime is False
536
                self.exoplanetObsTime += 0 * u.d
1✔
537
            return True
1✔
538

539
        # Use 5 and 7 #extended to accomodate any current and future time between OBs
540
        tNorm = (tAbs - self.missionStart).value * u.d
1✔
541
        if np.any(
1✔
542
            (tNorm <= self.OBstartTimes[1:]) & (tNorm >= self.OBendTimes[0:-1])
543
        ):  # The tAbs is between end End of an OB and start of the Next OB
544
            # Return OBnumber of End Index
545
            endIndex = np.where(
1✔
546
                (tNorm <= self.OBstartTimes[1:]) & (tNorm >= self.OBendTimes[0:-1])
547
            )[0][0]
548
            # self.OBendTimes[endIndex+1] - self.currentTimeNorm
549
            # Time to be added to exoplanetObsTime from current OB
550
            t_added = self.OBendTimes[self.OBnumber] - self.currentTimeNorm
1✔
551
            for ind in np.arange(
1✔
552
                self.OBnumber, endIndex
553
            ):  # ,len(self.OBendTimes)):  # Add time for all additional OB
554
                t_added += self.OBendTimes[ind] - self.OBstartTimes[ind]
1✔
555
            while self.OBnumber < endIndex + 1:
1✔
556
                self.advancetToStartOfNextOB()
1✔
557
            # self.OBnumber = endIndex + 1  # set OBnumber to correct Observing Block
558
            # self.currentTimeNorm = self.OBstartTimes[self.OBnumber]
559
            # Advance Time to start of next OB
560
            # self.currentTimeAbs = self.OBstartTimes[self.OBnumber] + self.missionStart
561
            # Advance Time to start of next OB
562
            if addExoplanetObsTime:  # Count time towards exoplanetObs Time
1✔
563
                if (
1✔
564
                    self.exoplanetObsTime + t_added
565
                    > self.missionLife.to("day") * self.missionPortion
566
                ):  # We can CANNOT allocate that time to exoplanetObsTime
567
                    self.vprint(
1✔
568
                        (
569
                            "Adding {} to current exoplanetObsTime ({:0.2f}) would "
570
                            "exceed (missionLife*missionPortion) = {:0.2f}"
571
                        ).format(
572
                            t_added.to("day"),
573
                            self.exoplanetObsTime.to("day"),
574
                            self.missionLife.to("day") * self.missionPortion,
575
                        )
576
                    )
577
                    # This kind of failure is by design.
578
                    # It just means the mission has come to an end
579
                    self.vprint("Advancing to tAbs failed under Use Case 7")
1✔
580
                    self.exoplanetObsTime = (
1✔
581
                        self.missionLife.to("day") * self.missionPortion
582
                    )
583
                    return False
1✔
584
                self.exoplanetObsTime += t_added
1✔
585
            else:
586
                self.exoplanetObsTime += 0 * u.d
1✔
587
            return True
1✔
588

589
        # Use 6 and 8 #extended to accomodate any current and future time inside OBs
590
        # The tAbs is between start of a future OB and end of that OB
591
        if np.any(
1✔
592
            # fmt: off
593
            (tNorm >= self.OBstartTimes[self.OBnumber:])
594
            & (tNorm <= self.OBendTimes[self.OBnumber:])
595
            # fmt: on
596
        ):
597
            # fmt: off
598
            endIndex = np.where(
1✔
599
                (tNorm >= self.OBstartTimes[self.OBnumber:])
600
                & (tNorm <= self.OBendTimes[self.OBnumber:])
601
            )[0][
602
                0
603
            ]  # Return index of OB that tAbs will be inside of
604
            # fmt: on
605
            endIndex += self.OBnumber
1✔
606
            t_added = 0 * u.d
1✔
607
            if addExoplanetObsTime:  # Count time towards exoplanetObs Time
1✔
608
                t_added += (tAbs - self.currentTimeAbs).to("day")
1✔
609
                for i in np.arange(self.OBnumber, endIndex):
1✔
610
                    # accumulate time to subtract (time not counted against Exoplanet
611
                    # Obs Time)
612
                    # Subtract the time between these OB from the t_added to
613
                    # exoplanetObsTime
614
                    index = self.OBnumber
1✔
615
                    t_added -= self.OBstartTimes[index + 1] - self.OBendTimes[index]
1✔
616
                # Check if exoplanetObsTime would be exceeded
617
                if (
1✔
618
                    self.exoplanetObsTime + t_added
619
                    > self.missionLife.to("day") * self.missionPortion
620
                ):
621
                    self.vprint(
1✔
622
                        (
623
                            "Adding {} to current exoplanetObsTime ({:0.2f}) would "
624
                            "exceed (missionLife*missionPortion) = {:0.2f}"
625
                        ).format(
626
                            t_added.to("day"),
627
                            self.exoplanetObsTime.to("day"),
628
                            self.missionLife.to("day") * self.missionPortion,
629
                        )
630
                    )
631

632
                    # This kind of failure is by design.
633
                    # It just means the mission has come to an end
634
                    self.vprint("Advancing to tAbs failed under Use Case 8")
1✔
635
                    self.exoplanetObsTime = (
1✔
636
                        self.missionLife.to("day") * self.missionPortion
637
                    )
638
                    self.OBnumber = endIndex  # set OBnumber to correct Observing Block
1✔
639
                    self.currentTimeNorm = (tAbs - self.missionStart).to(
1✔
640
                        "day"
641
                    )  # Advance Time to start of next OB
642
                    self.currentTimeAbs = tAbs  # Advance Time to start of next OB
1✔
643
                    return False
1✔
644
                else:
645
                    self.exoplanetObsTime += t_added
1✔
646
            else:  # addExoplanetObsTime is False
647
                self.exoplanetObsTime += 0 * u.d
1✔
648
            self.OBnumber = endIndex  # set OBnumber to correct Observing Block
1✔
649
            self.currentTimeNorm = (tAbs - self.missionStart).to(
1✔
650
                "day"
651
            )  # Advance Time to start of next OB
652
            self.currentTimeAbs = tAbs  # Advance Time to start of next OB
1✔
653
            return True
1✔
654

655
        # Generic error if there exists some use case that I have not encountered yet.
656
        assert False, "No Use Case Found in AdvanceToAbsTime"
×
657

658
    def get_ObsDetectionMaxIntTime(
1✔
659
        self, Obs, mode, currentTimeNorm=None, OBnumber=None
660
    ):
661
        """Tells you the maximum Detection Observation Integration Time you can pass
662
        into observation_detection(X,intTime,X)
663

664
        Args:
665
            Obs (:ref:`Observatory`):
666
                Observatory object
667
            mode (dict):
668
                Selected observing mode for detection
669
            currentTimeNorm (astropy.unit.Quantity, optional):
670
                Time since mission start
671
            OBnumber (float, optional):
672
                Current observing block
673

674
        Returns:
675
            tuple:
676
                maxIntTimeOBendTime (astropy.units.Quantity):
677
                    The maximum integration time bounded by Observation Block end Time
678
                maxIntTimeExoplanetObsTime (astropy.units.Quantity):
679
                    The maximum integration time bounded by exoplanetObsTime
680
                maxIntTimeMissionLife (astropy.units.Quantity):
681
                    The maximum integration time bounded by MissionLife
682
        """
683
        if OBnumber is None:
1✔
684
            OBnumber = self.OBnumber
1✔
685
        if currentTimeNorm is None:
1✔
686
            currentTimeNorm = self.currentTimeNorm.copy()
1✔
687

688
        OBendTime_d = self.OBendTimes[OBnumber].to_value(u.d)
1✔
689
        currentTimeNorm_d = currentTimeNorm.to_value(u.d)
1✔
690
        settling_oh_time_d = Obs.settlingTime.to_value(u.d) + mode["syst"][
1✔
691
            "ohTime"
692
        ].to_value(u.d)
693
        maxTimeOBendTime_d = OBendTime_d - currentTimeNorm_d
1✔
694
        maxIntTimeOBendTime_d = (maxTimeOBendTime_d - settling_oh_time_d) / (
1✔
695
            1.0 + mode["timeMultiplier"] - 1.0
696
        )
697

698
        maxTimeExoplanetObsTime_d = (
1✔
699
            self.allocated_time_d - self.exoplanetObsTime.to_value(u.d)
700
        )
701
        maxIntTimeExoplanetObsTime_d = (
1✔
702
            maxTimeExoplanetObsTime_d - settling_oh_time_d
703
        ) / (1.0 + mode["timeMultiplier"] - 1.0)
704

705
        maxTimeMissionLife_d = self.missionLife_d - currentTimeNorm_d
1✔
706
        maxIntTimeMissionLife_d = (maxTimeMissionLife_d - settling_oh_time_d) / (
1✔
707
            1.0 + mode["timeMultiplier"] - 1.0
708
        )
709

710
        # Ensure all are positive or zero
711
        if maxIntTimeOBendTime_d < 0.0:
1✔
NEW
712
            maxIntTimeOBendTime_d = 0.0
×
713
        if maxIntTimeExoplanetObsTime_d < 0.0:
1✔
NEW
714
            maxIntTimeExoplanetObsTime_d = 0.0
×
715
        if maxIntTimeMissionLife_d < 0.0:
1✔
NEW
716
            maxIntTimeMissionLife_d = 0.0
×
717

718
        return (
1✔
719
            maxIntTimeOBendTime_d << u.d,
720
            maxIntTimeExoplanetObsTime_d << u.d,
721
            maxIntTimeMissionLife_d << u.d,
722
        )
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