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

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
16def smallest_signed_angle(a1, a2):
17 """
18 via https://stackoverflow.com/questions/1878907/the-smallest-difference-between-2-angles"""
19 TwoPi = 2.*np.pi
20 x = a1 % TwoPi
21 y = a2 % TwoPi
22 a = (x - y) % TwoPi
23 b = (y - x) % TwoPi
24 result = b+0
25 alb = np.where(a < b)[0]
26 result[alb] = -1.*a[alb]
27 return result
29class int_rounded(object):
30 """
31 Class to help force comparisons be made on scaled up integers,
32 preventing machine precision issues cross-platforms
34 Parameters
35 ----------
36 inval : number-like thing
37 Some number that we want to compare
38 scale : float (1e5)
39 How much to scale inval before rounding and converting to an int.
40 """
41 def __init__(self, inval, scale=1e5):
42 self.initial = inval
43 self.value = np.round(inval * scale).astype(int)
44 self.scale = scale
46 def __eq__(self, other):
47 return self.value == other.value
49 def __ne__(self, other):
50 return self.value != other.value
52 def __lt__(self, other):
53 return self.value < other.value
55 def __le__(self, other):
56 return self.value <= other.value
58 def __gt__(self, other):
59 return self.value > other.value
61 def __ge__(self, other):
62 return self.value >= other.value
64 def __repr__(self):
65 return str(self.initial)
67 def __add__(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
72 def __sub__(self, other):
73 out_scale = np.min([self.scale, other.scale])
74 result = int_rounded(self.initial - other.initial, scale=out_scale)
75 return result
77 def __mul__(self, other):
78 out_scale = np.min([self.scale, other.scale])
79 result = int_rounded(self.initial * other.initial, scale=out_scale)
80 return result
82 def __div__(self, other):
83 out_scale = np.min([self.scale, other.scale])
84 result = int_rounded(self.initial / other.initial, scale=out_scale)
85 return result
88def set_default_nside(nside=None):
89 """
90 Utility function to set a default nside value across the scheduler.
92 XXX-there might be a better way to do this.
94 Parameters
95 ----------
96 nside : int (None)
97 A valid healpixel nside.
98 """
99 if not hasattr(set_default_nside, 'nside'): 99 ↛ 103line 99 didn't jump to line 103, because the condition on line 99 was never false
100 if nside is None: 100 ↛ 102line 100 didn't jump to line 102, because the condition on line 100 was never false
101 nside = 32
102 set_default_nside.nside = nside
103 if nside is not None: 103 ↛ 105line 103 didn't jump to line 105, because the condition on line 103 was never false
104 set_default_nside.nside = nside
105 return set_default_nside.nside
108def restore_scheduler(observationId, scheduler, observatory, filename, filter_sched=None):
109 """Put the scheduler and observatory in the state they were in. Handy for checking reward fucnction
111 Parameters
112 ----------
113 observationId : int
114 The ID of the last observation that should be completed
115 scheduler : lsst.sims.featureScheduler.scheduler object
116 Scheduler object.
117 observatory : lsst.sims.featureSchedler.observatory.Model_observatory
118 The observaotry object
119 filename : str
120 The output sqlite dayabase to use
121 filter_sched : lsst.sims.featureScheduler.scheduler object
122 The filter scheduler. Note that we don't look up the official end of the previous night,
123 so there is potential for the loaded filters to not match.
124 """
125 sc = schema_converter()
126 # load up the observations
127 observations = sc.opsim2obs(filename)
128 good_obs = np.where(observations['ID'] <= observationId)[0]
129 observations = observations[good_obs]
131 # replay the observations back into the scheduler
132 for obs in observations:
133 scheduler.add_observation(obs)
134 if filter_sched is not None:
135 filter_sched.add_observation(obs)
137 if filter_sched is not None:
138 # Make sure we have mounted the right filters for the night
139 # XXX--note, this might not be exact, but should work most of the time.
140 mjd_start_night = np.min(observations['mjd'][np.where(observations['night'] == obs['night'])])
141 observatory.mjd = mjd_start_night
142 conditions = observatory.return_conditions()
143 filters_needed = filter_sched(conditions)
144 else:
145 filters_needed = ['u', 'g', 'r', 'i', 'y']
147 # update the observatory
148 observatory.mjd = obs['mjd'] + observatory.observatory.visit_time(obs)/3600./24.
149 observatory.observatory.parked = False
150 observatory.observatory.current_RA_rad = obs['RA']
151 observatory.observatory.current_dec_rad = obs['dec']
152 observatory.observatory.current_rotSkyPos_rad = obs['rotSkyPos']
153 observatory.observatory.cumulative_azimuth_rad = obs['cummTelAz']
154 observatory.observatory.mounted_filters = filters_needed
155 # Note that we haven't updated last_az_rad, etc, but those values should be ignored.
157 return scheduler, observatory
160def int_binned_stat(ids, values, statistic=np.mean):
161 """
162 Like scipy.binned_statistic, but for unique int ids
163 """
165 uids = np.unique(ids)
166 order = np.argsort(ids)
168 ordered_ids = ids[order]
169 ordered_values = values[order]
171 left = np.searchsorted(ordered_ids, uids, side='left')
172 right = np.searchsorted(ordered_ids, uids, side='right')
174 stat_results = []
175 for le, ri in zip(left, right):
176 stat_results.append(statistic(ordered_values[le:ri]))
178 return uids, np.array(stat_results)
181def gnomonic_project_toxy(RA1, Dec1, RAcen, Deccen):
182 """Calculate x/y projection of RA1/Dec1 in system with center at RAcen, Deccen.
183 Input radians. Grabbed from sims_selfcal"""
184 # also used in Global Telescope Network website
185 cosc = np.sin(Deccen) * np.sin(Dec1) + np.cos(Deccen) * np.cos(Dec1) * np.cos(RA1-RAcen)
186 x = np.cos(Dec1) * np.sin(RA1-RAcen) / cosc
187 y = (np.cos(Deccen)*np.sin(Dec1) - np.sin(Deccen)*np.cos(Dec1)*np.cos(RA1-RAcen)) / cosc
188 return x, y
191def gnomonic_project_tosky(x, y, RAcen, Deccen):
192 """Calculate RA/Dec on sky of object with x/y and RA/Cen of field of view.
193 Returns Ra/Dec in radians."""
194 denom = np.cos(Deccen) - y * np.sin(Deccen)
195 RA = RAcen + np.arctan2(x, denom)
196 Dec = np.arctan2(np.sin(Deccen) + y * np.cos(Deccen), np.sqrt(x*x + denom*denom))
197 return RA, Dec
200def match_hp_resolution(in_map, nside_out, UNSEEN2nan=True):
201 """Utility to convert healpix map resolution if needed and change hp.UNSEEN values to
202 np.nan.
204 Parameters
205 ----------
206 in_map : np.array
207 A valie healpix map
208 nside_out : int
209 The desired resolution to convert in_map to
210 UNSEEN2nan : bool (True)
211 If True, convert any hp.UNSEEN values to np.nan
212 """
213 current_nside = hp.npix2nside(np.size(in_map))
214 if current_nside != nside_out:
215 out_map = hp.ud_grade(in_map, nside_out=nside_out)
216 else:
217 out_map = in_map
218 if UNSEEN2nan:
219 out_map[np.where(out_map == hp.UNSEEN)] = np.nan
220 return out_map
223def raster_sort(x0, order=['x', 'y'], xbin=1.):
224 """XXXX--depriciated, use tsp instead.
226 Do a sort to scan a grid up and down. Simple starting guess to traveling salesman.
228 Parameters
229 ----------
230 x0 : array
231 order : list
232 Keys for the order x0 should be sorted in.
233 xbin : float (1.)
234 The binsize to round off the first coordinate into
236 returns
237 -------
238 array sorted so that it rasters up and down.
239 """
240 coords = x0.copy()
241 bins = np.arange(coords[order[0]].min()-xbin/2., coords[order[0]].max()+3.*xbin/2., xbin)
242 # digitize my bins
243 coords[order[0]] = np.digitize(coords[order[0]], bins)
244 order1 = np.argsort(coords, order=order)
245 coords = coords[order1]
246 places_to_invert = np.where(np.diff(coords[order[-1]]) < 0)[0]
247 if np.size(places_to_invert) > 0:
248 places_to_invert += 1
249 indx = np.arange(coords.size)
250 index_sorted = np.zeros(indx.size, dtype=int)
251 index_sorted[0:places_to_invert[0]] = indx[0:places_to_invert[0]]
253 for i, inv_pt in enumerate(places_to_invert[:-1]):
254 if i % 2 == 0:
255 index_sorted[inv_pt:places_to_invert[i+1]] = indx[inv_pt:places_to_invert[i+1]][::-1]
256 else:
257 index_sorted[inv_pt:places_to_invert[i+1]] = indx[inv_pt:places_to_invert[i+1]]
259 if np.size(places_to_invert) % 2 != 0:
260 index_sorted[places_to_invert[-1]:] = indx[places_to_invert[-1]:][::-1]
261 else:
262 index_sorted[places_to_invert[-1]:] = indx[places_to_invert[-1]:]
263 return order1[index_sorted]
264 else:
265 return order1
268class schema_converter(object):
269 """
270 Record how to convert an observation array to the standard opsim schema
271 """
272 def __init__(self):
273 # Conversion dictionary, keys are opsim schema, values are observation dtype names
274 self.convert_dict = {'observationId': 'ID', 'night': 'night',
275 'observationStartMJD': 'mjd',
276 'observationStartLST': 'lmst', 'numExposures': 'nexp',
277 'visitTime': 'visittime', 'visitExposureTime': 'exptime',
278 'proposalId': 'survey_id', 'fieldId': 'field_id',
279 'fieldRA': 'RA', 'fieldDec': 'dec', 'altitude': 'alt', 'azimuth': 'az',
280 'filter': 'filter', 'airmass': 'airmass', 'skyBrightness': 'skybrightness',
281 'cloud': 'clouds', 'seeingFwhm500': 'FWHM_500',
282 'seeingFwhmGeom': 'FWHM_geometric', 'seeingFwhmEff': 'FWHMeff',
283 'fiveSigmaDepth': 'fivesigmadepth', 'slewTime': 'slewtime',
284 'slewDistance': 'slewdist', 'paraAngle': 'pa', 'rotTelPos': 'rotTelPos',
285 'rotSkyPos': 'rotSkyPos', 'moonRA': 'moonRA',
286 'moonDec': 'moonDec', 'moonAlt': 'moonAlt', 'moonAz': 'moonAz',
287 'moonDistance': 'moonDist', 'moonPhase': 'moonPhase',
288 'sunAlt': 'sunAlt', 'sunAz': 'sunAz', 'solarElong': 'solarElong', 'note':'note'}
289 # Column(s) not bothering to remap: 'observationStartTime': None,
290 self.inv_map = {v: k for k, v in self.convert_dict.items()}
291 # angles to converts
292 self.angles_rad2deg = ['fieldRA', 'fieldDec', 'altitude', 'azimuth', 'slewDistance',
293 'paraAngle', 'rotTelPos', 'rotSkyPos', 'moonRA', 'moonDec',
294 'moonAlt', 'moonAz', 'moonDistance', 'sunAlt', 'sunAz', 'solarElong',
295 'cummTelAz']
296 # Put LMST into degrees too
297 self.angles_hours2deg = ['observationStartLST']
299 def obs2opsim(self, obs_array, filename=None, info=None, delete_past=False):
300 """convert an array of observations into a pandas dataframe with Opsim schema
301 """
302 if delete_past:
303 try:
304 os.remove(filename)
305 except OSError:
306 pass
308 df = pd.DataFrame(obs_array)
309 df = df.rename(index=str, columns=self.inv_map)
310 for colname in self.angles_rad2deg:
311 df[colname] = np.degrees(df[colname])
312 for colname in self.angles_hours2deg:
313 df[colname] = df[colname] * 360./24.
315 if filename is not None:
316 con = db.connect(filename)
317 df.to_sql('SummaryAllProps', con, index=False)
318 if info is not None:
319 df = pd.DataFrame(info)
320 df.to_sql('info', con)
322 def opsim2obs(self, filename):
323 """convert an opsim schema dataframe into an observation array.
324 """
326 con = db.connect(filename)
327 df = pd.read_sql('select * from SummaryAllProps;', con)
328 for key in self.angles_rad2deg:
329 df[key] = np.radians(df[key])
330 for key in self.angles_hours2deg:
331 df[key] = df[key] * 24./360.
333 df = df.rename(index=str, columns=self.convert_dict)
335 blank = empty_observation()
336 final_result = np.empty(df.shape[0], dtype=blank.dtype)
337 # XXX-ugh, there has to be a better way.
338 for i, key in enumerate(df.columns):
339 if key in self.inv_map.keys():
340 final_result[key] = df[key].values
342 return final_result
345def empty_observation():
346 """
347 Return a numpy array that could be a handy observation record
349 XXX: Should this really be "empty visit"? Should we have "visits" made
350 up of multple "observations" to support multi-exposure time visits?
352 XXX-Could add a bool flag for "observed". Then easy to track all proposed
353 observations. Could also add an mjd_min, mjd_max for when an observation should be observed.
354 That way we could drop things into the queue for DD fields.
356 XXX--might be nice to add a generic "sched_note" str field, to record any metadata that
357 would be useful to the scheduler once it's observed. and/or observationID.
359 Returns
360 -------
361 numpy array
363 Notes
364 -----
365 The numpy fields have the following structure
366 RA : float
367 The Right Acension of the observation (center of the field) (Radians)
368 dec : float
369 Declination of the observation (Radians)
370 mjd : float
371 Modified Julian Date at the start of the observation (time shutter opens)
372 exptime : float
373 Total exposure time of the visit (seconds)
374 filter : str
375 The filter used. Should be one of u, g, r, i, z, y.
376 rotSkyPos : float
377 The rotation angle of the camera relative to the sky E of N (Radians)
378 nexp : int
379 Number of exposures in the visit.
380 airmass : float
381 Airmass at the center of the field
382 FWHMeff : float
383 The effective seeing FWHM at the center of the field. (arcsec)
384 skybrightness : float
385 The surface brightness of the sky background at the center of the
386 field. (mag/sq arcsec)
387 night : int
388 The night number of the observation (days)
389 flush_by_mjd : float
390 If we hit this MJD, we should flush the queue and refill it.
391 cummTelAz : float
392 The cummulative telescope rotation in azimuth
393 """
395 names = ['ID', 'RA', 'dec', 'mjd', 'flush_by_mjd', 'exptime', 'filter', 'rotSkyPos', 'nexp',
396 'airmass', 'FWHM_500', 'FWHMeff', 'FWHM_geometric', 'skybrightness', 'night',
397 'slewtime', 'visittime', 'slewdist', 'fivesigmadepth',
398 'alt', 'az', 'pa', 'clouds', 'moonAlt', 'sunAlt', 'note',
399 'field_id', 'survey_id', 'block_id',
400 'lmst', 'rotTelPos', 'moonAz', 'sunAz', 'sunRA', 'sunDec', 'moonRA', 'moonDec',
401 'moonDist', 'solarElong', 'moonPhase', 'cummTelAz']
403 types = [int, float, float, float, float, float, 'U1', float, int,
404 float, float, float, float, float, int,
405 float, float, float, float,
406 float, float, float, float, float, float, 'U40',
407 int, int, int,
408 float, float, float, float, float, float, float, float,
409 float, float, float, float]
410 result = np.zeros(1, dtype=list(zip(names, types)))
411 return result
414def scheduled_observation():
415 """Make an array for pre-scheduling observations
417 mjd_tol : float
418 The tolerance on how early an observation can execute (days).
420 """
422 # Standard things from the usual observations
423 names = ['ID', 'RA', 'dec', 'mjd', 'flush_by_mjd', 'exptime', 'filter', 'rotSkyPos', 'nexp',
424 'note']
425 types = [int, float, float, float, float, float, 'U1', float, float, 'U40']
426 names += ['mjd_tol', 'dist_tol', 'alt_min', 'alt_max', 'HA_max', 'HA_min', 'observed']
427 types += [float, float, float, float, float, float, bool]
428 result = np.zeros(1, dtype=list(zip(names, types)))
429 return result
432def read_fields():
433 """
434 Read in the Field coordinates
435 Returns
436 -------
437 numpy.array
438 With RA and dec in radians.
439 """
440 query = 'select fieldId, fieldRA, fieldDEC from Field;'
441 fd = FieldsDatabase()
442 fields = np.array(list(fd.get_field_set(query)))
443 # order by field ID
444 fields = fields[fields[:,0].argsort()]
446 names = ['RA', 'dec']
447 types = [float, float]
448 result = np.zeros(np.size(fields[:, 1]), dtype=list(zip(names, types)))
449 result['RA'] = np.radians(fields[:, 1])
450 result['dec'] = np.radians(fields[:, 2])
452 return result
455def hp_kd_tree(nside=None, leafsize=100, scale=1e5):
456 """
457 Generate a KD-tree of healpixel locations
459 Parameters
460 ----------
461 nside : int
462 A valid healpix nside
463 leafsize : int (100)
464 Leafsize of the kdtree
466 Returns
467 -------
468 tree : scipy kdtree
469 """
470 if nside is None:
471 nside = set_default_nside()
473 hpid = np.arange(hp.nside2npix(nside))
474 ra, dec = _hpid2RaDec(nside, hpid)
475 return _buildTree(ra, dec, leafsize, scale=scale)
478class hp_in_lsst_fov(object):
479 """
480 Return the healpixels within a pointing. A very simple LSST camera model with
481 no chip/raft gaps.
482 """
483 def __init__(self, nside=None, fov_radius=1.75, scale=1e5):
484 """
485 Parameters
486 ----------
487 fov_radius : float (1.75)
488 Radius of the filed of view in degrees
489 """
490 if nside is None:
491 nside = set_default_nside()
493 self.tree = hp_kd_tree(nside=nside, scale=scale)
494 self.radius = np.round(xyz_angular_radius(fov_radius)*scale).astype(int)
495 self.scale = scale
497 def __call__(self, ra, dec, **kwargs):
498 """
499 Parameters
500 ----------
501 ra : float
502 RA in radians
503 dec : float
504 Dec in radians
506 Returns
507 -------
508 indx : numpy array
509 The healpixels that are within the FoV
510 """
512 x, y, z = _xyz_from_ra_dec(np.max(ra), np.max(dec))
513 x = np.round(x * self.scale).astype(int)
514 y = np.round(y * self.scale).astype(int)
515 z = np.round(z * self.scale).astype(int)
517 indices = self.tree.query_ball_point((x, y, z), self.radius)
518 return np.array(indices)
521class hp_in_comcam_fov(object):
522 """
523 Return the healpixels within a ComCam pointing. Simple camera model
524 with no chip gaps.
525 """
526 def __init__(self, nside=None, side_length=0.7):
527 """
528 Parameters
529 ----------
530 side_length : float (0.7)
531 The length of one side of the square field of view (degrees).
532 """
533 if nside is None:
534 nside = set_default_nside()
535 self.nside = nside
536 self.tree = hp_kd_tree(nside=nside)
537 self.side_length = np.radians(side_length)
538 self.inner_radius = xyz_angular_radius(side_length/2.)
539 self.outter_radius = xyz_angular_radius(side_length/2.*np.sqrt(2.))
540 # The positions of the raft corners, unrotated
541 self.corners_x = np.array([-self.side_length/2., -self.side_length/2., self.side_length/2.,
542 self.side_length/2.])
543 self.corners_y = np.array([self.side_length/2., -self.side_length/2., -self.side_length/2.,
544 self.side_length/2.])
546 def __call__(self, ra, dec, rotSkyPos=0.):
547 """
548 Parameters
549 ----------
550 ra : float
551 RA in radians
552 dec : float
553 Dec in radians
554 rotSkyPos : float
555 The rotation angle of the camera in radians
556 Returns
557 -------
558 indx : numpy array
559 The healpixels that are within the FoV
560 """
561 x, y, z = _xyz_from_ra_dec(np.max(ra), np.max(dec))
562 # Healpixels within the inner circle
563 indices = self.tree.query_ball_point((x, y, z), self.inner_radius)
564 # Healpixels withing the outer circle
565 indices_all = np.array(self.tree.query_ball_point((x, y, z), self.outter_radius))
566 indices_to_check = indices_all[np.in1d(indices_all, indices, invert=True)]
568 cos_rot = np.cos(rotSkyPos)
569 sin_rot = np.sin(rotSkyPos)
570 x_rotated = self.corners_x*cos_rot - self.corners_y*sin_rot
571 y_rotated = self.corners_x*sin_rot + self.corners_y*cos_rot
573 # Draw the square that we want to check if points are in.
574 bbPath = mplPath.Path(np.array([[x_rotated[0], y_rotated[0]],
575 [x_rotated[1], y_rotated[1]],
576 [x_rotated[2], y_rotated[2]],
577 [x_rotated[3], y_rotated[3]],
578 [x_rotated[0], y_rotated[0]]]))
580 ra_to_check, dec_to_check = _hpid2RaDec(self.nside, indices_to_check)
582 # Project the indices to check to the tangent plane, see if they fall inside the polygon
583 x, y = gnomonic_project_toxy(ra_to_check, dec_to_check, ra, dec)
584 for i, xcheck in enumerate(x):
585 # I wonder if I can do this all at once rather than a loop?
586 if bbPath.contains_point((x[i], y[i])):
587 indices.append(indices_to_check[i])
589 return np.array(indices)
592def run_info_table(observatory, extra_info=None):
593 """
594 Make a little table for recording the information about a run
595 """
597 observatory_info = observatory.get_info()
598 for key in extra_info:
599 observatory_info.append([key, extra_info[key]])
600 observatory_info = np.array(observatory_info)
602 n_feature_entries = 4
604 names = ['Parameter', 'Value']
605 dtypes = ['|U200', '|U200']
606 result = np.zeros(observatory_info[:, 0].size + n_feature_entries,
607 dtype=list(zip(names, dtypes)))
609 # Fill in info about the run
610 result[0]['Parameter'] = 'Date, ymd'
611 now = datetime.datetime.now()
612 result[0]['Value'] = '%i, %i, %i' % (now.year, now.month, now.day)
614 result[1]['Parameter'] = 'hostname'
615 result[1]['Value'] = socket.gethostname()
617 result[2]['Parameter'] = 'featureScheduler version'
618 result[2]['Value'] = version.__version__
620 result[3]['Parameter'] = 'featureScheduler fingerprint'
621 result[3]['Value'] = version.__fingerprint__
623 result[4:]['Parameter'] = observatory_info[:, 0]
624 result[4:]['Value'] = observatory_info[:, 1]
626 return result
629def inrange(inval, minimum=-1., maximum=1.):
630 """
631 Make sure values are within min/max
632 """
633 inval = np.array(inval)
634 below = np.where(inval < minimum)
635 inval[below] = minimum
636 above = np.where(inval > maximum)
637 inval[above] = maximum
638 return inval
641def warm_start(scheduler, observations, mjd_key='mjd'):
642 """Replay a list of observations into the scheduler
644 Parameters
645 ----------
646 scheduler : scheduler object
648 observations : np.array
649 An array of observation (e.g., from sqlite2observations)
650 """
652 # Check that observations are in order
653 observations.sort(order=mjd_key)
654 for observation in observations:
655 scheduler.add_observation(observation)
657 return scheduler
660def season_calc(night, offset=0, modulo=None, max_season=None, season_length=365.25, floor=True):
661 """
662 Compute what season a night is in with possible offset and modulo
664 using convention that night -365 to 0 is season -1.
666 Parameters
667 ----------
668 night : int or array
669 The night we want to convert to a season
670 offset : float or array (0)
671 Offset to be applied to night (days)
672 modulo : int (None)
673 If the season should be modulated (i.e., so we can get all even years)
674 (seasons, years w/default season_length)
675 max_season : int (None)
676 For any season above this value (before modulo), set to -1
677 season_length : float (365.25)
678 How long to consider one season (nights)
679 floor : bool (True)
680 If true, take the floor of the season. Otherwise, returns season as a float
681 """
682 if np.size(night) == 1:
683 night = np.ravel(np.array([night]))
684 result = night + offset
685 result = result/season_length
686 if floor:
687 result = np.floor(result)
688 if max_season is not None:
689 over_indx = np.where(int_rounded(result) >= int_rounded(max_season))
691 if modulo is not None:
692 neg = np.where(int_rounded(result) < int_rounded(0))
693 result = result % modulo
694 result[neg] = -1
695 if max_season is not None:
696 result[over_indx] = -1
697 if floor:
698 result = result.astype(int)
699 return result
702def create_season_offset(nside, sun_RA_rad):
703 """
704 Make an offset map so seasons roll properly
705 """
706 hpindx = np.arange(hp.nside2npix(nside))
707 ra, dec = _hpid2RaDec(nside, hpindx)
708 offset = ra - sun_RA_rad + 2.*np.pi
709 offset = offset % (np.pi*2)
710 offset = offset * 365.25/(np.pi*2)
711 offset = -offset - 365.25
712 return offset
715class TargetoO(object):
716 """Class to hold information about a target of opportunity object
718 Parameters
719 ----------
720 tooid : int
721 Unique ID for the ToO.
722 footprints : np.array
723 np.array healpix maps. 1 for areas to observe, 0 for no observe.
724 mjd_start : float
725 The MJD the ToO starts
726 duration : float
727 Duration of the ToO (days).
728 """
729 def __init__(self, tooid, footprint, mjd_start, duration):
730 self.footprint = footprint
731 self.duration = duration
732 self.id = tooid
733 self.mjd_start = mjd_start
736class Sim_targetoO_server(object):
737 """Wrapper to deliver a targetoO object at the right time
738 """
740 def __init__(self, targetoO_list):
741 self.targetoO_list = targetoO_list
742 self.mjd_starts = np.array([too.mjd_start for too in self.targetoO_list])
743 durations = np.array([too.duration for too in self.targetoO_list])
744 self.mjd_ends = self.mjd_starts + durations
746 def __call__(self, mjd):
747 in_range = np.where((mjd > self.mjd_starts) & (mjd < self.mjd_ends))[0]
748 result = None
749 if in_range.size > 0:
750 result = [self.targetoO_list[i] for i in in_range]
751 return result