Commit db97908e4eecd708999e1e74e4424232bb6981ac

Authored by Jürgen Knödlseder
1 parent 28cac9e0

Implementation of visibility computation using the Sun constraint (Moon constraint is still missing)

cscripts/csviscube.par
... ... @@ -28,7 +28,9 @@ outfile, f, a, viscube.fits,,, "Visibility cube output file"
28 28 #
29 29 # Visibility cube parameters
30 30 #===========================
31   -binsz, r, h, 1.0,,, "Image scale (in degrees/pixel)"
  31 +binsz, r, h, 1.0,,, "Image scale (deg/pixel)"
  32 +dz, r, h, 1.0,,, "Zenith angle bin size (deg)"
  33 +zmax, r, h, 60.0,,, "Maximum zenith angle (deg)"
32 34  
33 35 #
34 36 # Standard parameters
... ...
cscripts/csviscube.py
... ... @@ -37,7 +37,7 @@ class csviscube(ctools.cscript):
37 37 # Constructor
38 38 def __init__(self, *argv):
39 39 """
40   - Constructor.
  40 + Constructor
41 41  
42 42 Parameters
43 43 ----------
... ... @@ -54,8 +54,10 @@ class csviscube(ctools.cscript):
54 54 self._name = 'csviscube'
55 55 self._version = '1.0.0'
56 56  
57   - # Initialise application by calling the appropriate class
58   - # constructor.
  57 + # Initialise members
  58 + self._cube = gammalib.GSkyMap()
  59 +
  60 + # Initialise application by calling the appropriate class constructor
59 61 self._init_cscript(argv)
60 62  
61 63 # Return
... ... @@ -72,10 +74,7 @@ class csviscube(ctools.cscript):
72 74 self['tmax'].real()
73 75 self['geolon'].real()
74 76 self['geolat'].real()
75   - self['sunzenith'].real()
76   - self['moonzenith'].real()
77 77 self['outfile'].filename()
78   - self['binsz'].real()
79 78  
80 79 # Write input parameters into logger
81 80 self._log_parameters(gammalib.TERSE)
... ... @@ -83,38 +82,301 @@ class csviscube(ctools.cscript):
83 82 # Return
84 83 return
85 84  
86   - def _hour_angle_weight(self):
  85 + def _zenith_angle_map(self, nx, ny, dx, dy):
  86 + """
  87 + Compute zenith angle map
  88 +
  89 + Parameters
  90 + ----------
  91 + nx : int
  92 + Number of Right Ascension pixels.
  93 + ny : int
  94 + Number of Declination pixels.
  95 + dx : float
  96 + Right Ascension pixel size in degrees.
  97 + dy : float
  98 + Declination pixel size in degrees.
  99 +
  100 + Returns
  101 + -------
  102 + map : `~gammalib.GSkyMap`
  103 + Allsky map comprising the zenith angle for an hour angle of 0.
  104 +
  105 + The zenith angle of a position (ra,dec) depends on the declination and
  106 + the hour angle h and is given by
  107 +
  108 + zenith(h,dec) = arccos( sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(h) )
  109 +
  110 + The hour angle h (or local hour angle, LHA) is defined as the difference
  111 + between local siderial time (LST) and the Right Ascension
  112 +
  113 + h = LST - ra
  114 +
  115 + The map is computed for h=-ra which is equivalent to GST=lon (or LST=0).
  116 + In other words, the map corresponds to the time when ra=0 passes through
  117 + the local meridian.
87 118 """
88   - Compute hour angle weight vector
  119 + # Initialise zenith angle map
  120 + map = gammalib.GSkyMap('CAR','CEL',0.0,0.0,-dx,dy,nx,ny)
  121 +
  122 + # Set hour angle and declination vectors
  123 + hours = [(float(i)+0.5)*dx for i in range(nx)]
  124 + decs = [(float(i)+0.5)*dy-90.0 for i in range(ny)]
  125 +
  126 + # Get array geographic latitude
  127 + geolat = self['geolat'].real()
  128 +
  129 + # Precompute latitude terms
  130 + cos_lat = math.cos(geolat*gammalib.deg2rad)
  131 + sin_lat = math.sin(geolat*gammalib.deg2rad)
  132 +
  133 + # Loop over all declinations and hour angles and compute the zenith
  134 + # angle. Store the zenith angle in the map
  135 + index = 0
  136 + for dec in decs:
  137 + cos_dec = math.cos(dec*gammalib.deg2rad)
  138 + sin_dec = math.sin(dec*gammalib.deg2rad)
  139 + for h in hours:
  140 + cos_h = math.cos(h*gammalib.deg2rad)
  141 + zenith = math.acos(sin_lat*sin_dec +
  142 + cos_lat*cos_dec*cos_h)*gammalib.rad2deg
  143 + map[index] = zenith
  144 + index += 1
  145 +
  146 + # Log zenith angle map
  147 + self._log_header2(gammalib.EXPLICIT, 'Zenith angle map')
  148 + self._log_string(gammalib.EXPLICIT, str(map))
  149 +
  150 + # Return zenith angle map
  151 + return map
  152 +
  153 + def _sun_radec(self, time):
  154 + """
  155 + Compute Right Ascension and Declination of the Sun
  156 +
  157 + Parameters
  158 + ----------
  159 + time : `~gammalib.GTime()`
  160 + Time for which declination is to be computed
  161 +
  162 + Returns
  163 + -------
  164 + ra, dec : tuple of float
  165 + Right Ascension and Declination of the Sun in degrees
  166 +
  167 + The formulae are inspired from
  168 + https://en.wikipedia.org/wiki/Position_of_the_Sun
  169 + """
  170 + # Compute number of days since Greenwich noon, Terrestrial Time, on
  171 + # 1 January 2000
  172 + n = time.jd() - 2451545.0
  173 +
  174 + # Compute mean longitude of the Sun in degrees, corrected for the
  175 + # aberration of light
  176 + L = 280.460 + 0.9856474 * n
  177 + while L < 0.0:
  178 + L += 360.0
  179 + while L >= 360.0:
  180 + L -= 360.0
  181 +
  182 + # Compute the mean anomaly of the Sun in radians
  183 + g = (357.528 + 0.9856003 * n) * gammalib.deg2rad
  184 +
  185 + # Compute the ecliptic longitude of the Sun in degrees
  186 + lam = L + 1.915 * math.sin(g) + 0.020 * math.sin(2.0*g)
  187 +
  188 + # Compute Right Ascension and Declination of the Sun in degrees
  189 + ra = math.atan(math.cos(23.43711*gammalib.deg2rad) *
  190 + math.tan(lam*gammalib.deg2rad))*gammalib.rad2deg
  191 + dec = math.asin(math.sin(23.43711*gammalib.deg2rad) *
  192 + math.sin(lam*gammalib.deg2rad))*gammalib.rad2deg
  193 +
  194 + # Put ra in the right quadrant (same quadrant as lam)
  195 + while lam < 0.0:
  196 + lam += 360.0
  197 + while lam >= 360.0:
  198 + lam -= 360.0
  199 + while ra < 0.0:
  200 + ra += 360.0
  201 + while ra >= 360.0:
  202 + ra -= 360.0
  203 + q_lam = int(lam / 90.0)
  204 + q_ra = int(ra / 90.0)
  205 + ra += (q_lam-q_ra)*90.0
  206 +
  207 + # Return declination of the Sun
  208 + return ra, dec
  209 +
  210 + def _sun_ra_exclusion(self, time):
  211 + """
  212 + Compute the Right Ascension difference in degrees before and after
  213 + noon that are excluded due to the Sun constraint
  214 +
  215 + Parameters
  216 + ----------
  217 + time : `~gammalib.GTime()`
  218 + Time for which the Sun exclusion is to be computed
  219 +
  220 + Returns
  221 + -------
  222 + dra : float
  223 + Positive Right Ascension difference in degrees.
  224 + """
  225 + # Get array geographic latitude and minimum Sun zenith angle in radians
  226 + geolat = self['geolat'].real() * gammalib.deg2rad
  227 + sunzenith = self['sunzenith'].real() * gammalib.deg2rad
  228 +
  229 + # Compute Right Ascension and Declination of Sun in degrees and
  230 + # convert the Declination into radians
  231 + sunra, sundec = self._sun_radec(time)
  232 + sundec *= gammalib.deg2rad
  233 +
  234 + # Compute some sines and cosines
  235 + cos_sunzenith = math.cos(sunzenith)
  236 + sin_geolat = math.sin(geolat)
  237 + cos_geolat = math.cos(geolat)
  238 + sin_sundec = math.sin(sundec)
  239 + cos_sundec = math.cos(sundec)
89 240  
  241 + # Compute Right Ascension difference when Sun is below the mimimum
  242 + # zenith angle in degrees
  243 + dra = math.acos((cos_sunzenith - sin_geolat * sin_sundec) /
  244 + (cos_geolat * cos_sundec)) * gammalib.rad2deg
  245 +
  246 + # Return
  247 + return dra
  248 +
  249 + def _hour_angle_weight(self):
  250 + """
  251 + Compute hour angle weights
  252 +
  253 + Computes an array specifying how many hours the array was observing
  254 + for a given hour angle during the time interval [tmin,tmax]. The hour
  255 + angle runs from 0 to 360 degrees.
  256 +
90 257 Compute during which time the array was observing due to hour angle
91 258 constraints. We take here a very simple model where the night lasts
92 259 between 6 and 10 hours (or 90-150 degrees), with a sinusoidal variation
93 260 along the year. Every day the hour angle of the Sun set is advancing by
94 261 4 minutes.
95 262 """
96   - # Set number of hour angle bins
  263 + # Write header
  264 + self._log_header2(gammalib.NORMAL, 'Hour angle weights')
  265 +
  266 + # Get array geographic longitude
  267 + geolon = self['geolon'].real()
  268 +
  269 + # Get time interval
  270 + tmin = gammalib.GTime(self['tmin'].real(), 's')
  271 + tmax = gammalib.GTime(self['tmax'].real(), 's')
  272 +
  273 + # Initialise hour angle list
  274 + hour_angles = []
  275 +
  276 + # Set number of hour angle bins and compute conversion factor and weight
97 277 nsteps = 1000
98   - d_min = 6.0/24.0*nsteps
99   - d_max = 10.0/24.0*nsteps
  278 + ra2inx = float(nsteps)/360.0 # Conversion from RA (deg) to index
  279 + weight = 24.0/float(nsteps) # Weight per hour angle step
100 280  
101 281 # Initialise hour angle array and weight
102 282 hours = [0.0 for i in range(nsteps)]
103   - weight = 24.0/float(nsteps) # Weight per hour angle step
104 283  
105   - # Loop over all days in one year
106   - for day in range(nsteps):
107   - h_set = day
108   - h_rise = day + \
109   - int(d_min + (d_max-d_min)*math.sin(day/float(nsteps)*gammalib.pi))
110   - for i in range(h_set,h_rise):
111   - if i >= nsteps:
112   - hours[i-nsteps] += weight
113   - else:
  284 + # Initialise time and loop until the end time is reached
  285 + time = tmin
  286 + while time <= tmax:
  287 +
  288 + # Compute Right Ascension and Declination of Sun in degrees
  289 + sunra, sundec = self._sun_radec(time)
  290 +
  291 + # Compute by how much the zenith angle map needs to be shifted
  292 + # to correspond to the actual time
  293 + #
  294 + # NOTE: The local apparent siderial time only is relevant if
  295 + # the time interval is shorter than a day. Only in that case
  296 + # we have to set the corresponding hour angles to zero. We need
  297 + # to think how to properly implement that.
  298 + #last = time.last(geolon) * 15.0
  299 + #print(last)
  300 +
  301 + # Compute the time from now when the Sun will culminate. The time
  302 + # is here expressed in degrees
  303 + dra_sun = self._sun_ra_exclusion(time)
  304 +
  305 + # Set [0,ra_start] and [ra_stop,360.0]
  306 + ra_start = sunra - dra_sun
  307 + ra_stop = sunra + dra_sun
  308 +
  309 + # Case 1: The RA interval during which the Sun is above the zenith
  310 + # angle constraint is fully comprised within the [0,360] interval.
  311 + # In that case the dark time is comprised of two intervals:
  312 + # [0,ra_start] and [ra_stop,360]
  313 + if ra_start >= 0.0 and ra_stop <= 360.0:
  314 + inx_stop = int(ra_start * ra2inx + 0.5)
  315 + inx_start = int(ra_stop * ra2inx + 0.5)
  316 + for i in range(0,inx_stop):
  317 + hours[i] += weight
  318 + for i in range(inx_start,nsteps):
114 319 hours[i] += weight
115 320  
116   - # Return
117   - return hours
  321 + # Log setting
  322 + logs = 'Sun=(%.3f,%.3f) dRA=%.3f RA_excl=[%.3f,%.3f] '\
  323 + 'Indices=[0-%d] & [%d-%d] Dark time=%.2f h %s' % \
  324 + (sunra, sundec, dra_sun, ra_start, ra_stop, inx_stop, inx_start,
  325 + nsteps, float(inx_stop+(nsteps-inx_start))*weight,
  326 + '(Case 1)')
  327 + self._log_value(gammalib.VERBOSE, time.utc(), logs)
  328 +
  329 + # Case 2: The RA interval during which the Sun is above the zenith
  330 + # angle constraint is starting at negative RA. In that case the
  331 + # dark time is comprised of a single interval
  332 + # [ra_stop,ra_start+360]
  333 + elif ra_start < 0.0:
  334 + inx_start = int(ra_stop * ra2inx + 0.5)
  335 + inx_stop = int((ra_start+360.0) * ra2inx + 0.5)
  336 + for i in range(inx_start,inx_stop):
  337 + hours[i] += weight
  338 +
  339 + # Log setting
  340 + logs = 'Sun=(%.3f,%.3f) dRA=%.3f RA_excl=[%.3f,%.3f] '\
  341 + 'Indices=[%d-%d] Dark time=%.2f h %s' % \
  342 + (sunra, sundec, dra_sun, ra_start, ra_stop, inx_start, inx_stop,
  343 + float(inx_stop-inx_start)*weight, '(Case 2)')
  344 + self._log_value(gammalib.VERBOSE, time.utc(), logs)
  345 +
  346 + # Case 3: The RA interval during which the Sun is above the zenith
  347 + # angle constraint is stopping at RA>360. In that case the dark
  348 + # time is comprised of a single interval [ra_stop-360,ra_start]
  349 + elif ra_stop > 360.0:
  350 + inx_start = int((ra_stop-360.0) * ra2inx + 0.5)
  351 + inx_stop = int(ra_start * ra2inx + 0.5)
  352 + for i in range(inx_start,inx_stop):
  353 + hours[i] += weight
  354 +
  355 + # Log setting
  356 + logs = 'Sun=(%.3f,%.3f) dRA=%.3f RA_excl=[%.3f,%.3f] '\
  357 + 'Indices=[%d-%d] Dark time=%.2f h %s' % \
  358 + (sunra, sundec, dra_sun, ra_start, ra_stop, inx_start, inx_stop,
  359 + float(inx_stop-inx_start)*weight, '(Case 3)')
  360 + self._log_value(gammalib.VERBOSE, time.utc(), logs)
  361 +
  362 + # Add seconds of one day and start with next day
  363 + time += 86400.0
  364 +
  365 + # Build hour angle list
  366 + dh = 360.0/float(nsteps)
  367 + for i in range(nsteps):
  368 + hour_angle = {'angle': dh*float(i), 'hours': hours[i]}
  369 + hour_angles.append(hour_angle)
  370 +
  371 + # Log hour angle weights
  372 + total = 0.0
  373 + for hour_angle in hour_angles:
  374 + total += hour_angle['hours']
  375 + self._log_value(gammalib.EXPLICIT, 'Observing time', str(total)+' h')
  376 + self._log_value(gammalib.EXPLICIT, 'Number of hour angle bins', len(hour_angles))
  377 +
  378 + # Return hour angle weights
  379 + return hour_angles
118 380  
119 381 def _visibility_cube(self):
120 382 """
... ... @@ -126,102 +388,64 @@ class csviscube(ctools.cscript):
126 388 Summing over all zenith angle intervals specifies for how long a given
127 389 celestial position will be visible.
128 390 """
  391 + # Write header
  392 + self._log_header1(gammalib.TERSE, 'Compute visibility cube')
  393 +
  394 + # Set visibility cube and zenith angle map dimensions and bin size
  395 + binsz = self['binsz'].real()
  396 + nx = int(360.0/binsz+0.5)
  397 + ny = int(180.0/binsz+0.5)
  398 + dx = 360.0/float(nx)
  399 + dy = 180.0/float(ny)
  400 +
  401 + # Set zenith angle dimension and bin size
  402 + dz = self['dz'].real()
  403 + zmax = self['zmax'].real()
  404 + nz = int(zmax/dz+0.5)
  405 + dz = zmax/float(nz)
  406 +
  407 + # Log dimensions
  408 + self._log_header2(gammalib.NORMAL, 'Visibility cube dimensions')
  409 + self._log_value(gammalib.NORMAL, 'Number of RA bins', nx)
  410 + self._log_value(gammalib.NORMAL, 'Number of Dec bins', ny)
  411 + self._log_value(gammalib.NORMAL, 'Number of zenith angles', nz)
  412 + self._log_value(gammalib.NORMAL, 'RA pixel size', str(dx)+' deg')
  413 + self._log_value(gammalib.NORMAL, 'Dec pixel size', str(dy)+' deg')
  414 + self._log_value(gammalib.NORMAL, 'Zenith angle bin size', str(dz)+' deg')
  415 + self._log_value(gammalib.NORMAL, 'Maximum zenith angle', str(zmax)+' deg')
  416 +
129 417 # Compute zenith angle map
130   - map = self._zenith_angle_map()
131   -
132   - # Set zenith angle range and bin size
133   - dz = 1.0
134   - nz = int(60.0/dz)
135   -
136   - # Initialise visibility cube
137   - self._cube = gammalib.GSkyMap('CAR','CEL',0.0,0.0,-1.0,1.0,360,180,nz)
  418 + map = self._zenith_angle_map(nx,ny,dx,dy)
138 419  
139 420 # Setup hour angle weights. They specify for how many hours a given
140   - # hour angle will be observed during one year.
141   - hours = self._hour_angle_weight()
  421 + # hour angle is observed during the covered time period.
  422 + hour_angles = self._hour_angle_weight()
142 423  
143   - # Compute normalisation factor
144   - norm = 365.0/float(len(hours))
  424 + # Initialise visibility cube
  425 + self._cube = gammalib.GSkyMap('CAR','CEL',0.0,0.0,-dx,dy,nx,ny,nz)
145 426  
146 427 # Loop over all hour angle weights
147   - for h, weight in enumerate(hours):
  428 + for hour_angle in hour_angles:
148 429  
149   - # Compute temporal shift
150   - shift = 360.0/float(len(hours)) * float(h)
  430 + # Compute temporal shift in degrees
  431 + shift = hour_angle['angle']
151 432  
152 433 # Initialise shifted zenith angle map
153   - map_shift = gammalib.GSkyMap('CAR','CEL',shift,0.0,-1.0,1.0,360,180)
  434 + map_shift = gammalib.GSkyMap('CAR','CEL',shift,0.0,-dx,dy,nx,ny)
154 435  
155 436 # Merge zenith angle map into shifted map
156 437 map_shift += map
157 438  
158   - # Loop over all pixels of shifted map
  439 + # Loop over all pixels of the shifted map and add the hours during
  440 + # which the shifted map occurs to the relevant zenith angle bin
159 441 for i, zenith in enumerate(map_shift):
160   - iz = int(zenith/dz)
  442 + iz = int(zenith/dz)
161 443 if iz < nz:
162   - self._cube[i,iz] += weight * norm
  444 + self._cube[i,iz] += hour_angle['hours']
163 445  
164 446 # Return
165 447 return
166 448  
167   - def _zenith_angle_map(self):
168   - """
169   - Compute zenith angle map
170   -
171   - The zenith angle of a position (ra,dec) depends on the declination and
172   - the hour angle h and is given by
173   -
174   - zenith(h,dec) = arccos( sin(lat) * sin(dec) + cos(lat) * cos(dec) * cos(h) )
175   -
176   - The hour angle h (or local hour angle, LHA) is defined as the difference
177   - between local siderial time (LST) and the Right Ascension
178   -
179   - h = LST - ra
180   -
181   - LST is the difference between the Greenwich siderial time (GST) and the
182   - geographic longitude of the observer
183   -
184   - LST = GST - lon
185   -
186   - where lon is counted positive west from the meridian. Hence
187   -
188   - h = GST - lon - ra
189   -
190   - The map is computed for h=-ra which is equivalent to GST=lon (or LST=0).
191   - In other words, the map corresponds to the time when ra=0 passes the
192   - local meridian.
193   - """
194   - # Initialise zenith angle map
195   - map = gammalib.GSkyMap('CAR','CEL',0.0,0.0,-1.0,1.0,360,180)
196   -
197   - # Set hour angle and declination vectors
198   - hours = [float(i) for i in range(360)]
199   - decs = [float(i)-89.5 for i in range(180)]
200   -
201   - # Get array geographic longitude and latitude
202   - geolon = self['geolon'].real()
203   - geolat = self['geolat'].real()
204   -
205   - # Precompute latitude terms
206   - cos_lat = math.cos(geolat*gammalib.deg2rad)
207   - sin_lat = math.sin(geolat*gammalib.deg2rad)
208   -
209   - # Loop over all declination and hour angles and compute the zenith
210   - # angle. Store the zenith angle in the map
211   - index = 0
212   - for dec in decs:
213   - cos_dec = math.cos(dec*gammalib.deg2rad)
214   - sin_dec = math.sin(dec*gammalib.deg2rad)
215   - for h in hours:
216   - cos_h = math.cos(h*gammalib.deg2rad)
217   - zenith = math.acos(sin_lat*sin_dec +
218   - cos_lat*cos_dec*cos_h)*gammalib.rad2deg
219   - map[index] = zenith
220   - index += 1
221   -
222   - # Return zenith angle map
223   - return map
224   -
225 449  
226 450 # Public methods
227 451 def run(self):
... ... @@ -252,7 +476,7 @@ class csviscube(ctools.cscript):
252 476 outfile = self['outfile'].filename()
253 477  
254 478 # Log file name
255   - self._log_value(gammalib.TERSE, 'Visibility cube file', outfile.url())
  479 + self._log_value(gammalib.NORMAL, 'Visibility cube file', outfile.url())
256 480  
257 481 # Save the visibility cube
258 482 self._cube.save(outfile, self['clobber'].boolean())
... ...
test/test_csviscube.py
... ... @@ -67,7 +67,7 @@ class Test(test):
67 67  
68 68 # Setup csviscube command
69 69 cmd = csviscube+' tmin=0.0 tmax=31557600.0 geolon=79.4041'+\
70   - ' geolat=-24.6272 outfile=csviscube_cmd1.fits'+\
  70 + ' geolat=-24.6272 binsz=3.0 outfile=csviscube_cmd1.fits'+\
71 71 ' logfile="csviscube_cmd1.log" chatter=1'
72 72  
73 73 # Check if execution of wrong command fails
... ... @@ -92,6 +92,7 @@ class Test(test):
92 92 viscube['tmax'] = 31557600.0
93 93 viscube['geolon'] = 79.4041
94 94 viscube['geolat'] = -24.6272
  95 + viscube['binsz'] = 3.0
95 96 viscube['outfile'] = 'csviscube_py1.fits'
96 97 viscube['logfile'] = 'csviscube_py1.log'
97 98 viscube['chatter'] = 2
... ...