Coverage for python/lsst/sims/featureScheduler/utils/utils.py : 17%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import os
2import sqlite3 as db
3import datetime
4import socket
5import numpy as np
6import healpy as hp
7import pandas as pd
8import matplotlib.path as mplPath
9from lsst.sims.utils import _hpid2RaDec, xyz_angular_radius, _buildTree, _xyz_from_ra_dec
10from lsst.sims.featureScheduler import version
11from lsst.sims.survey.fields import FieldsDatabase
14class int_rounded(object):
15 """
16 Class to help force comparisons be made on scaled up integers,
17 preventing machine precision issues cross-platforms
19 Parameters
20 ----------
21 inval : number-like thing
22 Some number that we want to compare
23 scale : float (1e5)
24 How much to scale inval before rounding and converting to an int.
25 """
26 def __init__(self, inval, scale=1e5):
27 self.initial = inval
28 self.value = np.round(inval * scale).astype(int)
29 self.scale = scale
31 def __eq__(self, other):
32 return self.value == other.value
34 def __ne__(self, other):
35 return self.value != other.value
37 def __lt__(self, other):
38 return self.value < other.value
40 def __le__(self, other):
41 return self.value <= other.value
43 def __gt__(self, other):
44 return self.value > other.value
46 def __ge__(self, other):
47 return self.value >= other.value
49 def __repr__(self):
50 return str(self.initial)
52 def __add__(self, other):
53 out_scale = np.min([self.scale, other.scale])
54 result = int_rounded(self.initial + other.initial, scale=out_scale)
55 return result
57 def __sub__(self, other):
58 out_scale = np.min([self.scale, other.scale])
59 result = int_rounded(self.initial - other.initial, scale=out_scale)
60 return result
62 def __mul__(self, other):
63 out_scale = np.min([self.scale, other.scale])
64 result = int_rounded(self.initial * other.initial, scale=out_scale)
65 return result
67 def __div__(self, other):
68 out_scale = np.min([self.scale, other.scale])
69 result = int_rounded(self.initial / other.initial, scale=out_scale)
70 return result
73def set_default_nside(nside=None):
74 """
75 Utility function to set a default nside value across the scheduler.
77 XXX-there might be a better way to do this.
79 Parameters
80 ----------
81 nside : int (None)
82 A valid healpixel nside.
83 """
84 if not hasattr(set_default_nside, 'nside'): 84 ↛ 88line 84 didn't jump to line 88, because the condition on line 84 was never false
85 if nside is None: 85 ↛ 87line 85 didn't jump to line 87, because the condition on line 85 was never false
86 nside = 32
87 set_default_nside.nside = nside
88 if nside is not None: 88 ↛ 90line 88 didn't jump to line 90, because the condition on line 88 was never false
89 set_default_nside.nside = nside
90 return set_default_nside.nside
93def int_binned_stat(ids, values, statistic=np.mean):
94 """
95 Like scipy.binned_statistic, but for unique int ids
96 """
98 uids = np.unique(ids)
99 order = np.argsort(ids)
101 ordered_ids = ids[order]
102 ordered_values = values[order]
104 left = np.searchsorted(ordered_ids, uids, side='left')
105 right = np.searchsorted(ordered_ids, uids, side='right')
107 stat_results = []
108 for le, ri in zip(left, right):
109 stat_results.append(statistic(ordered_values[le:ri]))
111 return uids, np.array(stat_results)
114def gnomonic_project_toxy(RA1, Dec1, RAcen, Deccen):
115 """Calculate x/y projection of RA1/Dec1 in system with center at RAcen, Deccen.
116 Input radians. Grabbed from sims_selfcal"""
117 # also used in Global Telescope Network website
118 cosc = np.sin(Deccen) * np.sin(Dec1) + np.cos(Deccen) * np.cos(Dec1) * np.cos(RA1-RAcen)
119 x = np.cos(Dec1) * np.sin(RA1-RAcen) / cosc
120 y = (np.cos(Deccen)*np.sin(Dec1) - np.sin(Deccen)*np.cos(Dec1)*np.cos(RA1-RAcen)) / cosc
121 return x, y
124def gnomonic_project_tosky(x, y, RAcen, Deccen):
125 """Calculate RA/Dec on sky of object with x/y and RA/Cen of field of view.
126 Returns Ra/Dec in radians."""
127 denom = np.cos(Deccen) - y * np.sin(Deccen)
128 RA = RAcen + np.arctan2(x, denom)
129 Dec = np.arctan2(np.sin(Deccen) + y * np.cos(Deccen), np.sqrt(x*x + denom*denom))
130 return RA, Dec
133def match_hp_resolution(in_map, nside_out, UNSEEN2nan=True):
134 """Utility to convert healpix map resolution if needed and change hp.UNSEEN values to
135 np.nan.
137 Parameters
138 ----------
139 in_map : np.array
140 A valie healpix map
141 nside_out : int
142 The desired resolution to convert in_map to
143 UNSEEN2nan : bool (True)
144 If True, convert any hp.UNSEEN values to np.nan
145 """
146 current_nside = hp.npix2nside(np.size(in_map))
147 if current_nside != nside_out:
148 out_map = hp.ud_grade(in_map, nside_out=nside_out)
149 else:
150 out_map = in_map
151 if UNSEEN2nan:
152 out_map[np.where(out_map == hp.UNSEEN)] = np.nan
153 return out_map
156def raster_sort(x0, order=['x', 'y'], xbin=1.):
157 """XXXX--depriciated, use tsp instead.
159 Do a sort to scan a grid up and down. Simple starting guess to traveling salesman.
161 Parameters
162 ----------
163 x0 : array
164 order : list
165 Keys for the order x0 should be sorted in.
166 xbin : float (1.)
167 The binsize to round off the first coordinate into
169 returns
170 -------
171 array sorted so that it rasters up and down.
172 """
173 coords = x0.copy()
174 bins = np.arange(coords[order[0]].min()-xbin/2., coords[order[0]].max()+3.*xbin/2., xbin)
175 # digitize my bins
176 coords[order[0]] = np.digitize(coords[order[0]], bins)
177 order1 = np.argsort(coords, order=order)
178 coords = coords[order1]
179 places_to_invert = np.where(np.diff(coords[order[-1]]) < 0)[0]
180 if np.size(places_to_invert) > 0:
181 places_to_invert += 1
182 indx = np.arange(coords.size)
183 index_sorted = np.zeros(indx.size, dtype=int)
184 index_sorted[0:places_to_invert[0]] = indx[0:places_to_invert[0]]
186 for i, inv_pt in enumerate(places_to_invert[:-1]):
187 if i % 2 == 0:
188 index_sorted[inv_pt:places_to_invert[i+1]] = indx[inv_pt:places_to_invert[i+1]][::-1]
189 else:
190 index_sorted[inv_pt:places_to_invert[i+1]] = indx[inv_pt:places_to_invert[i+1]]
192 if np.size(places_to_invert) % 2 != 0:
193 index_sorted[places_to_invert[-1]:] = indx[places_to_invert[-1]:][::-1]
194 else:
195 index_sorted[places_to_invert[-1]:] = indx[places_to_invert[-1]:]
196 return order1[index_sorted]
197 else:
198 return order1
201class schema_converter(object):
202 """
203 Record how to convert an observation array to the standard opsim schema
204 """
205 def __init__(self):
206 # Conversion dictionary, keys are opsim schema, values are observation dtype names
207 self.convert_dict = {'observationId': 'ID', 'night': 'night',
208 'observationStartMJD': 'mjd',
209 'observationStartLST': 'lmst', 'numExposures': 'nexp',
210 'visitTime': 'visittime', 'visitExposureTime': 'exptime',
211 'proposalId': 'survey_id', 'fieldId': 'field_id',
212 'fieldRA': 'RA', 'fieldDec': 'dec', 'altitude': 'alt', 'azimuth': 'az',
213 'filter': 'filter', 'airmass': 'airmass', 'skyBrightness': 'skybrightness',
214 'cloud': 'clouds', 'seeingFwhm500': 'FWHM_500',
215 'seeingFwhmGeom': 'FWHM_geometric', 'seeingFwhmEff': 'FWHMeff',
216 'fiveSigmaDepth': 'fivesigmadepth', 'slewTime': 'slewtime',
217 'slewDistance': 'slewdist', 'paraAngle': 'pa', 'rotTelPos': 'rotTelPos',
218 'rotSkyPos': 'rotSkyPos', 'moonRA': 'moonRA',
219 'moonDec': 'moonDec', 'moonAlt': 'moonAlt', 'moonAz': 'moonAz',
220 'moonDistance': 'moonDist', 'moonPhase': 'moonPhase',
221 'sunAlt': 'sunAlt', 'sunAz': 'sunAz', 'solarElong': 'solarElong', 'note':'note'}
222 # Column(s) not bothering to remap: 'observationStartTime': None,
223 self.inv_map = {v: k for k, v in self.convert_dict.items()}
224 # angles to converts
225 self.angles_rad2deg = ['fieldRA', 'fieldDec', 'altitude', 'azimuth', 'slewDistance',
226 'paraAngle', 'rotTelPos', 'rotSkyPos', 'moonRA', 'moonDec',
227 'moonAlt', 'moonAz', 'moonDistance', 'sunAlt', 'sunAz', 'solarElong']
228 # Put LMST into degrees too
229 self.angles_hours2deg = ['observationStartLST']
231 def obs2opsim(self, obs_array, filename=None, info=None, delete_past=False):
232 """convert an array of observations into a pandas dataframe with Opsim schema
233 """
234 if delete_past:
235 try:
236 os.remove(filename)
237 except OSError:
238 pass
240 df = pd.DataFrame(obs_array)
241 df = df.rename(index=str, columns=self.inv_map)
242 for colname in self.angles_rad2deg:
243 df[colname] = np.degrees(df[colname])
244 for colname in self.angles_hours2deg:
245 df[colname] = df[colname] * 360./24.
247 if filename is not None:
248 con = db.connect(filename)
249 df.to_sql('SummaryAllProps', con, index=False)
250 if info is not None:
251 df = pd.DataFrame(info)
252 df.to_sql('info', con)
254 def opsim2obs(self, filename):
255 """convert an opsim schema dataframe into an observation array.
256 """
258 con = db.connect(filename)
259 df = pd.read_sql('select * from SummaryAllProps;', con)
260 for key in self.angles_rad2deg:
261 df[key] = np.radians(df[key])
262 for key in self.angles_hours2deg:
263 df[key] = df[key] * 24./360.
265 df = df.rename(index=str, columns=self.convert_dict)
267 blank = empty_observation()
268 final_result = np.empty(df.shape[0], dtype=blank.dtype)
269 # XXX-ugh, there has to be a better way.
270 for i, key in enumerate(df.columns):
271 if key in self.inv_map.keys():
272 final_result[key] = df[key].values
274 return final_result
277def empty_observation():
278 """
279 Return a numpy array that could be a handy observation record
281 XXX: Should this really be "empty visit"? Should we have "visits" made
282 up of multple "observations" to support multi-exposure time visits?
284 XXX-Could add a bool flag for "observed". Then easy to track all proposed
285 observations. Could also add an mjd_min, mjd_max for when an observation should be observed.
286 That way we could drop things into the queue for DD fields.
288 XXX--might be nice to add a generic "sched_note" str field, to record any metadata that
289 would be useful to the scheduler once it's observed. and/or observationID.
291 Returns
292 -------
293 numpy array
295 Notes
296 -----
297 The numpy fields have the following structure
298 RA : float
299 The Right Acension of the observation (center of the field) (Radians)
300 dec : float
301 Declination of the observation (Radians)
302 mjd : float
303 Modified Julian Date at the start of the observation (time shutter opens)
304 exptime : float
305 Total exposure time of the visit (seconds)
306 filter : str
307 The filter used. Should be one of u, g, r, i, z, y.
308 rotSkyPos : float
309 The rotation angle of the camera relative to the sky E of N (Radians)
310 nexp : int
311 Number of exposures in the visit.
312 airmass : float
313 Airmass at the center of the field
314 FWHMeff : float
315 The effective seeing FWHM at the center of the field. (arcsec)
316 skybrightness : float
317 The surface brightness of the sky background at the center of the
318 field. (mag/sq arcsec)
319 night : int
320 The night number of the observation (days)
321 flush_by_mjd : float
322 If we hit this MJD, we should flush the queue and refill it.
323 """
325 names = ['ID', 'RA', 'dec', 'mjd', 'flush_by_mjd', 'exptime', 'filter', 'rotSkyPos', 'nexp',
326 'airmass', 'FWHM_500', 'FWHMeff', 'FWHM_geometric', 'skybrightness', 'night',
327 'slewtime', 'visittime', 'slewdist', 'fivesigmadepth',
328 'alt', 'az', 'pa', 'clouds', 'moonAlt', 'sunAlt', 'note',
329 'field_id', 'survey_id', 'block_id',
330 'lmst', 'rotTelPos', 'moonAz', 'sunAz', 'sunRA', 'sunDec', 'moonRA', 'moonDec',
331 'moonDist', 'solarElong', 'moonPhase']
333 types = [int, float, float, float, float, float, 'U1', float, int,
334 float, float, float, float, float, int,
335 float, float, float, float,
336 float, float, float, float, float, float, 'U40',
337 int, int, int,
338 float, float, float, float, float, float, float, float,
339 float, float, float]
340 result = np.zeros(1, dtype=list(zip(names, types)))
341 return result
344def scheduled_observation():
345 """Make an array for pre-scheduling observations
347 mjd_tol : float
348 The tolerance on how early an observation can execute (days).
350 """
352 # Standard things from the usual observations
353 names = ['ID', 'RA', 'dec', 'mjd', 'flush_by_mjd', 'exptime', 'filter', 'rotSkyPos', 'nexp',
354 'note']
355 types = [int, float, float, float, float, float, 'U1', float, float, 'U40']
356 names += ['mjd_tol', 'dist_tol', 'alt_min', 'alt_max', 'HA_max', 'HA_min', 'observed']
357 types += [float, float, float, float, float, float, bool]
358 result = np.zeros(1, dtype=list(zip(names, types)))
359 return result
363def obs_to_fbsobs(obs):
364 """
365 converts an Observation from the Driver (which is a normal python class)
366 to an observation for the feature based scheduler (a numpy ndarray).
367 """
369 fbsobs = empty_observation()
370 fbsobs['RA'] = obs.ra_rad
371 fbsobs['dec'] = obs.dec_rad
372 log.debug('Observation MJD: %.4f', obs.observation_start_mjd)
373 fbsobs['mjd'] = obs.observation_start_mjd
374 fbsobs['exptime'] = obs.exp_time
375 fbsobs['filter'] = obs.filter
376 fbsobs['rotSkyPos'] = obs.ang_rad
377 fbsobs['nexp'] = obs.num_exp
378 fbsobs['airmass'] = obs.airmass
379 fbsobs['FWHMeff'] = obs.seeing_fwhm_eff
380 fbsobs['FWHM_geometric'] = obs.seeing_fwhm_geom
381 fbsobs['skybrightness'] = obs.sky_brightness
382 fbsobs['night'] = obs.night
383 fbsobs['slewtime'] = obs.slewtime
384 fbsobs['fivesigmadepth'] = obs.five_sigma_depth
385 fbsobs['alt'] = obs.alt_rad
386 fbsobs['az'] = obs.az_rad
387 fbsobs['clouds'] = obs.cloud
388 fbsobs['moonAlt'] = obs.moon_alt
389 fbsobs['sunAlt'] = obs.sun_alt
390 fbsobs['note'] = obs.note
391 fbsobs['field_id'] = obs.fieldid
392 fbsobs['survey_id'] = obs.propid_list[0]
394 return fbsobs
397def empty_scheduled_observation():
398 """
399 Same as empty observation, but with mjd_min, mjd_max columns
400 """
401 start = empty_observation()
402 names = start.dtype.names
403 types = start.dtype.types
404 names.extend(['mjd_min', 'mjd_max'])
405 types.extend([float, float])
407 result = np.zeros(1, dtype=list(zip(names, types)))
408 return result
411def read_fields():
412 """
413 Read in the Field coordinates
414 Returns
415 -------
416 numpy.array
417 With RA and dec in radians.
418 """
419 query = 'select fieldId, fieldRA, fieldDEC from Field;'
420 fd = FieldsDatabase()
421 fields = np.array(list(fd.get_field_set(query)))
422 # order by field ID
423 fields = fields[fields[:,0].argsort()]
425 names = ['RA', 'dec']
426 types = [float, float]
427 result = np.zeros(np.size(fields[:, 1]), dtype=list(zip(names, types)))
428 result['RA'] = np.radians(fields[:, 1])
429 result['dec'] = np.radians(fields[:, 2])
431 return result
434def hp_kd_tree(nside=None, leafsize=100, scale=1e5):
435 """
436 Generate a KD-tree of healpixel locations
438 Parameters
439 ----------
440 nside : int
441 A valid healpix nside
442 leafsize : int (100)
443 Leafsize of the kdtree
445 Returns
446 -------
447 tree : scipy kdtree
448 """
449 if nside is None:
450 nside = set_default_nside()
452 hpid = np.arange(hp.nside2npix(nside))
453 ra, dec = _hpid2RaDec(nside, hpid)
454 return _buildTree(ra, dec, leafsize, scale=scale)
457class hp_in_lsst_fov(object):
458 """
459 Return the healpixels within a pointing. A very simple LSST camera model with
460 no chip/raft gaps.
461 """
462 def __init__(self, nside=None, fov_radius=1.75, scale=1e5):
463 """
464 Parameters
465 ----------
466 fov_radius : float (1.75)
467 Radius of the filed of view in degrees
468 """
469 if nside is None:
470 nside = set_default_nside()
472 self.tree = hp_kd_tree(nside=nside, scale=scale)
473 self.radius = np.round(xyz_angular_radius(fov_radius)*scale).astype(int)
474 self.scale = scale
476 def __call__(self, ra, dec, **kwargs):
477 """
478 Parameters
479 ----------
480 ra : float
481 RA in radians
482 dec : float
483 Dec in radians
485 Returns
486 -------
487 indx : numpy array
488 The healpixels that are within the FoV
489 """
491 x, y, z = _xyz_from_ra_dec(np.max(ra), np.max(dec))
492 x = np.round(x * self.scale).astype(int)
493 y = np.round(y * self.scale).astype(int)
494 z = np.round(z * self.scale).astype(int)
496 indices = self.tree.query_ball_point((x, y, z), self.radius)
497 return np.array(indices)
500class hp_in_comcam_fov(object):
501 """
502 Return the healpixels within a ComCam pointing. Simple camera model
503 with no chip gaps.
504 """
505 def __init__(self, nside=None, side_length=0.7):
506 """
507 Parameters
508 ----------
509 side_length : float (0.7)
510 The length of one side of the square field of view (degrees).
511 """
512 if nside is None:
513 nside = set_default_nside()
514 self.nside = nside
515 self.tree = hp_kd_tree(nside=nside)
516 self.side_length = np.radians(side_length)
517 self.inner_radius = xyz_angular_radius(side_length/2.)
518 self.outter_radius = xyz_angular_radius(side_length/2.*np.sqrt(2.))
519 # The positions of the raft corners, unrotated
520 self.corners_x = np.array([-self.side_length/2., -self.side_length/2., self.side_length/2.,
521 self.side_length/2.])
522 self.corners_y = np.array([self.side_length/2., -self.side_length/2., -self.side_length/2.,
523 self.side_length/2.])
525 def __call__(self, ra, dec, rotSkyPos=0.):
526 """
527 Parameters
528 ----------
529 ra : float
530 RA in radians
531 dec : float
532 Dec in radians
533 rotSkyPos : float
534 The rotation angle of the camera in radians
535 Returns
536 -------
537 indx : numpy array
538 The healpixels that are within the FoV
539 """
540 x, y, z = _xyz_from_ra_dec(np.max(ra), np.max(dec))
541 # Healpixels within the inner circle
542 indices = self.tree.query_ball_point((x, y, z), self.inner_radius)
543 # Healpixels withing the outer circle
544 indices_all = np.array(self.tree.query_ball_point((x, y, z), self.outter_radius))
545 indices_to_check = indices_all[np.in1d(indices_all, indices, invert=True)]
547 cos_rot = np.cos(rotSkyPos)
548 sin_rot = np.sin(rotSkyPos)
549 x_rotated = self.corners_x*cos_rot - self.corners_y*sin_rot
550 y_rotated = self.corners_x*sin_rot + self.corners_y*cos_rot
552 # Draw the square that we want to check if points are in.
553 bbPath = mplPath.Path(np.array([[x_rotated[0], y_rotated[0]],
554 [x_rotated[1], y_rotated[1]],
555 [x_rotated[2], y_rotated[2]],
556 [x_rotated[3], y_rotated[3]],
557 [x_rotated[0], y_rotated[0]]]))
559 ra_to_check, dec_to_check = _hpid2RaDec(self.nside, indices_to_check)
561 # Project the indices to check to the tangent plane, see if they fall inside the polygon
562 x, y = gnomonic_project_toxy(ra_to_check, dec_to_check, ra, dec)
563 for i, xcheck in enumerate(x):
564 # I wonder if I can do this all at once rather than a loop?
565 if bbPath.contains_point((x[i], y[i])):
566 indices.append(indices_to_check[i])
568 return np.array(indices)
571def run_info_table(observatory, extra_info=None):
572 """
573 Make a little table for recording the information about a run
574 """
576 observatory_info = observatory.get_info()
577 for key in extra_info:
578 observatory_info.append([key, extra_info[key]])
579 observatory_info = np.array(observatory_info)
581 n_feature_entries = 4
583 names = ['Parameter', 'Value']
584 dtypes = ['|U200', '|U200']
585 result = np.zeros(observatory_info[:, 0].size + n_feature_entries,
586 dtype=list(zip(names, dtypes)))
588 # Fill in info about the run
589 result[0]['Parameter'] = 'Date, ymd'
590 now = datetime.datetime.now()
591 result[0]['Value'] = '%i, %i, %i' % (now.year, now.month, now.day)
593 result[1]['Parameter'] = 'hostname'
594 result[1]['Value'] = socket.gethostname()
596 result[2]['Parameter'] = 'featureScheduler version'
597 result[2]['Value'] = version.__version__
599 result[3]['Parameter'] = 'featureScheduler fingerprint'
600 result[3]['Value'] = version.__fingerprint__
602 result[4:]['Parameter'] = observatory_info[:, 0]
603 result[4:]['Value'] = observatory_info[:, 1]
605 return result
608def inrange(inval, minimum=-1., maximum=1.):
609 """
610 Make sure values are within min/max
611 """
612 inval = np.array(inval)
613 below = np.where(inval < minimum)
614 inval[below] = minimum
615 above = np.where(inval > maximum)
616 inval[above] = maximum
617 return inval
620def warm_start(scheduler, observations, mjd_key='mjd'):
621 """Replay a list of observations into the scheduler
623 Parameters
624 ----------
625 scheduler : scheduler object
627 observations : np.array
628 An array of observation (e.g., from sqlite2observations)
629 """
631 # Check that observations are in order
632 observations.sort(order=mjd_key)
633 for observation in observations:
634 scheduler.add_observation(observation)
636 return scheduler
639def season_calc(night, offset=0, modulo=None, max_season=None, season_length=365.25, floor=True):
640 """
641 Compute what season a night is in with possible offset and modulo
643 using convention that night -365 to 0 is season -1.
645 Parameters
646 ----------
647 night : int or array
648 The night we want to convert to a season
649 offset : float or array (0)
650 Offset to be applied to night (days)
651 modulo : int (None)
652 If the season should be modulated (i.e., so we can get all even years)
653 (seasons, years w/default season_length)
654 max_season : int (None)
655 For any season above this value (before modulo), set to -1
656 season_length : float (365.25)
657 How long to consider one season (nights)
658 floor : bool (True)
659 If true, take the floor of the season. Otherwise, returns season as a float
660 """
661 if np.size(night) == 1:
662 night = np.ravel(np.array([night]))
663 result = night + offset
664 result = result/season_length
665 if floor:
666 result = np.floor(result)
667 if max_season is not None:
668 over_indx = np.where(int_rounded(result) >= int_rounded(max_season))
670 if modulo is not None:
671 neg = np.where(int_rounded(result) < int_rounded(0))
672 result = result % modulo
673 result[neg] = -1
674 if max_season is not None:
675 result[over_indx] = -1
676 if floor:
677 result = result.astype(int)
678 return result
681def create_season_offset(nside, sun_RA_rad):
682 """
683 Make an offset map so seasons roll properly
684 """
685 hpindx = np.arange(hp.nside2npix(nside))
686 ra, dec = _hpid2RaDec(nside, hpindx)
687 offset = ra - sun_RA_rad + 2.*np.pi
688 offset = offset % (np.pi*2)
689 offset = offset * 365.25/(np.pi*2)
690 offset = -offset - 365.25
691 return offset
694class TargetoO(object):
695 """Class to hold information about a target of opportunity object
697 Parameters
698 ----------
699 tooid : int
700 Unique ID for the ToO.
701 footprints : np.array
702 np.array healpix maps. 1 for areas to observe, 0 for no observe.
703 mjd_start : float
704 The MJD the ToO starts
705 duration : float
706 Duration of the ToO (days).
707 """
708 def __init__(self, tooid, footprint, mjd_start, duration):
709 self.footprint = footprint
710 self.duration = duration
711 self.id = tooid
712 self.mjd_start = mjd_start
715class Sim_targetoO_server(object):
716 """Wrapper to deliver a targetoO object at the right time
717 """
719 def __init__(self, targetoO_list):
720 self.targetoO_list = targetoO_list
721 self.mjd_starts = np.array([too.mjd_start for too in self.targetoO_list])
722 durations = np.array([too.duration for too in self.targetoO_list])
723 self.mjd_ends = self.mjd_starts + durations
725 def __call__(self, mjd):
726 in_range = np.where((mjd > self.mjd_starts) & (mjd < self.mjd_ends))[0]
727 result = None
728 if in_range.size > 0:
729 result = [self.targetoO_list[i] for i in in_range]
730 return result