def __call__(self, telescope: ITelescope) -> Future: """Move telescope. Args: telescope: Telescope to use. Returns: Future for the movement call. """ if self._initialized: return Future(empty=True) self._initialized = True # calculate Alt/Az position of sun sun = self.observer.sun_altaz(Time.now()) log.info('Sun is currently located at alt=%.2f°, az=%.2f°', sun.alt.degree, sun.az.degree) # get sweet spot for flat-fielding altaz = SkyCoord(alt=80 * u.deg, az=sun.az + 180 * u.degree, obstime=Time.now(), location=self.observer.location, frame='altaz') log.info('Sweet spot for flat fielding is at alt=80°, az=%.2f°', altaz.az.degree) # move telescope log.info('Moving telescope to Alt=80, Az=%.2f...', altaz.az.degree) return telescope.move_altaz(80, float(altaz.az.degree))
def fetch_tasks(self, end_after: Time, start_before: Time, state: str = 'PENDING') -> Dict[str, Task]: """Fetch tasks from portal. Args: end_after: Task must end after this time. start_before: Task must start before this time. state: State of tasks. Returns: Dictionary with tasks. Raises: Timeout if request timed out. ValueError if something goes wrong. """ # get url and params url = urljoin(self._url, '/api/observations/') params = { 'site': self._site, 'end_after': end_after.isot, 'start_before': start_before.isot, 'state': state } # do request r = requests.get(url, params=params, headers=self._header, timeout=10, proxies=self._proxies) # success? if r.status_code != 200: raise ValueError() # get schedule schedules = r.json()['results'] # create tasks tasks = {} for sched in schedules: # parse start and end sched['start'] = Time(sched['start']) sched['end'] = Time(sched['end']) # create task task = self._create_task(LcoTask, sched, scripts=self.scripts) tasks[sched['request']['id']] = task # finished return tasks
def update_gui(self) -> None: # enable myself and set filter self.setEnabled(True) # get current weather cur = self._current_weather["sensors"] # update current if "sensors" in self._current_weather: # get current list of sensors current_sensors = list(sorted(cur.keys())) # did it change? if current_sensors != self._current_sensors: layout = self.frameCurrent.layout() # remove all widgets from frameCurrent for w in self._current_widgets.values(): w.setParent(None) # add time self._current_widgets["time"] = WidgetCurrentSensor("Time", "") layout.addWidget(self._current_widgets["time"]) # loop sensor types for sensor in AVERAGE_SENSOR_FIELDS: if sensor["field"] in current_sensors: widget = WidgetCurrentSensor(sensor["label"], sensor["unit"]) self._current_widgets[sensor["field"]] = widget layout.addWidget(widget) # set time if "time" in self._current_weather and self._current_weather[ "time"] is not None: t = Time(self._current_weather["time"]) self._current_widgets["time"].set_value( t.strftime("%Y-%m-%d\n%H:%M:%S")) else: self._current_widgets["time"].set_value("") # set values for sensor in AVERAGE_SENSOR_FIELDS: f = sensor["field"] if f in current_sensors: format = "%d" if f == "rain" else "%.2f" s = "N/A" if cur[f][ "value"] is None else format % cur[f]["value"] self._current_widgets[f].set_value(s) self._current_widgets[f].set_good(cur[f]["good"]) # store it self._current_sensors = current_sensors
async def _init_system(self, telescope: ITelescope, camera: Union[ICamera, IExposureTime], filters: Optional[IFilters] = None) -> None: """Initialize whole system.""" # which twilight are we in? if self.observer is None: raise ValueError("No observer given.") sun = self.observer.sun_altaz(Time.now()) sun_10min = self.observer.sun_altaz(Time.now() + TimeDelta(10 * u.minute)) self._twilight = (FlatFielder.Twilight.DUSK if sun_10min.alt.degree < sun.alt.degree else FlatFielder.Twilight.DAWN) log.info("We are currently in %s twilight.", self._twilight.value) # do initial check if not self._initial_check(): return # set binning if isinstance(camera, IBinning): log.info("Setting binning to %dx%d...", self._cur_binning[0], self._cur_binning[1]) await camera.set_binning(*self._cur_binning) # get bias level self._bias_level = await self._get_bias(camera) # move telescope future_track = self._pointing( telescope) if self._pointing is not None else None # get filter from first step and set it if filters is not None and self._cur_filter is not None: log.info("Setting filter to %s...", self._cur_filter) future_filter = await filters.set_filter(self._cur_filter) else: future_filter = Future(empty=True) # wait for both await Future.wait_all([future_track, future_filter]) log.info("Finished initializing system.") # change stats log.info("Waiting for flat-field time...") self._state = FlatFielder.State.WAITING
async def _wait(self) -> None: """Wait for flat-field time.""" # get solar elevation and evaluate function sun_alt, self._exptime = self._eval_function(Time.now()) log.info( "Calculated optimal exposure time of %.2fs in %dx%d at solar elevation of %.2f°.", self._exptime, self._cur_binning[0], self._cur_binning[1], sun_alt, ) # then evaluate exposure time within a larger range state = self._eval_exptime(self._min_exptime * 0.5, self._max_exptime * 2.0) if state < 0: log.info("Sleeping a little...") await event_wait(self._abort, 10) elif state == 0: log.info("Starting to take test flat-fields...") self._state = FlatFielder.State.TESTING else: log.info("Missed flat-fielding time, finish task...") self._state = FlatFielder.State.FINISHED
def __call__(self): # get all reduced skyflat frames of the last 100 days now = Time.now() frames = self._archive.list_frames(start=now - TimeDelta(100 * u.day), end=now, site=self._site, instrument=self._instrument, image_type=ImageType.SKYFLAT, rlevel=1) # get priorities from_archive = {} for f in frames: # get number of days since flat was taken, which is our priority prio = (now - f.dateobs).sec / 86400. # get key in priorities key = (f.filter_name, f.binning) # need to update it? if key not in from_archive or prio < from_archive[key]: from_archive[key] = prio # create priorities priorities = {} for fn in self._filter_names: for b in self._binnings: priorities[fn, b] = from_archive[fn, b] if ( fn, b) in from_archive else 100. # finished return priorities
def _initial_check(self) -> bool: """Do a quick initial check. Returns: False, if flat-field time for this filter is over, True otherwise. """ # get solar elevation and evaluate function sun_alt, self._exptime = self._eval_function(Time.now()) log.info( "Calculated optimal exposure time of %.2fs in %dx%d at solar elevation of %.2f°.", self._exptime, self._cur_binning[0], self._cur_binning[1], sun_alt, ) # then evaluate exposure time within a larger range state = self._eval_exptime(self._min_exptime * 0.5, self._max_exptime * 2.0) if state > 0: log.info("Missed flat-fielding time, finishing task...") self._state = FlatFielder.State.FINISHED return False else: log.info("Flat-field time is still coming, keep going...") return True
def _calc_dest_helioprojective_radial(self) -> None: # get sun sun = self.observer.sun_altaz(Time.now()) sun_radec = sun.icrs # display self._show_dest_coords(sun_radec.ra, sun_radec.dec, sun.alt, sun.az)
def _calc_dest_heliographic_stonyhurst(self) -> None: # get sun sun = self.observer.sun_altaz(Time.now()) sun_radec = sun.icrs # display self._show_dest_coords(sun_radec.ra, sun_radec.dec, sun.alt, sun.az)
def _calc_dest_equatorial(self, clear: bool = True) -> None: """Called, whenever RA/Dec input changes. Calculates destination.""" # reset fields if clear: self.textSimbadName.clear() self.comboSolarSystemBody.setCurrentText("") self.textJplHorizonsName.clear() # parse RA/Dec try: ra_dec = SkyCoord( self.textMoveRA.text() + " " + self.textMoveDec.text(), frame=ICRS, unit=(u.hour, u.deg), ) except ValueError: # on error, show it self._show_dest_coords() return # to alt/az alt_az = self.observer.altaz(Time.now(), ra_dec) # display self._show_dest_coords(ra_dec.ra, ra_dec.dec, alt_az.alt, alt_az.az)
def _select_solar_system(self, body: str) -> None: """Set RA/Dec for selected solar system body.""" from astropy.coordinates import solar_system_ephemeris, get_body # nothing? if body == "": return # clear simbad and JPL self.textSimbadName.clear() self.textJplHorizonsName.clear() QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) with solar_system_ephemeris.set("builtin"): # get coordinates body = get_body(body, Time.now(), self.observer.location) QtWidgets.QApplication.restoreOverrideCursor() # set them self.textMoveRA.setText( body.ra.to_string(unit=u.hour, sep=" ", precision=2)) self.textMoveDec.setText(body.dec.to_string(sep=" ", precision=2)) # update destination self._calc_dest_equatorial(clear=False)
def _move_altaz(self, alt: float, az: float, abort_event: threading.Event): """Actually moves to given coordinates. Must be implemented by derived classes. Args: alt: Alt in deg to move to. az: Az in deg to move to. abort_event: Event that gets triggered when movement should be aborted. Raises: Exception: On error. """ # alt/az coordinates to ra/dec coords = SkyCoord(alt=alt * u.degree, az=az * u.degree, obstime=Time.now(), location=self.location, frame='altaz') icrs = coords.icrs # send event self.comm.send_event(TelescopeMovingEvent(alt=alt, az=az)) # start slewing self.__move(icrs.ra.degree, icrs.dec.degree, abort_event)
def _update_thread(self): # time of last change in blocks last_change = None # run forever while not self.closing.is_set(): # not running? if self._running is False: self.closing.wait(1) continue # got new time of last change? t = self._task_archive.last_changed() if last_change is None or last_change < t: # get schedulable blocks and sort them log.info( 'Found update in schedulable block, downloading them...') self._blocks = sorted( self._task_archive.get_schedulable_blocks(), key=lambda x: json.dumps(x.configuration, sort_keys=True)) log.info('Downloaded %d schedulable block(s).', len(self._blocks)) # schedule update log.info('Triggering scheduler run...') self._need_update = True # remember now last_change = Time.now() # sleep a little self.closing.wait(5)
async def _on_focus_found(self, event: Event, sender: str) -> bool: """Receive FocusFoundEvent. Args: event: The event itself sender: The name of the sender. """ if not isinstance(event, FocusFoundEvent): raise ValueError("Not a focus event.") log.info("Received new focus of %.4f +- %.4f.", event.focus, event.error) # collect values for model values = await self._get_values() # add focus and datetime values["focus"] = event.focus values["error"] = event.error values["datetime"] = Time.now().isot values["filter"] = event.filter_name # write log if self._publisher is not None: await self._publisher(**values) # finally, calculate new model log.info("Re-calculating model...") await self._calc_focus_model() # finished log.info("Done.") return True
async def _update_celestial_headers(self) -> None: """Calculate positions and distances to celestial objects like moon and sun.""" # get now now = Time.now() alt: Optional[float] az: Optional[float] # no observer? if self.observer is None: return # get telescope alt/az try: alt, az = await self.get_altaz() tel_altaz = SkyCoord(alt=alt * u.deg, az=az * u.deg, frame="altaz") except: alt, az, tel_altaz = None, None, None # get current moon and sun information moon_altaz = self.observer.moon_altaz(now) moon_frac = self.observer.moon_illumination(now) sun_altaz = self.observer.sun_altaz(now) # calculate distance to telescope moon_dist = tel_altaz.separation(moon_altaz) if tel_altaz is not None else None sun_dist = tel_altaz.separation(sun_altaz) if tel_altaz is not None else None # store it self._celestial_headers = { "MOONALT": (float(moon_altaz.alt.degree), "Lunar altitude"), "MOONFRAC": (float(moon_frac), "Fraction of the moon illuminated"), "MOONDIST": (None if moon_dist is None else float(moon_dist.degree), "Lunar distance from target"), "SUNALT": (float(sun_altaz.alt.degree), "Solar altitude"), "SUNDIST": (None if sun_dist is None else float(sun_dist.degree), "Solar Distance from Target"), }
def _find_master(self, image_class: Type[CalibrationImage], instrument: str, binning: str, filter_name: str = None) -> Optional[Image]: """Find master calibration frame for given parameters using a cache. Args: image_class: Image class. instrument: Instrument name. binning: Binning. filter_name: Name of filter. Returns: Image or None """ # is in cache? if (image_class, instrument, binning, filter_name) in self._master_frames: return self._master_frames[image_class, instrument, binning, filter_name] # try to download one midnight = Time(self._night + ' 23:59:59') frame = image_class.find_master(self._archive, midnight, instrument, binning, filter_name) if frame is not None: # download it calib = self._archive.download_frames([frame])[0] # store and return it self._master_frames[image_class, instrument, binning, filter_name] = calib return calib else: # still nothing return None
def __init__(self, state='ATTEMPTED', reason=''): """Initializes a new Status with an ATTEMPTED.""" self.start = Time.now() self.end = None self.state = state self.reason = reason self.time_completed = 0
async def __call__(self, telescope: ITelescope) -> None: """Move telescope. Args: telescope: Telescope to use. Returns: Future for the movement call. """ if self._initialized: return self._initialized = True # calculate Alt/Az position of sun now = Time.now() sun = self.observer.sun_altaz(now) log.info("Sun is currently located at alt=%.2f°, az=%.2f°", sun.alt.degree, sun.az.degree) # get sweet spot for flat-fielding altaz = SkyCoord(alt=80 * u.deg, az=sun.az + 180 * u.degree, obstime=now, location=self.observer.location, frame="altaz") log.info("Sweet spot for flat fielding is at alt=80°, az=%.2f°", altaz.az.degree) # move telescope log.info("Moving telescope to Alt=80, Az=%.2f...", altaz.az.degree) return await telescope.move_altaz(80, float(altaz.az.degree))
async def test_scheduler(): # init observer and time observer = Observer.at_site('SAAO') now = Time('2019-11-21T17:10:00Z') # have some test functions functions = { 'B': ' exp(-1.22034 * (h + 3.16086))', 'V': ' exp(-1.27565 * (h + 3.48265))', 'R': ' exp(-1.39148 * (h + 3.63401))', } # set constant priorities priorities = ConstSkyflatPriorities({('B', (1, 1)): 1, ('V', (1, 1)): 2, ('R', (1, 1)): 3}) # create scheduler scheduler = Scheduler(functions, priorities, observer) await scheduler(now) # test order assert scheduler[0].filter_name == 'B' assert scheduler[1].filter_name == 'V' assert scheduler[2].filter_name == 'R' # test start/end times assert scheduler[0].start == 1160 assert pytest.approx(scheduler[0].end, 0.01) == 1200.46 assert scheduler[1].start == 1270 assert pytest.approx(scheduler[1].end, 0.01) == 1310.59 assert scheduler[2].start == 1330 assert pytest.approx(scheduler[2].end, 0.01) == 1370.59
def _on_focus_found(self, event: FocusFoundEvent, sender: str): """Receive FocusFoundEvent. Args: event: The event itself sender: The name of the sender. """ log.info('Received new focus of %.4f +- %.4f.', event.focus, event.error) # collect values for model values = self._get_values() # add focus and datetime values['focus'] = event.focus values['error'] = event.error values['datetime'] = Time.now().isot values['filter'] = event.filter_name # write log if self._publisher is not None: self._publisher(**values) # finally, calculate new model log.info('Re-calculating model...') self._calc_focus_model() # finished log.info('Done.')
async def _on_task_finished(self, event: Event, sender: str) -> bool: """Reset current task, when it has finished. Args: event: The task finished event. sender: Who sent it. """ if not isinstance(event, TaskFinishedEvent): return False # reset current task self._current_task_id = None # trigger? if self._trigger_on_task_finished: # get ETA in minutes log.info( "Received task finished event, triggering new scheduler run..." ) # set it self._need_update = True self._schedule_start = Time.now() return True
async def _on_task_started(self, event: Event, sender: str) -> bool: """Re-schedule when task has started and we can predict its end. Args: event: The task started event. sender: Who sent it. """ if not isinstance(event, TaskStartedEvent): return False # store it self._current_task_id = event.id self._last_task_id = event.id # trigger? if self._trigger_on_task_started: # get ETA in minutes eta = (event.eta - Time.now()).sec / 60 log.info( "Received task started event with ETA of %.0f minutes, triggering new scheduler run...", eta) # set it self._need_update = True self._schedule_start = event.eta return True
def night_obs( self, time: Optional[Union[datetime.datetime, Time]] = None) -> datetime.date: """Returns the date of the night for the given night, i.e. the date of the start of the night. Args: time: Time to return night for. If none is given, current time is used. Returns: Night of observation. """ # None given? if time is None: time = Time.now() # convert to Time if isinstance(time, Time): time = time.datetime # get local datetime if not isinstance(time, datetime.datetime): raise ValueError("Invalid time") utc_dt = pytz.utc.localize(time) loc_dt = utc_dt.astimezone(self._timezone) # get night if loc_dt.hour < 15: loc_dt += datetime.timedelta(days=-1) return loc_dt.date()
def process_new_image_event(self, event: NewImageEvent, sender: str, *args, **kwargs): """Puts a new images in the DB with the given ID. Args: event: New image event sender: Who sent the event? Returns: Success """ # filter by source if self._sources is not None and sender not in self._sources: return # put into queue log.info('Received new image event from %s.', sender) # download image try: log.info('Downloading file %s...', event.filename) image = self.vfs.read_image(event.filename) except FileNotFoundError: log.error('Could not download image.') return # get catalog cat = image.catalog if cat is None: # no catalog found in file return # filter by ellipticity cat = cat[cat['ellipticity'] < self._max_ellipticity] # get WCS and pixel size wcs = WCS(image.header) pix_size = abs(proj_plane_pixel_scales(wcs)[0] * 3600.) # calculate seeing seeing = np.mean(cat['fwhm']) * pix_size # correct for airmass? if self._correct_for_airmass: # Seeing S as function of seeing S0 at zenith and airmass a: # S = S0 * a^0.6 # see https://www.astro.auth.gr/~seeing-gr/seeing_gr_files/theory/node17.html # need airmass if 'AIRMASS' in image.header: seeing /= image.header['AIRMASS']**0.6 else: # could not correct return # log it if self._publisher is not None: self._publisher(time=Time.now().isot, seeing=seeing)
def from_dict(cls, d: Dict[str, Any]) -> Event: # get eta eta: Optional[Time] = None if "eta" in d and isinstance(d["eta"], str): eta = Time(d["eta"]) # return object return GoodWeatherEvent(eta=eta)
def __init__(self, info: Dict[str, str]): FrameInfo.__init__(self) self.info = info self.id = self.info["id"] self.filename = self.info["basename"] self.dateobs = Time(self.info["DATE_OBS"]) self.filter_name = self.info["FILTER"] self.binning = int(self.info["binning"][0]) self.url = self.info["url"]
def parse_cli(parser: argparse.ArgumentParser): from pyobs.utils.time import Time # parse args args = parser.parse_args() # set debug time now if args.debug_time is not None: # calculate difference between now and given time delta = Time(args.debug_time) - Time.now() Time.set_offset_to_now(delta) # get full path of config if args.config: args.config = os.path.abspath(args.config) # finished return vars(args)
async def _find_master(self, image: Image, image_type: ImageType) -> Optional[Image]: """Find master calibration frame for given parameters using a cache. Args: image_type: image type. Returns: Image or None Raises: ValueError: if no calibration frame could be found. """ # get mode try: instrument = image.header["INSTRUME"] binning = "{0}x{0}".format(image.header["XBINNING"]) filter_name = cast( str, image.header["FILTER"]) if "FILTER" in image.header else None time = Time(image.header["DATE-OBS"]) mode = image_type, instrument, binning, filter_name except KeyError: # could not fetch header items raise ValueError("Could not fetch items from image header.") # is in cache? for m, item in Calibration.calib_cache: if m == mode: return item # try to download one master = await Pipeline.find_master( self._archive, image_type, time, instrument, binning, None if image_type in [ImageType.BIAS, ImageType.DARK] else filter_name, max_days=30, ) # nothing? if master is None: raise ValueError("No master frame found.") # store it in cache Calibration.calib_cache.append((mode, master)) # too many entries? while len(Calibration.calib_cache) > self._max_cache_size: Calibration.calib_cache.pop(0) # return it return master
async def _flat_field(self, telescope: ITelescope, camera: ICamera) -> None: """Take flat-fields.""" # set window await self._set_window(camera, testing=False) # move telescope if self._pointing is not None: await self._pointing(telescope) # do exposures, do not broadcast while testing now = Time.now() log.info("Exposing flat field %d/%d for %.2fs...", self._exposures_done + 1, self._exposures_total, self._exptime) if isinstance(camera, IExposureTime): await camera.set_exposure_time(float(self._exptime)) if isinstance(camera, IImageType): await camera.set_image_type(ImageType.SKYFLAT) filename = await camera.grab_image() # analyse image if await self._analyse_image(filename): # increase count and quite here, if finished self._exptime_done += self._exptime self._exposures_done += 1 if self._exposures_done >= self._exposures_total: log.info("Finished all requested flat-fields..") self._state = FlatFielder.State.FINISHED return # call callback if self._callback is not None: if self.observer is None: raise ValueError("No observer given.") sun = self.observer.sun_altaz(now) await self._callback( datetime=now.isot, solalt=sun.alt.degree, exptime=self._exptime, counts=self._target_count, filter_name=self._cur_filter, binning=self._cur_binning, ) # then evaluate exposure time state = self._eval_exptime() if state < 0: log.info("Going back to testing...") self._state = FlatFielder.State.TESTING elif state == 0: pass else: log.info("Missed flat-fielding time, finish task...") self._state = FlatFielder.State.FINISHED
async def _update(self) -> None: now = Time.now() # get RA/Dec if isinstance(self.module, IPointingRaDec): ra, dec = await self.module.get_radec() self._ra_dec = SkyCoord( ra=ra * u.deg, dec=dec * u.deg, frame="icrs", location=self.observer.location, obstime=now, ) else: self._ra_dec = None # get Alt/Az if isinstance(self.module, IPointingAltAz): alt, az = await self.module.get_altaz() self._alt_az = SkyCoord( alt=alt * u.deg, az=az * u.deg, frame="altaz", location=self.observer.location, obstime=now, ) else: self._alt_az = None # get offsets if isinstance(self.module, IOffsetsAltAz) and self._alt_az is not None: # get offsets self._off_alt, self._off_az = await self.module.get_offsets_altaz() # convert to ra/dec self._off_ra, self._off_dec = self._offset_altaz_to_radec( self._off_alt, self._off_az) elif isinstance(self.module, IOffsetsRaDec) and self._ra_dec is not None: # get offsets self._off_ra, self._off_dec = await self.module.get_offsets_radec() # convert to alt/az self._off_alt, self._off_az = self._offset_radec_to_altaz( self._off_ra, self._off_dec) else: self._off_ra, self._off_dec, self._off_alt, self._off_az = ( None, None, None, None, ) # signal GUI update self.signal_update_gui.emit()