from EXOSIMS.Prototypes.SurveySimulation import SurveySimulation import EXOSIMS, os import astropy.units as u import astropy.constants as const import numpy as np import itertools from scipy import interpolate try: import cPickle as pickle except: import pickle import time from EXOSIMS.util.deltaMag import deltaMag class tieredScheduler(SurveySimulation): """tieredScheduler This class implements a tiered scheduler that independantly schedules the observatory while the starshade slews to its next target. Args: coeffs (iterable 7x1): Cost function coefficients: slew distance, completeness, intTime, deep-dive least visited ramp, deep-dive unvisited ramp, unvisited ramp, and least-visited ramp occHIPs (iterable nx1): List of star HIP numbers to initialize occulter target list. topstars (integer): Number of HIP numbers to recieve preferential treatment. revisit_wait (float): Wait time threshold for star revisits. The value given is the fraction of a characterized planet's period that must be waited before scheduling a revisit. revisit_weight (float): Weight used to increase preference for coronograph revisits. GAPortion (float): Portion of mission time used for general astrophysics. int_inflection (boolean): Calculate integration time using the pre-calculated integration time curves. Default is False. GA_simult_det_fraction (float): Fraction of detection time to be considered as GA time. promote_hz_stars (boolean): Flag that allows promotion of targets with planets in the habitable zone to the occulter target list. phase1_end (int): Number of days to wait before the end of phase 1, when phase 1 ends, target promotion begins. n_det_remove (int): Minimum number of visits with no detections required to filter off star n_det_min (int): Minimum number of detections required for promotion occ_max_visits (int): Number of maximum visits to a star allowed by the occulter. max_successful_chars (int): Maximum number of successful characterizations on a given star before it is removed from the target list. max_successful_dets (int): Maximum number of successful detections on a given star before it is removed from the target list. nmax_promo_det (int): Number of detection on a star required to be promoted regardless of detection occurance times. lum_exp (int): Exponent used in the luminosity weighting function. tot_det_int_cutoff (float): Number of total days the scheduler is allowed to spend on detections. \*\*specs: user specified values """ def __init__(self, coeffs=[2,1,1,8,4,1,1], occHIPs=[], topstars=0, revisit_wait=0.5, revisit_weight=1.0, GAPortion=.25, int_inflection=False, GA_simult_det_fraction=.07, promote_hz_stars=False, phase1_end=365, n_det_remove=3, n_det_min=3, occ_max_visits=3, max_successful_chars=1, max_successful_dets=4, nmax_promo_det=4, lum_exp=1, tot_det_int_cutoff=None, **specs): SurveySimulation.__init__(self, **specs) #verify that coefficients input is iterable 4x1 if not(isinstance(coeffs,(list,tuple,np.ndarray))) or (len(coeffs) != 7): raise TypeError("coeffs must be a 7 element iterable") TK = self.TimeKeeping TL = self.TargetList OS = self.OpticalSystem SU = self.SimulatedUniverse #Add to outspec self._outspec['coeffs'] = coeffs self._outspec['occHIPs'] = occHIPs self._outspec['topstars'] = topstars self._outspec['revisit_wait'] = revisit_wait self._outspec['revisit_weight'] = revisit_weight self._outspec['GAPortion'] = GAPortion self._outspec['int_inflection'] = int_inflection self._outspec['GA_simult_det_fraction'] = GA_simult_det_fraction self._outspec['promote_hz_stars'] = promote_hz_stars self._outspec['phase1_end'] = phase1_end self._outspec['n_det_remove'] = n_det_remove self._outspec['n_det_min'] = n_det_min self._outspec['occ_max_visits'] = occ_max_visits self._outspec['max_successful_chars'] = max_successful_chars self._outspec['lum_exp'] = lum_exp #normalize coefficients coeffs = np.array(coeffs) coeffs = coeffs/np.linalg.norm(coeffs, ord=1) self.coeffs = coeffs if occHIPs != []: occHIPs_path = os.path.join(EXOSIMS.__path__[0],'Scripts',occHIPs) assert os.path.isfile(occHIPs_path), "%s is not a file."%occHIPs_path with open(occHIPs_path, 'r') as ofile: HIPsfile = ofile.read() self.occHIPs = HIPsfile.split(',') if len(self.occHIPs) <= 1: self.occHIPs = HIPsfile.split('\n') else: # assert occHIPs != [], "occHIPs target list is empty, occHIPs file must be specified in script file" self.occHIPs = occHIPs self.occHIPs = [hip.strip() for hip in self.occHIPs] self.occ_arrives = TK.currentTimeAbs.copy() # The timestamp at which the occulter finishes slewing self.occ_starRevisit = np.array([]) # Array of star revisit times self.occ_starVisits = np.zeros(TL.nStars, dtype=int) # The number of times each star was visited by the occulter self.is_phase1 = True # Flag that determines whether or not we are in phase 1 self.phase1_end = TK.missionStart.copy() + phase1_end*u.d # The designated end time for the first observing phase self.FA_status = np.zeros(TL.nStars, dtype=bool) # False Alarm status array self.GA_percentage = GAPortion # Percentage of mission devoted to general astrophysics self.GAtime = 0.*u.d # Current amount of time devoted to GA self.GA_simult_det_fraction = GA_simult_det_fraction # Fraction of detection time allocated to GA self.goal_GAtime = None # The desired amount of GA time based off of mission time self.curves = None self.ao = None self.int_inflection = int_inflection # Use int_inflection to calculate int times self.promote_hz_stars = promote_hz_stars # Flag to promote hz stars self.last_chard = None # Keeps track of last characterized star to avoid repeats self.lum_exp = lum_exp # The exponent to use for luminosity weighting on coronograph targets self.ready_to_update = False self.occ_slewTime = 0.*u.d self.occ_sd = 0.*u.rad self.sInd_charcounts = {} # Number of characterizations by star index self.sInd_detcounts = np.zeros(TL.nStars, dtype=int) # Number of detections by star index self.sInd_dettimes = {} self.n_det_remove = n_det_remove # Minimum number of visits with no detections required to filter off star self.n_det_min = n_det_min # Minimum number of detections required for promotion self.occ_max_visits = occ_max_visits # Maximum number of allowed occulter visits self.max_successful_chars = max_successful_chars # Maximum allowed number of successful chars of deep dive targets before removal from target list self.max_successful_dets = max_successful_dets self.nmax_promo_det = nmax_promo_det if tot_det_int_cutoff is None: self.tot_det_int_cutoff = np.inf else: self.tot_det_int_cutoff = tot_det_int_cutoff*u.d self.tot_dettime = 0.0*u.d self.topstars = topstars # Allow preferential treatment of top n stars in occ_sInds target list self.coeff_data_a3 = [] self.coeff_data_a4 = [] self.coeff_time = [] # self.revisit_wait = revisit_wait * u.d EEID = 1*u.AU*np.sqrt(TL.L) mu = const.G*(TL.MsTrue) T = (2.*np.pi*np.sqrt(EEID**3/mu)).to('d') self.revisit_wait = revisit_wait * T self.revisit_weight = revisit_weight self.no_dets = np.ones(self.TargetList.nStars, dtype=bool) self.promoted_stars = [] # list of stars promoted from the coronograph list to the starshade list self.ignore_stars = [] # list of stars that have been removed from the occ_sInd list self.t_char_earths = np.array([]) # corresponding integration times for earths # Precalculating intTimeFilter allModes = OS.observingModes char_mode = list(filter(lambda mode: 'spec' in mode['inst']['name'], allModes))[0] sInds = np.arange(TL.nStars) #Initialize some sInds array #ORIGINALself.occ_valfZmin, self.occ_absTimefZmin = self.ZodiacalLight.calcfZmin(sInds, self.Observatory, TL, self.TimeKeeping, char_mode, self.cachefname) # find fZmin to use in intTimeFilter koMap = self.koMaps[char_mode['syst']['name']] self.fZQuads = self.ZodiacalLight.calcfZmin(sInds, self.Observatory, TL, self.TimeKeeping, char_mode, self.cachefname, koMap, self.koTimes) # find fZmin to use in intTimeFilter self.occ_valfZmin, self.occ_absTimefZmin = self.ZodiacalLight.extractfZmin_fZQuads(self.fZQuads) fEZ = self.ZodiacalLight.fEZ0 # grabbing fEZ0 dMag = self.dMagint[sInds] # grabbing dMag WA = self.WAint[sInds] # grabbing WA self.occ_intTimesIntTimeFilter = self.OpticalSystem.calc_intTime(TL, sInds, self.occ_valfZmin, fEZ, dMag, WA, self.mode)*char_mode['timeMultiplier'] # intTimes to filter by self.occ_intTimeFilterInds = np.where((self.occ_intTimesIntTimeFilter > 0)*(self.occ_intTimesIntTimeFilter <= self.OpticalSystem.intCutoff) > 0)[0] # These indices are acceptable for use simulating # Promote all stars assuming they have known earths occ_sInds_with_earths = [] if TL.earths_only: Obs = self.Observatory ZL = self.ZodiacalLight char_mode = list(filter(lambda mode: 'spec' in mode['inst']['name'], OS.observingModes))[0] # check for earths around the available stars for sInd in np.arange(TL.nStars): pInds = np.where(SU.plan2star == sInd)[0] pinds_earthlike = self.is_earthlike(pInds, sInd) if np.any(pinds_earthlike): self.known_earths = np.union1d(self.known_earths, pInds[pinds_earthlike]).astype(int) occ_sInds_with_earths.append(sInd) self.promoted_stars = np.union1d(self.promoted_stars, occ_sInds_with_earths).astype(int) # calculate example integration times sInds = SU.plan2star[self.known_earths] fZ = ZL.fZ(Obs, TL, sInds, TK.currentTimeAbs.copy(), char_mode) fEZ = SU.fEZ[self.known_earths].to('1/arcsec2') WAp = SU.WA[self.known_earths] dMag = SU.dMag[self.known_earths] self.t_char_earths = OS.calc_intTime(TL, sInds, fZ, fEZ, dMag, WAp, char_mode) def run_sim(self): """Performs the survey simulation Returns: mission_end (string): Message printed at the end of a survey simulation. """ OS = self.OpticalSystem TL = self.TargetList SU = self.SimulatedUniverse Obs = self.Observatory TK = self.TimeKeeping Comp = self.Completeness # TODO: start using this self.currentSep # set occulter separation if haveOcculter self.currentSep = Obs.occulterSep # Choose observing modes selected for detection (default marked with a flag), det_mode = list(filter(lambda mode: mode['detectionMode'] == True, OS.observingModes))[0] # and for characterization (default is first spectro/IFS mode) spectroModes = list(filter(lambda mode: 'spec' in mode['inst']['name'], OS.observingModes)) if np.any(spectroModes): char_mode = spectroModes[0] # if no spectro mode, default char mode is first observing mode else: char_mode = OS.observingModes[0] # Begin Survey, and loop until mission is finished self.logger.info('OB{}: survey beginning.'.format(TK.OBnumber+1)) self.vprint('OB{}: survey beginning.'.format(TK.OBnumber+1)) t0 = time.time() sInd = None occ_sInd = None cnt = 0 while not TK.mission_is_over(OS, Obs, det_mode): # Acquire the NEXT TARGET star index and create DRM prev_occ_sInd = occ_sInd old_sInd = sInd #used to save sInd if returned sInd is None waitTime = None DRM, sInd, occ_sInd, t_det, sd, occ_sInds = self.next_target(sInd, occ_sInd, det_mode, char_mode) true_t_det = t_det*det_mode['timeMultiplier'] + Obs.settlingTime + det_mode['syst']['ohTime'] if sInd != occ_sInd and sInd is not None: assert t_det != 0, "Integration time can't be 0." if sInd is not None and (TK.currentTimeAbs.copy() + true_t_det) >= self.occ_arrives and occ_sInd != self.last_chard: sInd = occ_sInd if sInd == occ_sInd: self.ready_to_update = True time2arrive = self.occ_arrives - TK.currentTimeAbs.copy() if sInd is not None: cnt += 1 # clean up revisit list when one occurs to prevent repeats if np.any(self.starRevisit) and np.any(np.where(self.starRevisit[:,0] == float(sInd))): s_revs = np.where(self.starRevisit[:,0] == float(sInd))[0] t_revs = np.where(self.starRevisit[:,1]*u.day - TK.currentTimeNorm.copy() < 0*u.d)[0] self.starRevisit = np.delete(self.starRevisit, np.intersect1d(s_revs,t_revs),0) # get the index of the selected target for the extended list if TK.currentTimeNorm.copy() > TK.missionLife and self.starExtended.shape[0] == 0: for i in range(len(self.DRM)): if np.any([x == 1 for x in self.DRM[i]['plan_detected']]): self.starExtended = np.hstack((self.starExtended, self.DRM[i]['star_ind'])) self.starExtended = np.unique(self.starExtended) # Beginning of observation, start to populate DRM DRM['OB_nb'] = TK.OBnumber+1 DRM['ObsNum'] = cnt DRM['star_ind'] = sInd pInds = np.where(SU.plan2star == sInd)[0] DRM['plan_inds'] = pInds.astype(int).tolist() if sInd == occ_sInd: # wait until expected arrival time is observed if time2arrive > 0*u.d: TK.advanceToAbsTime(self.occ_arrives) if time2arrive > 1*u.d: self.GAtime = self.GAtime + time2arrive.to('day') TK.obsStart = TK.currentTimeNorm.copy().to('day') self.logger.info(' Observation #%s, target #%s/%s with %s planet(s), mission time: %s'\ %(cnt, sInd+1, TL.nStars, len(pInds), TK.obsStart.round(2))) self.vprint(' Observation #%s, target #%s/%s with %s planet(s), mission time: %s'\ %(cnt, sInd+1, TL.nStars, len(pInds), TK.obsStart.round(2))) DRM['arrival_time'] = TK.currentTimeNorm.copy().to('day') if sInd != occ_sInd: self.starVisits[sInd] += 1 # PERFORM DETECTION and populate revisit list attribute. # First store fEZ, dMag, WA if np.any(pInds): DRM['det_fEZ'] = SU.fEZ[pInds].to('1/arcsec2').value.tolist() DRM['det_dMag'] = SU.dMag[pInds].tolist() DRM['det_WA'] = SU.WA[pInds].to('mas').value.tolist() detected, det_fZ, det_systemParams, det_SNR, FA = self.observation_detection(sInd, t_det, det_mode) if np.any(detected): self.sInd_detcounts[sInd] += 1 self.sInd_dettimes[sInd] = (self.sInd_dettimes.get(sInd) or []) + [TK.currentTimeNorm.copy().to('day')] self.vprint(' Det. results are: %s'%(detected)) # update GAtime self.GAtime = self.GAtime + t_det.to('day')*self.GA_simult_det_fraction self.tot_dettime += t_det.to('day') # populate the DRM with detection results DRM['det_time'] = t_det.to('day') DRM['det_status'] = detected DRM['det_SNR'] = det_SNR DRM['det_fZ'] = det_fZ.to('1/arcsec2') DRM['det_params'] = det_systemParams DRM['FA_det_status'] = int(FA) det_comp = Comp.comp_per_intTime(t_det, TL, sInd, det_fZ, self.ZodiacalLight.fEZ0, self.WAint[sInd], det_mode)[0] DRM['det_comp'] = det_comp DRM['det_mode'] = dict(det_mode) del DRM['det_mode']['inst'], DRM['det_mode']['syst'] elif sInd == occ_sInd: self.occ_starVisits[occ_sInd] += 1 self.last_chard = occ_sInd # PERFORM CHARACTERIZATION and populate spectra list attribute. occ_pInds = np.where(SU.plan2star == occ_sInd)[0] sInd = occ_sInd DRM['slew_time'] = self.occ_slewTime.to('day').value DRM['slew_angle'] = self.occ_sd.to('deg').value slew_mass_used = self.occ_slewTime*Obs.defburnPortion*Obs.flowRate DRM['slew_dV'] = (self.occ_slewTime*self.ao*Obs.defburnPortion).to('m/s').value DRM['slew_mass_used'] = slew_mass_used.to('kg') Obs.scMass = Obs.scMass - slew_mass_used DRM['scMass'] = Obs.scMass.to('kg') self.logger.info(' Starshade and telescope aligned at target star') self.vprint(' Starshade and telescope aligned at target star') # PERFORM CHARACTERIZATION and populate spectra list attribute characterized, char_fZ, char_systemParams, char_SNR, char_intTime = \ self.observation_characterization(sInd, char_mode) if np.any(characterized): self.vprint(' Char. results are: %s'%(characterized.T)) else: # make sure we don't accidentally double characterize TK.advanceToAbsTime(TK.currentTimeAbs.copy() + .01*u.d) assert char_intTime != 0, "Integration time can't be 0." if np.any(occ_pInds): DRM['char_fEZ'] = SU.fEZ[occ_pInds].to('1/arcsec2').value.tolist() DRM['char_dMag'] = SU.dMag[occ_pInds].tolist() DRM['char_WA'] = SU.WA[occ_pInds].to('mas').value.tolist() DRM['char_mode'] = dict(char_mode) del DRM['char_mode']['inst'], DRM['char_mode']['syst'] # update the occulter wet mass if OS.haveOcculter and char_intTime is not None: DRM = self.update_occulter_mass(DRM, sInd, char_intTime, 'char') char_comp = Comp.comp_per_intTime(char_intTime, TL, occ_sInd, char_fZ, self.ZodiacalLight.fEZ0, self.WAint[occ_sInd], char_mode)[0] DRM['char_comp'] = char_comp FA = False # populate the DRM with characterization results DRM['char_time'] = char_intTime.to('day') if char_intTime else 0.*u.day #DRM['char_counts'] = self.sInd_charcounts[sInd] DRM['char_status'] = characterized[:-1] if FA else characterized DRM['char_SNR'] = char_SNR[:-1] if FA else char_SNR DRM['char_fZ'] = char_fZ.to('1/arcsec2') DRM['char_params'] = char_systemParams # populate the DRM with FA results DRM['FA_det_status'] = int(FA) DRM['FA_char_status'] = characterized[-1] if FA else 0 DRM['FA_char_SNR'] = char_SNR[-1] if FA else 0. DRM['FA_char_fEZ'] = self.lastDetected[sInd,1][-1]/u.arcsec**2 if FA else 0./u.arcsec**2 DRM['FA_char_dMag'] = self.lastDetected[sInd,2][-1] if FA else 0. DRM['FA_char_WA'] = self.lastDetected[sInd,3][-1]*u.arcsec if FA else 0.*u.arcsec # add star back into the revisit list if np.any(characterized): char = np.where(characterized)[0] pInds = np.where(SU.plan2star == sInd)[0] smin = np.min(SU.s[pInds[char]]) pInd_smin = pInds[np.argmin(SU.s[pInds[char]])] Ms = TL.MsTrue[sInd] sp = smin Mp = SU.Mp[pInd_smin] mu = const.G*(Mp + Ms) T = 2.*np.pi*np.sqrt(sp**3/mu) t_rev = TK.currentTimeNorm.copy() + T/2. self.goal_GAtime = self.GA_percentage * TK.currentTimeNorm.copy().to('day') goal_GAdiff = self.goal_GAtime - self.GAtime # allocate extra time to GA if we are falling behind if goal_GAdiff > 1*u.d and TK.currentTimeAbs.copy() < self.occ_arrives: GA_diff = min(self.occ_arrives - TK.currentTimeAbs.copy(), goal_GAdiff) self.vprint('Allocating time %s to general astrophysics'%(GA_diff)) self.GAtime = self.GAtime + GA_diff TK.advanceToAbsTime(TK.currentTimeAbs.copy() + GA_diff) # allocate time if there is no target for the starshade elif goal_GAdiff > 1*u.d and (self.occ_arrives - TK.currentTimeAbs.copy()) < -5*u.d and not np.any(occ_sInds): self.vprint('No Available Occulter Targets: Allocating time %s to general astrophysics'%(goal_GAdiff)) self.GAtime = self.GAtime + goal_GAdiff TK.advanceToAbsTime(TK.currentTimeAbs.copy() + goal_GAdiff) DRM['exoplanetObsTime'] = TK.exoplanetObsTime.copy() # Append result values to self.DRM self.DRM.append(DRM) # Calculate observation end time TK.obsEnd = TK.currentTimeNorm.copy().to('day') # With prototype TimeKeeping, if no OB duration was specified, advance # to the next OB with timestep equivalent to time spent on one target if np.isinf(TK.OBduration) and (TK.missionPortion < 1): self.arbitrary_time_advancement(TK.currentTimeNorm.to('day').copy() - DRM['arrival_time']) # With occulter, if spacecraft fuel is depleted, exit loop if Obs.scMass < Obs.dryMass: self.vprint('Total fuel mass exceeded at %s' %TK.obsEnd.round(2)) break else:#sInd == None sInd = old_sInd#Retain the last observed star if(TK.currentTimeNorm.copy() >= TK.OBendTimes[TK.OBnumber]): # currentTime is at end of OB #Conditional Advance To Start of Next OB if not TK.mission_is_over(OS, Obs,det_mode):#as long as the mission is not over TK.advancetToStartOfNextOB()#Advance To Start of Next OB elif(waitTime is not None): #CASE 1: Advance specific wait time success = TK.advanceToAbsTime(TK.currentTimeAbs.copy() + waitTime) self.vprint('waitTime is not None') else: startTimes = TK.currentTimeAbs.copy() + np.zeros(TL.nStars)*u.d # Start Times of Observations observableTimes = Obs.calculate_observableTimes(TL,np.arange(TL.nStars),startTimes,self.koMaps,self.koTimes,self.mode)[0] #CASE 2 If There are no observable targets for the rest of the mission if((observableTimes[(TK.missionFinishAbs.copy().value*u.d > observableTimes.value*u.d)*(observableTimes.value*u.d >= TK.currentTimeAbs.copy().value*u.d)].shape[0]) == 0):#Are there any stars coming out of keepout before end of mission self.vprint('No Observable Targets for Remainder of mission at currentTimeNorm= ' + str(TK.currentTimeNorm.copy())) #Manually advancing time to mission end TK.currentTimeNorm = TK.missionLife.copy() TK.currentTimeAbs = TK.missionFinishAbs.copy() else:#CASE 3 nominal wait time if at least 1 target is still in list and observable #TODO: ADD ADVANCE TO WHEN FZMIN OCURS inds1 = np.arange(TL.nStars)[observableTimes.value*u.d > TK.currentTimeAbs.copy().value*u.d] inds2 = np.intersect1d(self.intTimeFilterInds, inds1) #apply intTime filter inds3 = self.revisitFilter(inds2, TK.currentTimeNorm.copy() + self.dt_max.to(u.d)) #apply revisit Filter #NOTE this means stars you added to the revisit list self.vprint("Filtering %d stars from advanceToAbsTime"%(TL.nStars - len(inds3))) oTnowToEnd = observableTimes[inds3] if not oTnowToEnd.value.shape[0] == 0: #there is at least one observableTime between now and the end of the mission tAbs = np.min(oTnowToEnd)#advance to that observable time else: tAbs = TK.missionStart + TK.missionLife#advance to end of mission tmpcurrentTimeNorm = TK.currentTimeNorm.copy() success = TK.advanceToAbsTime(tAbs)#Advance Time to this time OR start of next OB following this time self.vprint('No Observable Targets a currentTimeNorm= %.2f Advanced To currentTimeNorm= %.2f'%(tmpcurrentTimeNorm.to('day').value, TK.currentTimeNorm.to('day').value)) else: dtsim = (time.time()-t0)*u.s mission_end = "Mission complete: no more time available.\n"\ + "Simulation duration: %s.\n" %dtsim.astype('int')\ + "Results stored in SurveySimulation.DRM (Design Reference Mission)." self.logger.info(mission_end) self.vprint(mission_end) return mission_end def promote_coro_targets(self, occ_sInds, sInds): """ Determines which coronograph targets to promote to occulter targets Args: occ_sInds (numpy array): occulter targets sInds (numpy array): coronograph targets Returns: occ_sInds (numpy array): updated occulter targets """ TK = self.TimeKeeping SU = self.SimulatedUniverse TL = self.TargetList promoted_occ_sInds = np.array([], dtype=int) # if phase 1 has ended if TK.currentTimeAbs > self.phase1_end: if self.is_phase1 is True: self.vprint('Entering detection phase 2: target list for occulter expanded') self.is_phase1 = False # If we only want to promote stars that have planets in the habitable zone if self.promote_hz_stars: # stars must have had >= n_det_min detections promote_stars = sInds[np.where(self.sInd_detcounts[sInds] >= self.n_det_min)[0]] if np.any(promote_stars): for sInd in promote_stars: pInds = np.where(SU.plan2star == sInd)[0] sp = SU.s[pInds] Ms = TL.MsTrue[sInd] Mp = SU.Mp[pInds] mu = const.G*(Mp + Ms) T = (2.*np.pi*np.sqrt(sp**3/mu)).to('d') # star must have detections that span longer than half a period and be in the habitable zone # and have a smaller radius that a sub-neptune pinds_earthlike = self.is_earthlike(pInds, sInd) if (np.any((T/2.0 < (self.sInd_dettimes[sInd][-1] - self.sInd_dettimes[sInd][0]))) and np.any(pinds_earthlike)) \ or ((self.sInd_detcounts[sInd] >= self.nmax_promo_det) and np.any(pinds_earthlike)): earthlikes = pInds[pinds_earthlike] self.known_earths = np.union1d(self.known_earths, pInds[pinds_earthlike]).astype(int) promoted_occ_sInds = np.append(promoted_occ_sInds, sInd) if sInd not in self.promoted_stars: self.promoted_stars.append(sInd) occ_sInds = np.union1d(occ_sInds, promoted_occ_sInds) else: occ_sInds = np.union1d(occ_sInds, sInds[np.where((self.starVisits[sInds] == self.nVisitsMax) & (self.occ_starVisits[sInds] == 0))[0]]) occ_sInds = np.union1d(occ_sInds, np.intersect1d(sInds, self.known_rocky)) self.promoted_stars = list(np.union1d(self.promoted_stars, np.intersect1d(sInds, self.known_rocky)).astype(int)) return occ_sInds.astype(int) def next_target(self, old_sInd, old_occ_sInd, det_mode, char_mode): """Finds index of next target star and calculates its integration time. This method chooses the next target star index based on which stars are available, their integration time, and maximum completeness. Returns None if no target could be found. Args: old_sInd (integer): Index of the previous target star for the telescope old_occ_sInd (integer): Index of the previous target star for the occulter det_mode (dict): Selected observing mode for detection char_mode (dict): Selected observing mode for characterization Returns: DRM (dicts): Contains the results of survey simulation sInd (integer): Index of next target star. Defaults to None. occ_sInd (integer): Index of next occulter target star. Defaults to None. t_det (astropy Quantity): Selected star integration time for detection in units of day. Defaults to None. """ OS = self.OpticalSystem ZL = self.ZodiacalLight TL = self.TargetList Obs = self.Observatory TK = self.TimeKeeping SU = self.SimulatedUniverse # selecting appropriate koMap occ_koMap = self.koMaps[char_mode['syst']['name']] koMap = self.koMaps[det_mode['syst']['name']] # Create DRM DRM = {} # In case of an occulter, initialize slew time factor # (add transit time and reduce starshade mass) assert OS.haveOcculter == True self.ao = Obs.thrust/Obs.scMass # Star indices that correspond with the given HIPs numbers for the occulter # XXX ToDo: print out HIPs that don't show up in TL HIP_sInds = np.where(np.in1d(TL.Name, self.occHIPs))[0] if TL.earths_only: HIP_sInds = np.union1d(HIP_sInds, self.promoted_stars).astype(int) sInd = None # Now, start to look for available targets while not TK.mission_is_over(OS, Obs, det_mode): # allocate settling time + overhead time tmpCurrentTimeAbs = TK.currentTimeAbs.copy() tmpCurrentTimeNorm = TK.currentTimeNorm.copy() occ_tmpCurrentTimeAbs = TK.currentTimeAbs.copy() occ_tmpCurrentTimeNorm = TK.currentTimeNorm.copy() # 0 initialize arrays slewTimes = np.zeros(TL.nStars)*u.d fZs = np.zeros(TL.nStars)/u.arcsec**2 dV = np.zeros(TL.nStars)*u.m/u.s intTimes = np.zeros(TL.nStars)*u.d occ_intTimes = np.zeros(TL.nStars)*u.d occ_tovisit = np.zeros(TL.nStars, dtype=bool) sInds = np.arange(TL.nStars) # 1 Find spacecraft orbital START positions and filter out unavailable # targets. If occulter, each target has its own START position. sd = Obs.star_angularSep(TL, old_occ_sInd, sInds, tmpCurrentTimeAbs) obsTimes = Obs.calculate_observableTimes(TL, sInds, tmpCurrentTimeAbs, self.koMaps, self.koTimes, char_mode) slewTimes = Obs.calculate_slewTimes(TL, old_occ_sInd, sInds, sd, obsTimes, tmpCurrentTimeAbs) # 2.1 filter out totTimes > integration cutoff if len(sInds) > 0: occ_sInds = np.intersect1d(self.occ_intTimeFilterInds, sInds) if len(sInds) > 0: sInds = np.intersect1d(self.intTimeFilterInds, sInds) # Starttimes based off of slewtime occ_startTimes = occ_tmpCurrentTimeAbs.copy() + slewTimes occ_startTimesNorm = occ_tmpCurrentTimeNorm.copy() + slewTimes startTimes = tmpCurrentTimeAbs.copy() + np.zeros(TL.nStars)*u.d startTimesNorm = tmpCurrentTimeNorm.copy() # 2.5 Filter stars not observable at startTimes try: tmpIndsbool = list() for i in np.arange(len(occ_sInds)): koTimeInd = np.where(np.round(occ_startTimes[occ_sInds[i]].value) - self.koTimes.value==0)[0][0] # find indice where koTime is endTime[0] tmpIndsbool.append(occ_koMap[occ_sInds[i]][koTimeInd].astype(bool)) #Is star observable at time ind sInds_occ_ko = occ_sInds[tmpIndsbool] occ_sInds = sInds_occ_ko[np.where(np.in1d(sInds_occ_ko, HIP_sInds))[0]] del tmpIndsbool except:#If there are no target stars to observe sInds_occ_ko = np.asarray([],dtype=int) occ_sInds = np.asarray([],dtype=int) try: tmpIndsbool = list() for i in np.arange(len(sInds)): koTimeInd = np.where(np.round(startTimes[sInds[i]].value) - self.koTimes.value==0)[0][0] # find indice where koTime is endTime[0] tmpIndsbool.append(koMap[sInds[i]][koTimeInd].astype(bool)) #Is star observable at time ind sInds = sInds[tmpIndsbool] del tmpIndsbool except:#If there are no target stars to observe sInds = np.asarray([],dtype=int) # 2.9 Occulter target promotion step occ_sInds = self.promote_coro_targets(occ_sInds, sInds_occ_ko) # 3 Filter out all previously (more-)visited targets, unless in # revisit list if len(sInds.tolist()) > 0: sInds = self.revisitFilter(sInds, TK.currentTimeNorm.copy()) # revisit list, with time after start if np.any(occ_sInds): occ_tovisit[occ_sInds] = (self.occ_starVisits[occ_sInds] == self.occ_starVisits[occ_sInds].min()) if self.occ_starRevisit.size != 0: dt_rev = TK.currentTimeNorm.copy() - self.occ_starRevisit[:,1]*u.day ind_rev = [int(x) for x in self.occ_starRevisit[dt_rev > 0, 0] if x in occ_sInds] occ_tovisit[ind_rev] = True occ_sInds = np.where(occ_tovisit)[0] # 4 calculate integration times for ALL preselected targets, # and filter out totTimes > integration cutoff maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife = TK.get_ObsDetectionMaxIntTime(Obs, det_mode) maxIntTime = min(maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife, OS.intCutoff)#Maximum intTime allowed maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife = TK.get_ObsDetectionMaxIntTime(Obs, char_mode) occ_maxIntTime = min(maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife, OS.intCutoff)#Maximum intTime allowed if len(occ_sInds) > 0: if self.int_inflection: fEZ = ZL.fEZ0 WA = self.WAint occ_intTimes[occ_sInds] = self.calc_int_inflection(occ_sInds, fEZ, occ_startTimes, WA[occ_sInds], char_mode, ischar=True) totTimes = occ_intTimes*char_mode['timeMultiplier'] occ_endTimes = occ_startTimes + totTimes else: # characterization_start = occ_startTimes occ_intTimes[occ_sInds] = self.calc_targ_intTime(occ_sInds, occ_startTimes[occ_sInds], char_mode) * (1 + self.charMargin) # Adjust integration time for stars with known earths around them for occ_star in occ_sInds: if occ_star in self.promoted_stars: occ_earths = np.intersect1d(np.where(SU.plan2star == occ_star)[0], self.known_earths).astype(int) if np.any(occ_earths): fZ = ZL.fZ(Obs, TL, occ_star, occ_startTimes[occ_star], char_mode) fEZ = SU.fEZ[occ_earths].to('1/arcsec2').value/u.arcsec**2 if SU.lucky_planets: phi = (1/np.pi)*np.ones(len(SU.d)) dMag = deltaMag(SU.p, SU.Rp, SU.d, phi)[occ_earths] # delta magnitude WA = np.arctan(SU.a/TL.dist[SU.plan2star]).to('arcsec')[occ_earths] # working angle else: dMag = SU.dMag[occ_earths] WA = SU.WA[occ_earths] if np.all((WA < char_mode['IWA']) | (WA > char_mode['OWA'])): occ_intTimes[occ_star] = 0.*u.d else: earthlike_inttimes = OS.calc_intTime(TL, occ_star, fZ, fEZ, dMag, WA, char_mode) * (1 + self.charMargin) earthlike_inttime = earthlike_inttimes[(earthlike_inttimes < occ_maxIntTime)] if len(earthlike_inttime) > 0: occ_intTimes[occ_star] = np.max(earthlike_inttime) else: occ_intTimes[occ_star] = np.max(earthlike_inttimes) occ_endTimes = occ_startTimes + (occ_intTimes * char_mode['timeMultiplier']) + Obs.settlingTime + char_mode['syst']['ohTime'] occ_sInds = occ_sInds[(occ_intTimes[occ_sInds] <= occ_maxIntTime)] # Filters targets exceeding maximum intTime occ_sInds = occ_sInds[(occ_intTimes[occ_sInds] > 0.0*u.d)] # Filters with an inttime of 0 if occ_maxIntTime.value <= 0: occ_sInds = np.asarray([],dtype=int) if len(sInds.tolist()) > 0: intTimes[sInds] = self.calc_targ_intTime(sInds, startTimes[sInds], det_mode) sInds = sInds[(intTimes[sInds] <= maxIntTime)] # Filters targets exceeding end of OB endTimes = startTimes + intTimes if maxIntTime.value <= 0: sInds = np.asarray([],dtype=int) # 5.2 find spacecraft orbital END positions (for each candidate target), # and filter out unavailable targets if len(occ_sInds.tolist()) > 0 and Obs.checkKeepoutEnd: try: # endTimes may exist past koTimes so we have an exception to hand this case tmpIndsbool = list() for i in np.arange(len(occ_sInds)): koTimeInd = np.where(np.round(occ_endTimes[occ_sInds[i]].value)-self.koTimes.value==0)[0][0] # find indice where koTime is endTime[0] tmpIndsbool.append(occ_koMap[occ_sInds[i]][koTimeInd].astype(bool)) #Is star observable at time ind occ_sInds = occ_sInds[tmpIndsbool] del tmpIndsbool except: occ_sInds = np.asarray([],dtype=int) if len(sInds.tolist()) > 0 and Obs.checkKeepoutEnd: try: # endTimes may exist past koTimes so we have an exception to hand this case tmpIndsbool = list() for i in np.arange(len(sInds)): koTimeInd = np.where(np.round(endTimes[sInds[i]].value)-self.koTimes.value==0)[0][0] # find indice where koTime is endTime[0] tmpIndsbool.append(koMap[sInds[i]][koTimeInd].astype(bool)) #Is star observable at time ind sInds = sInds[tmpIndsbool] del tmpIndsbool except: sInds = np.asarray([],dtype=int) # 5.3 Filter off current occulter target star from detection list if old_occ_sInd is not None: sInds = sInds[np.where(sInds != old_occ_sInd)] occ_sInds = occ_sInds[(occ_sInds != old_occ_sInd)] # 6.1 Filter off any stars visited by the occulter more than the max number of times if np.any(occ_sInds): occ_sInds = occ_sInds[(self.occ_starVisits[occ_sInds] < self.occ_max_visits)] # 6.2 Filter off coronograph stars with too many visits and no detections no_dets = np.logical_and((self.starVisits[sInds] > self.n_det_remove), (self.sInd_detcounts[sInds] == 0)) sInds = sInds[np.where(np.invert(no_dets))[0]] max_dets = np.where(self.sInd_detcounts[sInds] < self.max_successful_dets)[0] sInds = sInds[max_dets] # 7 Filter off cornograph stars with too-long inttimes if self.occ_arrives > TK.currentTimeAbs: available_time = self.occ_arrives - TK.currentTimeAbs.copy() if np.any(sInds[intTimes[sInds] < available_time]): sInds = sInds[intTimes[sInds] < available_time] # 8 remove occ targets on ignore_stars list occ_sInds = np.setdiff1d(occ_sInds, np.intersect1d(occ_sInds, self.ignore_stars)) t_det = 0*u.d occ_sInd = old_occ_sInd if np.any(sInds): # choose sInd of next target sInd = self.choose_next_telescope_target(old_sInd, sInds, intTimes[sInds]) # store relevant values t_det = intTimes[sInd] # 8 Choose best target from remaining # if the starshade has arrived at its destination, or it is the first observation if np.any(occ_sInds): if old_occ_sInd is None or ((TK.currentTimeAbs.copy() + t_det) >= self.occ_arrives and self.ready_to_update): occ_sInd = self.choose_next_occulter_target(old_occ_sInd, occ_sInds, occ_intTimes) if old_occ_sInd is None: self.occ_arrives = TK.currentTimeAbs.copy() else: self.occ_arrives = occ_startTimes[occ_sInd] self.occ_slewTime = slewTimes[occ_sInd] self.occ_sd = sd[occ_sInd] self.ready_to_update = False elif not np.any(sInds): TK.advanceToAbsTime(TK.currentTimeAbs.copy() + 1*u.d) continue if occ_sInd is not None: sInds = sInds[(sInds != occ_sInd)] if self.tot_det_int_cutoff < self.tot_dettime: sInds = np.array([]) if np.any(sInds): # choose sInd of next target sInd = self.choose_next_telescope_target(old_sInd, sInds, intTimes[sInds]) # store relevant values t_det = intTimes[sInd] else: sInd = None # if no observable target, call the TimeKeeping.wait() method if not np.any(sInds) and not np.any(occ_sInds): self.vprint('No Observable Targets at currentTimeNorm= ' + str(TK.currentTimeNorm.copy())) return DRM, None, None, None, None, None break else: self.logger.info('Mission complete: no more time available') self.vprint( 'Mission complete: no more time available') return DRM, None, None, None, None, None if TK.mission_is_over(OS, Obs, det_mode): self.logger.info('Mission complete: no more time available') self.vprint( 'Mission complete: no more time available') return DRM, None, None, None, None, None return DRM, sInd, occ_sInd, t_det, sd, occ_sInds def choose_next_occulter_target(self, old_occ_sInd, occ_sInds, intTimes): """Choose next target for the occulter based on truncated depth first search of linear cost function. Args: old_occ_sInd (integer): Index of the previous target star occ_sInds (integer array): Indices of available targets intTimes (astropy Quantity array): Integration times for detection in units of day Returns: sInd (integer): Index of next target star """ # Choose next Occulter target Comp = self.Completeness TL = self.TargetList TK = self.TimeKeeping OS = self.OpticalSystem # reshape sInds, store available top9 sInds occ_sInds = np.array(occ_sInds, ndmin=1) top_HIPs = self.occHIPs[:self.topstars] top_sInds = np.intersect1d(np.where(np.in1d(TL.Name, top_HIPs))[0], occ_sInds) # current stars have to be in the adjmat if (old_occ_sInd is not None) and (old_occ_sInd not in occ_sInds): occ_sInds = np.append(occ_sInds, old_occ_sInd) # get completeness values comps = Comp.completeness_update(TL, occ_sInds, self.occ_starVisits[occ_sInds], TK.currentTimeNorm.copy()) # if first target, or if only 1 available target, choose highest available completeness nStars = len(occ_sInds) if (old_occ_sInd is None) or (nStars == 1): occ_sInd = np.random.choice(occ_sInds[comps == max(comps)]) return occ_sInd # define adjacency matrix A = np.zeros((nStars, nStars)) # consider slew distance when there's an occulter r_ts = TL.starprop(occ_sInds, TK.currentTimeAbs.copy()) u_ts = (r_ts.to('AU').value.T/np.linalg.norm(r_ts.to('AU').value, axis=1)).T angdists = np.arccos(np.clip(np.dot(u_ts, u_ts.T), -1, 1)) A[np.ones((nStars),dtype=bool)] = angdists A = self.coeffs[0]*(A)/np.pi # add factor due to completeness A = A + self.coeffs[1]*(1 - comps) # add factor due to intTime intTimes[old_occ_sInd] = np.inf A = A + self.coeffs[2]*(intTimes[occ_sInds]/OS.intCutoff) # add factor for unvisited ramp for deep dive stars if np.any(top_sInds): # add factor for least visited deep dive stars f_uv = np.zeros(nStars) u1 = np.in1d(occ_sInds, top_sInds) u2 = self.occ_starVisits[occ_sInds]==min(self.occ_starVisits[top_sInds]) unvisited = np.logical_and(u1, u2) f_uv[unvisited] = float(TK.currentTimeNorm.copy()/TK.missionLife.copy())**2 A = A - self.coeffs[3]*f_uv self.coeff_data_a3.append([occ_sInds,f_uv]) # add factor for unvisited deep dive stars no_visits = np.zeros(nStars) #no_visits[u1] = np.ones(len(top_sInds)) u2 = self.occ_starVisits[occ_sInds]==0 unvisited = np.logical_and(u1, u2) no_visits[unvisited] = 1. A = A - self.coeffs[4]*no_visits self.coeff_data_a4.append([occ_sInds, no_visits]) self.coeff_time.append(TK.currentTimeNorm.copy().value) # add factor due to unvisited ramp f_uv = np.zeros(nStars) unvisited = self.occ_starVisits[occ_sInds]==0 f_uv[unvisited] = float(TK.currentTimeNorm.copy()/TK.missionLife.copy())**2 A = A - self.coeffs[5]*f_uv # add factor due to revisited ramp if self.occ_starRevisit.size != 0: f2_uv = 1 - (np.in1d(occ_sInds, self.occ_starRevisit[:,0])) A = A + self.coeffs[6]*f2_uv # kill diagonal A = A + np.diag(np.ones(nStars)*np.Inf) # take two traversal steps step1 = np.tile(A[occ_sInds==old_occ_sInd,:],(nStars,1)).flatten('F') step2 = A[np.array(np.ones((nStars,nStars)),dtype=bool)] tmp = np.nanargmin(step1+step2) occ_sInd = occ_sInds[int(np.floor(tmp/float(nStars)))] return occ_sInd def choose_next_telescope_target(self, old_sInd, sInds, t_dets): """Choose next telescope target based on star completeness and integration time. Args: old_sInd (integer): Index of the previous target star sInds (integer array): Indices of available targets t_dets (astropy Quantity array): Integration times for detection in units of day Returns: sInd (integer): Index of next target star """ Comp = self.Completeness TL = self.TargetList TK = self.TimeKeeping OS = self.OpticalSystem Obs = self.Observatory allModes = OS.observingModes nStars = len(sInds) # reshape sInds sInds = np.array(sInds,ndmin=1) # 1/ Choose next telescope target comps = Comp.completeness_update(TL, sInds, self.starVisits[sInds], TK.currentTimeNorm.copy()) # add weight for star revisits ind_rev = [] if self.starRevisit.size != 0: dt_rev = self.starRevisit[:,1]*u.day - TK.currentTimeNorm.copy() ind_rev = [int(x) for x in self.starRevisit[dt_rev < 0*u.d, 0] if x in sInds] f2_uv = np.where((self.starVisits[sInds] > 0) & (self.starVisits[sInds] < self.nVisitsMax), self.starVisits[sInds], 0) * (1 - (np.in1d(sInds, ind_rev, invert=True))) L = TL.L[sInds] l_extreme = max([np.abs(np.log10(np.min(TL.L[sInds]))), np.abs(np.log10(np.max(TL.L[sInds])))]) if l_extreme == 0.0: l_weight = 1 else: l_weight = 1 - np.abs(np.log10(TL.L[sInds])/l_extreme)**self.lum_exp t_weight = t_dets/np.max(t_dets) weights = ((comps + self.revisit_weight*f2_uv/float(self.nVisitsMax))/t_weight)*l_weight # weights = (comps + self.revisit_weight*f2_uv/float(self.nVisitsMax))*l_weight sInd = np.random.choice(sInds[weights == max(weights)]) #Check if exoplanetObsTime would be exceeded mode = list(filter(lambda mode: mode['detectionMode'] == True, allModes))[0] maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife = TK.get_ObsDetectionMaxIntTime(Obs, mode) maxIntTime = min(maxIntTimeOBendTime, maxIntTimeExoplanetObsTime, maxIntTimeMissionLife)#Maximum intTime allowed intTimes2 = self.calc_targ_intTime(np.array([sInd]), TK.currentTimeAbs.copy(), mode) if intTimes2 > maxIntTime: # check if max allowed integration time would be exceeded self.vprint('max allowed integration time would be exceeded') sInd = None waitTime = 1.*u.d return sInd def calc_int_inflection(self, t_sInds, fEZ, startTime, WA, mode, ischar=False): """Calculate integration time based on inflection point of Completeness as a function of int_time Args: t_sInds (integer array): Indices of the target stars fEZ (astropy Quantity array): Surface brightness of exo-zodiacal light in units of 1/arcsec2 startTime (astropy Quantity array): Surface brightness of local zodiacal light in units of 1/arcsec2 WA (astropy Quantity): Working angle of the planet of interest in units of arcsec mode (dict): Selected observing mode Returns: int_times (astropy quantity array): The suggested integration time """ Comp = self.Completeness TL = self.TargetList ZL = self.ZodiacalLight Obs = self.Observatory num_points = 500 intTimes = np.logspace(-5, 2, num_points)*u.d sInds = np.arange(TL.nStars) WA = self.WAint # don't use WA input because we don't know planet positions before characterization curve = np.zeros([1, sInds.size, intTimes.size]) Cpath = os.path.join(Comp.classpath, Comp.filename+'.fcomp') # if no preexisting curves exist, either load from file or calculate if self.curves is None: if os.path.exists(Cpath): self.vprint( 'Loading cached completeness file from "{}".'.format(Cpath)) with open(Cpath, 'rb') as cfile: curves = pickle.load(cfile) self.vprint( 'Completeness curves loaded from cache.') else: # calculate completeness curves for all sInds self.vprint( 'Cached completeness file not found at "{}".'.format(Cpath)) self.vprint( 'Beginning completeness curve calculations.') curves = {} for t_i, t in enumerate(intTimes): fZ = ZL.fZ(Obs, TL, sInds, startTime, mode) # curves[0,:,t_i] = OS.calc_dMag_per_intTime(t, TL, sInds, fZ, fEZ, WA, mode) curve[0,:,t_i] = Comp.comp_per_intTime(t, TL, sInds, fZ, fEZ, WA, mode) curves[mode['systName']] = curve with open(Cpath, 'wb') as cfile: pickle.dump(curves, cfile) self.vprint( 'completeness curves stored in {}'.format(Cpath)) self.curves = curves # if no curves for current mode if mode['systName'] not in self.curves.keys() or TL.nStars != self.curves[mode['systName']].shape[1]: for t_i, t in enumerate(intTimes): fZ = ZL.fZ(Obs, TL, sInds, startTime, mode) curve[0,:,t_i] = Comp.comp_per_intTime(t, TL, sInds, fZ, fEZ, WA, mode) self.curves[mode['systName']] = curve with open(Cpath, 'wb') as cfile: pickle.dump(self.curves, cfile) self.vprint( 'recalculated completeness curves stored in {}'.format(Cpath)) int_times = np.zeros(len(t_sInds))*u.d for i, sInd in enumerate(t_sInds): c_v_t = self.curves[mode['systName']][0,sInd,:] dcdt = np.diff(c_v_t)/np.diff(intTimes) # find the inflection point of the completeness graph if ischar is False: target_point = max(dcdt).value + 10*np.var(dcdt).value idc = np.abs(dcdt - target_point/(1*u.d)).argmin() int_time = intTimes[idc] int_time = int_time*self.starVisits[sInd] # update star completeness idx = (np.abs(intTimes - int_time)).argmin() comp = c_v_t[idx] TL.comp[sInd] = comp else: idt = np.abs(intTimes - max(intTimes)).argmin() idx = np.abs(c_v_t - c_v_t[idt]*.9).argmin() # idx = np.abs(comps - max(comps)*.9).argmin() int_time = intTimes[idx] comp = c_v_t[idx] int_times[i] = int_time int_times[int_times<2.000e-5*u.d] = 0.0 *u.d return int_times def observation_characterization(self, sInd, mode): """Finds if characterizations are possible and relevant information Args: sInd (integer): Integer index of the star of interest mode (dict): Selected observing mode for characterization Returns: characterized (integer list): Characterization status for each planet orbiting the observed target star including False Alarm if any, where 1 is full spectrum, -1 partial spectrum, and 0 not characterized fZ (astropy Quantity): Surface brightness of local zodiacal light in units of 1/arcsec2 systemParams (dict): Dictionary of time-dependant planet properties averaged over the duration of the integration SNR (float ndarray): Characterization signal-to-noise ratio of the observable planets. Defaults to None. intTime (astropy Quantity): Selected star characterization time in units of day. Defaults to None. """ OS = self.OpticalSystem ZL = self.ZodiacalLight TL = self.TargetList SU = self.SimulatedUniverse Obs = self.Observatory TK = self.TimeKeeping # selecting appropriate koMap koMap = self.koMaps[mode['syst']['name']] # find indices of planets around the target pInds = np.where(SU.plan2star == sInd)[0] pinds_earthlike = np.array([]) det = np.ones(pInds.size, dtype=bool) fEZs = SU.fEZ[pInds].to('1/arcsec2').value dMags = SU.dMag[pInds] WAs = SU.WA[pInds].to('arcsec').value FA = (det.size == pInds.size + 1) if FA == True: pIndsDet = np.append(pInds, -1)[det] else: pIndsDet = pInds[det] # initialize outputs, and check if any planet to characterize characterized = np.zeros(det.size, dtype=int) fZ = 0./u.arcsec**2 systemParams = SU.dump_system_params(sInd) # write current system params by default SNR = np.zeros(len(det)) intTime = None if len(det) == 0: # nothing to characterize HIP_sInds = np.where(np.in1d(TL.Name, self.occHIPs))[0] if sInd in HIP_sInds: startTime = TK.currentTimeAbs.copy() startTimeNorm = TK.currentTimeNorm.copy() intTime = self.calc_targ_intTime(np.array([sInd]), startTime, mode)[0] extraTime = intTime*(mode['timeMultiplier'] - 1.)#calculates extraTime # add a predetermined margin to the integration times intTime = intTime*(1 + self.charMargin) # apply time multiplier totTime = intTime*(mode['timeMultiplier']) # end times endTimes = startTime + totTime endTimesNorm = startTimeNorm + totTime # planets to characterize tochar = ((totTime > 0) & (totTime <= OS.intCutoff) & (endTimesNorm <= TK.OBendTimes[TK.OBnumber])) success = TK.allocate_time(intTime + extraTime + mode['syst']['ohTime'] + Obs.settlingTime, True)#allocates time if success == False or not tochar: intTime = None if sInd not in self.sInd_charcounts.keys(): self.sInd_charcounts[sInd] = characterized return characterized, fZ, systemParams, SNR, intTime # look for last detected planets that have not been fully characterized if (FA == False): # only true planets, no FA tochar = (self.fullSpectra[pIndsDet] != -2) else: # mix of planets and a FA truePlans = pIndsDet[:-1] tochar = np.append((self.fullSpectra[truePlans] == 0), True) # 1/ find spacecraft orbital START position and check keepout angle if np.any(tochar): # start times startTime = TK.currentTimeAbs.copy() startTimeNorm = TK.currentTimeNorm.copy() # planets to characterize koTimeInd = np.where(np.round(startTime.value)-self.koTimes.value==0)[0][0] # find indice where koTime is startTime[0] # wherever koMap is 1, the target is observable tochar[tochar] = koMap[sInd][koTimeInd] # 2/ if any planet to characterize, find the characterization times if np.any(tochar): # propagate the whole system to match up with current time # calculate characterization times at the detected fEZ, dMag, and WA pinds_earthlike = np.logical_and(np.array([(p in self.known_earths) for p in pIndsDet]), tochar) fZ = ZL.fZ(Obs, TL, sInd, startTime, mode) fEZ = fEZs[tochar]/u.arcsec**2 WAp = self.WAint[sInd]*np.ones(len(tochar)) dMag = self.dMagint[sInd]*np.ones(len(tochar)) # if lucky_planets, use lucky planet params for dMag and WA if SU.lucky_planets: phi = (1/np.pi)*np.ones(len(SU.d)) e_dMag = deltaMag(SU.p, SU.Rp, SU.d, phi) # delta magnitude e_WA = np.arctan(SU.a/TL.dist[SU.plan2star]).to('arcsec')# working angle else: e_dMag = SU.dMag e_WA = SU.WA WAp[pinds_earthlike[tochar]] = e_WA[pIndsDet[pinds_earthlike]] dMag[pinds_earthlike[tochar]] = e_dMag[pIndsDet[pinds_earthlike]] intTimes = np.zeros(len(tochar))*u.day if self.int_inflection: for i,j in enumerate(WAp): if tochar[i]: intTimes[i] = self.calc_int_inflection([sInd], fEZ[i], startTime, j, mode, ischar=True)[0] else: intTimes[tochar] = OS.calc_intTime(TL, sInd, fZ, fEZ, dMag, WAp, mode) # add a predetermined margin to the integration times intTimes = intTimes*(1 + self.charMargin) # apply time multiplier totTimes = intTimes*(mode['timeMultiplier']) # end times endTimes = startTime + totTimes endTimesNorm = startTimeNorm + totTimes # planets to characterize tochar = ((totTimes > 0) & (totTimes <= OS.intCutoff) & (endTimesNorm <= TK.OBendTimes[TK.OBnumber])) # 3/ is target still observable at the end of any char time? if np.any(tochar) and Obs.checkKeepoutEnd: koTimeInds = np.zeros(len(endTimes.value[tochar]),dtype=int) # find index in koMap where each endTime is closest to koTimes for t,endTime in enumerate(endTimes.value[tochar]): if endTime > self.koTimes.value[-1]: # case where endTime exceeds largest koTimes element endTimeInBounds = np.where(np.floor(endTime)-self.koTimes.value==0)[0] koTimeInds[t] = endTimeInBounds[0] if endTimeInBounds.size is not 0 else -1 else: koTimeInds[t] = np.where(np.round(endTime)-self.koTimes.value==0)[0][0] # find indice where koTime is endTimes[0] tochar[tochar] = [koMap[sInd][koT] if koT >= 0 else 0 for koT in koTimeInds] # 4/ if yes, perform the characterization for the maximum char time if np.any(tochar): #Save Current Time before attempting time allocation currentTimeNorm = TK.currentTimeNorm.copy() currentTimeAbs = TK.currentTimeAbs.copy() if np.any(np.logical_and(pinds_earthlike, tochar)): intTime = np.max(intTimes[np.logical_and(pinds_earthlike, tochar)]) else: intTime = np.max(intTimes[tochar]) extraTime = intTime*(mode['timeMultiplier'] - 1.)#calculates extraTime success = TK.allocate_time(intTime + extraTime + mode['syst']['ohTime'] + Obs.settlingTime, True)#allocates time if success == False: #Time was not successfully allocated #Identical to when "if char_mode['SNR'] not in [0, np.inf]:" in run_sim() char_intTime = None lenChar = len(pInds) + 1 if FA else len(pInds) characterized = np.zeros(lenChar, dtype=float) char_SNR = np.zeros(lenChar, dtype=float) char_fZ = 0./u.arcsec**2 char_systemParams = SU.dump_system_params(sInd) return characterized, char_fZ, char_systemParams, char_SNR, char_intTime pIndsChar = pIndsDet[tochar] log_char = ' - Charact. planet(s) %s (%s/%s detected)'%(pIndsChar, len(pIndsChar), len(pIndsDet)) self.logger.info(log_char) self.vprint(log_char) # SNR CALCULATION: # first, calculate SNR for observable planets (without false alarm) planinds = pIndsChar[:-1] if pIndsChar[-1] == -1 else pIndsChar SNRplans = np.zeros(len(planinds)) if len(planinds) > 0: # initialize arrays for SNR integration fZs = np.zeros(self.ntFlux)/u.arcsec**2 systemParamss = np.empty(self.ntFlux, dtype='object') Ss = np.zeros((self.ntFlux, len(planinds))) Ns = np.zeros((self.ntFlux, len(planinds))) # integrate the signal (planet flux) and noise dt = intTime/float(self.ntFlux) timePlus = Obs.settlingTime.copy() + mode['syst']['ohTime'].copy()#accounts for the time since the current time for i in range(self.ntFlux): # calculate signal and noise (electron count rates) if SU.lucky_planets: fZs[i] = ZL.fZ(Obs, TL, sInd, currentTimeAbs, mode)[0] Ss[i,:], Ns[i,:] = self.calc_signal_noise(sInd, planinds, dt, mode, fZ=fZs[i]) # allocate first half of dt timePlus += dt/2. # calculate current zodiacal light brightness fZs[i] = ZL.fZ(Obs, TL, sInd, currentTimeAbs + timePlus, mode)[0] # propagate the system to match up with current time SU.propag_system(sInd, currentTimeNorm + timePlus - self.propagTimes[sInd]) self.propagTimes[sInd] = currentTimeNorm + timePlus # save planet parameters systemParamss[i] = SU.dump_system_params(sInd) # calculate signal and noise (electron count rates) if not SU.lucky_planets: Ss[i,:], Ns[i,:] = self.calc_signal_noise(sInd, planinds, dt, mode, fZ=fZs[i]) # allocate second half of dt timePlus += dt/2. # average output parameters fZ = np.mean(fZs) systemParams = {key: sum([systemParamss[x][key] for x in range(self.ntFlux)])/float(self.ntFlux) for key in sorted(systemParamss[0])} # calculate planets SNR S = Ss.sum(0) N = Ns.sum(0) SNRplans[N > 0] = S[N > 0]/N[N > 0] # allocate extra time for timeMultiplier # if only a FA, just save zodiacal brightness in the middle of the integration else: totTime = intTime*(mode['timeMultiplier']) fZ = ZL.fZ(Obs, TL, sInd, TK.currentTimeAbs.copy() + totTime/2., mode)[0] # calculate the false alarm SNR (if any) SNRfa = [] if pIndsChar[-1] == -1: fEZ = fEZs[-1]/u.arcsec**2 dMag = dMags[-1] WA = WAs[-1]*u.arcsec C_p, C_b, C_sp = OS.Cp_Cb_Csp(TL, sInd, fZ, fEZ, dMag, WA, mode) S = (C_p*intTime).decompose().value N = np.sqrt((C_b*intTime + (C_sp*intTime)**2).decompose().value) SNRfa = S/N if N > 0 else 0. # save all SNRs (planets and FA) to one array SNRinds = np.where(det)[0][tochar] SNR[SNRinds] = np.append(SNRplans, SNRfa) # now, store characterization status: 1 for full spectrum, # -1 for partial spectrum, 0 for not characterized char = (SNR >= mode['SNR']) # initialize with full spectra characterized = char.astype(int) WAchar = WAs[char]*u.arcsec # find the current WAs of characterized planets WA = WAs*u.arcsec if FA: WAs = np.append(WAs, WAs[-1]*u.arcsec) all_full = np.copy(characterized) all_full[char] = 1 if sInd not in self.sInd_charcounts.keys(): self.sInd_charcounts[sInd] = all_full else: self.sInd_charcounts[sInd] = self.sInd_charcounts[sInd] + all_full # encode results in spectra lists (only for planets, not FA) charplans = characterized[:-1] if FA else characterized self.fullSpectra[pInds[charplans == 1]] += 1 self.partialSpectra[pInds[charplans == -1]] += 1 # in both cases (detection or false alarm), schedule a revisit smin = np.min(SU.s[pInds[det]]) Ms = TL.MsTrue[sInd] # if target in promoted_stars list, schedule revisit based off of semi-major axis if sInd in self.promoted_stars: sp = np.min(SU.a[pInds[det]]).to('AU') if np.any(det): pInd_smin = pInds[det][np.argmin(SU.a[pInds[det]])] Mp = SU.Mp[pInd_smin] else: Mp = SU.Mp.mean() mu = const.G*(Mp + Ms) T = 2.*np.pi*np.sqrt(sp**3/mu) t_rev = TK.currentTimeNorm.copy() + T/3. # otherwise schedule revisit based off of seperation elif smin is not None: sp = smin if np.any(det): pInd_smin = pInds[det][np.argmin(SU.s[pInds[det]])] Mp = SU.Mp[pInd_smin] else: Mp = SU.Mp.mean() mu = const.G*(Mp + Ms) T = 2.*np.pi*np.sqrt(sp**3/mu) t_rev = TK.currentTimeNorm.copy() + T/2. # otherwise, revisit based on average of population semi-major axis and mass else: sp = SU.s.mean() Mp = SU.Mp.mean() mu = const.G*(Mp + Ms) T = 2.*np.pi*np.sqrt(sp**3/mu) t_rev = TK.currentTimeNorm.copy() + 0.75*T # finally, populate the revisit list (NOTE: sInd becomes a float) revisit = np.array([sInd, t_rev.to('day').value]) if self.occ_starRevisit.size == 0: self.occ_starRevisit = np.array([revisit]) else: revInd = np.where(self.occ_starRevisit[:,0] == sInd)[0] if revInd.size == 0: self.occ_starRevisit = np.vstack((self.occ_starRevisit, revisit)) else: self.occ_starRevisit[revInd, 1] = revisit[1] # add stars to filter list if np.any(characterized.astype(int) == 1): top_HIPs = self.occHIPs[:self.topstars] # if a top star has had max_successful_chars remove from list if np.any(self.sInd_charcounts[sInd] >= self.max_successful_chars): self.ignore_stars.append(sInd) # if a promoted star has an earthlike char, then ignore # if sInd in self.promoted_stars: # c_plans = pInds[charplans == 1] # if np.any(np.logical_and((SU.a[c_plans] > .95*u.AU),(SU.a[c_plans] < 1.67*u.AU))): # if np.any((.8*(SU.a[c_plans]**-.5).value < SU.Rp[c_plans].value) & (SU.Rp[c_plans].value < 1.4)): # self.ignore_stars.append(sInd) return characterized.astype(int), fZ, systemParams, SNR, intTime def revisitFilter(self, sInds, tmpCurrentTimeNorm): """Helper method for Overloading Revisit Filtering Args: sInds - indices of stars still in observation list tmpCurrentTimeNorm (MJD) - the simulation time after overhead was added in MJD form Returns: sInds - indices of stars still in observation list """ tovisit = np.zeros(self.TargetList.nStars, dtype=bool)#tovisit is a boolean array containing the if len(sInds) > 0:#so long as there is at least 1 star left in sInds tovisit[sInds] = ((self.starVisits[sInds] == min(self.starVisits[sInds])) \ & (self.starVisits[sInds] < self.nVisitsMax))# Checks that no star has exceeded the number of revisits if self.starRevisit.size != 0:#There is at least one revisit planned in starRevisit dt_rev = self.starRevisit[:,1]*u.day - tmpCurrentTimeNorm#absolute temporal spacing between revisit and now. #return indices of all revisits within a threshold dt_max of revisit day and indices of all revisits with no detections past the revisit time ind_rev2 = [int(x) for x in self.starRevisit[dt_rev < 0*u.d, 0] if (x in sInds)] tovisit[ind_rev2] = (self.starVisits[ind_rev2] < self.nVisitsMax) sInds = np.where(tovisit)[0] return sInds def scheduleRevisit(self, sInd, smin, det, pInds): """A Helper Method for scheduling revisits after observation detection Args: sInd - sInd of the star just detected smin - minimum separation of the planet to star of planet just detected det - pInds - Indices of planets around target star Return: updates self.starRevisit attribute """ TK = self.TimeKeeping TL = self.TargetList SU = self.SimulatedUniverse # in both cases (detection or false alarm), schedule a revisit # based on minimum separation Ms = TL.MsTrue[sInd] if smin is not None and np.nan not in smin: #smin is None if no planet was detected sp = smin if np.any(det): pInd_smin = pInds[det][np.argmin(SU.s[pInds[det]])] Mp = SU.Mp[pInd_smin] else: Mp = SU.Mp.mean() mu = const.G*(Mp + Ms) T = 2.*np.pi*np.sqrt(sp**3/mu) t_rev = TK.currentTimeNorm.copy() + T/2. # otherwise, revisit based on average of population semi-major axis and mass else: sp = SU.s.mean() Mp = SU.Mp.mean() mu = const.G*(Mp + Ms) T = 2.*np.pi*np.sqrt(sp**3/mu) t_rev = TK.currentTimeNorm.copy() + 0.75*T # if no detections then schedule revisit based off of revisit_wait t_rev = TK.currentTimeNorm.copy() + self.revisit_wait[sInd] # finally, populate the revisit list (NOTE: sInd becomes a float) revisit = np.array([sInd, t_rev.to('day').value]) if self.starRevisit.size == 0:#If starRevisit has nothing in it self.starRevisit = np.array([revisit])#initialize sterRevisit else: revInd = np.where(self.starRevisit[:,0] == sInd)[0]#indices of the first column of the starRevisit list containing sInd if revInd.size == 0: self.starRevisit = np.vstack((self.starRevisit, revisit)) else: self.starRevisit[revInd,1] = revisit[1]#over