def from_regions(cls, regions, **kwargs): """Create region geom from list of regions The regions are combined with union to a compound region. Parameters ---------- regions : list of `~regions.SkyRegion` or str Regions **kwargs: dict Keyword arguments forwarded to `RegionGeom` Returns ------- geom : `RegionGeom` Region map geometry """ if isinstance(regions, str): regions = Regions.parse(data=regions, format="ds9") elif isinstance(regions, SkyRegion): regions = [regions] elif isinstance(regions, SkyCoord): regions = [PointSkyRegion(center=regions)] if regions: regions = regions_to_compound_region(regions) return cls(region=regions, **kwargs)
def test_compound_region_center(): regions_ds9 = ("galactic;" "circle(1,1,0.1);" "circle(-1,1,0.1);" "circle(1,-1,0.1);" "circle(-1,-1,0.1);") regions = Regions.parse(regions_ds9, format="ds9") region = regions_to_compound_region(regions) center = compound_region_center(region) assert_allclose(center.galactic.l.wrap_at("180d"), 0 * u.deg, atol=1e-6) assert_allclose(center.galactic.b, 0 * u.deg, atol=1e-6)
def from_hdulist(cls, hdulist, format="ogip", hdu=None): """Read region table and convert it to region list. Parameters ---------- hdulist : `~astropy.io.fits.HDUList` HDU list format : {"ogip", "ogip-arf", "gadf"} HDU format Returns ------- geom : `RegionGeom` Region map geometry """ region_hdu = "REGION" if format == "gadf" and hdu: region_hdu = hdu + "_" + region_hdu if region_hdu in hdulist: region_table = Table.read(hdulist[region_hdu]) wcs = WcsGeom.from_header(region_table.meta).wcs regions = [] for reg in Regions.parse(data=region_table, format="fits"): # TODO: remove workaround once regions issue with fits serialization is sorted out # see https://github.com/astropy/regions/issues/400 reg.meta["include"] = True regions.append(reg.to_sky(wcs)) region = regions_to_compound_region(regions) else: region, wcs = None, None if format == "ogip": hdu_bands = "EBOUNDS" elif format == "ogip-arf": hdu_bands = "SPECRESP" elif format == "gadf": hdu_bands = hdu + "_BANDS" else: raise ValueError(f"Unknown format {format}") axes = MapAxes.from_table_hdu(hdulist[hdu_bands], format=format) return cls(region=region, wcs=wcs, axes=axes)
def generate_plan(observation_plan_id, request_id, user_id): """Use gwemopt to construct observing plan.""" from ..models import DBSession from skyportal.handlers.api.instrument import add_tiles Session = scoped_session( sessionmaker(bind=DBSession.session_factory.kw["bind"])) import gwemopt import gwemopt.utils import gwemopt.segments import gwemopt.skyportal from ..models import ( EventObservationPlan, Galaxy, InstrumentField, ObservationPlanRequest, PlannedObservation, User, ) session = Session() try: plan = session.query(EventObservationPlan).get(observation_plan_id) request = session.query(ObservationPlanRequest).get(request_id) user = session.query(User).get(user_id) event_time = Time(request.gcnevent.dateobs, format='datetime', scale='utc') start_time = Time(request.payload["start_date"], format='iso', scale='utc') end_time = Time(request.payload["end_date"], format='iso', scale='utc') params = { 'config': { request.instrument.name: { # field list from skyportal 'tesselation': request.instrument.fields, # telescope longitude [deg] 'longitude': request.instrument.telescope.lon, # telescope latitude [deg] 'latitude': request.instrument.telescope.lat, # telescope elevation [m] 'elevation': request.instrument.telescope.elevation, # telescope name 'telescope': request.instrument.name, # telescope horizon 'horizon': -12.0, # time in seconds to change the filter 'filt_change_time': 0.0, # extra overhead in seconds 'overhead_per_exposure': 0.0, # slew rate for the telescope [deg/s] 'slew_rate': 2.6, # camera readout time 'readout': 0.0, # telescope field of view 'FOV': 0.0, # exposure time for the given limiting magnitude 'exposuretime': 1.0, # limiting magnitude given telescope time 'magnitude': 0.0, }, }, # gwemopt filter strategy # options: block (blocks of single filters), integrated (series of alternating filters) 'doAlternativeFilters': request.payload["filter_strategy"] == "block", # flag to indicate fields come from DB 'doDatabase': True, # only keep tiles within powerlaw_cl 'doMinimalTiling': True, # single set of scheduled observations 'doSingleExposure': True, # gwemopt scheduling algorithms # options: greedy, greedy_slew, sear, airmass_weighted 'scheduleType': request.payload["schedule_type"], # list of filters to use for observations 'filters': request.payload["filters"].split(","), # GPS time for event 'gpstime': event_time.gps, # Healpix nside for the skymap 'nside': 512, # maximum integrated probability of the skymap to consider 'powerlaw_cl': request.payload["integrated_probability"], 'telescopes': [request.instrument.name], # minimum difference between observations of the same field 'mindiff': request.payload["minimum_time_difference"], # maximum airmass with which to observae 'airmass': request.payload["maximum_airmass"], # array of exposure times (same length as filter array) 'exposuretimes': np.array([int(request.payload["exposure_time"])] * len(request.payload["filters"].split(","))), } if request.payload["schedule_strategy"] == "galaxy": params = { **params, 'tilesType': 'galaxy', 'galaxy_catalog': request.payload["galaxy_catalog"], 'galaxy_grade': 'S', 'writeCatalog': False, 'catalog_n': 1.0, 'powerlaw_dist_exp': 1.0, } elif request.payload["schedule_strategy"] == "tiling": params = {**params, 'tilesType': 'moc'} else: raise AttributeError( 'scheduling_strategy should be tiling or galaxy') params = gwemopt.utils.params_checker(params) params = gwemopt.segments.get_telescope_segments(params) params["Tobs"] = [ start_time.mjd - event_time.mjd, end_time.mjd - event_time.mjd, ] params['map_struct'] = dict( zip(['prob', 'distmu', 'distsigma', 'distnorm'], request.localization.flat)) params['is3D'] = request.localization.is_3d # Function to read maps map_struct = gwemopt.utils.read_skymap(params, is3D=params["do3D"], map_struct=params['map_struct']) if params["tilesType"] == "galaxy": query = Galaxy.query_records_accessible_by(user, mode="read") query = query.filter( Galaxy.catalog_name == params["galaxy_catalog"]) galaxies = query.all() catalog_struct = {} catalog_struct["ra"] = np.array([g.ra for g in galaxies]) catalog_struct["dec"] = np.array([g.dec for g in galaxies]) catalog_struct["S"] = np.array([1.0 for g in galaxies]) catalog_struct["Sloc"] = np.array([1.0 for g in galaxies]) catalog_struct["Smass"] = np.array([1.0 for g in galaxies]) if params["tilesType"] == "moc": moc_structs = gwemopt.skyportal.create_moc_from_skyportal( params, map_struct=map_struct) tile_structs = gwemopt.tiles.moc(params, map_struct, moc_structs) elif params["tilesType"] == "galaxy": if request.instrument.region is None: raise ValueError( 'Must define the instrument region in the case of galaxy requests' ) regions = Regions.parse(request.instrument.region, format='ds9') tile_structs = gwemopt.skyportal.create_galaxy_from_skyportal( params, map_struct, catalog_struct, regions=regions) tile_structs, coverage_struct = gwemopt.coverage.timeallocation( params, map_struct, tile_structs) # if the fields do not yet exist, we need to add them if params["tilesType"] == "galaxy": regions = Regions.parse(request.instrument.region, format='ds9') data = { 'RA': coverage_struct["data"][:, 0], 'Dec': coverage_struct["data"][:, 1], } field_data = pd.DataFrame.from_dict(data) field_ids = add_tiles( request.instrument.id, request.instrument.name, regions, field_data, session=session, ) planned_observations = [] for ii in range(len(coverage_struct["ipix"])): data = coverage_struct["data"][ii, :] filt = coverage_struct["filters"][ii] mjd = data[2] tt = Time(mjd, format='mjd') overhead_per_exposure = params["config"][ request.instrument.name]["overhead_per_exposure"] exposure_time, prob = data[4], data[6] if params["tilesType"] == "galaxy": field_id = field_ids[ii] else: field_id = data[5] field = InstrumentField.query.filter( InstrumentField.instrument_id == request.instrument.id, InstrumentField.field_id == field_id, ).first() if field is None: return log(f"Missing field {field_id} from list") planned_observation = PlannedObservation( obstime=tt.datetime, dateobs=request.gcnevent.dateobs, field_id=field.id, exposure_time=exposure_time, weight=prob, filt=filt, instrument_id=request.instrument.id, planned_observation_id=ii, observation_plan_id=plan.id, overhead_per_exposure=overhead_per_exposure, ) planned_observations.append(planned_observation) session.add_all(planned_observations) plan.status = 'complete' session.merge(plan) session.commit() request.status = 'complete' session.merge(request) session.commit() flow = Flow() flow.push( '*', "skyportal/REFRESH_GCNEVENT", payload={"gcnEvent_dateobs": request.gcnevent.dateobs}, ) return log( f"Generated plan for observation plan {observation_plan_id}") except Exception as e: return log( f"Unable to generate plan for observation plan {observation_plan_id}: {e}" ) finally: Session.remove()
def test_compound_region_center_single(): region = Regions.parse("galactic;circle(1,1,0.1)", format="ds9")[0] center = compound_region_center(region) assert_allclose(center.galactic.l.wrap_at("180d"), 1 * u.deg, atol=1e-6) assert_allclose(center.galactic.b, 1 * u.deg, atol=1e-6)
def put(self, instrument_id): """ --- description: Update instrument tags: - instruments parameters: - in: path name: instrument_id required: true schema: type: integer requestBody: content: application/json: schema: InstrumentNoID responses: 200: content: application/json: schema: Success 400: content: application/json: schema: Error """ data = self.get_json() data['id'] = int(instrument_id) # permission check instrument = Instrument.get_if_accessible_by( int(instrument_id), self.current_user, mode='update' ) if instrument is None: return self.error(f'Missing instrument with ID {instrument_id}') filters = instrument.filters sensitivity_data = data.get('sensitivity_data', None) if sensitivity_data: if not set(sensitivity_data.keys()).issubset(filters): return self.error( 'Filter names must be present in both sensitivity_data property and filters property' ) field_data = data.pop("field_data", None) field_region = data.pop("field_region", None) field_fov_type = data.pop("field_fov_type", None) field_fov_attributes = data.pop("field_fov_attributes", None) if (field_region is not None) and (field_fov_type is not None): return self.error('must supply only one of field_region or field_fov_type') if field_region is not None: regions = Regions.parse(field_region, format='ds9') data['region'] = regions.serialize(format='ds9') if field_fov_type is not None: if field_fov_attributes is None: return self.error( 'field_fov_attributes required if field_fov_type supplied' ) if not field_fov_type.lower() in ["circle", "rectangle"]: return self.error('field_fov_type must be circle or rectangle') if isinstance(field_fov_attributes, list): field_fov_attributes = [float(x) for x in field_fov_attributes] else: field_fov_attributes = [float(field_fov_attributes)] center = SkyCoord(0.0, 0.0, unit='deg', frame='icrs') if field_fov_type.lower() == "circle": if not len(field_fov_attributes) == 1: return self.error( 'If field_fov_type is circle, then should supply only radius for field_fov_attributes' ) radius = field_fov_attributes[0] regions = CircleSkyRegion(center=center, radius=radius * u.deg) elif field_fov_type.lower() == "rectangle": if not len(field_fov_attributes) == 2: return self.error( 'If field_fov_type is rectangle, then should supply width and height for field_fov_attributes' ) width, height = field_fov_attributes regions = RectangleSkyRegion( center=center, width=width * u.deg, height=height * u.deg ) data['region'] = regions.serialize(format='ds9') schema = Instrument.__schema__() try: schema.load(data, partial=True) except ValidationError as exc: return self.error( 'Invalid/missing parameters: ' f'{exc.normalized_messages()}' ) self.verify_and_commit() if field_data is not None: if (field_region is None) and (field_fov_type is None): return self.error( 'field_region or field_fov_type is required with field_data' ) if type(field_data) is str: field_data = pd.read_table(StringIO(field_data), sep=",").to_dict( orient='list' ) if not {'ID', 'RA', 'Dec'}.issubset(field_data): return self.error("ID, RA, and Dec required in field_data.") log(f"Started generating fields for instrument {instrument.id}") # run async IOLoop.current().run_in_executor( None, lambda: add_tiles(instrument.id, instrument.name, regions, field_data), ) self.push_all(action="skyportal/REFRESH_INSTRUMENTS") return self.success()
def post(self): # See bottom of this file for redoc docstring -- moved it there so that # it could be made an f-string. data = self.get_json() telescope_id = data.get('telescope_id') telescope = Telescope.get_if_accessible_by( telescope_id, self.current_user, raise_if_none=True, mode="read" ) sensitivity_data = data.get("sensitivity_data", None) if sensitivity_data: filters = data.get("filters", []) if not set(sensitivity_data.keys()).issubset(filters): return self.error( 'Sensitivity_data filters must be a subset of the instrument filters' ) field_data = data.pop("field_data", None) field_region = data.pop("field_region", None) field_fov_type = data.pop("field_fov_type", None) field_fov_attributes = data.pop("field_fov_attributes", None) if (field_region is not None) and (field_fov_type is not None): return self.error('must supply only one of field_region or field_fov_type') if field_region is not None: regions = Regions.parse(field_region, format='ds9') data['region'] = regions.serialize(format='ds9') if field_fov_type is not None: if field_fov_attributes is None: return self.error( 'field_fov_attributes required if field_fov_type supplied' ) if not field_fov_type.lower() in ["circle", "rectangle"]: return self.error('field_fov_type must be circle or rectangle') if isinstance(field_fov_attributes, list): field_fov_attributes = [float(x) for x in field_fov_attributes] else: field_fov_attributes = [float(field_fov_attributes)] center = SkyCoord(0.0, 0.0, unit='deg', frame='icrs') if field_fov_type.lower() == "circle": if not len(field_fov_attributes) == 1: return self.error( 'If field_fov_type is circle, then should supply only radius for field_fov_attributes' ) radius = field_fov_attributes[0] regions = CircleSkyRegion(center=center, radius=radius * u.deg) elif field_fov_type.lower() == "rectangle": if not len(field_fov_attributes) == 2: return self.error( 'If field_fov_type is rectangle, then should supply width and height for field_fov_attributes' ) width, height = field_fov_attributes regions = RectangleSkyRegion( center=center, width=width * u.deg, height=height * u.deg ) data['region'] = regions.serialize(format='ds9') schema = Instrument.__schema__() try: instrument = schema.load(data) except ValidationError as exc: return self.error( 'Invalid/missing parameters: ' f'{exc.normalized_messages()}' ) existing_instrument = ( Instrument.query_records_accessible_by( self.current_user, ) .filter( Instrument.name == data.get('name'), Instrument.telescope_id == telescope_id, ) .first() ) if existing_instrument is None: instrument.telescope = telescope DBSession().add(instrument) DBSession().commit() else: instrument = existing_instrument if field_data is not None: if (field_region is None) and (field_fov_type is None): return self.error( 'field_region or field_fov_type is required with field_data' ) if type(field_data) is str: field_data = pd.read_table(StringIO(field_data), sep=",").to_dict( orient='list' ) if not {'ID', 'RA', 'Dec'}.issubset(field_data): return self.error("ID, RA, and Dec required in field_data.") log(f"Started generating fields for instrument {instrument.id}") # run async IOLoop.current().run_in_executor( None, lambda: add_tiles(instrument.id, instrument.name, regions, field_data), ) self.push_all(action="skyportal/REFRESH_INSTRUMENTS") return self.success(data={"id": instrument.id})
def add_observations(instrument_id, obstable): """Post executed observations for a given instrument. obstable is a pandas DataFrame of the form: observation_id field_id obstime seeing limmag exposure_time \ 0 84434604 1 2.458599e+06 1.57415 20.40705 30 1 84434651 1 2.458599e+06 1.58120 20.49405 30 2 84434696 1 2.458599e+06 1.64995 20.56030 30 3 84434741 1 2.458599e+06 1.54945 20.57400 30 4 84434788 1 2.458599e+06 1.62870 20.60385 30 filter processed_fraction airmass 0 ztfr 1.0 None 1 ztfr 1.0 None 2 ztfr 1.0 None 3 ztfr 1.0 None 4 ztfr 1.0 None """ session = Session() # if the fields do not yet exist, we need to add them if ('RA' in obstable) and ('Dec' in obstable) and not ('field_id' in obstable): instrument = session.query(Instrument).get(instrument_id) regions = Regions.parse(instrument.region, format='ds9') field_data = obstable[['RA', 'Dec']] field_ids = add_tiles(instrument.id, instrument.name, regions, field_data, session=session) obstable['field_id'] = field_ids try: observations = [] for index, row in obstable.iterrows(): field_id = int(row["field_id"]) field = (session.query(InstrumentField).filter( InstrumentField.instrument_id == instrument_id, InstrumentField.field_id == field_id, ).first()) if field is None: return log( f"Unable to add observations for instrument {instrument_id}: Missing field {field_id}" ) observation = (session.query(ExecutedObservation).filter_by( instrument_id=instrument_id, observation_id=row["observation_id"]).first()) if observation is not None: log(f"Observation {row['observation_id']} for instrument {instrument_id} already exists... continuing." ) continue # enable multiple obstime formats try: # can catch iso and isot this way obstime = Time(row["obstime"]) except ValueError: # otherwise catch jd as the numerical example obstime = Time(row["obstime"], format='jd') observations.append( ExecutedObservation( instrument_id=instrument_id, observation_id=row["observation_id"], instrument_field_id=field.id, obstime=obstime.datetime, seeing=row["seeing"], limmag=row["limmag"], exposure_time=row["exposure_time"], filt=row["filter"], processed_fraction=row["processed_fraction"], )) session.add_all(observations) session.commit() flow = Flow() flow.push('*', "skyportal/REFRESH_OBSERVATIONS") return log( f"Successfully added observations for instrument {instrument_id}") except Exception as e: return log( f"Unable to add observations for instrument {instrument_id}: {e}") finally: Session.remove()