diff --git a/gracedb/reports.py b/gracedb/reports.py index b01577193fc98fc7f5017404912f3aa985446831..ec9093a2a0095c7b2881d20b449f338d3bb0063a 100644 --- a/gracedb/reports.py +++ b/gracedb/reports.py @@ -1,5 +1,6 @@ from django.http import HttpResponse +from django.http import HttpResponseForbidden from django.template import RequestContext from django.shortcuts import render_to_response from django.conf import settings @@ -7,7 +8,26 @@ from django.conf import settings from gracedb.models import Event from django.db.models import Q -import os, datetime, json, time +import os, json + +from django.core.urlresolvers import reverse + +from models import CoincInspiralEvent ,SingleInspiral +from forms import SimpleSearchFormWithSubclasses +from query import parseQuery + + +from django.db.models import Max, Min +import matplotlib +matplotlib.use('Agg') +import numpy +import matplotlib.pyplot as plot +import StringIO +import base64 +import sys +import time +from datetime import datetime, timedelta +from utils import posixToGpsTime def histo(request): @@ -46,8 +66,8 @@ def histo(request): def rate_data(request): # XXX there is a better way -- should be using group_by or something. # WAAY too many queries (~300) going on here. - now = datetime.datetime.now() - day = datetime.timedelta(1) + now = datetime.now() + day = timedelta(1) ts_min = now - 60 * day ts_max = now @@ -84,3 +104,193 @@ def rate_data(request): return series + +# XXX This should be configurable / moddable or something +MAX_QUERY_RESULTS = 1000 + +# The following two util routines are for gstlalcbc_report. This is messy. +def cluster(events): + # FIXME N^2 clustering, but event list should always be small anyway... + def quieter(e1, events = events, win = 5): + for e2 in events: + if e2.far == e1.far: + # XXX I need subclass attributes here. + if (e2.gpstime < e1.gpstime + win) and (e2.gpstime > e1.gpstime - win) and (e2.snr > e1.snr): + return True + else: + if (e2.gpstime < e1.gpstime + win) and (e2.gpstime > e1.gpstime - win) and (e2.far < e1.far): + return True + return False + return [e for e in events if not quieter(e)] + +def to_png_image(out = sys.stdout): + f = StringIO.StringIO() + plot.savefig(f, format="png") + return base64.b64encode(f.getvalue()) + +def gstlalcbc_report(request, format=""): + if not request.user or not request.user.is_authenticated(): + return HttpResponseForbidden("Forbidden") + + if request.method == "GET": + if "query" not in request.GET: + # Use default query. LowMass events from the past week. + t_high = datetime.now() + dt = timedelta(days=7) + t_low = t_high - dt + t_high = posixToGpsTime(time.mktime(t_high.timetuple())) + t_low = posixToGpsTime(time.mktime(t_low.timetuple())) + query = 'CBC LowMass %d .. %d' % (t_low, t_high) + rawquery = query + form = SimpleSearchFormWithSubclasses({'query': query}) + else: + form = SimpleSearchFormWithSubclasses(request.GET) + rawquery = request.GET['query'] + else: + form = SimpleSearchFormWithSubclasses(request.POST) + rawquery = request.POST['query'] + if form.is_valid(): + objects = form.cleaned_data['query'] + + # Check for foreign objects. + for obj in objects: + if not isinstance(obj, CoincInspiralEvent): + errormsg = 'Your query returned items that are not CoincInspiral Events. ' + errormsg += 'Please try again.' + form = SimpleSearchFormWithSubclasses() + return render_to_response('gracedb/gstlalcbc_report.html', + { 'form':form, 'message':errormsg}, + context_instance=RequestContext(request)) + + # Check that we have a well-defined GPS time range. + # Find the gpstime limits of the query by diving into the query object. + # XXX Down the rabbit hole! + qthing = parseQuery(rawquery) + gpsrange = None + for child in qthing.children: + if 'gpstime' in str(child): + for subchild in child.children: + if isinstance(subchild, tuple): + gpsrange = subchild[1] + if not gpsrange: + # Bounce back to the user with an error message + errormsg = 'Your query does not have a gpstime range. Please try again.' + form = SimpleSearchFormWithSubclasses() + return render_to_response('gracedb/gstlalcbc_report.html', + { 'form':form, 'message':errormsg}, + context_instance=RequestContext(request)) + lt = int(gpsrange[1]) - int(gpsrange[0]) + + # Check that there aren't too many objects. + # XXX Hardcoded limit + if objects.count() > 2000: + errormsg = 'Your query returned too many events. Please try again.' + return render_to_response('gracedb/gstlalcbc_report.html', + { 'form':form, 'message':errormsg}, + context_instance=RequestContext(request)) + + # Zero events will break the min/max thing that comes next. + if objects.count() < 1: + errormsg = 'Your query returned no events. Please try again.' + return render_to_response('gracedb/gstlalcbc_report.html', + { 'form':form, 'message':errormsg}, + context_instance=RequestContext(request)) + + # Find the min and max on the set of objects. + #gpstime_limits = objects.aggregate(Max('gpstime'), Min('gpstime')) + mchirp_limits = objects.aggregate(Max('coincinspiralevent__mchirp'), + Min('coincinspiralevent__mchirp')) + mass_limits = objects.aggregate(Max('coincinspiralevent__mass'), + Min('coincinspiralevent__mass')) + + clustered_events = cluster(objects) + clustered_events = sorted(clustered_events, None, key=lambda x: x.far) + + # Make IFAR plot. + ifars = numpy.array(sorted([1.0 / e.far for e in clustered_events])[::-1]) + N = numpy.linspace(1, len(ifars), len(ifars)) + + eN = numpy.linspace(1, 1000 * len(ifars), 1000 * len(ifars)) / 1000. + expected_ifars = lt / eN + + up = eN + eN**.5 + down = eN - eN**.5 + down[down < 0.9] = 0.9 + + plot.figure(figsize=(6,5)) + plot.loglog(ifars[::-1], N[::-1]) + plot.fill_between(expected_ifars[::-1], down[::-1], up[::-1], alpha=0.1) + plot.loglog(expected_ifars[::-1], eN[::-1]) + plot.ylim([0.9, len(ifars)]) + plot.xlabel('IFAR (s)') + plot.ylabel('N') + plot.grid() + ifar_plot = to_png_image() + + # Set the color map for loudest event table. Depends on lt. + #FAR_color_map = [ { 'max_far' : 1.e-12, 'color' : 'gold', 'desc' : '< 1/10000 yrs'}, + # { 'max_far' : 3.e-10, 'color' : 'silver', 'desc' : '< 1/100 yrs'}, + # { 'max_far' : 3.e-8, 'color' : '#A67D3D', 'desc' : '< 1/yr'}, + # { 'max_far' : 1.0/lt, 'color' : '#B2C248', 'desc' : 'louder than expected'}, ] + + # XXX Okay, this sucks. There is no switch/case structure in the django template + # language. So I couldn't think of any way to do this without checking ranges. + # And the range is zero to infinity. So here I go... + FAR_color_map = [ { 'min_far' : 0.0, 'max_far' : 1.e-12, 'color' : 'gold', 'desc' : '< 1/10000 yrs'}, + { 'min_far' : 1.e-12, 'max_far' : 3.e-10, 'color' : 'silver', 'desc' : '< 1/100 yrs'}, + { 'min_far' : 3.e-10, 'max_far' : 3.e-8, 'color' : '#A67D3D', 'desc' : '< 1/yr'}, + { 'min_far' : 3.e-8, 'max_far' : 1.0/lt, 'color' : '#B2C248', 'desc' : 'louder than expected'}, + { 'min_far' : 1.0/lt, 'max_far' : float('inf'), 'color' : 'white', 'desc' : ''}, ] + + # Make mass distribution plots + mchirp = numpy.array([e.mchirp for e in clustered_events]) + plot.figure(figsize=(6,4)) + lower = int(mchirp_limits['coincinspiralevent__mchirp__min']) + upper = int(mchirp_limits['coincinspiralevent__mchirp__max']) + 1 + # How to decide the number of bins? + N_bins = 20 + delta = max(float(upper-lower)/N_bins,0.2) + plot.hist(mchirp, bins=numpy.arange(lower,upper,delta)) + plot.xlabel('Chirp Mass') + plot.ylabel('count') + plot.grid() + mchirp_dist = to_png_image() + + mass = numpy.array([e.mass for e in clustered_events]) + plot.figure(figsize=(6,4)) + lower = int(mass_limits['coincinspiralevent__mass__min']) + upper = int(mass_limits['coincinspiralevent__mass__max']) + 1 + N_bins = 20 + delta = max(float(upper-lower)/N_bins,0.2) + plot.hist(mass, bins=numpy.arange(lower,upper,delta)) + plot.xlabel('Total Mass') + plot.ylabel('count') + plot.grid() + mass_dist = to_png_image() + + if objects.count() == 1: + title = "Query returned %s event." % objects.count() + else: + title = "Query returned %s events." % objects.count() + + context = { + 'title': title, + 'form': form, + 'formAction': reverse(gstlalcbc_report), + 'count' : objects.count(), + 'rawquery' : rawquery, + 'FAR_color_map' : FAR_color_map, + 'mass_dist' : mass_dist, + 'mchirp_dist' : mchirp_dist, + 'ifar_plot' : ifar_plot, + 'clustered_events' : clustered_events, + } + return render_to_response('gracedb/gstlalcbc_report.html', context, + context_instance=RequestContext(request)) + + return render_to_response('gracedb/gstlalcbc_report.html', + { 'form' : form, + }, + context_instance=RequestContext(request)) + + diff --git a/gracedb/templatetags/timeutil.py b/gracedb/templatetags/timeutil.py index 7f7fd338948aff2178d5607e0fa0f81db65ffbd8..0e3a6180bf1c9218ba2444dd2867274f804f9d08 100644 --- a/gracedb/templatetags/timeutil.py +++ b/gracedb/templatetags/timeutil.py @@ -168,8 +168,9 @@ def end_time(event,digits=4): # in the django template, so that the final digits get truncated and padded # with zeros. Why? The current gpstime is only 10 digits. So I'm going to have to # make this into a string. Totally sick, I know. - decimal_part = float(event.end_time_ns)/1.e9 - decimal_part = round(decimal_part,digits) - return str(event.end_time) + str(decimal_part)[1:] + decimal_part = round(float(event.end_time_ns)/1.e9,digits) + # ugh. must pad with zeros to the right. + decimal_part = str(decimal_part)[1:].ljust(digits+1,'0') + return str(event.end_time) + decimal_part except Exception, e: return None diff --git a/gracedb/urls.py b/gracedb/urls.py index 8a23edceab032977ab060beb897124db4c71ca31..0636ffce013f5a20d06cf5e227b7efa3b40c0c67 100644 --- a/gracedb/urls.py +++ b/gracedb/urls.py @@ -9,7 +9,6 @@ urlpatterns = patterns('gracedb.views', url (r'^$', 'index', name="home"), url (r'^create/$', 'create', name="create"), url (r'^search/(?P<format>(json|flex))?$', 'search', name="search"), - url (r'^gstlalcbc_report/(?P<format>(json|flex))?$', 'gstlalcbc_report', name="gstlalcbc_report"), url (r'^view/(?P<graceid>[GEHT]\d+)', 'view', name="view"), url (r'^voevent/(?P<graceid>[GEHT]\d+)', 'voevent', name="voevent"), url (r'^skyalert/(?P<graceid>[GEHT]\d+)', 'skyalert', name="skyalert"), diff --git a/gracedb/views.py b/gracedb/views.py index 7fc919af40c7111b2cfcc0a6363f4a6b7b081e1d..8266ccd8efde923b6831face3700e8ea4f0bc3dd 100644 --- a/gracedb/views.py +++ b/gracedb/views.py @@ -38,16 +38,6 @@ from templatetags.scientific import scientific from buildVOEvent import buildVOEvent, submitToSkyalert -from django.db.models import Max, Min -import matplotlib -matplotlib.use('Agg') -import numpy -import matplotlib.pyplot as plot -import StringIO -import base64 -import sys -import logging - # XXX This should be configurable / moddable or something MAX_QUERY_RESULTS = 1000 @@ -808,173 +798,6 @@ def search(request, format=""): }, context_instance=RequestContext(request)) -# The following two util routines are for gstlalcbc_report. This is messy. -def cluster(events): - # FIXME N^2 clustering, but event list should always be small anyway... - def quieter(e1, events = events, win = 5): - for e2 in events: - if e2.far == e1.far: - # XXX I need subclass attributes here. - if (e2.gpstime < e1.gpstime + win) and (e2.gpstime > e1.gpstime - win) and (e2.snr > e1.snr): - return True - else: - if (e2.gpstime < e1.gpstime + win) and (e2.gpstime > e1.gpstime - win) and (e2.far < e1.far): - return True - return False - return [e for e in events if not quieter(e)] - -def to_png_image(out = sys.stdout): - f = StringIO.StringIO() - plot.savefig(f, format="png") - return base64.b64encode(f.getvalue()) - -def gstlalcbc_report(request, format=""): - if not request.user or not request.user.is_authenticated(): - return HttpResponseForbidden("Forbidden") - logger = logging.getLogger(__name__) - - if request.method == "GET" and "query" not in request.GET: - form = SimpleSearchFormWithSubclasses() - else: - if request.method == "GET": - form = SimpleSearchFormWithSubclasses(request.GET) - rawquery = request.GET['query'] - else: - form = SimpleSearchFormWithSubclasses(request.POST) - rawquery = request.POST['query'] - if form.is_valid(): - objects = form.cleaned_data['query'] - - # Check for foreign objects. - for obj in objects: - logger.debug("object is a %s" % obj.__class__.__name__) - if not isinstance(obj, CoincInspiralEvent): - errormsg = 'Your query returned item(s) that are not CoincInspiral Events. ' - errormsg += 'Please try again.' - form = SimpleSearchFormWithSubclasses() - return render_to_response('gracedb/gstlalcbc_report.html', - { 'form':form, 'message':errormsg}, - context_instance=RequestContext(request)) - - # Check that we have a well-defined GPS time range. - # Find the gpstime limits of the query by diving into the query object. - # XXX Down the rabbit hole! - qthing = parseQuery(rawquery) - gpsrange = None - for child in qthing.children: - if 'gpstime' in str(child): - for subchild in child.children: - if isinstance(subchild, tuple): - gpsrange = subchild[1] - if not gpsrange: - # Bounce back to the user with an error message - errormsg = 'Your query does not have a gpstime range. Please try again.' - form = SimpleSearchFormWithSubclasses() - return render_to_response('gracedb/gstlalcbc_report.html', - { 'form':form, 'message':errormsg}, - context_instance=RequestContext(request)) - lt = int(gpsrange[1]) - int(gpsrange[0]) - - # Check that there aren't too many objects. - # XXX Hardcoded limit - if objects.count() > 2000: - errormsg = 'Your query returned too many events. Please try again.' - return render_to_response('gracedb/gstlalcbc_report.html', - { 'form':form, 'message':errormsg}, - context_instance=RequestContext(request)) - - # Find the min and max on the set of objects. - #gpstime_limits = objects.aggregate(Max('gpstime'), Min('gpstime')) - mchirp_limits = objects.aggregate(Max('coincinspiralevent__mchirp'), - Min('coincinspiralevent__mchirp')) - mass_limits = objects.aggregate(Max('coincinspiralevent__mass'), - Min('coincinspiralevent__mass')) - - clustered_events = cluster(objects) - clustered_events = sorted(clustered_events, None, key=lambda x: x.far) - - # Make IFAR plot. - ifars = numpy.array(sorted([1.0 / e.far for e in clustered_events])[::-1]) - N = numpy.linspace(1, len(ifars), len(ifars)) - - eN = numpy.linspace(1, 1000 * len(ifars), 1000 * len(ifars)) / 1000. - expected_ifars = lt / eN - - up = eN + eN**.5 - down = eN - eN**.5 - down[down < 0.9] = 0.9 - - plot.figure(figsize=(6,5)) - plot.loglog(ifars[::-1], N[::-1]) - plot.fill_between(expected_ifars[::-1], down[::-1], up[::-1], alpha=0.1) - plot.loglog(expected_ifars[::-1], eN[::-1]) - plot.ylim([0.9, len(ifars)]) - plot.xlabel('IFAR (s)') - plot.ylabel('N') - plot.grid() - ifar_plot = to_png_image() - - # Set the color map for loudest event table. Depends on lt. - #FAR_color_map = [ { 'max_far' : 1.e-12, 'color' : 'gold', 'desc' : '< 1/10000 yrs'}, - # { 'max_far' : 3.e-10, 'color' : 'silver', 'desc' : '< 1/100 yrs'}, - # { 'max_far' : 3.e-8, 'color' : '#A67D3D', 'desc' : '< 1/yr'}, - # { 'max_far' : 1.0/lt, 'color' : '#B2C248', 'desc' : 'louder than expected'}, ] - - # XXX Okay, this sucks. There is no switch/case structure in the django template - # language. So I couldn't think of any way to do this without checking ranges. - # And the range is zero to infinity. So here I go... - FAR_color_map = [ { 'min_far' : 0.0, 'max_far' : 1.e-12, 'color' : 'gold', 'desc' : '< 1/10000 yrs'}, - { 'min_far' : 1.e-12, 'max_far' : 3.e-10, 'color' : 'silver', 'desc' : '< 1/100 yrs'}, - { 'min_far' : 3.e-10, 'max_far' : 3.e-8, 'color' : '#A67D3D', 'desc' : '< 1/yr'}, - { 'min_far' : 3.e-8, 'max_far' : 1.0/lt, 'color' : '#B2C248', 'desc' : 'louder than expected'}, - { 'min_far' : 1.0/lt, 'max_far' : float('inf'), 'color' : 'white', 'desc' : ''}, ] - - # Make mass distribution plots - mchirp = numpy.array([e.mchirp for e in clustered_events]) - plot.figure(figsize=(6,4)) - lower = int(mchirp_limits['coincinspiralevent__mchirp__min']) - upper = int(mchirp_limits['coincinspiralevent__mchirp__max']) + 1 - plot.hist(mchirp, bins=numpy.arange(lower,upper,0.25)) - plot.xlabel('Chirp Mass') - plot.ylabel('count') - plot.grid() - mchirp_dist = to_png_image() - - mass = numpy.array([e.mass for e in clustered_events]) - plot.figure(figsize=(6,4)) - lower = int(mass_limits['coincinspiralevent__mass__min']) - upper = int(mass_limits['coincinspiralevent__mass__max']) + 1 - plot.hist(mass, bins=numpy.arange(lower,upper,5/8.0)) - plot.xlabel('Total Mass') - plot.ylabel('count') - plot.grid() - mass_dist = to_png_image() - - if objects.count() == 1: - title = "Query returned %s event." % objects.count() - else: - title = "Query returned %s events." % objects.count() - - context = { - 'title': title, - 'form': form, - 'formAction': reverse(gstlalcbc_report), - 'count' : objects.count(), - 'rawquery' : rawquery, - 'FAR_color_map' : FAR_color_map, - 'mass_dist' : mass_dist, - 'mchirp_dist' : mchirp_dist, - 'ifar_plot' : ifar_plot, - 'clustered_events' : clustered_events, - } - return render_to_response('gracedb/gstlalcbc_report.html', context, - context_instance=RequestContext(request)) - - return render_to_response('gracedb/gstlalcbc_report.html', - { 'form' : form, - }, - context_instance=RequestContext(request)) - def oldsearch(request): assert request.user if request.method == 'GET': diff --git a/templates/gracedb/event_detail_LM.html b/templates/gracedb/event_detail_LM.html index fb3c2d3ff696a4eb0471183e5c65dcf176cc0638..11d81da57d377ee221b5bce8d74f3db29af900cb 100644 --- a/templates/gracedb/event_detail_LM.html +++ b/templates/gracedb/event_detail_LM.html @@ -5,6 +5,9 @@ {# Analysis-specific attributes for an LM event#} {% block analysis_specific %} +{# Test whether the object has the end_time property. Older (non CoincInspiral) events don't. #} +{% if object.end_time %} + <div id="container" style="display:table;width:100%"> <div style="display:table-row;width:100%"> @@ -364,4 +367,6 @@ {% endif %} +{% endif %} <!-- object has end_time property. --> + {% endblock %} diff --git a/templates/gracedb/gstlalcbc_report.html b/templates/gracedb/gstlalcbc_report.html index ac352c51df5e868a74960718cc750c7a8b57dafd..709199cfe3fd30642e3d7a34d702e0f790f80c5f 100644 --- a/templates/gracedb/gstlalcbc_report.html +++ b/templates/gracedb/gstlalcbc_report.html @@ -83,7 +83,6 @@ onload="document.search_form.query.focus();" <th>Rank</th> <th>GraceID</th> <th>gpstime</th> - <th>end_time_ns</th> <th>FAR</th> <th>IFOs</th> <th>Total Mass</th> @@ -98,8 +97,7 @@ onload="document.search_form.query.focus();" {% endfor %} <td>{{ forloop.counter }} </td> <td><a href="{% url view obj.graceid %}">{{ obj.graceid }}</a></td> - <td> {{ obj.gpstime }} </td> - <td> {{ obj.end_time_ns }} </td> + <td> {{ obj|end_time }} </td> <td> {{ obj.far|scientific }} </td> <td> {{ obj.instruments }} </td> <td> {{ obj.mass|floatformat:4 }} </td> diff --git a/templates/gracedb/histogram.html b/templates/gracedb/histogram.html index 173a3d3bedc4690e290ee7fc9d79cc9cca56dbfa..42b8c9613a2f34409be1e800e91251fe9d46ea0b 100644 --- a/templates/gracedb/histogram.html +++ b/templates/gracedb/histogram.html @@ -82,6 +82,10 @@ function toggle(id) { {% block content %} <br/> +<a href="{% url gstlalcbc_report %}"><h3>Dynamic CBC Report</h3></a> +<br/> +<br/> + <a name="latency" href="javascript:toggle('latency');"><h3>Latency</h3></a> <div id="latency" style="display:none;"> diff --git a/urls.py b/urls.py index a2d1085659ac87df9354cc7b2a563437e0b31857..b319de8822c928b8c7d79b7984ec2245dc2530e4 100644 --- a/urls.py +++ b/urls.py @@ -30,6 +30,7 @@ urlpatterns = patterns('', url (r'^feeds/$', feedview, name="feeds"), url (r'^reports/$', 'gracedb.reports.histo', name="reports"), + url (r'^reports/gstlalcbc_report/(?P<format>(json|flex))?$', 'gracedb.reports.gstlalcbc_report', name="gstlalcbc_report"), (r'^reports/(?P<path>.+)$', 'django.views.static.serve', {'document_root': settings.LATENCY_REPORT_DEST_DIR}),