wdflow: the Wavelet Detection Filter pipeline
The wavegram match is measured on a grid that can resolve the delay The bin two detectors' maps are compared on is derived by the network stage from what the comparison has to represent, instead of being inherited from the column width of whichever rendering was passed to it. Two bounds fix it and both are physical: a bin coarser than a tile adds one detector's signed coefficients to each other before either detector meets the other, so an oscillating transient cancels against itself and the bands whose tiles are shortest lose most, which is a frequency prior the search does not carry; a bin coarser than the light travel time cannot represent the delay the coincidence exists to measure, so the lag axis holds one usable point whatever tolerance the edge was admitted on. The bin is the shorter of the shortest tile the band ladder holds and the network's light travel time. The comparison of two wavegrams is a baseline, and is optional TriggerGraphBuilder takes match_wavegrams. The comparison of a coincident pair's two renderings is bounded by one, carries no loudness and maximises over displacements, so it is a morphological baseline and enters no ranking; it costs a correlation per candidate, and a study that does not read it need not pay for it. With it off network_wavegram_match, network_correlation and coherent_statistic are not measured, and network_wavegram_matched says so rather than a zero standing in for a measurement. The displacements searched are the timing tolerance's. Each rendering is laid on its own event's instant and the whole bins of the difference between the two instants are applied as a shift, at no cost whatever that difference is, so the alignment at an arrival-time difference the light travel time allows is already reachable. Searching as far as the transient lasts would only add agreements at displacements the geometry forbids, at a cost growing as the square of the transient's own length. One coherent amplitude, not two names for it network_block_morphology was a literal copy of network_morphology and is gone; CoherentRanking reads the one that remains. network_morphology is the root of the magnitude of the signed coherent energy, so it is the geometric mean of the two events' amplitudes on their noise scales reduced by the root of the agreement between their tiles --- a quantity that vanishes when either detector is silent, which the sum in quadrature of the two does not. The grid a pair is compared on comes from that pair An event is rendered once, on a window centred on its own instant and no wider than the event reaches. What two events are compared on is then the longer of the two, widened by the displacements to be searched, and is a property of that pair alone. Rendering every event on the widest event of the run made a transient lasting minutes fix the grid of every millisecond-long one; what a run holds is not known in advance, and at a bin of the shortest tile that array does not exist. Pairs are handled in groups sharing a grid, by powers of two, so a grid is built once per scale and no loop runs over pairs. Because each pair is searched over its own transient, two pairs' profiles are of one length only on the part they share. What TriggerGraph carries per edge is therefore the profile on the timing tolerance's own lags, which every pair can be read on and which a model can take as a feature of fixed size, together with the agreement, the displacement it was found at and whether the pair was compared at all --- taken where the comparison took them, over the pair's whole search, and not reduced a second time from the part that was kept. The match is slid over the transient, not over the light travel time Each event's rendering is laid on that event's own instant, the centre of the tile carrying its largest coefficient. Two detectors seeing one transient at different amplitudes need not make that the same part of it, so their two renderings can be displaced by as much as the transient lasts while their arrival times differ by no more than the light travel time. The displacements searched are therefore the longer of the two events' own extents --- measured on the tiles the event is made of --- widened by the timing tolerance, and not the tolerance alone. Searching the tolerance alone cannot reach the alignment of a pair whose two detectors anchored on different parts of one transient, and what it reports for such a pair is the largest agreement among displacements that do not align it. The displacement found is reported beside the agreement, from the same pass, so it is not a second estimate to be reconciled with it. It is not constrained to the light travel time: a displacement larger than the geometry allows is the match saying the two renderings do not align physically, which is a statement about the pair rather than a truncation of the search. candidate_table now also carries whether a pair was compared at all, so that a match of zero is distinguishable from a match that was never formed, and a pair never compared reports no displacement rather than the first point of the lag axis. The match is asked only of a pair already coincident in time A pair is admitted on the events' stretches of time, not on the difference of their instants, and that stays as it is: a transient longer than one analysis window is assembled as several events and the two detectors need not keep the same one, so gating admission on the instants would take a candidate away from detectors that did assemble it. The wavegram match, though, is a comparison of two morphologies at a displacement, and it is now formed only where the two events' own instants are already within the tolerance the geometry and their timing spreads allow. No displacement the tolerance permits brings the instants of a pair further apart than that together, so a search over those displacements would report the agreement between the tail of one event and the head of the other, and pay the trials factor of the search for it. Such a pair keeps its edge and every statistic that needs no displacement; its match is reported as no agreement. The lags searched are the ones the pair admits Each event's map is laid on that event's own instant, so a map lag L places the pair at the absolute displacement offset + L, with offset the difference of the two anchors. correlation_profiles now searches the lags that reach the displacements the tolerance admits rather than the lags within plus or minus the tolerance: a pair whose two anchors already differ by the tolerance reaches zero displacement only at L = -offset, and the agreement reported for it was the largest among displacements the coincidence does not admit. The anchor difference is split for that: a whole number of bins, applied as a shift of the map, and what the rounding leaves over. Pairs sharing a whole part share their shifts, so the axis searched is one axis --- the displacements the tolerance admits, widened by the half bin the rounding can leave --- whatever the anchors are, and the work stops growing with how far apart anchors happen to be. The function returns that axis together with each pair's leftover, and the absolute displacement of a pair at a lag is the sum of the two. The reduction inside that search is taken in numpy rather than through paired_dot. Sending a matrix to a device is the right trade when one call reduces a whole pair set against it, and the wrong one here: a shift changes the slice, so no device copy survives to the next lag, and each reduction touches a handful of pairs of a matrix of gigabytes. Measured on this stage's shape, the transfer is three orders of magnitude more than the arithmetic it carries. TriggerGraph.candidate_table reports that displacement as the difference of the two events' gpsPeak plus the map lag. It previously added the map lag to dt_s, which is measured on whichever instant column the coincidence prefers, so the two clocks differed by each detector's envelope-to-peak shift --- an error that does not cancel in the difference and goes straight into the arrival-time difference and the sky position. The normalisation is the event's, not the edge's The norm of a map is a property of the event. Taking it once per edge gathered one map per pair before reducing it, which is the allocation this stage cannot afford at a background's pair count. Removed network_coherent_shape, a sum of network_morphology and the logarithm of the wavegram match. The first term is a coherent amplitude and the second is dimensionless, so the sum has no reading; the floor the logarithm was guarded with turned a match of exactly zero into an exclusion of 708 nats in a column whose signal variation is of order one. A shape term belongs in wdf.analysis.network_statistic.CoherentRanking, where it enters as a measured log density ratio in the units the coherent energy is already in. A pair is admitted on the arrival-time difference The network stage admits a pair when the difference of the two events' own instants does not exceed the light travel time, widened by what each event declares its instant is worth --- the tolerance CoincidenceConfig.timing_tolerance already computed and which, until now, only widened an overlap of extents. An extent says how long a transient lasted and not when it arrived, so two long events overlapping for seconds were admitted as causally compatible when they are not; the accidental population was that of pairs which happen to be simultaneous. The window a pair is admitted in is therefore the coincidence's own tolerance cap, tens of milliseconds rather than the seconds an extent reaches. TimeSlideFAR checks a step against it, and against the length the trigger stream is correlated over, and refuses one below either: under the first a real coincidence stays admissible, under the second two lags re-use the same clusters. The step itself is a stated constant --- FARConfig.min_shift_s, four seconds --- as published burs