28Generic time interval coincidence engine.
32from bisect
import bisect_left
34 from fpconst
import NegInf
38 NegInf = float(
"-inf")
44from scipy
import spatial
46from collections
import ChainMap, Counter
50from igwn_ligolw
import ligolw
51from igwn_ligolw
import lsctables
52from igwn_ligolw.utils
import coincs
as ligolw_coincs
53import igwn_segments
as segments
55from .
import offsetvector
58__author__ =
"Kipp Cannon <kipp.cannon@ligo.org>"
59from .git_version
import date
as __date__
60from .git_version
import version
as __version__
74 Compute and return the time required for light to travel through
75 free space the distance separating the two instruments. The inputs
76 are two instrument prefixes (e.g., "H1"),
and the result
is
77 returned
in seconds. Note how this differs
from LAL
's
79 input,
and returns the time truncated to integer nanoseconds.
81 dx = lal.cached_detector_by_prefix[instrument1].location - lal.cached_detector_by_prefix[instrument2].location
82 return math.sqrt((dx * dx).sum()) / lal.C_SI
86# =============================================================================
88# Streaming Double Coincidence Generator
341 Queue a stream of partially time ordered events: as new events are
342 added, sort them into the queue
in time order; maintain a record
343 of the time up-to which the list of events
is complete;
and
344 provide events sorted
in time order to coincidence tests.
346 Searches must define a
class subclassed from this that defines an
347 override of the .event_time() method. See coincgen_doubles
for
348 information on what to do
with the subclass.
350 Implementation notes:
352 What
is actually inserted into the queue
is the time of the event
353 as defined by the .event_time() method provided by the subclass.
354 This allows sorts
and bisection searches to be performed quickly,
355 without the need
for much additional code. When we need to
356 retrieve an event, we need some way of turning the time of
357 the event (i.e., the thing that
is actually
in the queue) back into
358 that event,
and this
is done by adding an attribute to the time
359 object that points back to the original event. This means that
360 builtin Python numeric types cannot be used
for the times of the
361 events because new attributes cannot be attached to them.
362 Subclasses of the builtin numeric types can be used
as time
363 objects,
as can be LAL
's LIGOTimeGPS.
366 def event_time(event):
368 Override with a method to
return the
"time" of the given
369 event. The
"time" object that
is returned
is typically a
370 lal.LIGOTimeGPS object. If some other type
is used, it
371 must support arithmetic
and comparison
with python float
372 objects
and lal.LIGOTimeGPS objects,
and support comparison
373 with igwn_segments.PosInfinity
and
374 igwn_segments.NegInfinity,
and it must have a .__dict__
or
375 other mechanism allowing an additional attribute named
376 .event to be set on the object. The builtin float type
is
377 not suitable, but a subclass of float would be.
379 raise NotImplementedError
383 For internal use. Construct the object that is to be
384 inserted into the queue
from an event.
392 def __init__(self, offset, max_coinc_window):
394 Initialize a new, empty, singles queue. offset is the time
395 that will be added to events
in this queue when forming
396 coincidences
with events
from other queues,
and
397 max_coinc_window
is an upper bound on the maximum time that
398 can separate events
in this queue
from events
in other
399 queues
and they still be able to form coincidences --- it
400 is not that maximum time, necessarily, but the true maximum
401 time cannot be larger than max_coinc_window.
403 Note that max_coinc_window
is not defining the coincidence
404 test, that
is defined by the get_coincs machinery within
405 the coincgen_doubles
class. max_coinc_window
is used by
406 the streaming logic to determine where
in the event
407 sequences complete n-tuple candidates can be constructed.
408 Although max_coinc_window need only bound the time interval
409 described above, the latency of the coincidence engine
is
410 reduced
and the number of events stored
in the queue (
and
411 the computational overhead they cause)
is reduced
if the
412 number
is as small
as possible.
415 if max_coinc_window < 0.:
416 raise ValueError(
"max_coinc_window < 0 (%g)" % max_coinc_window)
440 Using .event_time() to define the times of events, the time
441 of the oldest event in the queue
or self.
t_complete if the
442 queue
is empty. The time scale
for .age
is that of the
443 events themselves,
not their shifted times (the offset
444 parameter
is not applied).
446 This
is not used by the coincidence engine. This
447 information
is provided
as a convenience
for calling code.
448 For example, some performance improvements might be
449 realized
if segment lists
or other information are clipped
450 to the range of times spanned by the contents of the
451 coincidence engine queues,
and this allows calling codes to
452 see what interval that
is,
in physical event times
not
453 time-shifted event times.
460 The time up to which the time-shifted events in other
461 detectors
' queues can be considered complete for the
462 purpose of forming n-tuple coincidences with the
463 time-shifted events
in this queue. This precedes the
464 (time-shifted) time up to which this event list
is complete
465 by the maximum coincidence window that can separate an
466 event
in this queue
from events
in others
and they still be
467 considered coincident
in time.
469 The earliest such time across all detectors
is the time up
470 to which the n-tuple candidate list can be completely
471 constructed, assuming the
"time" of an n-tuple candidate
is
472 defined to be the earliest time-shifted time of the events
477 def push(self, events, t_complete):
479 Add new events to the queue. Mark the queue complete up to
480 t_complete, meaning you promise no further events will be
481 added to the queue earlier than t_complete. The time scale
482 for t_complete
is that of the events themselves,
not their
483 shifted times (the offset parameter
is not applied). The
484 value of t_complete
is stored
for later retrieval.
486 NOTE: t_complete
is not permitted to decrease.
488 NOTE: the events sequence will be iterated over multiple
489 times. It may
not be a generator.
492 raise ValueError(
"t_complete has gone backwards: last was %s, new is %s" % (self.
t_complete, t_complete))
496 self.
index.update((
id(event), event)
for event
in events)
509 t is the time up to which the calling code
is in the
510 process of constructing all available n-tuple coincident
511 candidates. The
return value
is a tuple of all events
from
512 the queue whose time-shifted end times are up to (
not
513 including) t + max_coinc_window, which are all the events
514 from this queue that could form a coincidence
with an event
515 up to (
not including) t.
517 t
is defined
with respect to the time-shifted event times.
518 The contents of the returned tuple
is time-ordered
as
521 Assuming that t cannot go backwards, any of the reported
522 events that cannot be used again are removed
from the
525 If t
is None, then the queue
is completely flushed. All
526 remaining events are pulled
from the queue
and reported
in
527 the tuple, .t_complete
is reset to -inf
and all other
528 internal state
is reset. After calling .
pull()
with t =
529 None, the object
's state is equivalent to its initial state
530 and it
is ready to process a new stream of events.
538 events = tuple(entry.event
for entry
in self.
queue)
549 raise ValueError(
"pull to %g fails to precede time to which queue is complete, (%g + %g), by the required margin of %g s" % (t + self.
offset, self.
t_complete, self.
offset, self.
max_coinc_window))
556 flushed_events = tuple(entry.event
for entry
in self.
queue[:i])
558 for event
in flushed_events:
569 Using a pair of singlesqueue objects, constructs pairs of
570 coincident events from streams of partially time-ordered events.
572 Searches must subclass this. The .singlesqueue class attribute
573 must be set to the search-specific singlesqueue implementation to
574 be used, i.e., a subclass of singlesqueue
with an appropriate
575 .
event_time() override. The internally-defined .get_coincs
class
576 must be overridden
with an implementation that provides the
577 required .
__init__()
and .__call__() methods.
579 # subclass must override this attribute with a reference to an
580 # implementation of singlesqueue that implements the .event_time()
582 singlesqueue = singlesqueue
584 class get_coincs(object):
586 This class defines the coincidence test. An instance
is
587 initialized
with a sequence of events,
and is a callable
588 object. When the instance
is called
with a single event
589 from some other instrument, a time offset to apply to that
590 event (relative to the events
in this list)
and a time
591 coincidence window, the
return value must be a (possibly
592 empty) sequence of the initial events that are coincident
593 with that given event.
595 The sequence of events
with which the instance
is
596 initialized
is passed
in time order.
598 It
is not required that the implementation be subclassed
599 from this. This placeholder implementation
is merely
600 provided to document the required interface.
607 def __call__(self, event_a, offset_a, coinc_window):
608 return [event_b
for event_b
in self.events
if abs(event_a.time + offset_a - event_b.time) < coinc_window]
610 This
is performance-critical code
and a naive
611 implementation such
as the one above will likely be found
612 to be inadequate. Expect to implement this code
in C
for
619 Prepare to search a collection of events for
620 coincidences
with other single events. events
is a
621 time-ordered iterable of events. It
is recommended
622 that any additional indexing required to improve
623 search performance be performed
in this method.
625 raise NotImplementedError
627 def __call__(self, event_a, offset_a, coinc_window):
629 Return a sequence of the events from those passed
630 to .
__init__() that are coincident
with event_a.
631 The sequence need
not be
in time order. The object
632 returned by this method must be iterable
and
633 support being passed to bool() to test
if it
is
636 offset_a
is the time shift to be added to the time
637 of event_a before comparing to the times of events
638 passed to .
__init__(). This behaviour
is to
639 support the construction of time shifted
642 coinc_window
is the maximum time,
in seconds,
643 separating coincident events
from the shifted time of
644 event_a. Here, this
is the interval that defines
645 the coincidence test between the two detectors,
not
646 the bound on all such intervals used by the
647 singlesqueue object to determine the interval
for
648 which n-tuple candidates can be constructed.
650 raise NotImplementedError
652 def __init__(self, offset_vector, coinc_windows):
654 offset_vector must be a two-instrument offset vector. This
655 sets which two instruments' events are to be processed by
656 this object, and the offsets that should be applied to
657 their events when searching
for coincidences.
658 coinc_windows
is a dictionary mapping pairs of instruments
659 (
as sets) to the largest time that can separate events
from
660 those pairs of instruments (after applying the required
661 time offsets)
and they still be considered coincident.
663 if len(offset_vector) != 2:
664 raise ValueError(
"offset_vector must name exactly 2 instruments (got %d)" % len(offset_vector))
669 raise ValueError(
"coinc_window < 0 (%g)" % coinc_window)
672 max_coinc_window =
max(coinc_windows.values())
673 if max_coinc_window < 0.:
674 raise ValueError(
"max(coinc_window) < 0 (%g)" % max_coinc_window)
684 self.
queues =
dict((instrument, self.
singlesqueue(offset, max_coinc_window))
for instrument, offset
in offset_vector.items())
686 self.
index = ChainMap(*(queue.index
for queue
in self.
queues.values()))
696 The earliest of the internal queues' .age.
698 return min(queue.age
for queue
in self.
queues.values())
703 The earliest of the internal queues' .t_coinc_complete.
705 return min(queue.t_coinc_complete
for queue
in self.
queues.values())
707 def push(self, instrument, events, t_complete):
709 Push new events from some instrument into the internal
710 queues. The iterable of events need
not be time-ordered.
711 With self.
singlesqueue.event_time() defining the time of
712 events, t_complete sets the time up-to which the collection
713 of events
is known to be complete. That
is,
in the future
714 no new events will be pushed whose times are earlier than
715 t_complete. t_complete
is not permitted to decrease.
717 self.queues[instrument].push(events, t_complete)
719 def doublesgen(self, eventsa, offset_a, eventsb, singles_ids):
721 For internal use only.
733 if len(eventsb) > len(eventsa):
734 eventsa, eventsb = eventsb, eventsa
736 unswap =
lambda a, b: (b, a)
738 unswap =
lambda a, b: (a, b)
743 singles_ids.update(
id(event)
for event
in eventsb)
744 singles_ids_add = singles_ids.add
745 singles_ids_discard = singles_ids.discard
753 for eventa
in eventsa:
754 matches = queueb_get_coincs(eventa, offset_a, coinc_window)
757 for eventb
in matches:
759 singles_ids_discard(eventb)
760 yield unswap(eventa, eventb)
762 singles_ids_add(eventa)
764 def pull(self, t, singles_ids):
766 Generate a sequence of 2-element tuples of Python IDs of
767 coincident event pairs. Calling code is assumed to be
in
768 the process of constructing all possible n-tuple
769 coincidences such that at least one time-shifted event
in
770 the n-tuple
is before t,
and so we construct
and return all
771 pairs required to decide that set, meaning we will
772 construct
and return pairs that might contain only events
773 after t so that the calling code can test them
for
774 coincidence
with events
from other instruments that precede
775 t. The internal queues are flushed of events that cannot
776 be used again assuming that t never goes backwards. If t
777 is the special value
None then all coincident pairs that
778 can be constructed
from the internal queues are constructed
779 and returned, the queues are emptied
and returned to their
782 The order of the IDs
in each tuple
is alphabetical by
785 The Python IDs of events that are
not reported
in a
786 coincident pair are placed
in the singles_ids set.
788 NOTE: the singles_ids parameter passed to this function
789 must a set
or set-like object. It must support Python set
790 manipulation methods, like .clear(), .update(),
and so on.
816 def __init__(self, coincgen_doubles_type, offset_vector, coinc_windows, min_instruments, time_slide_id = None):
831 if len(offset_vector) > 2:
841 self.
index = ChainMap(*(index
for node
in self.
components for index
in node.index.maps))
842 elif len(offset_vector) == 2:
843 self.
components = (coincgen_doubles_type(offset_vector, coinc_windows),)
845 elif len(offset_vector) == 1:
855 assert not coinc_windows
856 offset, = offset_vector.values()
857 self.
components = (coincgen_doubles_type.singlesqueue(offset, 0.),)
860 raise ValueError(
"offset_vector cannot be empty")
865 self.
event_time = coincgen_doubles_type.singlesqueue.event_time
870 Equivalent to offsetvector.component_offsetvectors() except
871 only one input offsetvector
is considered,
not a sequence
872 of them,
and the output offsetvector objects preserve the
873 absolute offsets of each instrument,
not only the relative
874 offsets between instruments.
876 assert 1 <= n <= len(offset_vector)
877 for selected_instruments
in itertools.combinations(offset_vector, n):
883 The earliest of the component nodes' .age.
890 The earliest of the component nodes' .t_coinc_complete.
892 return min(node.t_coinc_complete
for node
in self.
components)
894 def push(self, instrument, events, t_complete):
896 Push new events from some instrument into the internal
897 queues. The iterable of events need
not be time-ordered.
898 With self.singlesqueue.
event_time() defining the time of
899 events, t_complete sets the time up-to which the collection
900 of events
is known to be complete. That
is,
in the future
901 no new events will be pushed whose times are earlier than
910 if instrument
in node.offset_vector:
911 node.push(instrument, events, t_complete)
915 Using the events contained in the internal queues construct
916 and return all coincidences they participate
in. It
is
917 assumed calling code
is in the process of constructing
918 n-tuple candidates such that each candidate contains at
919 least one event preceding t,
and so we construct
and return
920 all candidates required to decide that set, which might
921 include candidates all of whose events come after t.
923 Two objects are returned: a tuple of the (n=N)-way
924 coincidences, where N
is the number of instruments
in this
925 node
's offset vector, and a set of the
926 (min_instruments<=n<N)-way coincidences, which will be
927 empty if N < min_instruments.
929 Since the (min_instruments<=n<N)-tuple coincidences cannot
930 go on to form (n>N)-tuple coincidences, any that fail to
931 contain at least one event preceding t could, at this stage,
932 be discarded, however testing
for that would require paying
933 the price of the ID-->event look-up on all events
in all
934 candidates, which we will have to perform at each level of
935 the graph, including the final stage before reporting
936 candidates, so it
is more efficient to leave the invalid
937 candidates
in the stream at this stage
and cull them
in a
938 single
pass at the end.
940 The coincidences are reported
as tuples of the Python IDs
941 of the events that form coincidences. The .index look-up
942 table can be used to map these IDs back to the original
943 events. To do that, however, a copy of the contents of the
944 .index mapping must be made before calling this method
945 because this method will result
in some of the events
in
946 question being removed
from the queues,
and thus also
from
955 return tuple(), set()
964 self.
previous_t = t
if t
is not None else segments.NegInfinity
984 return tuple((
id(event),)
for event
in self.
components[0].
pull(t)[0]), set()
1004 return coincs, (set((eventid,)
for eventid
in singles_ids)
if self.
keep_partial else set())
1019 component_coincs_and_partial_coincs = tuple(component.pull(t)
for component
in self.
components)
1020 component_coincs = tuple(elem[0]
for elem
in component_coincs_and_partial_coincs)
1029 partial_coincs = set(itertools.chain(*component_coincs))
1062 partial_coincs.update(coinc
for coinc, count
in Counter(itertools.chain(*(elem[1]
for elem
in component_coincs_and_partial_coincs))).items()
if count >= 2)
1064 partial_coincs = set()
1065 del component_coincs_and_partial_coincs
1071 component_coincs0 = component_coincs[0]
1072 component_coincs1 = component_coincs[1]
1073 component_coincs2 = component_coincs[-1]
1074 del component_coincs
1076 for coinc0
in component_coincs0:
1087 coincs1 = component_coincs1[bisect_left(component_coincs1, coinc0[:-1]):bisect_left(component_coincs1, coinc0[:-2] + (coinc0[-2] + 1,))]
1093 coincs2 = component_coincs2[bisect_left(component_coincs2, coinc0[1:]):bisect_left(component_coincs2, coinc0[1:-1] + (coinc0[-1] + 1,))]
1115 for coinc1
in coincs1:
1116 confirmation = coinc0[1:] + coinc1[-1:]
1117 i = bisect_left(coincs2, confirmation)
1118 if i < len(coincs2)
and coincs2[i] == confirmation:
1119 new_coinc = coinc0[:1] + confirmation
1125 partial_coincs.difference_update(itertools.combinations(new_coinc, len(new_coinc) - 1))
1126 coincs.append(new_coinc)
1130 coincs = tuple(coincs)
1136 return coincs, partial_coincs
1140 def __init__(self, coincgen_doubles_type, offset_vector_dict, coinc_window, min_instruments = 2, verbose = False):
1145 if min_instruments < 1:
1146 raise ValueError(
"require min_instruments >= 1 (%d)" % min_instruments)
1147 if min(len(offset_vector)
for offset_vector
in offset_vector_dict.values()) < min_instruments:
1155 raise ValueError(
"encountered offset vector (%s) smaller than min_instruments (%d)", (str(
min(offset_vector_dict.values(), key =
lambda offset_vector: len(offset_vector))), min_instruments))
1163 coinc_windows =
dict((frozenset(pair), coinc_window +
light_travel_time(*pair))
for pair
in itertools.combinations(set(instrument
for offset_vector
in offset_vector_dict.values()
for instrument
in offset_vector), 2))
1165 print(
"coincidence windows:\n\t%s" %
",\n\t".join(
"%s = %g s" % (
"+".join(sorted(pair)), dt)
for pair, dt
in coinc_windows.items()))
1173 print(
"constructing coincidence assembly graph for %d offset vectors ..." % len(offset_vector_dict), file=sys.stderr)
1176 coincgen_doubles_type,
1180 time_slide_id = time_slide_id
1181 )
for time_slide_id, offset_vector
in sorted(offset_vector_dict.items())
1215 return numpy.array((1, 0))
if len(node.components) == 1
else numpy.array((0, 1)) + sum(walk(node)
for node
in node.components)
1216 print(
"graph contains %d fundamental nodes, %d higher-order nodes" % tuple(sum(walk(node)
for node
in self.
head)), file=sys.stderr)
1222 The earliest of the graph's head nodes' .age.
1224 return min(node.age
for node
in self.
head)
1227 def push(self, instrument, events, t_complete):
1229 Push new events from some instrument into the internal
1230 queues. The iterable of events need
not be time-ordered.
1231 With self.singlesqueue.event_time() defining the time of
1232 events, t_complete sets the time up-to which the event
1233 stream
from this instrument
is known to be complete. That
1234 is,
in the future no new events will be pushed
from this
1235 instrument whose times are earlier than t_complete.
1236 t_complete
is measured
with respect to the unshifted times
1239 Returns
True if the graph
's .t_coinc_complete has changed,
1240 indicating that it might be possible to form new
1241 candidates; False if the addition of these events does
not
1242 yet allow new candidates to be formed. If the
return value
1243 is False, .
pull()
is guaranteed to
return an empty
1244 candidate list unless the graph
is flushed of all
1248 for node
in self.
head:
1249 if instrument
in node.offset_vector:
1250 t_before = node.t_coinc_complete
1251 node.push(instrument, events, t_complete)
1252 t_changed |= t_before != node.t_coinc_complete
1256 def pull(self, newly_reported = None, flushed = None, flushed_unused = None, flush = False, coinc_sieve = None, event_collector = None):
1258 Extract newly available coincident candidates from the
1259 internal queues. If flush
if True, then all remaining
1260 candidates are extracted
and the internal queues are reset,
1261 otherwise only those candidates that can be constructed
1262 given the times up to which the individual event queues are
1263 known to be complete
and given the offset vectors being
1264 considered will be reported.
1266 This method returns a generator whose elements consist of
1267 (node, events) pairs, wherein events
is a tuple of
1268 single-detector event objects comprising a single
1269 coincident n-tuple,
and node
is the TimeSlideGraphNode
1270 object
from which the coincidence was retrieved. The node
1271 object can be consulted to retrieve the offset vector
1272 applied to the event times to form the coincidence,
and
1273 other information about the state of the analysis. If the
1274 optional coinc_sieve() function
is supplied (see below)
1275 then only those n-tuples that
pass that function
's test are
1276 included in the sequence.
1278 Two output streams are available. One stream
is the
1279 generator returned to the calling code explained above,
1280 which consists only of n-tuples that
pass the optional
1281 coinc_sieve() test. The other stream exits this code via
1282 calls to event_collector() (see below),
and consists of all
1283 coincident n-tuples formed by the coincidence engine,
1284 regardless of whether they
pass the optional coinc_sieve()
1285 test
or not. The n-tuples passed to event_collector()
1286 contain the Python IDs of the events,
not the event objects
1287 themselves. If coinc_sieve()
is not supplied, both streams
1288 contain the same n-tuples, one of tuples of event objects,
1289 the other of tuples of the Python IDs of those events.
1291 event_collector(),
if supplied, must be an object
with a
1292 .
push() method
with the signature
1294 event_collector.push(event_ids, offset_vector)
1296 The
return value
is ignored. It will be called once
for
1297 every coincident n-tuple that
is formed by the coincidence
1298 engine, regardless of whether
or not that n-tuple
is
1299 ultimately reported to the calling code. event_ids will be
1300 a tuple of Python IDs of the single-detector events
in the
1303 coinc_sieve(),
if supplied, must be a callable object
with
1306 coinc_sieve(events, offset_vector)
1308 If the
return value
is False the event tuple
is reported to
1309 the calling code, otherwise it
is discarded. The default
1310 is to report all n-tuples. This feature can be used to
1311 remove obviously uninteresting candidates
from the reported
1312 candidate stream, to reduce the memory requirements of the
1313 calling code
and the disk requirements of output documents.
1315 The optional parameters newly_reported, flushed,
and
1316 flushed_unused may be used to
pass lists to this code,
1317 which will be populated
with additional information to
1318 assist the calling code. If lists are supplied, their
1319 contents will be erased
and populated
with event objects
as
1322 newly_reported: list of single-detector events that have
1323 participated
in n-tuple candidates reported to the calling
1324 code
for the first time this invocation of .
pull(). Note
1325 that only those events that particate
in n-tuples that
1326 survive the optional coinc_sieve() test are included. This
1327 is meant to allow the calling code to populate an event
1328 table
with the list of just the events needed to describe
1329 the surviving n-tuple candidates.
1331 flushed: list of single-detector events that have been
1332 removed
from the internal queues because they are now
1333 sufficiently old that all n-tuples that they can
1334 participate
in have been formed
and reported. This can be
1335 used to assist the calling code
with house keeping tasks,
1336 for example emptying its event_collector implementation of
1337 unnecessary internal state.
1339 flushed_unused: list of the single-detector events
in the
1340 flushed list that never participated
in an n-tuple
1341 candidate. Note that events are considered to have
1342 participated
in an n-tuple candidate
if the coincidence
1343 engine reported them
in one, regardless of whether
or not
1344 the optional coinc_sieve() test ultimately rejected the
1345 candidate before it was reported to the calling code.
1347 NOTE: the intent of the coinc_sieve() feature
is not to
1348 alter the definition of the coincidence test, but merely
1349 reduce the data volume going into the output document.
1350 Therefore, the coinc_sieve() feature affects the events
1351 appearing
in the newly_reported list because that list
is
1352 expected to be used to construct the output document, but
1353 does
not affect the events appearing
in the flushed_unused
1354 list because that list
is meant to convey information about
1355 which events failed to form coincidenes,
for example
for
1356 the purpose of informing false-alarm probability noise
1367 if coinc_sieve
is None:
1368 coinc_sieve =
lambda events, offset_vector:
False
1371 if event_collector
is None:
1372 event_collector_push =
lambda event_ids, offset_vector:
None
1374 event_collector_push = event_collector.push
1376 newly_reported_ids = set()
1379 flushed_ids = set(index)
1381 newly_reported_update = newly_reported_ids.update
1383 id_to_event = index.__getitem__
1384 for node
in self.
head:
1386 event_time = node.event_time
1387 offset_vector = node.offset_vector
1391 candidate_seg = segments.segment(node.previous_t, segments.PosInfinity)
1393 t = node.t_coinc_complete
1394 candidate_seg = segments.segment(node.previous_t, t)
1400 for event_ids
in itertools.chain(*node.pull(t)):
1403 events = tuple(map(id_to_event, event_ids))
1407 if min(event_time(event) + offset_vector[event.ifo]
for event
in events)
not in candidate_seg:
1411 used_update(event_ids)
1412 event_collector_push(event_ids, offset_vector)
1422 if not coinc_sieve(events, offset_vector):
1423 newly_reported_update(event_ids)
1426 if newly_reported_ids:
1431 flushed_ids -= set(self.
index)
1436 flushed_unused_ids = flushed_ids - self.
used_ids
1440 flushed_unused_ids = set()
1449 if newly_reported
is not None:
1450 newly_reported[:] = map(id_to_event, newly_reported_ids)
1451 if flushed
is not None:
1452 flushed[:] = map(id_to_event, flushed_ids)
1453 if flushed_unused
is not None:
1454 flushed_unused[:] = map(id_to_event, flushed_unused_ids)
1468 A convenience interface to the XML document's coincidence tables,
1469 allowing for easy addition of coincidence events.
1476 self.
coinctable = lsctables.New(lsctables.CoincTable)
1477 xmldoc.childNodes[0].appendChild(self.
coinctable)
1488 self.
coinc_def_id = ligolw_coincs.get_coinc_def_id(xmldoc, coinc_definer_row.search, coinc_definer_row.search_coinc_type, create_new =
True, description = coinc_definer_row.description)
1494 def coinc_rows(self, process_id, time_slide_id, events, table_name):
1496 From a process ID, a time slide ID, and a sequence of
1497 events (generator expressions are OK), constructs
and
1498 initializes a coinc_event table row object
and a sequence
1499 of coinc_event_map table row objects describing the
1500 coincident event. The
return value
is the coinc_event row
1501 and a sequence of the coinc_event_map rows.
1503 The coinc_event
is *
not* assigned a coinc_event_id by this
1504 method. It
is expected that will be done
in
1506 question of whether
or not to include the coincidence
in
1507 the search results without consuming additional IDs.
1509 The coinc_event row
's .instruments and .likelihood
1510 attributes are initialized to null values. The calling
1511 code should populate as needed.
1513 When subclassing this method,
if the time shifts that were
1514 applied to the events
in constructing the coincidence are
1515 required to compute additional metadata, they can be
1520 coinc_event_id = None,
1521 table_name = table_name,
1522 event_id = event.event_id
1523 )
for event
in events]
1524 assert coincmaps,
"coincs must contain >= 1 event"
1527 process_id = process_id,
1529 coinc_event_id =
None,
1530 time_slide_id = time_slide_id,
1532 nevents = len(coincmaps),
1536 return coinc, coincmaps
1538 def append_coinc(self, coinc_event_row, coinc_event_map_rows):
1540 Appends the coinc_event row object and coinc_event_map row
1541 objects to the coinc_event
and coinc_event_map tables
1542 respectively after assigning a coinc_event_id to the
1543 coincidence. Returns the coinc_event row object.
1545 coinc_event_row.coinc_event_id = self.coinctable.get_next_id()
1547 for row
in coinc_event_map_rows:
1548 row.coinc_event_id = coinc_event_row.coinc_event_id
1550 return coinc_event_row
1563 def __init__(self, instruments, delta_t, min_instruments = 2, abundance_rel_accuracy = 1e-4):
1565 Model for coincidences among noise events collected
from
1566 several antennas. Independent Poisson processes are
1567 assumed. The coincidence window
for each pair of
1568 instruments
is (delta_t + light travel time between that
1569 pair). A coincidence
is assumed to require full N-way
1570 coincidence
and require at least min_instruments to
1573 Initial configuration requires some relatively expensive
1574 pre-calculation of internal quantities, but once
1575 initialized coincidence rates can be computed quickly
from
1576 observed event rates. Several other related quantities can
1581 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1587 raise ValueError(
"require len(instruments) >= min_instruments")
1589 raise ValueError(
"require delta_t >= 0.")
1590 if abundance_rel_accuracy <= 0.:
1591 raise ValueError(
"require abundance_rel_accuracy > 0.")
1615 anchor, *instruments = instruments
1711 elif len(instruments) == 1:
1717 dimensions = len(instruments)
1718 halfspaces = numpy.zeros((dimensions * (dimensions + 1), dimensions + 1), dtype =
"double")
1720 for i, instrument
in enumerate(instruments):
1723 halfspaces[i, j] = +1.
1724 halfspaces[i + 1, j] = -1.
1725 halfspaces[i, -1] = halfspaces[i + 1, -1] = -self.
tau[frozenset((anchor, instrument))]
1727 for i, ((j1, a), (j2, b))
in enumerate(itertools.combinations(enumerate(instruments), 2), dimensions):
1729 halfspaces[i, j1] = +1.
1730 halfspaces[i, j2] = -1.
1731 halfspaces[i + 1, j1] = -1.
1732 halfspaces[i + 1, j2] = +1.
1733 halfspaces[i, -1] = halfspaces[i + 1, -1] = -self.
tau[frozenset((a, b))]
1735 interior = numpy.zeros((len(instruments),), dtype =
"double")
1737 self.
rate_factors[key] = spatial.ConvexHull(spatial.HalfspaceIntersection(halfspaces, interior).intersections).volume
1744 A tuple of all possible instrument combinations (as
1749 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 1)
1750 >>> coincrates.all_instrument_combos
1751 (frozenset([
'V1']), frozenset([
'H1']), frozenset([
'L1']), frozenset([
'V1',
'H1']), frozenset([
'V1',
'L1']), frozenset([
'H1',
'L1']), frozenset([
'V1',
'H1',
'L1']))
1752 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1753 >>> coincrates.all_instrument_combos
1754 (frozenset([
'V1',
'H1']), frozenset([
'V1',
'L1']), frozenset([
'H1',
'L1']), frozenset([
'V1',
'H1',
'L1']))
1757 return tuple(frozenset(instruments)
for n
in range(self.
min_instruments, len(all_instruments) + 1)
for instruments
in itertools.combinations(all_instruments, n))
1762 Given the event rates for a collection of instruments,
1763 compute the rates at which N-way coincidences occur among
1764 them where N >= min_instruments. The
return value
is a
1765 dictionary whose keys are frozensets of instruments
and
1766 whose values are the rate of coincidences
for that set.
1768 NOTE: the computed rates are the rates at which
1769 coincidences among at least those instruments occur,
not
1770 the rate at which coincidences among exactly those
1771 instruments occur. e.g., considering the H1, L1, V1
1772 network,
for the pair H1, L1 the
return value
is the sum of
1773 the rate at which those two instruments form double
1774 coincidences
and also the rate at which they participate
in
1775 H1, L1, V1 triple coincidences.
1781 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1782 >>> coincrates.coinc_rates(H1 = 0.001, L1 = 0.002, V1 = 0.003)
1783 {frozenset([
'V1',
'H1']): 1.9372787960306537e-07, frozenset([
'V1',
'H1',
'L1']): 1.0125819710267318e-11, frozenset([
'H1',
'L1']): 6.00513846088957e-08, frozenset([
'V1',
'L1']): 3.77380092200718e-07}
1784 >>> coincrates.coinc_rates(H1 = 0.001, L1 = 0.002, V1 = 0.002)
1785 {frozenset([
'V1',
'H1']): 1.291519197353769e-07, frozenset([
'V1',
'H1',
'L1']): 6.750546473511545e-12, frozenset([
'H1',
'L1']): 6.00513846088957e-08, frozenset([
'V1',
'L1']): 2.5158672813381197e-07}
1786 >>> coincrates.coinc_rates(H1 = 0.001, L1 = 0.002, V1 = 0.001)
1787 {frozenset([
'V1',
'H1']): 6.457595986768845e-08, frozenset([
'V1',
'H1',
'L1']): 3.3752732367557724e-12, frozenset([
'H1',
'L1']): 6.00513846088957e-08, frozenset([
'V1',
'L1']): 1.2579336406690598e-07}
1790 raise ValueError(
"require event rates for %s, got rates for %s" % (
", ".join(sorted(self.
instruments)),
", ".join(sorted(rates))))
1791 if any(rate < 0.
for rate
in rates.values()):
1799 raise ValueError(
"rates must be >= 0")
1800 if self.
tau and max(rates.values()) *
max(self.
tau.values()) >= 1.:
1801 raise ValueError(
"events per coincidence window must be << 1: rates = %s, max window = %g" % (rates,
max(self.
tau.values())))
1808 for instruments
in coinc_rates:
1809 for instrument
in instruments:
1810 coinc_rates[instruments] *= rates[instrument]
1816 Given the event rates for a collection of instruments,
1817 compute the rates at which strict N-way coincidences occur
1818 among them where N >= min_instruments. The
return value
is
1819 a dictionary whose keys are frozensets of instruments
and
1820 whose values are the rate of coincidences
for that set.
1822 NOTE: the computed rates are the rates at which
1823 coincidences occur among exactly those instrument
1824 combinations, excluding the rate at which each combination
1825 participates
in higher-order coincs. e.g., considering the
1826 H1, L1, V1 network,
for the pair H1, L1 the
return value
is
1827 the rate at which H1, L1 doubles occur,
not including the
1828 rate at which the H1, L1 pair participates
in H1, L1, V1
1835 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1836 >>> coincrates.strict_coinc_rates(H1 = 0.001, L1 = 0.002, V1 = 0.003)
1837 {frozenset([
'V1',
'H1']): 1.937177537833551e-07, frozenset([
'V1',
'H1',
'L1']): 1.0125819710267318e-11, frozenset([
'H1',
'L1']): 6.004125878918543e-08, frozenset([
'V1',
'L1']): 3.7736996638100773e-07}
1838 >>> coincrates.strict_coinc_rates(H1 = 0.001, L1 = 0.002, V1 = 0.002)
1839 {frozenset([
'V1',
'H1']): 1.2914516918890337e-07, frozenset([
'V1',
'H1',
'L1']): 6.750546473511545e-12, frozenset([
'H1',
'L1']): 6.004463406242219e-08, frozenset([
'V1',
'L1']): 2.5157997758733847e-07}
1840 >>> coincrates.strict_coinc_rates(H1 = 0.001, L1 = 0.002, V1 = 0.001)
1841 {frozenset([
'V1',
'H1']): 6.457258459445168e-08, frozenset([
'V1',
'H1',
'L1']): 3.3752732367557724e-12, frozenset([
'H1',
'L1']): 6.004800933565894e-08, frozenset([
'V1',
'L1']): 1.2578998879366924e-07}
1848 for instruments
in sorted(strict_coinc_rates, reverse =
True, key =
lambda instruments: len(instruments)):
1854 for key, rate
in strict_coinc_rates.items():
1855 if instruments < key:
1856 strict_coinc_rates[instruments] -= rate
1858 assert all(rate >= 0.
for rate
in strict_coinc_rates.values()),
"encountered negative rate: %s" % strict_coinc_rates
1859 return strict_coinc_rates
1864 A dictionary mapping instrument combination (as a
1865 frozenset) to the total number of coincidences involving
1866 precisely that combination of instruments expected
from the
1870 raise ValueError(
"require segmentlists for %s, got %s" % (
", ".join(sorted(self.
instruments)),
", ".join(sorted(seglists))))
1873 livetime =
dict((on_instruments, float(abs(seglists.intersection(on_instruments) - seglists.union(self.
instruments - on_instruments))))
for on_instruments
in self.
all_instrument_combos)
1875 coinc_count = dict.fromkeys(livetime, 0.0)
1876 for on_instruments, T
in livetime.items():
1877 for coinc_instruments, rate
in self.
strict_coinc_rates(**
dict((instrument, (rate
if instrument
in on_instruments
else 0.))
for instrument, rate
in rates.items())).items():
1878 coinc_count[coinc_instruments] += T * rate
1885 Given the event rates for a collection of instruments,
1886 compute the natural logarithm of the probability that a
1887 coincidence
is found to involve exactly a given set of
1888 instruments. This
is equivalent to the ratios of the
1892 Raises ZeroDivisionError
if all coincidence rates are 0.
1898 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1899 >>> coincrates.lnP_instruments(H1 = 0.001, L1 = 0.002, V1 = 0.003)
1900 {frozenset([
'V1',
'H1']): -1.181124067253893, frozenset([
'V1',
'H1',
'L1']): -11.040192999777876, frozenset([
'H1',
'L1']): -2.352494317162074, frozenset([
'V1',
'L1']): -0.5143002401188091}
1903 total_rate = sum(strict_coinc_rates.values())
1904 if total_rate == 0.:
1905 raise ZeroDivisionError(
"all rates are 0")
1906 P_instruments =
dict((instruments, rate / total_rate)
for instruments, rate
in strict_coinc_rates.items())
1907 norm = sum(sorted(P_instruments.values()))
1909 assert abs(1.0 - norm) < 1e-14
1910 return dict((instruments, (math.log(P / norm)
if P
else NegInf))
for instruments, P
in P_instruments.items())
1915 Generator that, given the event rates for a collection of
1916 instruments, yields a sequence of two-element tuples each
1917 containing a randomly-selected frozen set of instruments
1918 and the natural logarithm of the ratio of the rate at which
1919 that combination of instruments forms coincidences to the
1920 rate at which it
is being yielded by this generator.
1926 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1927 >>> x = iter(coincrates.random_instruments(H1 = 0.001, L1 = 0.002, V1 = 0.003))
1929 (frozenset([
'H1',
'L1']), -3.738788683913535)
1931 NOTE: the generator yields instrument combinations drawn
1932 uniformly
from the set of allowed instrument combinations,
1933 but this
is not considered part of the definition of the
1934 behaviour of this generator
and this implementation detail
1935 should
not be relied upon by calling code. It
is stated
1936 here simply
for clarity
in case it helps improve
1937 optimization choices elsewhere.
1941 lnN = math.log(len(lnP_instruments))
1942 results = tuple((instruments, lnP - lnN)
for instruments, lnP
in lnP_instruments.items())
1943 choice = random.choice
1945 yield choice(results)
1950 Generator that yields dictionaries of random event
1951 time-of-arrival offsets for the given instruments such that
1952 the time-of-arrivals are mutually coincident given the
1953 maximum allowed inter-instrument \\Delta t
's. The values
1954 returned are offsets, and would need to be added to some
1955 common time to
yield absolute arrival times.
1959 >>> coincrates =
CoincRates((
"H1",
"L1",
"V1"), 0.005, 2)
1960 >>> x = iter(coincrates.plausible_toas((
"H1",
"L1")))
1962 {
'H1': 0.0,
'L1': -0.010229226372297711}
1964 instruments = tuple(instruments)
1966 raise ValueError(
"not configured for %s" %
", ".join(sorted(set(instruments) - self.
instruments)))
1968 raise ValueError(
"require at least %d instruments, got %d" % (self.
min_instruments, len(instruments)))
1969 anchor, instruments = instruments[0], instruments[1:]
1970 anchor_offset = ((anchor, 0.0),)
1971 uniform = random.uniform
1972 windows = tuple((instrument, -self.
tau[frozenset((anchor, instrument))], +self.
tau[frozenset((anchor, instrument))])
for instrument
in instruments)
1973 ijseq = tuple((i, j, self.
tau[frozenset((instruments[i], instruments[j]))])
for (i, j)
in itertools.combinations(range(len(instruments)), 2))
1975 dt = tuple((instrument, uniform(lo, hi))
for instrument, lo, hi
in windows)
1976 if all(abs(dt[i][1] - dt[j][1]) <= maxdt
for i, j, maxdt
in ijseq):
1977 yield dict(anchor_offset + dt)
1991 Time-of-arrival triangulator. See section 6.6.4 of
1992 "Gravitational-Wave Physics and Astronomy" by Creighton
and
1995 An instance of this
class is
a function-like object that accepts a
1996 tuple of event arival times
and returns a tuple providing
1997 information derived by solving
for the maximum-likelihood source
1998 location assuming Gaussian-distributed timing errors.
2000 def __init__(self, rs, sigmas, v = lal.C_SI):
2002 Create and initialize a triangulator object.
2004 rs
is a sequence of location 3-vectors, sigmas
is a
2005 sequence of the timing uncertainties
for those locations.
2006 Both sequences must be
in the same order --- the first
2007 sigma
in the sequence
is interpreted
as belonging to the
2008 first location 3-vector ---
and, of course, they must be
2011 v
is the speed at which the wave carrying the signals
2012 travels. The rs 3-vectors carry units of distance, the
2013 sigmas carry units of time, v carries units of
2014 distance/time. What units are used
for the three
is
2015 arbitrary, but they must be mutually consistent. The
2016 default value
for v
in c, the speed of light,
in
2017 metres/second, therefore the location 3-vectors should be
2018 given
in metres
and the sigmas should be given
in seconds
2019 unless a value
for v
is provided
with different units.
2023 >>>
from numpy
import array
2025 ... array([-2161414.92636, -3834695.17889, 4600350.22664]),
2026 ... array([ -74276.0447238, -5496283.71971 , 3224257.01744 ]),
2027 ... array([ 4546374.099 , 842989.697626, 4378576.96241 ])
2036 This creates a TOATriangulator instance configured
for the
2037 LIGO Hanford, LIGO Livingston
and Virgo antennas
with 5 ms
2038 time-of-arrival uncertainties at each location.
2040 Note: rs
and sigmas may be iterated over multiple times.
2042 assert len(rs) == len(sigmas)
2045 self.
rs = numpy.vstack(rs)
2050 rbar = sum(self.
rs / self.
sigmas[:,numpy.newaxis]**2) / sum(1 / self.
sigmas**2)
2056 M = self.
R / (self.
v * self.
sigmas[:,numpy.newaxis])
2058 self.U, self.S, self.
VT = numpy.linalg.svd(M)
2070 Triangulate the direction to the source of a signal based
2071 on a tuple of times when the signal was observed. ts is a
2072 sequence of signal arrival times. One arrival time must be
2073 provided
for each of the observation locations provided
2074 when the instance was created,
and the units of the arrival
2075 times must be the same
as the units used
for the sequence
2078 The
return value
is a tuple of information derived by
2079 solving
for the maximum-likelihood source location assuming
2080 Gaussian-distributed timing errors. The
return value
is
2082 (n, toa, chi2 / DOF, dt)
2084 where n
is a unit 3-vector pointing
from the co-ordinate
2085 origin towards the source of the signal, toa
is the
2086 time-of-arrival of the signal at the co-ordinate origin,
2087 chi2 / DOF
is the \\chi^{2} per degree-of-freedom
from to
2088 the arrival time residuals,
and dt
is the root-sum-square
2089 of the arrival time residuals.
2093 >>>
from numpy
import array
2094 >>>
from numpy
import testing
2096 ... array([-2161414.92636, -3834695.17889, 4600350.22664]),
2097 ... array([ -74276.0447238, -5496283.71971 , 3224257.01744 ]),
2098 ... array([ 4546374.099 , 842989.697626, 4378576.96241 ])
2105 >>> n, toa, chi2_per_dof, dt = triangulator([
2106 ... 794546669.429688,
2107 ... 794546669.41333,
2108 ... 794546669.431885
2112 array([[-0.45605637, 0.75800934, 0.46629865],
2113 [-0.45605637, 0.75800934, 0.46629865]])
2114 >>> testing.assert_approx_equal(toa, 794546669.4269662)
2115 >>> testing.assert_approx_equal(chi2_per_dof, 0.47941941158371465)
2116 >>> testing.assert_approx_equal(dt, 0.005996370224459011)
2118 NOTE:
if len(rs) >= 4, n
is a 1x3 array.
2119 if len(rs) == 3, n
is a 2x3 array.
2120 if len(rs) == 2, n
is None.
2121 NOTE: n
is not the source direction but the propagation
2123 Therefore,
if you want source direction, you have to
2125 NOTE: n
is represented by earth fixed coordinate,
not
2126 celestial coordinate.
2127 Up to your purpose, you should transform \\phi -> RA.
2128 To do it, you can use dir2coord.
2130 assert len(ts) == len(self.
sigmas)
2134 ts = numpy.array([float(t - t0)
for t
in ts])
2137 tbar = sum(ts / self.
sigmas**2) / sum(1 / self.
sigmas**2)
2139 tau = (ts - tbar) / self.
sigmas
2141 tau_prime = numpy.dot(self.U.T, tau)[:3]
2143 if len(self.
rs) >= 3:
2146 np = numpy.array([tau_prime / self.S, tau_prime / self.S])
2148 np[0][2] = math.sqrt(1.0 - np[0][0]**2 - np[0][1]**2)
2149 np[1][2] = -math.sqrt(1.0 - np[1][0]**2 - np[1][1]**2)
2153 np /= math.sqrt(numpy.dot(np[0], np[0]))
2156 n = numpy.array([numpy.zeros(3), numpy.zeros(3)])
2157 n = numpy.dot(self.
VT.T, np.T).T
2160 assert abs(numpy.dot(n[0], n[0]) - 1.0) < 1e-8
2161 assert abs(numpy.dot(n[1], n[1]) - 1.0) < 1e-8
2164 toa = sum((ts - numpy.dot(self.
rs, n[0]) / self.
v) / self.
sigmas**2) / sum(1 / self.
sigmas**2)
2167 chi2 = sum((numpy.dot(self.
R, n[0]) / (self.
v * self.
sigmas) - tau)**2)
2170 dt = ts - toa - numpy.dot(self.
rs, n[0]) / self.
v
2171 dt = math.sqrt(numpy.dot(dt, dt))
2174 def n_prime(l, Stauprime = self.S * tau_prime, S2 = self.S * self.S):
2175 return Stauprime / (S2 + l)
2176 def secular_equation(l):
2178 return numpy.dot(np, np) - 1
2182 lsing = -self.S * self.S
2199 while secular_equation(l_lo) / secular_equation(l_hi) > 0:
2200 l_lo, l_hi = l_hi, l_hi * 2
2203 l = scipy.optimize.brentq(secular_equation, l_lo, l_hi)
2209 n = numpy.dot(self.
VT.T, np)
2212 assert abs(numpy.dot(n, n) - 1.0) < 1e-8
2215 toa = sum((ts - numpy.dot(self.
rs, n) / self.
v) / self.
sigmas**2) / sum(1 / self.
sigmas**2)
2218 chi2 = sum((numpy.dot(self.
R, n) / (self.
v * self.
sigmas) - tau)**2)
2221 dt = ts - toa - numpy.dot(self.
rs, n) / self.
v
2222 dt = math.sqrt(numpy.dot(dt, dt))
2228 xp = tau_prime[0] / self.S[0]
2230 np = numpy.array([xp, 0, math.sqrt(1-xp**2)])
2233 np = numpy.array([xp, 0, 0])
2236 n = numpy.dot(self.
VT.T, np)
2239 assert abs(numpy.dot(n, n) - 1.0) < 1e-8
2242 toa = sum((ts - numpy.dot(self.
rs, n) / self.
v) / self.
sigmas**2) / sum(1 / self.
sigmas**2)
2245 chi2 = sum((numpy.dot(self.
R, n) / (self.
v * self.
sigmas) - tau)**2)
2248 dt = ts - toa - numpy.dot(self.
rs, n) / self.
v
2249 dt = math.sqrt(numpy.dot(dt, dt))
2255 return n, t0 + toa, chi2 / len(self.
sigmas), dt
2260 This transforms from propagation direction vector to right
2261 ascension
and declination source co-ordinates.
2263 The input
is the propagation vector, n,
in Earth fixed
2264 co-ordinates (x axis through Greenwich merdian
and equator,
2265 z axis through North Pole),
and the time. n does
not need
2266 to be a unit vector. The
return value
is the (ra, dec)
2267 pair,
in radians. NOTE: right ascension
is not
2268 necessarily
in [0, 2\\pi).
2272 raise ValueError(
"n must be a 3-vector")
2275 n /= math.sqrt(numpy.dot(n, n))
2281 RA = numpy.arctan2(n[1], n[0]) + lal.GreenwichMeanSiderealTime(gps)
2282 DEC = numpy.arcsin(n[2])
2305 Base class for parameter distribution densities for use in log
2306 likelihood ratio ranking statistics. Generally several instances
2307 of (subclasses of) this will be grouped together to construct a log
2308 likelihood ratio class for use as
a ranking statistic in
a
2309 trigger-based search. For example,
as a minimum one would expect
2310 one instance
for the numerator
and another
for the denominator, but
2311 additional ones might be included
in a practical ranking statistic
2312 implementation,
for example a third might be used
for storing a
2313 histogram of the candidates observed
in a search.
2315 Typically, the ranking statistic implementation will provide a
2316 function to transform a candidate to the arguments to use
with the
2317 .
__call__() implementation,
and so
in this way a LnLRDensity object
2318 is generally only meaningful
in the context of the ranking
2319 statistic
class for which it has been constructed.
2321 def __call__(self, *args, **kwargs):
2323 Evaluate. Return the natural logarithm of the density
2324 evaluated at the given parameters.
2326 raise NotImplementedError
2330 Marginalize the two densities.
2332 raise NotImplementedError
2336 Increment the counts defining this density at the given
2339 raise NotImplementedError
2343 Return a duplicate copy of this object.
2345 raise NotImplementedError
2349 Ensure all internal densities are normalized, and
2350 initialize interpolator objects
as needed
for smooth
2351 evaluation. Must be invoked before .
__call__() will
yield
2354 NOTE:
for some implementations this operation will
2355 irreversibly alter the contents of the counts array,
for
2356 example often this operation will involve the convolution
2357 of the counts
with a density estimation kernel. If it
is
2358 necessary to preserve a pristine copy of the counts data,
2359 use the .
copy() method to obtain a copy of the data, first,
2360 and then .
finish() the copy.
2362 raise NotImplementedError
2366 Generator returning a sequence of parameter values drawn
2367 from the distribution density. Some subclasses might
2368 choose
not to implement this,
and those that do might
2369 choose to use an MCMC-style sample generator
and so the
2370 samples should
not be assumed to be statistically
2373 raise NotImplementedError
2377 Serialize to an XML fragment and return the root element of
2378 the resulting XML tree.
2380 Subclasses must chain to this method, then customize the
2381 return value
as needed.
2383 return ligolw.LIGO_LW({
"Name":
"%s:lnlrdensity" % name})
2388 Sub-classes can use this in their overrides of the
2389 .
from_xml() method to find the root element of the XML
2392 name = "%s:lnlrdensity" % name
2393 xml = [elem
for elem
in xml.getElementsByTagName(ligolw.LIGO_LW.tagName)
if elem.hasAttribute(
"Name")
and elem.Name == name]
2395 raise ValueError(
"XML tree must contain exactly one %s element named %s" % (ligolw.LIGO_LW.tagName, name))
2401 In the XML document tree rooted at xml, search for the
2402 serialized LnLRDensity object named name,
and deserialize
2403 it. The
return value
is the deserialized LnLRDensity
2411 raise NotImplementedError
2421 Mixin to assist in implementing a log likelihood ratio ranking
2422 statistic class. The ranking statistic class that inherits from
2423 this must: (i) define a callable .numerator attribute that returns
2424 ln P(*args, **kwargs | signal); (ii) define a callable
2425 .denominator attribute that returns ln P(*args, **kwargs | noise).
2427 Inheriting
from this will:
2429 1. Add a .
__call__() method that returns the natural logarithm of
2430 the likelihood ratio
2432 ln P(*args, **kwargs | signal) - ln P(*args, **kwargs | noise)
2434 The implementation handles various special cases sensibly, such
as
2435 when either
or both of the logarithms of the numerator
and
2436 denominator diverge.
2438 2. Add a .ln_lr_samples() method that makes use of the .numerator
2439 and .denominator attributes, together
with the .
__call__() method
2440 to transform a sequence of (*args, **kwargs) into a sequence of log
2441 likelihood ratios
and their respective relative frequencies. This
2442 can be used to construct histograms of P(ln L | signal)
and P(ln L
2443 | noise). These distributions are required
for,
for example,
2444 signal rate estimation
and false-alarm probability estimation.
2446 Why
is the likelihood ratio useful? Starting
from Bayes
' theorem,
2447 and using the fact that
"signal" and "noise" are the only two
2450 P(data | signal) * P(signal)
2451 P(signal | data) = ----------------------------
2454 P(data | signal) * P(signal)
2455 = ---------------------------------------------------------
2456 P(data | noise) * P(noise) + P(data | signal) * P(signal)
2458 [P(data | signal) / P(data | noise)] * P(signal)
2459 = ----------------------------------------------------------------
2460 1 - P(signal) + [P(data | signal) / P(data | noise)] * P(signal)
2463 P(signal | data) = ----------------------------
2464 1 + (Lambda - 1) * P(signal)
2467 where Lambda = ----------------
2470 Differentiating P(signal | data) w.r.t. Lambda shows the derivative
2471 is always positive, so the probability that a candidate
is the
2472 result of a gravitiational wave
is a monotonically increasing
2473 function of the likelihood ratio. Ranking events
from "most likely
2474 to be a genuine signal" to "least likely to be a genuine signal
"
2475 can be performed by sorting candidates by likelihood ratio. Or, if
2476 one wanted to set a threshold on P(signal | data) to select a
2477 subset of candidates such that the candidates selected have a given
2478 purity (the fraction of them that are real
is fixed), that
is
2479 equivalent to selecting candidates using a likelihood ratio
2482 These are expressions of the Neyman-Pearson lemma which tells us
2483 that thresholding on Lambda creates a detector that extremizes the
2484 detection efficiency at fixed false-alarm rate.
2486 def __call__(self, *args, **kwargs):
2488 Return the natural logarithm of the likelihood ratio for
2489 the given parameters,
2491 ln P(*args, **kwargs | signal) - ln P(*args, **kwargs | noise)
2493 The arguments are passed verbatim to the .
__call__()
2494 methods of the .numerator
and .denominator attributes of
2495 self
and the
return value
is computed
from the results.
2497 NOTE: sub-classes may override this method, possibly
2499 mechanism does
not require this method to
return exactly
2500 the natural logarithm of the .numerator/.denominator ratio.
2502 numerator, denominator
and ranking statistic are related to
2503 each other
as the latter being the ratio of the former two,
2504 it evaluates all three separately. For this reason, the
2505 .
__call__() method that implements the ranking statistic
is
2506 free to include other logic, such
as hierarchical cuts
or
2507 bail-outs that are
not stricly equivalent to the ratio of
2508 the numerator
and denominator.
2512 .numerator/.denominator=0/0
is mapped to ln Lambda = -inf,
2513 meaning P(signal | data) = 0. Although this condition
is
2514 nonsensical because the data
is claimed to be inconsistent
2515 with both noise
and signal --- the only two things it can
2516 be --- our task here
is to compute something that
is
2517 monotonic
in the probability the data
is the result of a
2518 signal,
and since this data cannot be
from a signal the
2519 correct answer
is -inf.
2521 .numerator/.denominator = +inf/+inf
is mapped to ln Lambda
2522 = NaN. This
is sufficiently nonsensical that there
is no
2523 correct interpretation. A warning will be displayed when
2524 this
is encountered.
2526 lnP_signal = self.numerator(*args, **kwargs)
2527 lnP_noise = self.denominator(*args, **kwargs)
2528 if math.isinf(lnP_noise)
and math.isinf(lnP_signal):
2530 if lnP_noise < 0.
and lnP_signal < 0.:
2547 if lnP_noise > 0.
and lnP_signal > 0.:
2552 warnings.warn(
"inf/inf encountered")
2553 return lnP_signal - lnP_noise
2555 def ln_lr_samples(self, random_params_seq, signal_noise_pdfs = None):
2557 Generator that transforms a sequence of candidate parameter
2558 samples into a sequence of log likelihood ratio samples.
2560 random_params_seq is a sequence (generator
is OK) yielding
2561 3-element tuples whose first two elements provide the *args
2562 and **kwargs values to be passed to the .numerator
and
2563 .denominator functions,
and whose third element
is the
2564 natural logarithm of the probability density
from which the
2565 (*args, **kwargs) parameters have been drawn evaluated at
2568 The output of this generator
is a sequence of 3-element
2569 tuples, each of whose elements are:
2571 1. a value of the natural logarithm of the likelihood
2574 2. the natural logarithm of the relative frequency of
2575 occurance of that likelihood ratio
in the signal population
2576 corrected
for the relative frequency at which the
2577 random_params_seq sampler
is causing that value to be
2580 3. the natural logarithm of the relative frequency of
2581 occurance of that likelihood ratio
in the noise population
2582 similarly corrected
for the relative frequency at which the
2583 random_params_seq sampler
is causing that value to be
2586 The intention
is for the first element of each tuple to be
2587 added to histograms using the two relative frequencies
as
2588 weights, i.e., the two relative frequencies give the number
2589 of times one should consider this one draw of log
2590 likelihood ratio to have occured
in the two populations.
2592 On each iteration, the *args
and **kwargs values yielded by
2593 random_params_seq are passed to our own .
__call__() method
2594 to evalute the log likelihood ratio at that choice of
2595 parameter values. The parameters are also passed to the
2596 .
__call__() mehods of our own .numerator
and .denominator
2597 attributes to obtain the signal
and noise population
2598 densities at those parameters.
2600 If signal_noise_pdfs
is not None then, instead of using our
2601 own .numerator
and .denominator attributes, the parameters
2602 are passed to the .
__call__() methods of its .numerator
and
2603 .denominator attributes to obtain those densities. This
2604 allows the distribution of ranking statistic values
2605 obtained
from alternative signal
and noise populations to
2606 be modelled. This
is sometimes useful
for diagnostic
2611 Within the context of the intended application, it
is
2612 sufficient
for all of the probabilities involved (the
2613 .numerator
and .denominator probability densities,
and the
2614 probability density supplied by the random_params_seq
2615 geneator) to be correct up to unknown normalization
2616 constants, i.e., the natural logarithms of the probabilties
2617 to be correct up to unknown additive constants. That
is
2618 why the two probability densities yielded by each iteration
2619 of this generator are described
as relative frequencies:
2620 the ratios among their values are meaningful, but
not their
2623 If all of the supplied probabilities are,
in fact, properly
2624 normalized, then the relative frequencies returned by this
2625 generator are, also, correctly normalized probability
2628 if signal_noise_pdfs
is None:
2629 lnP_signal_func = self.numerator
2630 lnP_noise_func = self.denominator
2632 lnP_signal_func = signal_noise_pdfs.numerator
2633 lnP_noise_func = signal_noise_pdfs.denominator
2634 for args, kwargs, lnP_params
in random_params_seq:
2635 lnP_signal = lnP_signal_func(*args, **kwargs)
2636 lnP_noise = lnP_noise_func(*args, **kwargs)
2637 yield self(*args, **kwargs), lnP_signal - lnP_params, lnP_noise - lnP_params
static double max(double a, double b)
static double min(double a, double b)
Subclass of the dict built-in type for storing mappings of instrument to time offset.
def __init__(self, instruments, delta_t, min_instruments=2, abundance_rel_accuracy=1e-4)
Model for coincidences among noise events collected from several antennas.
def marginalized_strict_coinc_counts(self, seglists, **rates)
A dictionary mapping instrument combination (as a frozenset) to the total number of coincidences invo...
def random_instruments(self, **rates)
Generator that, given the event rates for a collection of instruments, yields a sequence of two-eleme...
def strict_coinc_rates(self, **rates)
Given the event rates for a collection of instruments, compute the rates at which strict N-way coinci...
def lnP_instruments(self, **rates)
Given the event rates for a collection of instruments, compute the natural logarithm of the probabili...
def coinc_rates(self, **rates)
Given the event rates for a collection of instruments, compute the rates at which N-way coincidences ...
def plausible_toas(self, instruments)
Generator that yields dictionaries of random event time-of-arrival offsets for the given instruments ...
def all_instrument_combos(self)
A tuple of all possible instrument combinations (as frozensets).
A convenience interface to the XML document's coincidence tables, allowing for easy addition of coinc...
def coinc_rows(self, process_id, time_slide_id, events, table_name)
From a process ID, a time slide ID, and a sequence of events (generator expressions are OK),...
def __init__(self, xmldoc, coinc_definer_row)
def append_coinc(self, coinc_event_row, coinc_event_map_rows)
Appends the coinc_event row object and coinc_event_map row objects to the coinc_event and coinc_event...
Base class for parameter distribution densities for use in log likelihood ratio ranking statistics.
def copy(self)
Return a duplicate copy of this object.
def finish(self)
Ensure all internal densities are normalized, and initialize interpolator objects as needed for smoot...
def from_xml(cls, xml, name)
In the XML document tree rooted at xml, search for the serialized LnLRDensity object named name,...
def get_xml_root(cls, xml, name)
Sub-classes can use this in their overrides of the .from_xml() method to find the root element of the...
def __call__(self, *args, **kwargs)
Evaluate.
def __iadd__(self, other)
Marginalize the two densities.
def to_xml(self, name)
Serialize to an XML fragment and return the root element of the resulting XML tree.
def samples(self)
Generator returning a sequence of parameter values drawn from the distribution density.
def increment(self, *args, **kwargs)
Increment the counts defining this density at the given parameters.
Mixin to assist in implementing a log likelihood ratio ranking statistic class.
def __call__(self, *args, **kwargs)
Return the natural logarithm of the likelihood ratio for the given parameters,.
def ln_lr_samples(self, random_params_seq, signal_noise_pdfs=None)
Generator that transforms a sequence of candidate parameter samples into a sequence of log likelihood...
Time-of-arrival triangulator.
def dir2coord(n, gps)
This transforms from propagation direction vector to right ascension and declination source co-ordina...
def __call__(self, ts)
Triangulate the direction to the source of a signal based on a tuple of times when the signal was obs...
def age(self)
The earliest of the graph's head nodes' .age.
def __init__(self, coincgen_doubles_type, offset_vector_dict, coinc_window, min_instruments=2, verbose=False)
def pull(self, newly_reported=None, flushed=None, flushed_unused=None, flush=False, coinc_sieve=None, event_collector=None)
Extract newly available coincident candidates from the internal queues.
def push(self, instrument, events, t_complete)
Push new events from some instrument into the internal queues.
def age(self)
The earliest of the component nodes' .age.
def t_coinc_complete(self)
The earliest of the component nodes' .t_coinc_complete.
def component_offsetvectors(offset_vector, n)
Equivalent to offsetvector.component_offsetvectors() except only one input offsetvector is considered...
def push(self, instrument, events, t_complete)
Push new events from some instrument into the internal queues.
def __init__(self, coincgen_doubles_type, offset_vector, coinc_windows, min_instruments, time_slide_id=None)
def pull(self, t)
Using the events contained in the internal queues construct and return all coincidences they particip...
This class defines the coincidence test.
def __init__(self, events)
Prepare to search a collection of events for coincidences with other single events.
def __call__(self, event_a, offset_a, coinc_window)
Return a sequence of the events from those passed to .__init__() that are coincident with event_a.
Using a pair of singlesqueue objects, constructs pairs of coincident events from streams of partially...
def age(self)
The earliest of the internal queues' .age.
def __init__(self, offset_vector, coinc_windows)
offset_vector must be a two-instrument offset vector.
def t_coinc_complete(self)
The earliest of the internal queues' .t_coinc_complete.
def pull(self, t, singles_ids)
Generate a sequence of 2-element tuples of Python IDs of coincident event pairs.
def doublesgen(self, eventsa, offset_a, eventsb, singles_ids)
For internal use only.
def push(self, instrument, events, t_complete)
Push new events from some instrument into the internal queues.
Queue a stream of partially time ordered events: as new events are added, sort them into the queue in...
def age(self)
Using .event_time() to define the times of events, the time of the oldest event in the queue or self....
def event_time(event)
Override with a method to return the "time" of the given event.
def queueentry_from_event(self, event)
For internal use.
def t_coinc_complete(self)
The time up to which the time-shifted events in other detectors' queues can be considered complete fo...
def push(self, events, t_complete)
Add new events to the queue.
def pull(self, t)
t is the time up to which the calling code is in the process of constructing all available n-tuple co...
def __init__(self, offset, max_coinc_window)
Initialize a new, empty, singles queue.
INT8 XLALLightTravelTime(const LALDetector *aDet, const LALDetector *bDet)
def light_travel_time(instrument1, instrument2)
Compute and return the time required for light to travel through free space the distance separating t...