Source code for rubin_scheduler.scheduler.model_observatory.model_observatory
__all__=("ModelObservatory",)importcopyimportwarningsimporthealpyashpimportnumpyasnpfromastropy.coordinatesimportEarthLocationfromastropy.timeimportTimeimportrubin_scheduler.skybrightness_preassbfromrubin_scheduler.dataimportdata_versionsfromrubin_scheduler.scheduler.featuresimportConditionsfromrubin_scheduler.scheduler.model_observatoryimportKinemModel# For backwards compatibilityfromrubin_scheduler.site_modelsimport(Almanac,CloudData,ConstantCloudData,ConstantSeeingData,ScheduledDowntimeData,SeeingData,SeeingModel,UnscheduledDowntimeMoreY1Data,)fromrubin_scheduler.utilsimport(DEFAULT_NSIDE,SURVEY_START_MJD,Site,_angular_separation,_approx_altaz2pa,_approx_ra_dec2_alt_az,_hpid2_ra_dec,_ra_dec2_hpid,calc_lmst,calc_season,m5_flat_sed,rotation_converter,)
[docs]classModelObservatory:"""Generate a realistic telemetry stream for the scheduler in simulations, including simulating the acquisition of observations. Parameters ---------- nside : `int`, optional The healpix nside resolution. Default None uses `set_default_nside()`. mjd : `float`, optional The MJD to start the model observatory for observations. Used to set current conditions and load sky. Default None uses mjd_start. mjd_start : `float`, optional The MJD of the start of the survey. This must be set to start of whole survey, for tracking purposes. Default None uses `survey_start_mjd()`. alt_min : `float`, optional The minimum altitude to compute models at (degrees). lax_dome : `bool`, optional Passed to observatory model. If true, allows dome creep. cloud_limit : `float`, optional. The limit to stop taking observations if the cloud model returns something equal or higher. Default of 0.3 is validated as a "somewhat pessimistic" weather downtime choice. sim_to_o : `sim_targetoO` object, optional If one would like to inject simulated ToOs into the telemetry stream. Default None adds no ToOs. park_after : `float`, optional Park the telescope after a gap longer than park_after (minutes). Default 10 minutes is used to park the telescope during downtime. init_load_length : `int`, optional The length of pre-scheduled sky brightness values to load initially (days). The default is 10 days; shorter values can be used for quicker load times. kinem_model : `~.scheduler.model_observatory.Kinem_model`, optional An instantiated rubin_scheduler Kinem_model object. Default of None uses a default Kinem_model. seeing_db : `str`, optional The filename of the seeing data database, if one would like to use an alternate seeing database. Default None uses the default seeing database. seeing_data : `~.site_models.SeeingData`-like, optional If one wants to replace the default seeing_data object. Should be an object with a __call__ method that takes MJD and returns zenith fwhm_500 in arcsec. Set to "ideal" to have constant 0.7" seeing. cloud_db : `str`, optional The filename of the cloud data database. Default of None uses the default database from rubin_sim_data. cloud_offset_year : `int`, optional The year offset to be passed to CloudData. Default 0. cloud_data : `~.site_models.CloudData`-like, optional If one wants to replace the default cloud data. Should be an object with a __call__ method that takes MJD and returns cloudy level. Set to "ideal" for no clouds. downtimes : `np.ndarray`, (N,3) or None, optional If one wants to replace the default downtimes. Should be a np.array with columns of "start" and "end" with MJD values and should include both scheduled and unscheduled downtime. Set to "ideal" for no downtime. Default of None will use the downtime models from `~.site_models.ScheduledDowntime` and `~.site_models.UnscheduledDowntime`. no_sky : `bool`, optional If True, then don't load any skybrightness files. Handy if one wants a well filled out Conditions object, but doesn't need the sky since that can be slower to load. Default False. wind_data : ~.site_models.WindData`-like, optional If one wants to replace the default wind_data object. Should be an object with a __call__ method that takes the time and returns a tuple with the wind speed (m/s) and originating direction (radians east of north). Default of None uses an idealized WindData object with no wind. starting_time_key : `str`, optional What key in the almanac to use to determine the start of observing on a night. Default "sun_n12_setting", e.g., sun at -12 degrees and setting. Other options are "sun_n18_setting" and "sunset". If surveys are not configured to wait until the sun is lower in the sky, observing will start as soon as the time passes the time returned by this key, in each night. ending_time_key : `str`, optional What key in the almanac to use to signify it is time to skip to the next night. Default "sun_n12_rising", e.g., sun at -12 degrees and rising. Other options are "sun_n18_rising" and "sunrise". sky_az_limits : `list` [[`float`, `float`]] A list of lists giving valid azimuth ranges. e.g., [0, 180] would mean all azimuth values are valid, while [[0, 90], [270, 360]] or [270, 90] would mean anywhere in the south is invalid. Degrees. sky_alt_limits : `list` [[`float`, `float`]] A list of lists giving valid altitude ranges. Degrees. For both the alt and az limits, if a particular alt (or az) value is included in any limit, it is valid for all of them. Altitude limits of [[20, 40], [40, 60]] would allow altitudes betwen 20-40 and 40-60, but [[20, 40], [40, 60], [20, 86]] will allow altitudes anywhere between 20-86 degrees. telescope : `str` Telescope name for rotation computations. Default "rubin". """def__init__(self,nside=DEFAULT_NSIDE,mjd=None,mjd_start=SURVEY_START_MJD,alt_min=5.0,lax_dome=True,cloud_limit=0.3,sim_to_o=None,park_after=10.0,init_load_length=10,kinem_model=None,seeing_db=None,seeing_data=None,cloud_db=None,cloud_offset_year=0,cloud_data=None,downtimes=None,no_sky=False,wind_data=None,starting_time_key="sun_n12_setting",ending_time_key="sun_n12_rising",sky_alt_limits=None,sky_az_limits=None,telescope="rubin",cloud_maps=None,):self.nside=nside# Set the time now - mjd# and the time of the survey startself.mjd_start=mjd_startifmjdisNone:mjd=mjd_startself.bandlist=["u","g","r","i","z","y"]self.cloud_limit=cloud_limitself.no_sky=no_skyself.alt_min=np.radians(alt_min)self.lax_dome=lax_domeself.starting_time_key=starting_time_keyself.ending_time_key=ending_time_keyself.sim__to_o=sim_to_oself.park_after=park_after/60.0/24.0# To days# Rotation converterself.rc=rotation_converter(telescope=telescope)# Create an astropy locationself.site=Site("LSST")self.location=EarthLocation(lat=self.site.latitude,lon=self.site.longitude,height=self.site.height)# Set up the almanac - use mjd_start to keep "night" count the same.self.almanac=Almanac(mjd_start=self.mjd_start)# Load up all the models we need# Use mjd_start to ensure models always initialize to the same# starting point in time.mjd_start_time=Time(self.mjd_start,format="mjd")# Set up the downtimeifisinstance(downtimes,str):ifdowntimes=="ideal":self.downtimes=np.array(list(zip([],[])),dtype=list(zip(["start","end"],[float,float])),)else:warnings.warn("Downtimes should be a string equal to ""'ideal', an array or None")elifdowntimesisNone:self.down_nights=[]self.sched_downtime_data=ScheduledDowntimeData(mjd_start_time)self.unsched_downtime_data=UnscheduledDowntimeMoreY1Data(mjd_start_time)sched_downtimes=self.sched_downtime_data()unsched_downtimes=self.unsched_downtime_data()down_starts=[]down_ends=[]fordtinsched_downtimes:down_starts.append(dt["start"].mjd)down_ends.append(dt["end"].mjd)fordtinunsched_downtimes:down_starts.append(dt["start"].mjd)down_ends.append(dt["end"].mjd)self.downtimes=np.array(list(zip(down_starts,down_ends)),dtype=list(zip(["start","end"],[float,float])),)self.downtimes.sort(order="start")# Make sure there aren't any overlapping downtimesdiff=self.downtimes["start"][1:]-self.downtimes["end"][0:-1]whilenp.min(diff)<0:# Should be able to do this without a loop, but this worksfori,dtinenumerate(self.downtimes[0:-1]):ifself.downtimes["start"][i+1]<dt["end"]:new_end=np.max([dt["end"],self.downtimes["end"][i+1]])self.downtimes[i]["end"]=new_endself.downtimes[i+1]["end"]=new_endgood=np.where(self.downtimes["end"]-np.roll(self.downtimes["end"],1)!=0)self.downtimes=self.downtimes[good]diff=self.downtimes["start"][1:]-self.downtimes["end"][0:-1]else:self.downtimes=downtimes# Set the wind dataself.wind_data=wind_data# Set up the seeing dataifseeing_data=="ideal":self.seeing_data=ConstantSeeingData()elifseeing_dataisnotNone:self.seeing_data=seeing_dataelse:self.seeing_data=SeeingData(mjd_start_time,seeing_db=seeing_db)self.seeing_model=SeeingModel()self.seeing_indx_dict={}fori,bandnameinenumerate(self.seeing_model.band_list):self.seeing_indx_dict[bandname]=iself.seeing_fwhm_eff={}forkeyinself.bandlist:self.seeing_fwhm_eff[key]=np.zeros(hp.nside2npix(self.nside),dtype=float)# Set up the cloud dataifcloud_data=="ideal":self.cloud_data=ConstantCloudData()elifcloud_dataisnotNone:self.cloud_data=cloud_dataelse:self.cloud_data=CloudData(mjd_start_time,cloud_db=cloud_db,offset_year=cloud_offset_year)# Set up the skybrightnessifnotself.no_sky:self.sky_model=sb.SkyModelPre(init_load_length=init_load_length)else:self.sky_model=None# Set up the kinematic modelifkinem_modelisNone:self.observatory=KinemModel(mjd0=self.mjd_start)else:self.observatory=kinem_model# Pick up tel alt and az limits from kwargsifsky_alt_limitsisnotNone:self.sky_alt_limits=np.radians(sky_alt_limits)else:self.sky_alt_limits=Noneifsky_az_limitsisnotNone:self.sky_az_limits=np.radians(sky_az_limits)else:self.sky_az_limits=None# Add the observatory alt/az limits to the user-defined limits# But we do have to be careful that we're not overriding more# restrictive limits that were already set.# So we'll just keep these separate.self.tel_alt_limits=copy.deepcopy([self.observatory.telalt_minpos_rad,self.observatory.telalt_maxpos_rad,])self.tel_az_limits=copy.deepcopy([self.observatory.telaz_minpos_rad,self.observatory.telaz_maxpos_rad])# Each of these limits will be treated as hard limits that we don't# want pointings to stray into, so add a pad around the valuesself.altaz_limit_pad=np.radians(2.0)# Let's make sure we're at an openable MJDgood_mjd=Falseto_set_mjd=mjdwhilenotgood_mjd:good_mjd,to_set_mjd=self.check_mjd(to_set_mjd)self.mjd=to_set_mjd# Create the map of the season offsets - this map is constantra,dec=_hpid2_ra_dec(nside,np.arange(hp.nside2npix(self.nside)))ra_deg=np.degrees(ra)self.season_map=calc_season(ra_deg,[self.mjd_start],self.mjd_start).flatten()# Set the sun_ra_start information, for the rolling footprintssun_moon_info=self.almanac.get_sun_moon_positions(self.mjd_start)self.sun_ra_start=sun_moon_info["sun_RA"]+0# Conditions object to update and return on request# (at present, this is not updated -- recreated, below).self.conditions=Conditions(nside=self.nside,)self.obs_id_counter=0self.cloud_maps=cloud_maps
[docs]defget_info(self):""" Returns ------- Array with model versions that were instantiated """# Could add in the data versionresult=[]versions=data_versions()forkeyinversions:result.append([key,versions[key]])returnresult
[docs]defreturn_conditions(self):""" Returns ------- rubin_scheduler.scheduler.features.conditions object """self.conditions=Conditions(nside=self.nside,mjd=self.mjd,)self.conditions.night=int(self.night)# Current time as astropy timecurrent_time=Time(self.mjd,format="mjd")# Clouds. XXX--just the raw valueself.conditions.bulk_cloud=self.cloud_data(current_time)# use conditions object itself to get approx altitude of each# healpxalts=self.conditions.altazs=self.conditions.azgood=np.where(alts>self.alt_min)# reset the seeingforkeyinself.seeing_fwhm_eff:self.seeing_fwhm_eff[key].fill(np.nan)# Use the model to get the seeing at this time and airmasses.fwhm_500=self.seeing_data(current_time)self.fwhm_500=fwhm_500seeing_dict=self.seeing_model(fwhm_500,self.conditions.airmass[good])fwhm_eff=seeing_dict["fwhmEff"]fori,keyinenumerate(self.seeing_model.band_list):self.seeing_fwhm_eff[key][good]=fwhm_eff[i,:]self.conditions.fwhm_eff=self.seeing_fwhm_eff# sky brightnessifself.sky_modelisnotNone:self.conditions.skybrightness=self.sky_model.return_mags(self.mjd)# Model observatory can continue to only refer to band and not filter# However, just to ensure set_auxtel_info and set_maintel_info# have something to pass in for filters (if those calls being used):self.conditions.mounted_filters=self.observatory.mounted_bandsself.conditions.current_filter=self.observatory.current_band[0]# And technically, these next lines are unnecessaryself.conditions.mounted_bands=self.observatory.mounted_bandsself.conditions.current_band=self.observatory.current_band[0]# Compute the slewtimesslewtimes=np.empty(alts.size,dtype=float)slewtimes.fill(np.nan)# If there has been a gap, park the telescopegap=self.mjd-self.observatory.last_mjdifgap>self.park_after:self.observatory.park()slewtimes[good]=self.observatory.slew_times(0.0,0.0,self.mjd,alt_rad=alts[good],az_rad=azs[good],bandname=self.observatory.current_band,lax_dome=self.lax_dome,update_tracking=False,)self.conditions.slewtime=slewtimes# Let's get the sun and moonsun_moon_info=self.almanac.get_sun_moon_positions(self.mjd)# convert these to scalarsforkeyinsun_moon_info:sun_moon_info[key]=sun_moon_info[key].max()self.conditions.moon_phase=sun_moon_info["moon_phase"]self.conditions.moon_alt=sun_moon_info["moon_alt"]self.conditions.moon_az=sun_moon_info["moon_az"]self.conditions.moon_ra=sun_moon_info["moon_RA"]self.conditions.moon_dec=sun_moon_info["moon_dec"]self.conditions.sun_alt=sun_moon_info["sun_alt"]self.conditions.sun_az=sun_moon_info["sun_az"]self.conditions.sun_ra=sun_moon_info["sun_RA"]self.conditions.sun_dec=sun_moon_info["sun_dec"]self.conditions.lmst=calc_lmst(self.mjd,self.site.longitude_rad)self.conditions.tel_ra=self.observatory.current_ra_radself.conditions.tel_dec=self.observatory.current_dec_radself.conditions.tel_alt=self.observatory.last_alt_radself.conditions.tel_az=self.observatory.last_az_radself.conditions.rot_tel_pos=self.observatory.last_rot_tel_pos_radself.conditions.cumulative_azimuth_rad=self.observatory.cumulative_azimuth_rad# Add in the almanac informationself.conditions.sunset=self.almanac.sunsets["sunset"][self.almanac_indx]self.conditions.sun_n12_setting=self.almanac.sunsets["sun_n12_setting"][self.almanac_indx]self.conditions.sun_n18_setting=self.almanac.sunsets["sun_n18_setting"][self.almanac_indx]self.conditions.sun_n18_rising=self.almanac.sunsets["sun_n18_rising"][self.almanac_indx]self.conditions.sun_n12_rising=self.almanac.sunsets["sun_n12_rising"][self.almanac_indx]self.conditions.sunrise=self.almanac.sunsets["sunrise"][self.almanac_indx]self.conditions.moonrise=self.almanac.sunsets["moonrise"][self.almanac_indx]self.conditions.moonset=self.almanac.sunsets["moonset"][self.almanac_indx]sun_moon_info_start_of_night=self.almanac.get_sun_moon_positions(self.conditions.sunset)self.conditions.moon_phase_sunset=sun_moon_info_start_of_night["moon_phase"]# Telescope limitsself.conditions.sky_az_limits=self.sky_az_limitsself.conditions.sky_alt_limits=self.sky_alt_limitsself.conditions.tel_alt_limits=self.tel_alt_limitsself.conditions.tel_az_limits=self.tel_az_limits# Planet positions from almanacself.conditions.planet_positions=self.almanac.get_planet_positions(self.mjd)# See if there are any ToOs to includeifself.sim__to_oisnotNone:toos=self.sim__to_o(self.mjd)iftoosisnotNone:self.conditions.targets_of_opportunity=toosifself.wind_dataisnotNone:wind_speed,wind_direction=self.wind_data(current_time)self.conditions.wind_speed=wind_speedself.conditions.wind_direction=wind_directionifself.cloud_mapsisnotNone:self.conditions.cloud_maps=self.cloud_mapsreturnself.conditions
[docs]defobservation_add_data(self,observation):""" Fill in the metadata for a completed observation """current_time=Time(self.mjd,format="mjd")observation["clouds"]=self.cloud_data(current_time)observation["airmass"]=1.0/np.cos(np.pi/2.0-observation["alt"])# Seeingfwhm_500=self.seeing_data(current_time)seeing_dict=self.seeing_model(fwhm_500,observation["airmass"])observation["FWHMeff"]=seeing_dict["fwhmEff"][self.seeing_indx_dict[observation["band"][0]]]observation["FWHM_geometric"]=seeing_dict["fwhmGeom"][self.seeing_indx_dict[observation["band"][0]]]observation["FWHM_500"]=fwhm_500observation["night"]=self.nightobservation["mjd"]=self.mjdifself.sky_modelisnotNone:hpid=_ra_dec2_hpid(self.sky_model.nside,observation["RA"],observation["dec"])observation["skybrightness"]=self.sky_model.return_mags(self.mjd,indx=[hpid],extrapolate=True)[observation["band"][0]]observation["fivesigmadepth"]=m5_flat_sed(observation["band"][0],observation["skybrightness"],observation["FWHMeff"],observation["exptime"]/observation["nexp"],observation["airmass"],nexp=observation["nexp"],)# If there is cloud extinction, apply it.ifself.cloud_mapsisnotNone:hpid=_ra_dec2_hpid(self.sky_model.nside,observation["RA"],observation["dec"])cloud_extinction=self.cloud_maps.extinction_closest(self.mjd,hpid)observation["fivesigmadepth"]-=cloud_extinctionobservation["cloud_extinction"]=cloud_extinctionlmst=calc_lmst(self.mjd,self.site.longitude_rad)observation["lmst"]=lmstsun_moon_info=self.almanac.get_sun_moon_positions(self.mjd)observation["sunAlt"]=sun_moon_info["sun_alt"]observation["sunAz"]=sun_moon_info["sun_az"]observation["sunRA"]=sun_moon_info["sun_RA"]observation["sunDec"]=sun_moon_info["sun_dec"]observation["moonAlt"]=sun_moon_info["moon_alt"]observation["moonAz"]=sun_moon_info["moon_az"]observation["moonRA"]=sun_moon_info["moon_RA"]observation["moonDec"]=sun_moon_info["moon_dec"]observation["moonDist"]=_angular_separation(observation["RA"],observation["dec"],observation["moonRA"],observation["moonDec"],)observation["solarElong"]=_angular_separation(observation["RA"],observation["dec"],observation["sunRA"],observation["sunDec"],)observation["moonPhase"]=sun_moon_info["moon_phase"]observation["ID"]=self.obs_id_counterself.obs_id_counter+=1returnobservation
[docs]defcheck_up(self,mjd):"""See if we are in downtime Returns -------- is_up, mjd : `bool`, `float` Returns (True, current_mjd) if telescope is up and (False, downtime_ends_mjd) if in downtime """result=True,mjdindx=np.searchsorted(self.downtimes["start"],mjd,side="right")-1ifindx>=0:ifmjd<self.downtimes["end"][indx]:result=False,self.downtimes["end"][indx]returnresult
[docs]defcheck_mjd(self,mjd,cloud_skip=20.0):"""See if an mjd is ok to observe Parameters ---------- cloud_skip : float (20) How much time to skip ahead if it's cloudy (minutes) Returns ------- mjd_ok : `bool` mdj : `float` If True, the input mjd. If false, a good mjd to skip forward to. """passed=Truenew_mjd=mjd+0# Maybe set this to a while loop to make sure we don't# land on another cloudy time?# or just make this an entire recursive call?clouds=self.cloud_data(Time(mjd,format="mjd"))ifclouds>self.cloud_limit:passed=Falsewhileclouds>self.cloud_limit:new_mjd=new_mjd+cloud_skip/60.0/24.0clouds=self.cloud_data(Time(new_mjd,format="mjd"))alm_indx=np.searchsorted(self.almanac.sunsets[self.starting_time_key],mjd,side="right")-1# at the end of the night, advance to the next setting twilightifmjd>self.almanac.sunsets[self.ending_time_key][alm_indx]:passed=Falsenew_mjd=self.almanac.sunsets[self.starting_time_key][alm_indx+1]ifmjd<self.almanac.sunsets[self.starting_time_key][alm_indx]:passed=Falsenew_mjd=self.almanac.sunsets[self.starting_time_key][alm_indx+1]# We're in a down time, if down, advance to the end of the downtimeifnotself.check_up(mjd)[0]:passed=Falsenew_mjd=self.check_up(mjd)[1]# recursive call to make sure we skip far enough aheadifnotpassed:whilenotpassed:passed,new_mjd=self.check_mjd(new_mjd)returnFalse,new_mjdelse:returnTrue,mjd
def_update_rot_sky_pos(self,observation):"""If we have an undefined rotSkyPos, try to fill it out."""# Grab the rotator limit from the observatory modelrot_limit=[self.observatory.telrot_minpos_rad+2.0*np.pi,self.observatory.telrot_maxpos_rad,]alt,az=_approx_ra_dec2_alt_az(observation["RA"],observation["dec"],self.site.latitude_rad,self.site.longitude_rad,self.mjd,)obs_pa=_approx_altaz2pa(alt,az,self.site.latitude_rad)# If the observation has a rotTelPos set, use it to compute rotSkyPosifnp.isfinite(observation["rotTelPos"]):observation["rotSkyPos"]=self.rc._rottelpos2rotskypos(observation["rotTelPos"],obs_pa)observation["rotTelPos"]=np.nanelse:# Try to fall back to rotSkyPos_desiredpossible_rot_tel_pos=self.rc._rotskypos2rottelpos(observation["rotSkyPos_desired"],obs_pa)# If in range, use rotSkyPos_desired for rotSkyPosif(possible_rot_tel_pos>rot_limit[0])|(possible_rot_tel_pos<rot_limit[1]):observation["rotSkyPos"]=observation["rotSkyPos_desired"]observation["rotTelPos"]=np.nanelse:# Fall back to the backup rotation angle if needed.observation["rotSkyPos"]=np.nanobservation["rotTelPos"]=observation["rotTelPos_backup"]returnobservation
[docs]defobserve(self,observation):"""Try to make an observation Returns ------- observation : observation object None if there was no observation taken. Completed observation with meta data filled in. new_night : bool Have we started a new night. """start_night=self.night.copy()ifnp.isnan(observation["rotSkyPos"]):observation=self._update_rot_sky_pos(observation)# If there has been a long gap, assume telescope stopped# tracking and parkedgap=self.mjd-self.observatory.last_mjdifgap>self.park_after:self.observatory.park()# Compute what alt,az we have tracked to (or are parked at)start_alt,start_az,start_rot_tel_pos=self.observatory.current_alt_az(self.mjd)# Slew to new position and execute observation. Use the# requested rotTelPos position, obsevation['rotSkyPos'] will# be ignored.slewtime,visittime=self.observatory.observe(observation,self.mjd,rot_tel_pos=observation["rotTelPos"],lax_dome=self.lax_dome,)# inf slewtime means the observation failed (probably outside# alt limits)if~np.all(np.isfinite(slewtime)):returnNone,Falseobservation_worked,new_mjd=self.check_mjd(self.mjd+(slewtime+visittime)/24.0/3600.0)ifobservation_worked:observation["visittime"]=visittimeobservation["slewtime"]=slewtimeobservation["slewdist"]=_angular_separation(start_az,start_alt,self.observatory.last_az_rad,self.observatory.last_alt_rad,)self.mjd=self.mjd+slewtime/24.0/3600.0# Reach into the observatory model to pull out the# relevant data it has calculated# Not bothering to fetch alt,az,pa,rottelpos as those# were computed before the slew was executed# so will be off by seconds to minutes. And they# shouldn't be needed by the scheduler.observation["rotSkyPos"]=self.observatory.current_rot_sky_pos_radobservation["cummTelAz"]=self.observatory.cumulative_azimuth_rad# But we do need the altitude to# get the airmass and then 5-sigma depth# this altitude should get clobbered later# by sim_runner.observation["alt"]=self.observatory.last_alt_rad# Metadata on observation is after slew and settle,# so at start of exposure.result=self.observation_add_data(observation)self.mjd=self.mjd+visittime/24.0/3600.0new_night=Falseelse:result=Noneself.observatory.park()# Skip to next legitimate mjdself.mjd=new_mjdnow_night=self.nightifnow_night==start_night:new_night=Falseelse:new_night=Truereturnresult,new_night
# methods to reach through and adjust the kinematic model if desireddefsetup_camera(self,**kwargs):self.observatory.setup_camera(**kwargs)defsetup_dome(self,**kwargs):self.observatory.setup_dome(**kwargs)defsetup_telescope(self,**kwargs):self.observatory.setup_telescope(**kwargs)defsetup_setup_optics(self,**kwargs):self.observatory.setup_optics(**kwargs)