Beispiel #1
0
    def startup(self):
        # Initialize buffer for numba signal finding routine.
        # Although we're able to extend this buffer as needed, we must do so outside numba
        # and it involves copyping data, so if you pick too small a buffer size you will hurt performance.
        self.numba_signals_buffer = np.zeros(self.config['numba_signal_buffer_size'],
                                             dtype=TriggerSignal.get_dtype())

        # Initialize buffers for tallying pulses / coincidences
        # Reason for +1 is again 'ghost' channels, see trigger.py
        n_channels = self.trigger.pax_config['DEFAULT']['n_channels'] + 1
        self.all_pulses_tally = np.zeros(n_channels, dtype=np.int)
        self.lone_pulses_tally = np.zeros(n_channels, dtype=np.int)
        self.coincidence_tally = np.zeros((n_channels, n_channels), dtype=np.int)

        # Get conversion factor from ADC counts to pe for each pmt
        # The 'ghost' PMT will have gain 1 always
        self.gain_conversion_factors = np.array([adc_to_pe(self.trigger.pax_config['DEFAULT'], ch)
                                                 for ch in range(n_channels - 1)] +
                                                [1])

        # We must keep track of the next time to save the dark rate between batches, since a batch usually does not
        # end exactly at a save time.
        self.next_save_time = None
Beispiel #2
0
    def WriteHits(self, peak, event, index):
        ''' Write out 1D histos for this peak '''
        # hit_indices = {}
        directory = self.outfile.mkdir(peak.type + "_" + str(index))
        hists = []

        pulses_to_write = {}
        hitlist = {}

        for hit in peak.hits:
            hitdict = {
                "found_in_pulse":
                hit[3],
                "area":
                hit[0],
                "channel":
                hit[2],
                "center":
                hit[1] / 10,
                "left":
                hit[7] - 1,
                "right":
                hit[10],
                "max_index":
                hit[5],
                "height":
                hit[4] / dsputils.adc_to_pe(
                    self.config, hit[2], use_reference_gain=True)
            }
            if hitdict['channel'] not in hitlist:
                hitlist[hitdict['channel']] = []
                pulses_to_write[hitdict['channel']] = []
            hitlist[hitdict['channel']].append(hitdict)
            if hitdict['found_in_pulse'] not in pulses_to_write[
                    hitdict['channel']]:
                pulses_to_write[hitdict['channel']].append(
                    hitdict['found_in_pulse'])

        # Now we should have pulses_to_write with which
        # pulses to plot per channel and
        # hitlist with a list of all hit properties to add
        for channel, pulselist in pulses_to_write.items():
            leftbound = -1
            rightbound = -1

            # Needs an initial scan to find histogram range
            for pulseid in pulselist:
                if leftbound == -1 or event.pulses[pulseid].left < leftbound:
                    leftbound = event.pulses[pulseid].left
                if rightbound == -1 or event.pulses[pulseid].right > rightbound:
                    rightbound = event.pulses[pulseid].right

            # Make and book the histo. Put into hists so doesn't get overwritten
            histname = "%s_%i_channel_%i" % (peak.type, index, channel)
            histtitle = "Channel %i in %s[%i]" % (channel, peak.type, index)
            c = ROOT.TCanvas(histname, "")
            h = ROOT.TH1F(histname, histtitle, int(rightbound - leftbound),
                          float(leftbound), float(rightbound))

            # Now put the bin values in the histogram
            for pulseid in pulselist:
                pulse = event.pulses[pulseid]
                w = (self.config['digitizer_reference_baseline'] +
                     pulse.baseline - pulse.raw_data.astype(np.float64))
                for i, sample in enumerate(w):
                    h.SetBinContent(int(i + pulse.left - leftbound), sample)

            h.SetStats(0)
            h.GetXaxis().SetTitle("Time [samples]")
            h.GetYaxis().SetTitleOffset(0.8)
            h.GetYaxis().SetTitleSize(0.05)
            h.GetXaxis().SetTitleOffset(0.8)
            h.GetXaxis().SetTitleSize(0.05)
            h.GetYaxis().SetTitle("ADC Reading (baseline corrected)")

            c.cd()
            h.Draw()

            plist = {"x": [], "y": []}
            for i, hitdict in enumerate(hitlist[channel]):

                baseline = ROOT.TLine(hitdict['left'], pulse.baseline,
                                      hitdict['right'], pulse.baseline)
                leftline = ROOT.TLine(hitdict['left'], 0, hitdict['left'],
                                      hitdict['height'])
                rightline = ROOT.TLine(hitdict['right'], 0, hitdict['right'],
                                       hitdict['height'])

                plist['x'].append(hitdict['center'])
                plist['y'].append(hitdict['height'])
                leftline.SetLineStyle(2)
                rightline.SetLineStyle(2)
                leftline.SetLineColor(2)
                rightline.SetLineColor(2)
                baseline.SetLineStyle(2)
                baseline.SetLineColor(4)
                baseline.Draw("same")
                leftline.Draw("same")
                rightline.Draw("same")

                label = "hit " + str(i) + "({:.2f} p.e.)".format(
                    hitdict['area'])
                text = ROOT.TText(hitdict['center'], hitdict['height'], label)
                text.SetTextColor(2)
                text.SetTextSize(0.03)
                text.Draw("same")
                c.Update()
                hists.append({
                    "lline": leftline,
                    "text": text,
                    "rline": rightline,
                    "bline": baseline
                })
            c.cd()
            polymarker = ROOT.TPolyMarker()
            polymarker.SetMarkerStyle(23)
            polymarker.SetMarkerColor(2)
            polymarker.SetMarkerSize(1.1)
            polymarker.SetPolyMarker(len(plist['x']), np.array(plist['x']),
                                     np.array(plist['y']))
            polymarker.Draw("same")
            c.Update()
            hists.append({"poly": polymarker, "hist": h, "c": c})

            directory.cd()
            c.Write()
        return hists
Beispiel #3
0
    def transform_event(self, event):
        tpc_channels = np.array(self.config['channels_in_detector']['tpc'])

        # Boolean array, tells us which pulses are saturated
        is_saturated = np.array([
            p.maximum >= self.reference_baseline - p.baseline - 0.5
            for p in event.pulses
        ])

        for pulse_i, pulse in enumerate(event.pulses):
            # Consider only saturated pulses in the TPC
            if not is_saturated[pulse_i] or pulse.channel not in tpc_channels:
                continue

            # Where is the current pulse saturated?
            saturated = pulse.raw_data <= 0  # Boolean array, True if sample is saturated
            _where_saturated = np.where(saturated)[0]
            try:
                first_saturated = _where_saturated.min()
                last_saturated = _where_saturated.max()
            except (ValueError, RuntimeError, TypeError, NameError):
                continue

            # Select a reference region just before the start of the saturated region
            reference_slice = slice(
                max(0,
                    first_saturated - self.config['reference_region_samples']),
                first_saturated)

            # Find all pulses in TPC channels that overlap with the saturated & reference region
            other_pulses = [
                p for i, p in enumerate(event.pulses)
                if p.left < last_saturated +
                pulse.left and p.right > pulse.left and not is_saturated[i]
                and p.channel in tpc_channels
            ]

            if not len(other_pulses):
                # Rare case where no other pulses available, one channel going crazy?
                continue

            # Compute the (gain-weighted) sum waveform of the non-saturated pulses
            min_left = min([p.left for p in other_pulses + [pulse]])
            max_right = max([p.right for p in other_pulses + [pulse]])
            sumw = np.zeros(max_right - min_left + 1)
            for p in other_pulses:
                offset = p.left - min_left
                sumw[offset:offset + len(p.raw_data)] += self.waveform_in_pe(p)

            # Crop it to include just the part that overlaps with this pulse
            offset = pulse.left - min_left
            sumw = sumw[offset:offset + len(pulse.raw_data)]

            # Compute the ratio of this channel's waveform / the nonsaturated waveform in the reference region
            w = self.waveform_in_pe(pulse)
            ratio = w[reference_slice].sum() / sumw[reference_slice].sum()

            # not < is preferred over >, since it will catch nan
            if not ratio < self.config.get('min_reference_area_ratio', 1):
                # The pulse is saturated, but insufficient information is available in the other channels
                # to reliably reconstruct it
                continue

            if len(w[reference_slice][w[reference_slice] > 1]
                   ) < self.config['reference_region_samples_treshold']:
                # the pulse is saturated, but there are not enough reference samples to get a good ratio
                # This actually distinguished between S1 and S2 and will only correct S2 signals
                continue

            # Reconstruct the waveform in the saturated region according to this ratio.
            # The waveform should never be reduced due to this (then the correction is making things worse)
            w[saturated] = np.clip(sumw[saturated] * ratio, w[saturated],
                                   float('inf'))

            # Convert back to raw ADC counts and store the corrected waveform
            # Note this changes the type of pulse.w from int16 to float64: we don't have a choice,
            # int16 probably can't contain the large amplitudes we may be putting in.
            # As long as the raw data isn't saved again after applying this correction, this should be no problem
            # (as in later code converting to floats is anyway the first step).
            w /= adc_to_pe(self.config, pulse.channel)
            w = self.reference_baseline - w - pulse.baseline

            pulse.raw_data = w

        return event
Beispiel #4
0
 def waveform_in_pe(self, p):
     """Return waveform in pe/bin above baseline of a pulse"""
     w = self.reference_baseline - p.raw_data.astype(np.float) - p.baseline
     w *= adc_to_pe(self.config, p.channel)
     return w
Beispiel #5
0
    def transform_event(self, event):
        dt = self.config['sample_duration']
        hits_per_pulse = []

        left_extension = self.config['left_extension'] // dt
        right_extension = self.config['right_extension'] // dt

        # Allocate numpy arrays to hold numba hitfinder results
        # -1 is a placeholder for values that should never appear (0 would be bad as it often IS a possible value)
        hit_bounds_buffer = -1 * np.ones((self.max_hits_per_pulse, 2), dtype=np.int64)
        hits_buffer = np.zeros(self.max_hits_per_pulse, dtype=datastructure.Hit.get_dtype())

        for pulse_i, pulse in enumerate(event.pulses):
            start = pulse.left
            stop = pulse.right
            channel = pulse.channel
            pmt_gain = self.config['gains'][channel]

            # Check the pulse properties have been computed
            if np.isnan(pulse.minimum):
                raise RuntimeError("Attempt to perform hitfinding on pulses whose properties have not been computed!")

            # Retrieve waveform as floats: needed to subtract baseline (which can be in-between ADC counts)
            w = pulse.raw_data.astype(np.float64)

            # Subtract the baseline and invert(so hits point up from baseline)
            w = self.reference_baseline - w
            w -= pulse.baseline

            # Don't do hitfinding in dead channels, pulse property computation was enough
            # Could refactor pulse property computation to separate plugin,
            # but that would mean waveform has to be converted to floats twice
            if pmt_gain == 0:
                continue

            # Compute hitfinder threshold to use
            # Rounding down is ok, since hitfinder uses >, not >= for threshold crossing testing.
            pulse.hitfinder_threshold = int(max(self.config['height_over_noise_threshold'] * pulse.noise_sigma,
                                                self.config['absolute_adc_counts_threshold'],
                                                - self.config['height_over_min_threshold'] * pulse.minimum))
            if self.always_find_single_hit:
                # The config specifies a single range to integrate. Useful for gain calibration
                hit_bounds_buffer[0] = self.always_find_single_hit
                n_hits_found = 1

            else:
                # Call the numba hit finder -- see its docstring for description
                n_hits_found = dsputils.find_intervals_above_threshold(w,
                                                                       threshold=float(pulse.hitfinder_threshold),
                                                                       result_buffer=hit_bounds_buffer)

            # Only view the part of hit_bounds_buffer that contains hits found in this event
            # The rest of hit_bounds_buffer contains -1's or stuff from previous pulses
            pulse.n_hits_found = n_hits_found
            hit_bounds_found = hit_bounds_buffer[:n_hits_found]

            if self.always_find_single_hit:
                central_bounds = hit_bounds_found
            else:
                # Extend the boundaries of each hit, to be sure we integrate everything.
                # The original bounds are preserved: they are used in clustering
                central_bounds = hit_bounds_found.copy()
                dsputils.extend_intervals(w, hit_bounds_found, left_extension, right_extension)

            # If no hits were found, this is a noise pulse: update the noise pulse count
            if n_hits_found == 0:
                event.noise_pulses_in[channel] += 1
                # Don't 'continue' to the next pulse! There's stuff left to do!
            elif n_hits_found >= self.max_hits_per_pulse:
                self.log.debug("Pulse %s-%s in channel %s has more than %s hits. "
                               "This usually indicates a zero-length encoding breakdown after a very large S2. "
                               "Further hits in this pulse have been ignored." % (start, stop, channel,
                                                                                  self.max_hits_per_pulse))

            # Store the found hits in the datastructure
            # Convert area, noise_sigma and height from adc counts -> pe
            adc_to_pe = dsputils.adc_to_pe(self.config, channel)
            noise_sigma_pe = pulse.noise_sigma * adc_to_pe

            # If the DAQ pulse was ADC-saturated (clipped), the raw waveform dropped to 0,
            # i.e. we went digitizer_reference_baseline above the reference baseline
            # i.e. we went digitizer_reference_baseline - pulse.baseline above baseline
            # 0.5 is needed to avoid floating-point rounding errors to cause saturation not to be reported
            # Somehow happens only when you use simulated data -- apparently np.clip rounds slightly different
            saturation_threshold = self.reference_baseline - pulse.baseline - 0.5

            build_hits(w, hit_bounds_found, hits_buffer,
                       adc_to_pe, channel, noise_sigma_pe, dt, start, pulse_i, saturation_threshold, central_bounds)
            hits = hits_buffer[:n_hits_found].copy()
            hits_per_pulse.append(hits)

        if len(hits_per_pulse):
            event.all_hits = np.concatenate(hits_per_pulse)

            if not self.always_find_single_hit:
                # Remove hits with 0 or negative area (very rare, but possible due to rigid integration bound)
                # In always-find-single-hit mode (for PMT calibrations) this is undesirable
                event.all_hits = event.all_hits[event.all_hits['area'] > 0]

            self.log.debug("Found %d hits in %d pulses" % (len(event.all_hits), len(event.pulses)))
        else:
            self.log.warning("Event has no pulses??!")

        return event
Beispiel #6
0
    def transform_event(self, event):
        tpc_channels = np.array(self.config['channels_in_detector']['tpc'])

        # Boolean array, tells us which pulses are saturated
        is_saturated = np.array([
            p.maximum >= self.reference_baseline - p.baseline - 0.5
            for p in event.pulses
        ])

        for pulse_i, pulse in enumerate(event.pulses):
            # Consider only saturated pulses in the TPC
            if not is_saturated[pulse_i] or pulse.channel not in tpc_channels:
                continue

            # Where is the current pulse saturated?
            saturated = pulse.raw_data <= 0  # Boolean array, True if sample is saturated
            _where_saturated_all = np.where(saturated)[0]

            # Split saturation if there is long enough non-saturated samples in between
            _where_saturated_diff = np.diff(_where_saturated_all, n=1)
            _where_saturated_diff = np.where(
                _where_saturated_diff > self.config['reference_region_samples']
            )[0]
            _where_saturated_list = np.split(_where_saturated_all,
                                             _where_saturated_diff + 1)

            # Find all pulses in TPC channels that overlap with the saturated & reference region
            other_pulses = [
                p for i, p in enumerate(event.pulses)
                if p.left < pulse.right and p.right > pulse.left
                and not is_saturated[i] and p.channel in tpc_channels and
                p.channel not in self.config['large_after_pulsing_channels']
            ]

            if not len(other_pulses):
                # Rare case where no other pulses available, one channel going crazy?
                continue

            for peak_i, _where_saturated in enumerate(_where_saturated_list):
                try:
                    first_saturated = _where_saturated.min()
                    last_saturated = _where_saturated.max()
                except (ValueError, RuntimeError, TypeError, NameError):
                    continue

                # Select a reference region just before the start of the saturated region
                reference_slice = slice(
                    max(
                        0, first_saturated -
                        self.config['reference_region_samples']),
                    first_saturated)

                # Compute the (gain-weighted) sum waveform of the non-saturated pulses
                min_left = min([p.left for p in other_pulses + [pulse]])
                max_right = max([p.right for p in other_pulses + [pulse]])
                sumw = np.zeros(max_right - min_left + 1)
                for p in other_pulses:
                    offset = p.left - min_left
                    sumw[offset:offset +
                         len(p.raw_data)] += self.waveform_in_pe(p)

                # Crop it to include just the part that overlaps with this pulse
                offset = pulse.left - min_left
                sumw = sumw[offset:offset + len(pulse.raw_data)]

                # Compute the ratio of this channel's waveform / the nonsaturated waveform in the reference region
                w = self.waveform_in_pe(pulse)
                if len(sumw[reference_slice][sumw[reference_slice] > 1]) \
                        < self.config['reference_region_samples_treshold']:
                    # the pulse is saturated, but there are not enough reference samples to get a good ratio
                    # This actually distinguished between S1 and S2 and will only correct S2 signals
                    continue

                ratio = w[reference_slice].sum() / sumw[reference_slice].sum()

                # not < is preferred over >, since it will catch nan
                if not ratio < self.config.get('min_reference_area_ratio', 1):
                    # The pulse is saturated, but insufficient information is available in the other channels
                    # to reliably reconstruct it
                    continue

                if len(w[reference_slice][w[reference_slice] > 1]
                       ) < self.config['reference_region_samples_treshold']:
                    # the pulse is saturated, but there are not enough reference samples to get a good ratio
                    # This actually distinguished between S1 and S2 and will only correct S2 signals
                    continue

                # Finding individual section of wf for each peak
                # First end before the reference region of next peak
                if peak_i + 1 == len(_where_saturated_list):
                    end = len(w)
                else:
                    end = _where_saturated_list[
                        peak_i +
                        1][0] - self.config['reference_region_samples']

                # Second end before the first upwards turning point
                v = sumw[last_saturated:end]
                conv = np.ones(self.config['convolution_length']
                               ) / self.config['convolution_length']
                v = np.convolve(conv, v, mode='same')
                dv = np.diff(v, n=1)
                # Choose +2 pe/ns instead 0 to avoid ending on the flat waveform
                turning_point = np.where((np.hstack((dv, -10)) > 2)
                                         & (np.hstack((10, dv)) <= 2))[0]

                if len(turning_point) > 0:
                    end = last_saturated + turning_point[0]

                # Reconstruct the waveform in the saturated region according to this ratio.
                # The waveform should never be reduced due to this (then the correction is making things worse)
                saturated_to_correct = np.arange(int(first_saturated),
                                                 int(end))
                w[saturated_to_correct] = np.clip(
                    sumw[saturated_to_correct] * ratio, 0, float('inf'))

                # Convert back to raw ADC counts and store the corrected waveform
                # Note this changes the type of pulse.w from int16 to float64: we don't have a choice,
                # int16 probably can't contain the large amplitudes we may be putting in.
                # As long as the raw data isn't saved again after applying this correction, this should be no problem
                # (as in later code converting to floats is anyway the first step).
                w /= adc_to_pe(self.config, pulse.channel)
                w = self.reference_baseline - w - pulse.baseline

                pulse.raw_data = w

        return event
Beispiel #7
0
    def transform_event(self, event):
        tpc_channels = np.array(self.config['channels_in_detector']['tpc'])

        # Boolean array, tells us which pulses are saturated
        is_saturated = np.array([p.maximum >= self.reference_baseline - p.baseline - 0.5
                                 for p in event.pulses])

        for pulse_i, pulse in enumerate(event.pulses):
            # Consider only saturated pulses in the TPC
            if not is_saturated[pulse_i] or pulse.channel not in tpc_channels:
                continue

            # Where is the current pulse saturated?
            saturated = pulse.raw_data <= 0            # Boolean array, True if sample is saturated
            _where_saturated = np.where(saturated)[0]
            try:
                first_saturated = _where_saturated.min()
                last_saturated = _where_saturated.max()
            except (ValueError, RuntimeError, TypeError, NameError):
                continue

            # Select a reference region just before the start of the saturated region
            reference_slice = slice(max(0, first_saturated - self.config['reference_region_samples']),
                                    first_saturated)

            # Find all pulses in TPC channels that overlap with the saturated & reference region
            other_pulses = [p for i, p in enumerate(event.pulses)
                            if p.left < last_saturated + pulse.left and p.right > pulse.left and
                            not is_saturated[i] and
                            p.channel in tpc_channels]

            if not len(other_pulses):
                # Rare case where no other pulses available, one channel going crazy?
                continue

            # Compute the (gain-weighted) sum waveform of the non-saturated pulses
            min_left = min([p.left for p in other_pulses + [pulse]])
            max_right = max([p.right for p in other_pulses + [pulse]])
            sumw = np.zeros(max_right - min_left + 1)
            for p in other_pulses:
                offset = p.left - min_left
                sumw[offset:offset + len(p.raw_data)] += self.waveform_in_pe(p)

            # Crop it to include just the part that overlaps with this pulse
            offset = pulse.left - min_left
            sumw = sumw[offset:offset + len(pulse.raw_data)]

            # Compute the ratio of this channel's waveform / the nonsaturated waveform in the reference region
            w = self.waveform_in_pe(pulse)
            ratio = w[reference_slice].sum()/sumw[reference_slice].sum()

            # not < is preferred over >, since it will catch nan
            if not ratio < self.config.get('min_reference_area_ratio', 1):
                # The pulse is saturated, but insufficient information is available in the other channels
                # to reliably reconstruct it
                continue

            if len(w[reference_slice][w[reference_slice] > 1]) < self.config['reference_region_samples_treshold']:
                # the pulse is saturated, but there are not enough reference samples to get a good ratio
                # This actually distinguished between S1 and S2 and will only correct S2 signals
                continue

            # Reconstruct the waveform in the saturated region according to this ratio.
            # The waveform should never be reduced due to this (then the correction is making things worse)
            w[saturated] = np.clip(sumw[saturated] * ratio, w[saturated], float('inf'))

            # Convert back to raw ADC counts and store the corrected waveform
            # Note this changes the type of pulse.w from int16 to float64: we don't have a choice,
            # int16 probably can't contain the large amplitudes we may be putting in.
            # As long as the raw data isn't saved again after applying this correction, this should be no problem
            # (as in later code converting to floats is anyway the first step).
            w /= adc_to_pe(self.config, pulse.channel)
            w = self.reference_baseline - w - pulse.baseline

            pulse.raw_data = w

        return event
Beispiel #8
0
    def plot_event(self, event):
        dt = self.config['sample_duration']

        # Configurable limits
        channels_start = self.config.get(
            'channel_start', min(self.config['channels_in_detector']['tpc']))
        channels_end = self.config.get(
            'channel_start', max(self.config['channels_in_detector']['tpc']))
        t_start = self.config.get('t_start', 0 * units.us)
        t_end = self.config.get('t_end', event.duration())

        fig = plt.figure(figsize=(self.size_multiplier * 4,
                                  self.size_multiplier * 2))
        ax = fig.gca(projection='3d')

        global_max_amplitude = 0
        for pulse in event.pulses:
            # Take only channels in the range we want to plot
            if not channels_start <= pulse.channel <= channels_end:
                continue

            # Take only pulses that fall (at least partially) in the time window we want to plot
            if pulse.right * dt < t_start or pulse.left * dt > t_end:
                continue

            w = self.config[
                'digitizer_reference_baseline'] + pulse.baseline - pulse.raw_data.astype(
                    np.float64)
            w *= dsputils.adc_to_pe(self.config,
                                    pulse.channel,
                                    use_reference_gain=True)
            if self.config['log_scale']:
                # This will still give nan's if waveform drops below 1 pe_nominal / bin...
                # TODO: So... will it crash? or just fall outside range?
                w = np.log10(1 + w)

            # We need to keep track of this, apparently gca can't scale itself?
            global_max_amplitude = max(np.max(w), global_max_amplitude)

            ax.plot(np.linspace(pulse.left, pulse.right, pulse.length) * dt /
                    units.us,
                    pulse.channel * np.ones(pulse.length),
                    zs=w,
                    zdir='z',
                    label=str(pulse.channel))

        # Plot the sum waveform
        w = event.get_sum_waveform('tpc').samples[int(t_start /
                                                      dt):int(t_end / dt) + 1]
        ax.plot(np.linspace(t_start, t_end, len(w)) / units.us,
                (channels_end + 1) * np.ones(len(w)),
                zs=w * global_max_amplitude / np.max(w),
                zdir='z',
                label='Tpc')

        ax.set_xlabel('Time [$\mu$s]')
        ax.set_xlim3d(t_start / units.us, t_end / units.us)

        ax.set_ylabel('Channel number')
        ax.set_ylim3d(channels_start, channels_end)

        zlabel = 'Pulse height [pe_nominal / %d ns]' % (
            self.config['sample_duration'] / units.ns)
        if self.config['log_scale']:
            zlabel = 'Log10 1 + ' + zlabel
        ax.set_zlabel(zlabel)
        ax.set_zlim3d(0, global_max_amplitude)
Beispiel #9
0
    def transform_event(self, event):
        tpc_channels = np.array(self.config['channels_in_detector']['tpc'])

        # Boolean array, tells us which pulses are saturated
        is_saturated = np.array([p.maximum >= self.reference_baseline - p.baseline - 0.5
                                 for p in event.pulses])

        for pulse_i, pulse in enumerate(event.pulses):
            # Consider only saturated pulses in the TPC
            if not is_saturated[pulse_i] or pulse.channel not in tpc_channels:
                continue

            # Where is the current pulse saturated?
            saturated = pulse.raw_data <= 0            # Boolean array, True if sample is saturated
            _where_saturated_all = np.where(saturated)[0]

            # Split saturation if there is long enough non-saturated samples in between
            _where_saturated_diff = np.diff(_where_saturated_all, n=1)
            _where_saturated_diff = np.where(_where_saturated_diff > self.config['reference_region_samples'])[0]
            _where_saturated_list = np.split(_where_saturated_all, _where_saturated_diff+1)

            # Find all pulses in TPC channels that overlap with the saturated & reference region
            other_pulses = [p for i, p in enumerate(event.pulses)
                            if p.left < pulse.right and p.right > pulse.left and
                            not is_saturated[i] and
                            p.channel in tpc_channels and
                            p.channel not in self.config['large_after_pulsing_channels']]

            if not len(other_pulses):
                # Rare case where no other pulses available, one channel going crazy?
                continue

            for peak_i, _where_saturated in enumerate(_where_saturated_list):
                try:
                    first_saturated = _where_saturated.min()
                    last_saturated = _where_saturated.max()
                except (ValueError, RuntimeError, TypeError, NameError):
                    continue

                # Select a reference region just before the start of the saturated region
                reference_slice = slice(max(0, first_saturated - self.config['reference_region_samples']),
                                        first_saturated)

                # Compute the (gain-weighted) sum waveform of the non-saturated pulses
                min_left = min([p.left for p in other_pulses + [pulse]])
                max_right = max([p.right for p in other_pulses + [pulse]])
                sumw = np.zeros(max_right - min_left + 1)
                for p in other_pulses:
                    offset = p.left - min_left
                    sumw[offset:offset + len(p.raw_data)] += self.waveform_in_pe(p)

                # Crop it to include just the part that overlaps with this pulse
                offset = pulse.left - min_left
                sumw = sumw[offset:offset + len(pulse.raw_data)]

                # Compute the ratio of this channel's waveform / the nonsaturated waveform in the reference region
                w = self.waveform_in_pe(pulse)
                if len(sumw[reference_slice][sumw[reference_slice] > 1]) \
                        < self.config['reference_region_samples_treshold']:
                    # the pulse is saturated, but there are not enough reference samples to get a good ratio
                    # This actually distinguished between S1 and S2 and will only correct S2 signals
                    continue

                ratio = w[reference_slice].sum()/sumw[reference_slice].sum()

                # not < is preferred over >, since it will catch nan
                if not ratio < self.config.get('min_reference_area_ratio', 1):
                    # The pulse is saturated, but insufficient information is available in the other channels
                    # to reliably reconstruct it
                    continue

                if len(w[reference_slice][w[reference_slice] > 1]) < self.config['reference_region_samples_treshold']:
                    # the pulse is saturated, but there are not enough reference samples to get a good ratio
                    # This actually distinguished between S1 and S2 and will only correct S2 signals
                    continue

                # Finding individual section of wf for each peak
                # First end before the reference region of next peak
                if peak_i+1 == len(_where_saturated_list):
                    end = len(w)
                else:
                    end = _where_saturated_list[peak_i+1][0]-self.config['reference_region_samples']

                # Second end before the first upwards turning point
                v = sumw[last_saturated: end]
                conv = np.ones(self.config['convolution_length'])/self.config['convolution_length']
                v = np.convolve(conv, v, mode='same')
                dv = np.diff(v, n=1)
                # Choose +2 pe/ns instead 0 to avoid ending on the flat waveform
                turning_point = np.where((np.hstack((dv, -10)) > 2) & (np.hstack((10, dv)) <= 2))[0]

                if len(turning_point) > 0:
                    end = last_saturated + turning_point[0]

                # Reconstruct the waveform in the saturated region according to this ratio.
                # The waveform should never be reduced due to this (then the correction is making things worse)
                saturated_to_correct = np.arange(int(first_saturated), int(end))
                w[saturated_to_correct] = np.clip(sumw[saturated_to_correct] * ratio, 0, float('inf'))

                # Convert back to raw ADC counts and store the corrected waveform
                # Note this changes the type of pulse.w from int16 to float64: we don't have a choice,
                # int16 probably can't contain the large amplitudes we may be putting in.
                # As long as the raw data isn't saved again after applying this correction, this should be no problem
                # (as in later code converting to floats is anyway the first step).
                w /= adc_to_pe(self.config, pulse.channel)
                w = self.reference_baseline - w - pulse.baseline

                pulse.raw_data = w

        return event
Beispiel #10
0
 def waveform_in_pe(self, p):
     """Return waveform in pe/bin above baseline of a pulse"""
     w = self.reference_baseline - p.raw_data.astype(np.float) - p.baseline
     w *= adc_to_pe(self.config, p.channel)
     return w
Beispiel #11
0
    def transform_event(self, event):
        if self.make_diagnostic_plots == 'never':
            return event

        # Get the pulse-to-hit mapping
        # Note this relies on the hits being sorted by found_in_pulse
        # (probably shouldn't have rolled our own fake pandas for recarrays...)
        self.log.debug("Reconstructing hit/pulse mapping")
        hits_in_pulse = dict_group_by(event.all_hits, 'found_in_pulse')

        for pulse_i, pulse in enumerate(
                tqdm(event.pulses, desc='Making hitfinder diagnostic plots')):
            # Get the hits that were found in this pulse
            hits = hits_in_pulse.get(
                pulse_i, np.array([], datastructure.Hit.get_dtype()))

            # Reconstruct some variables we had in the hitfinder. Some code duplication unfortunately...
            # that's the price we pay for having diagnostic plotting cleanly separated from the hitfinder.
            start = pulse.left
            stop = pulse.right

            hit_bounds_found = np.vstack((hits['left'], hits['right'])).T
            hit_bounds_found -= start

            w = self.reference_baseline - pulse.raw_data.astype(
                np.float64) - pulse.baseline

            channel = pulse.channel
            adc_to_pe = dsputils.adc_to_pe(self.config, channel)
            noise_sigma_pe = pulse.noise_sigma * adc_to_pe
            threshold = pulse.hitfinder_threshold
            saturation_threshold = self.reference_baseline - pulse.baseline - 0.5
            is_saturated = pulse.maximum >= saturation_threshold

            # Do we need to show this pulse? If not: continue
            if self.make_diagnostic_plots == 'tricky cases':
                # Always show pulse if noise level is very high
                if noise_sigma_pe < 0.5:
                    if len(hit_bounds_found) == 0:
                        # Show pulse if it nearly went over threshold
                        if not pulse.maximum > 0.8 * threshold:
                            continue
                    else:
                        # Show pulse if any of its hit nearly didn't go over threshold
                        if not any([
                                event.all_hits[-(i + 1)].height <
                                1.2 * threshold * adc_to_pe
                                for i in range(len(hit_bounds_found))
                        ]):
                            continue
            elif self.make_diagnostic_plots == 'no hits':
                if len(hit_bounds_found) != 0:
                    continue
            elif self.make_diagnostic_plots == 'baseline shifts':
                if abs(pulse.baseline_increase) < 10:
                    continue
            elif self.make_diagnostic_plots == 'hits only':
                if len(hit_bounds_found) == 0:
                    continue
            elif self.make_diagnostic_plots == 'saturated':
                if not is_saturated:
                    continue
            elif self.make_diagnostic_plots == 'negative':
                # Select only pulses which had hits whose area and or height were originally negative
                # (the hitfinder helpfully capped them at 1e-9, so they're not actually negative...)
                if not (len(hits) and (np.any(hits['area'] < 1e-6)
                                       or np.any(hits['height'] < 1e-6))):
                    continue
            elif self.make_diagnostic_plots != 'always':
                raise ValueError("Invalid make_diagnostic_plots option: %s!" %
                                 self.make_diagnostic_plots)

            plt.figure(figsize=(14, 10))
            data_for_title = (event.event_number, start, stop, channel)
            plt.title('Event %s, pulse %d-%d, Channel %d' % data_for_title)
            ax1 = plt.gca()
            ax2 = ax1.twinx()
            ax1.set_position((.1, .1, .6, .85))
            ax2.set_position((.1, .1, .6, .85))
            ax1.set_xlabel("Sample number (%s ns)" % event.sample_duration)
            ax1.set_ylabel("ADC counts above baseline")
            ax2.set_ylabel("pe / sample")

            # Plot the signal and noise levels
            ax1.plot(w, drawstyle='steps-mid', label='Data')
            ax1.plot(np.ones_like(w) * threshold,
                     '--',
                     label='Threshold',
                     color='red')
            ax1.plot(np.ones_like(w) * pulse.noise_sigma,
                     ':',
                     label='Noise level',
                     color='gray')
            ax1.plot(np.ones_like(w) * pulse.minimum,
                     '--',
                     label='Minimum',
                     color='orange')

            # Mark the hit ranges & center of gravity point
            for hit_i, hit in enumerate(hit_bounds_found):
                ax1.axvspan(hit[0] - 0.5, hit[1] + 0.5, color='red', alpha=0.2)

            # Make sure the y-scales match
            ax2.set_ylim(ax1.get_ylim()[0] * adc_to_pe,
                         ax1.get_ylim()[1] * adc_to_pe)

            # Add pulse / hit information
            if len(hits) != 0:
                largest_hit = hits[np.argmax(hits['area'])]
                plt.figtext(0.75,
                            0.98,
                            dedent("""
                            Pulse maximum: {pulse.maximum:.5g}
                            Pulse minimum: {pulse.minimum:.5g}
                              (both in ADCc above baseline)
                            Pulse baseline: {pulse.baseline}
                              (in ADCc above reference baseline)
                            Baseline increase: {pulse.baseline_increase:.2f}

                            Gain in this PMT: {gain:.3g}

                            Noise level: {pulse.noise_sigma:.2f} ADCc
                            Hitfinder threshold: {pulse.hitfinder_threshold} ADCc

                            Largest hit info ({left}-{right}):
                            Area: {hit_area:.5g} pe
                            Height: {hit_height:.4g} pe
                            Saturated samples: {hit_n_saturated}
                            """.format(
                                pulse=pulse,
                                gain=self.config['gains'][pulse.channel],
                                left=largest_hit['left'] - pulse.left,
                                right=largest_hit['right'] - pulse.left,
                                hit_area=largest_hit['area'],
                                hit_height=largest_hit['height'],
                                hit_n_saturated=largest_hit['n_saturated'])),
                            fontsize=14,
                            verticalalignment='top')

            # Finish the plot, save, close
            leg = ax1.legend()
            leg.get_frame().set_alpha(0.5)
            plt.savefig(
                os.path.join(
                    self.make_diagnostic_plots_in,
                    'event%04d_pulse%05d-%05d_ch%03d.png' % data_for_title))
            plt.xlim(0, len(pulse.raw_data))
            plt.close()

        return event
Beispiel #12
0
    def transform_event(self, event):
        dt = self.config['sample_duration']
        hits_per_pulse = []

        left_extension = self.config['left_extension'] // dt
        right_extension = self.config['right_extension'] // dt

        # Allocate numpy arrays to hold numba hitfinder results
        # -1 is a placeholder for values that should never appear (0 would be bad as it often IS a possible value)
        hit_bounds_buffer = -1 * np.ones(
            (self.max_hits_per_pulse, 2), dtype=np.int64)
        hits_buffer = np.zeros(self.max_hits_per_pulse,
                               dtype=datastructure.Hit.get_dtype())

        for pulse_i, pulse in enumerate(event.pulses):
            start = pulse.left
            stop = pulse.right
            channel = pulse.channel
            pmt_gain = self.config['gains'][channel]

            # Check the pulse properties have been computed
            if np.isnan(pulse.minimum):
                raise RuntimeError(
                    "Attempt to perform hitfinding on pulses whose properties have not been computed!"
                )

            # Retrieve waveform as floats: needed to subtract baseline (which can be in-between ADC counts)
            w = pulse.raw_data.astype(np.float64)

            # Subtract the baseline and invert(so hits point up from baseline)
            w = self.reference_baseline - w
            w -= pulse.baseline

            # Don't do hitfinding in dead channels, pulse property computation was enough
            # Could refactor pulse property computation to separate plugin,
            # but that would mean waveform has to be converted to floats twice
            if pmt_gain == 0:
                continue

            # Compute hitfinder threshold to use
            # Rounding down is ok, since hitfinder uses >, not >= for threshold crossing testing.
            pulse.hitfinder_threshold = int(
                max(
                    self.config['height_over_noise_threshold'] *
                    pulse.noise_sigma,
                    self.config['absolute_adc_counts_threshold'],
                    -self.config['height_over_min_threshold'] * pulse.minimum))
            if self.always_find_single_hit:
                # The config specifies a single range to integrate. Useful for gain calibration
                hit_bounds_buffer[0] = self.always_find_single_hit
                n_hits_found = 1

            else:
                # Call the numba hit finder -- see its docstring for description
                n_hits_found = dsputils.find_intervals_above_threshold(
                    w,
                    threshold=float(pulse.hitfinder_threshold),
                    result_buffer=hit_bounds_buffer)

            # Only view the part of hit_bounds_buffer that contains hits found in this event
            # The rest of hit_bounds_buffer contains -1's or stuff from previous pulses
            pulse.n_hits_found = n_hits_found
            hit_bounds_found = hit_bounds_buffer[:n_hits_found]

            if self.always_find_single_hit:
                central_bounds = hit_bounds_found
            else:
                # Extend the boundaries of each hit, to be sure we integrate everything.
                # The original bounds are preserved: they are used in clustering
                central_bounds = hit_bounds_found.copy()
                dsputils.extend_intervals(w, hit_bounds_found, left_extension,
                                          right_extension)

            # If no hits were found, this is a noise pulse: update the noise pulse count
            if n_hits_found == 0:
                event.noise_pulses_in[channel] += 1
                # Don't 'continue' to the next pulse! There's stuff left to do!
            elif n_hits_found >= self.max_hits_per_pulse:
                self.log.debug(
                    "Pulse %s-%s in channel %s has more than %s hits. "
                    "This usually indicates a zero-length encoding breakdown after a very large S2. "
                    "Further hits in this pulse have been ignored." %
                    (start, stop, channel, self.max_hits_per_pulse))

            # Store the found hits in the datastructure
            # Convert area, noise_sigma and height from adc counts -> pe
            adc_to_pe = dsputils.adc_to_pe(self.config, channel)
            noise_sigma_pe = pulse.noise_sigma * adc_to_pe

            # If the DAQ pulse was ADC-saturated (clipped), the raw waveform dropped to 0,
            # i.e. we went digitizer_reference_baseline above the reference baseline
            # i.e. we went digitizer_reference_baseline - pulse.baseline above baseline
            # 0.5 is needed to avoid floating-point rounding errors to cause saturation not to be reported
            # Somehow happens only when you use simulated data -- apparently np.clip rounds slightly different
            saturation_threshold = self.reference_baseline - pulse.baseline - 0.5

            build_hits(w, hit_bounds_found, hits_buffer, adc_to_pe, channel,
                       noise_sigma_pe, dt, start, pulse_i,
                       saturation_threshold, central_bounds)
            hits = hits_buffer[:n_hits_found].copy()
            hits_per_pulse.append(hits)

        if len(hits_per_pulse):
            event.all_hits = np.concatenate(hits_per_pulse)

            if not self.always_find_single_hit:
                # Remove hits with 0 or negative area (very rare, but possible due to rigid integration bound)
                # In always-find-single-hit mode (for PMT calibrations) this is undesirable
                event.all_hits = event.all_hits[event.all_hits['area'] > 0]

            self.log.debug("Found %d hits in %d pulses" %
                           (len(event.all_hits), len(event.pulses)))
        else:
            self.log.warning("Event has no pulses??!")

        return event
    def transform_event(self, event):
        if self.make_diagnostic_plots == 'never':
            return event

        # Get the pulse-to-hit mapping
        # Note this relies on the hits being sorted by found_in_pulse
        # (probably shouldn't have rolled our own fake pandas for recarrays...)
        self.log.debug("Reconstructing hit/pulse mapping")
        hits_in_pulse = dict_group_by(event.all_hits, 'found_in_pulse')

        for pulse_i, pulse in enumerate(tqdm(event.pulses, desc='Making hitfinder diagnostic plots')):
            # Get the hits that were found in this pulse
            hits = hits_in_pulse.get(pulse_i, np.array([], datastructure.Hit.get_dtype()))

            # Reconstruct some variables we had in the hitfinder. Some code duplication unfortunately...
            # that's the price we pay for having diagnostic plotting cleanly separated from the hitfinder.
            start = pulse.left
            stop = pulse.right

            hit_bounds_found = np.vstack((hits['left'], hits['right'])).T
            hit_bounds_found -= start

            w = self.reference_baseline - pulse.raw_data.astype(np.float64) - pulse.baseline

            channel = pulse.channel
            adc_to_pe = dsputils.adc_to_pe(self.config, channel)
            noise_sigma_pe = pulse.noise_sigma * adc_to_pe
            threshold = pulse.hitfinder_threshold
            saturation_threshold = self.reference_baseline - pulse.baseline - 0.5
            is_saturated = pulse.maximum >= saturation_threshold

            # Do we need to show this pulse? If not: continue
            if self.make_diagnostic_plots == 'tricky cases':
                # Always show pulse if noise level is very high
                if noise_sigma_pe < 0.5:
                    if len(hit_bounds_found) == 0:
                        # Show pulse if it nearly went over threshold
                        if not pulse.maximum > 0.8 * threshold:
                            continue
                    else:
                        # Show pulse if any of its hit nearly didn't go over threshold
                        if not any([event.all_hits[-(i+1)].height < 1.2 * threshold * adc_to_pe
                                   for i in range(len(hit_bounds_found))]):
                            continue
            elif self.make_diagnostic_plots == 'no hits':
                if len(hit_bounds_found) != 0:
                    continue
            elif self.make_diagnostic_plots == 'baseline shifts':
                if abs(pulse.baseline_increase) < 10:
                    continue
            elif self.make_diagnostic_plots == 'hits only':
                if len(hit_bounds_found) == 0:
                    continue
            elif self.make_diagnostic_plots == 'saturated':
                if not is_saturated:
                    continue
            elif self.make_diagnostic_plots == 'negative':
                # Select only pulses which had hits whose area and or height were originally negative
                # (the hitfinder helpfully capped them at 1e-9, so they're not actually negative...)
                if not (len(hits) and (np.any(hits['area'] < 1e-6) or np.any(hits['height'] < 1e-6))):
                    continue
            elif self.make_diagnostic_plots != 'always':
                raise ValueError("Invalid make_diagnostic_plots option: %s!" % self.make_diagnostic_plots)

            plt.figure(figsize=(14, 10))
            data_for_title = (event.event_number, start, stop, channel)
            plt.title('Event %s, pulse %d-%d, Channel %d' % data_for_title)
            ax1 = plt.gca()
            ax2 = ax1.twinx()
            ax1.set_position((.1, .1, .6, .85))
            ax2.set_position((.1, .1, .6, .85))
            ax1.set_xlabel("Sample number (%s ns)" % event.sample_duration)
            ax1.set_ylabel("ADC counts above baseline")
            ax2.set_ylabel("pe / sample")

            # Plot the signal and noise levels
            ax1.plot(w, drawstyle='steps-mid', label='Data')
            ax1.plot(np.ones_like(w) * threshold, '--', label='Threshold', color='red')
            ax1.plot(np.ones_like(w) * pulse.noise_sigma, ':', label='Noise level', color='gray')
            ax1.plot(np.ones_like(w) * pulse.minimum, '--', label='Minimum', color='orange')

            # Mark the hit ranges & center of gravity point
            for hit_i, hit in enumerate(hit_bounds_found):
                ax1.axvspan(hit[0] - 0.5, hit[1] + 0.5, color='red', alpha=0.2)

            # Make sure the y-scales match
            ax2.set_ylim(ax1.get_ylim()[0] * adc_to_pe, ax1.get_ylim()[1] * adc_to_pe)

            # Add pulse / hit information
            if len(hits) != 0:
                largest_hit = hits[np.argmax(hits['area'])]
                plt.figtext(0.75, 0.98, dedent("""
                            Pulse maximum: {pulse.maximum:.5g}
                            Pulse minimum: {pulse.minimum:.5g}
                              (both in ADCc above baseline)
                            Pulse baseline: {pulse.baseline}
                              (in ADCc above reference baseline)
                            Baseline increase: {pulse.baseline_increase:.2f}

                            Gain in this PMT: {gain:.3g}

                            Noise level: {pulse.noise_sigma:.2f} ADCc
                            Hitfinder threshold: {pulse.hitfinder_threshold} ADCc

                            Largest hit info ({left}-{right}):
                            Area: {hit_area:.5g} pe
                            Height: {hit_height:.4g} pe
                            Saturated samples: {hit_n_saturated}
                            """.format(pulse=pulse,
                                       gain=self.config['gains'][pulse.channel],
                                       left=largest_hit['left']-pulse.left,
                                       right=largest_hit['right']-pulse.left,
                                       hit_area=largest_hit['area'],
                                       hit_height=largest_hit['height'],
                                       hit_n_saturated=largest_hit['n_saturated'])),
                            fontsize=14, verticalalignment='top')

            # Finish the plot, save, close
            leg = ax1.legend()
            leg.get_frame().set_alpha(0.5)
            plt.savefig(os.path.join(self.make_diagnostic_plots_in,
                                     'event%04d_pulse%05d-%05d_ch%03d.png' % data_for_title))
            plt.xlim(0, len(pulse.raw_data))
            plt.close()

        return event
Beispiel #14
0
    def transform_event(self, event):

        # Initialize empty waveforms for each detector
        # One with only hits, one with raw data
        for postfix in ('', '_raw'):
            for detector, chs in self.config['channels_in_detector'].items():
                event.sum_waveforms.append(datastructure.SumWaveform(
                    samples=np.zeros(event.length(), dtype=np.float32),
                    name=detector + postfix,
                    channel_list=np.array(list(chs), dtype=np.uint16),
                    detector=detector
                ))

        # Make dictionary mapping pulse -> non-rejected hits found in pulse
        # Assumes hits are still sorted by pulse
        event.all_hits = np.sort(event.all_hits, order='found_in_pulse')
        hits_per_pulse = recarray_tools.dict_group_by(event.all_hits[True ^ event.all_hits['is_rejected']],
                                                      'found_in_pulse')

        # Add top and bottom tpc sum waveforms
        for q in ('top', 'bottom'):
            event.sum_waveforms.append(datastructure.SumWaveform(
                samples=np.zeros(event.length(), dtype=np.float32),
                name='tpc_%s' % q,
                channel_list=np.array(self.config['channels_%s' % q], dtype=np.uint16),
                detector='tpc'
            ))

        for pulse_i, pulse in enumerate(event.pulses):
            channel = pulse.channel

            # Do some initialization only when we switch channel
            # The 'current_channel' variable can't be pulled outside the loop, that would hurt performance
            # (trust me, try it and time it)
            if pulse_i == 0 or channel != current_channel:      # noqa
                current_channel = channel                       # noqa
                detector = self.detector_by_channel[channel]
                adc_to_pe = dsputils.adc_to_pe(self.config, channel)

                if detector == 'tpc':
                    if channel in self.config['channels_top']:
                        sum_w = event.get_sum_waveform('tpc_top')
                    else:
                        sum_w = event.get_sum_waveform('tpc_bottom')
                else:
                    sum_w = event.get_sum_waveform(detector)

            # Don't consider dead channels
            if self.config['gains'][channel] == 0:
                continue

            # Get the pulse waveform in pe/bin
            baseline_to_subtract = self.config['digitizer_reference_baseline'] - pulse.baseline
            w = baseline_to_subtract - pulse.raw_data.astype(np.float64)
            w *= adc_to_pe

            sum_w_raw = event.get_sum_waveform(detector+'_raw').samples
            sum_w_raw[pulse.left:pulse.right+1] += w

            hits = hits_per_pulse.get(pulse_i, None)
            if hits is None:
                continue

            w_hits_only = w.copy()
            # Obtain an array of same length as w, indicating whether the sample is in a hit or not
            mask = np.zeros(len(w), dtype=np.bool)
            set_if_in_ranges(mask, hits['left'] - pulse.left, hits['right'] - pulse.left)
            w_hits_only[True ^ mask] = 0

            sum_w.samples[pulse.left:pulse.right+1] += w_hits_only

        # Sum the tpc top and bottom tpc waveforms
        event.get_sum_waveform('tpc').samples = event.get_sum_waveform('tpc_top').samples + \
            event.get_sum_waveform('tpc_bottom').samples

        return event
Beispiel #15
0
    def WriteHits(self, peak, event, index):
        ''' Write out 1D histos for this peak '''
        # hit_indices = {}
        directory = self.outfile.mkdir(peak.type+"_"+str(index))
        hists = []

        pulses_to_write = {}
        hitlist = {}

        for hit in peak.hits:
            hitdict = {
                "found_in_pulse": hit[3],
                "area": hit[0],
                "channel": hit[2],
                "center": hit[1]/10,
                "left": hit[7]-1,
                "right": hit[10],
                "max_index": hit[5],
                "height": hit[4]/dsputils.adc_to_pe(self.config, hit[2],
                                                    use_reference_gain=True)
            }
            if hitdict['channel'] not in hitlist:
                hitlist[hitdict['channel']] = []
                pulses_to_write[hitdict['channel']] = []
            hitlist[hitdict['channel']].append(hitdict)
            if hitdict['found_in_pulse'] not in pulses_to_write[hitdict['channel']]:
                pulses_to_write[hitdict['channel']].append(hitdict['found_in_pulse'])

        # Now we should have pulses_to_write with which
        # pulses to plot per channel and
        # hitlist with a list of all hit properties to add
        for channel, pulselist in pulses_to_write.items():
            leftbound = -1
            rightbound = -1

            # Needs an initial scan to find histogram range
            for pulseid in pulselist:
                if leftbound == -1 or event.pulses[pulseid].left < leftbound:
                    leftbound = event.pulses[pulseid].left
                if rightbound == -1 or event.pulses[pulseid].right > rightbound:
                    rightbound = event.pulses[pulseid].right

            # Make and book the histo. Put into hists so doesn't get overwritten
            histname = "%s_%i_channel_%i" % (peak.type, index, channel)
            histtitle = "Channel %i in %s[%i]" % (channel, peak.type, index)
            c = ROOT.TCanvas(histname, "")
            h = ROOT.TH1F(histname, histtitle, int(rightbound-leftbound),
                          float(leftbound), float(rightbound))

            # Now put the bin values in the histogram
            for pulseid in pulselist:
                pulse = event.pulses[pulseid]
                w = (self.config['digitizer_reference_baseline'] + pulse.baseline -
                     pulse.raw_data.astype(np.float64))
                for i, sample in enumerate(w):
                    h.SetBinContent(int(i+pulse.left-leftbound), sample)

            h.SetStats(0)
            h.GetXaxis().SetTitle("Time [samples]")
            h.GetYaxis().SetTitleOffset(0.8)
            h.GetYaxis().SetTitleSize(0.05)
            h.GetXaxis().SetTitleOffset(0.8)
            h.GetXaxis().SetTitleSize(0.05)
            h.GetYaxis().SetTitle("ADC Reading (baseline corrected)")

            c.cd()
            h.Draw()

            plist = {"x": [], "y": []}
            for i, hitdict in enumerate(hitlist[channel]):

                baseline = ROOT.TLine(hitdict['left'], pulse.baseline,
                                      hitdict['right'], pulse.baseline)
                leftline = ROOT.TLine(hitdict['left'], 0, hitdict['left'], hitdict['height'])
                rightline = ROOT.TLine(hitdict['right'], 0, hitdict['right'], hitdict['height'])

                plist['x'].append(hitdict['center'])
                plist['y'].append(hitdict['height'])
                leftline.SetLineStyle(2)
                rightline.SetLineStyle(2)
                leftline.SetLineColor(2)
                rightline.SetLineColor(2)
                baseline.SetLineStyle(2)
                baseline.SetLineColor(4)
                baseline.Draw("same")
                leftline.Draw("same")
                rightline.Draw("same")

                label = "hit " + str(i) + "({:.2f} p.e.)".format(hitdict['area'])
                text = ROOT.TText(hitdict['center'], hitdict['height'], label)
                text.SetTextColor(2)
                text.SetTextSize(0.03)
                text.Draw("same")
                c.Update()
                hists.append({"lline": leftline, "text": text,
                              "rline": rightline, "bline": baseline})
            c.cd()
            polymarker = ROOT.TPolyMarker()
            polymarker.SetMarkerStyle(23)
            polymarker.SetMarkerColor(2)
            polymarker.SetMarkerSize(1.1)
            polymarker.SetPolyMarker(len(plist['x']), np.array(plist['x']),
                                     np.array(plist['y']))
            polymarker.Draw("same")
            c.Update()
            hists.append({"poly": polymarker, "hist": h, "c": c})

            directory.cd()
            c.Write()
        return hists
Beispiel #16
0
def show_time_range(st, run_id, t0, dt=10):
    from functools import partial

    import numpy as np
    import pandas as pd

    import holoviews as hv
    from holoviews.operation.datashader import datashade, dynspread
    hv.extension('bokeh')

    import strax

    import gc
    # Somebody thought it was a good idea to call gc.collect explicitly somewhere in holoviews
    # This makes dynamic PMT maps super slow
    # Until I trace the offender:
    gc.collect = lambda *args, **kwargs: None

    # Custom wheel zoom tool that only zooms in time
    from bokeh.models import WheelZoomTool
    time_zoom = WheelZoomTool(dimensions='width')

    # Get ADC->pe multiplicative conversion factor
    from pax.configuration import load_configuration
    from pax.dsputils import adc_to_pe
    pax_config = load_configuration('XENON1T')["DEFAULT"]
    to_pe = np.array(
        [adc_to_pe(pax_config, ch) for ch in range(pax_config['n_channels'])])

    tpc_r = pax_config['tpc_radius']

    # Get locations of PMTs
    r = []
    for q in pax_config['pmts']:
        r.append(
            dict(x=q['position']['x'],
                 y=q['position']['y'],
                 i=q['pmt_position'],
                 array=q.get('array', 'other')))
    f = 1.08
    pmt_locs = pd.DataFrame(r)

    records = st.get_array(run_id,
                           'raw_records',
                           time_range=(t0, t0 + int(1e10)))

    # TOOD: don't reprocess, just load...
    hits = strax.find_hits(records)
    peaks = strax.find_peaks(hits,
                             to_pe,
                             gap_threshold=300,
                             min_hits=3,
                             result_dtype=strax.peak_dtype(n_channels=260))
    strax.sum_waveform(peaks, records, to_pe)
    # Integral in pe
    areas = records['data'].sum(axis=1) * to_pe[records['channel']]

    def normalize_time(t):
        return (t - records[0]['time']) / 1e9

    # Create dataframe with record metadata
    df = pd.DataFrame(
        dict(area=areas,
             time=normalize_time(records['time']),
             channel=records['channel']))

    # Convert to holoviews Points
    points = hv.Points(
        df,
        kdims=[
            hv.Dimension('time', label='Time', unit='sec'),
            hv.Dimension('channel', label='PMT number', range=(0, 260))
        ],
        vdims=[
            hv.Dimension(
                'area',
                label='Area',
                unit='pe',
                # range=(0, 1000)
            )
        ])

    def pmt_map(t_0, t_1, array='top', **kwargs):
        # Compute the PMT pattern (fast)
        ps = points[(t_0 <= points['time']) & (points['time'] < t_1)]
        areas = np.bincount(ps['channel'],
                            weights=ps['area'],
                            minlength=len(pmt_locs))

        # Which PMTs should we include?
        pmt_mask = pmt_locs['array'] == array
        d = pmt_locs[pmt_mask].copy()
        d['area'] = areas[pmt_mask]

        # Convert to holoviews points
        d = hv.Dataset(d,
                       kdims=[
                           hv.Dimension('x',
                                        unit='cm',
                                        range=(-tpc_r * f, tpc_r * f)),
                           hv.Dimension('y',
                                        unit='cm',
                                        range=(-tpc_r * f, tpc_r * f)),
                           hv.Dimension('i', label='PMT number'),
                           hv.Dimension('area', label='Area', unit='PE')
                       ])

        return d.to(hv.Points,
                    vdims=['area', 'i'],
                    group='PMTPattern',
                    label=array.capitalize(),
                    **kwargs).opts(plot=dict(color_index=2,
                                             tools=['hover'],
                                             show_grid=False),
                                   style=dict(size=17, cmap='magma'))

    def pmt_map_range(x_range, array='top', **kwargs):
        # For use in dynamicmap with streams
        if x_range is None:
            x_range = (0, 0)
        return pmt_map(x_range[0], x_range[1], array=array, **kwargs)

    xrange_stream = hv.streams.RangeX(source=points)

    # TODO: weigh by area

    def channel_map():
        return dynspread(
            datashade(
                points, y_range=(0, 260),
                streams=[xrange_stream])).opts(plot=dict(
                    width=600,
                    tools=[time_zoom, 'xpan'],
                    default_tools=['save', 'pan', 'box_zoom', 'save', 'reset'],
                    show_grid=False))

    def plot_peak(p):
        # It's better to plot amplitude /time than per bin, since
        # sampling times are now variable
        y = p['data'][:p['length']] / p['dt']
        t_edges = np.arange(p['length'] + 1, dtype=np.int64)
        t_edges = t_edges * p['dt'] + p['time']
        t_edges = normalize_time(t_edges)

        # Correct step plotting from Knut
        t_ = np.zeros(2 * len(y))
        y_ = np.zeros(2 * len(y))
        t_[0::2] = t_edges[0:-1]
        t_[1::2] = t_edges[1::]
        y_[0::2] = y
        y_[1::2] = y

        c = hv.Curve(dict(time=t_, amplitude=y_),
                     kdims=points.kdims[0],
                     vdims=hv.Dimension('amplitude',
                                        label='Amplitude',
                                        unit='PE/ns'),
                     group='PeakSumWaveform')
        return c.opts(
            plot=dict(  # interpolation='steps-mid',
                # default_tools=['save', 'pan', 'box_zoom', 'save', 'reset'],
                # tools=[time_zoom, 'xpan'],
                width=600,
                shared_axes=False,
                show_grid=True),
            style=dict(color='b')
            # norm=dict(framewise=True)
        )

    def peaks_in(t_0, t_1):
        return peaks[(normalize_time(peaks['time'] +
                                     peaks['length'] * peaks['dt']) > t_0)
                     & (normalize_time(peaks['time']) < t_1)]

    def plot_peaks(t_0, t_1, n_max=10):
        # Find peaks in this range
        ps = peaks_in(t_0, t_1)
        # Show only the largest n_max peaks
        if len(ps) > n_max:
            areas = ps['area']
            max_area = np.sort(areas)[-n_max]
            ps = ps[areas >= max_area]

        return hv.Overlay(items=[plot_peak(p) for p in ps])

    def plot_peak_range(x_range, **kwargs):
        # For use in dynamicmap with streams
        if x_range is None:
            x_range = (0, 10)
        return plot_peaks(x_range[0], x_range[1], **kwargs)

    top_map = hv.DynamicMap(partial(pmt_map_range, array='top'),
                            streams=[xrange_stream])
    bot_map = hv.DynamicMap(partial(pmt_map_range, array='bottom'),
                            streams=[xrange_stream])
    waveform = hv.DynamicMap(plot_peak_range, streams=[xrange_stream])
    layout = waveform + top_map + channel_map() + bot_map
    return layout.cols(2)
Beispiel #17
0
    def plot_event(self, event):
        dt = self.config['sample_duration']

        # Configurable limits
        channels_start = self.config.get('channel_start', min(self.config['channels_in_detector']['tpc']))
        channels_end = self.config.get('channel_start', max(self.config['channels_in_detector']['tpc']))
        t_start = self.config.get('t_start', 0 * units.us)
        t_end = self.config.get('t_end', event.duration())

        fig = plt.figure(figsize=(self.size_multiplier * 4, self.size_multiplier * 2))
        ax = fig.gca(projection='3d')

        global_max_amplitude = 0
        for pulse in event.pulses:
            # Take only channels in the range we want to plot
            if not channels_start <= pulse.channel <= channels_end:
                continue

            # Take only pulses that fall (at least partially) in the time window we want to plot
            if pulse.right * dt < t_start or pulse.left * dt > t_end:
                continue

            w = self.config['digitizer_reference_baseline'] + pulse.baseline - pulse.raw_data.astype(np.float64)
            w *= dsputils.adc_to_pe(self.config, pulse.channel, use_reference_gain=True)
            if self.config['log_scale']:
                # This will still give nan's if waveform drops below 1 pe_nominal / bin...
                # TODO: So... will it crash? or just fall outside range?
                w = np.log10(1 + w)

            # We need to keep track of this, apparently gca can't scale itself?
            global_max_amplitude = max(np.max(w), global_max_amplitude)

            ax.plot(
                np.linspace(pulse.left, pulse.right, pulse.length) * dt / units.us,
                pulse.channel * np.ones(pulse.length),
                zs=w,
                zdir='z',
                label=str(pulse.channel)
            )

        # Plot the sum waveform
        w = event.get_sum_waveform('tpc').samples[int(t_start / dt):int(t_end / dt) + 1]
        ax.plot(
            np.linspace(t_start, t_end, len(w)) / units.us,
            (channels_end + 1) * np.ones(len(w)),
            zs=w * global_max_amplitude / np.max(w),
            zdir='z',
            label='Tpc'
        )

        ax.set_xlabel('Time [$\mu$s]')
        ax.set_xlim3d(t_start / units.us, t_end / units.us)

        ax.set_ylabel('Channel number')
        ax.set_ylim3d(channels_start, channels_end)

        zlabel = 'Pulse height [pe_nominal / %d ns]' % (self.config['sample_duration'] / units.ns)
        if self.config['log_scale']:
            zlabel = 'Log10 1 + ' + zlabel
        ax.set_zlabel(zlabel)
        ax.set_zlim3d(0, global_max_amplitude)
Beispiel #18
0
    def split_peak(self, peak, split_points):
        """Yields new peaks split from peak at split_points = sample indices within peak
        Samples at the split points will fall to the right (so if we split [0, 5] on 2, you get [0, 1] and [2, 5]).
        Hits that straddle a split point are themselves split into two hits: peak.hits is updated.
        """
        # First, split hits that straddle the split points
        # Hits may have to be split several times; for each split point we modify the 'hits' list, splitting only
        # the hits we need.
        hits = peak.hits
        for x in split_points:
            x += peak.left   # Convert to index in event

            # Select hits that must be split: start before x and end after it.
            selection = (hits['left'] <= x) & (hits['right'] > x)
            hits_to_split = hits[selection]

            # new_hits will be a list of hit arrays, which we concatenate later to make the new 'hits' list
            # Start with the hits that don't have to be split: we definitely want to retain those!
            new_hits = [hits[True ^ selection]]

            for h in hits_to_split:
                pulse_i = h['found_in_pulse']
                pulse = self.event.pulses[pulse_i]

                # Get the pulse waveform in ADC counts above baseline (because it's what build_hits expect)
                baseline_to_subtract = self.config['digitizer_reference_baseline'] - pulse.baseline
                w = baseline_to_subtract - pulse.raw_data.astype(np.float64)

                # Use the hitfinder's build_hits to compute the properties of these hits
                # Damn this is ugly... but at least we don't have duplicate property computation code
                hits_buffer = np.zeros(2, dtype=datastructure.Hit.get_dtype())
                adc_to_pe = dsputils.adc_to_pe(self.config, h['channel'])
                hit_bounds = np.array([[h['left'], x], [x+1, h['right']]], dtype=np.int64)
                hit_bounds -= pulse.left   # build_hits expects hit bounds relative to pulse start
                build_hits(w,
                           hit_bounds=hit_bounds,
                           hits_buffer=hits_buffer,
                           adc_to_pe=adc_to_pe,
                           channel=h['channel'],
                           noise_sigma_pe=pulse.noise_sigma * adc_to_pe,
                           dt=self.config['sample_duration'],
                           start=pulse.left,
                           pulse_i=pulse_i,
                           saturation_threshold=self.config['digitizer_reference_baseline'] - pulse.baseline - 0.5,
                           central_bounds=hit_bounds)       # TODO: Recompute central bounds in an intelligent way...

                # Remove hits with 0 or negative area (very rare, but possible due to rigid integration bound)
                hits_buffer = hits_buffer[hits_buffer['area'] > 0]

                new_hits.append(hits_buffer)

            # Now remake the hits list, then go on to the next peak.
            hits = np.concatenate(new_hits)

        # Next, split the peaks, sorting hits to the right peak by their maximum index.
        # Iterate over left, right bounds of the new peaks
        boundaries = list(zip([0] + [y+1 for y in split_points], split_points + [float('inf')]))
        for l, r in boundaries:
            # Convert to index in event
            l += peak.left
            r += peak.left

            # Select hits which have their maximum within this peak bounds
            # The last new peak must also contain hits at the right bound (though this is unlikely to happen)
            hs = hits[(hits['index_of_maximum'] >= l) &
                      (hits['index_of_maximum'] <= r)]

            if not len(hs):
                # Hits have probably been removed by area > 0 condition
                self.log.info("Localminimumclustering requested creation of peak %s-%s without hits. "
                              "This is a possible outcome if there are large oscillations in one channel, "
                              "but it should be very rare." % (l, r))
                continue

            r = r if r < float('inf') else peak.right

            if not len(hs):
                raise RuntimeError("Attempt to create a peak without hits in LocalMinimumClustering!")

            yield self.build_peak(hits=hs, detector=peak.detector, left=l, right=r)
Beispiel #19
0
    def split_peak(self, peak, split_points):
        """Yields new peaks split from peak at split_points = sample indices within peak
        Samples at the split points will fall to the right (so if we split [0, 5] on 2, you get [0, 1] and [2, 5]).
        Hits that straddle a split point are themselves split into two hits: peak.hits is updated.
        """
        # First, split hits that straddle the split points
        # Hits may have to be split several times; for each split point we modify the 'hits' list, splitting only
        # the hits we need.
        hits = peak.hits
        for x in split_points:
            x += peak.left  # Convert to index in event

            # Select hits that must be split: start before x and end after it.
            selection = (hits['left'] <= x) & (hits['right'] > x)
            hits_to_split = hits[selection]

            # new_hits will be a list of hit arrays, which we concatenate later to make the new 'hits' list
            # Start with the hits that don't have to be split: we definitely want to retain those!
            new_hits = [hits[True ^ selection]]

            for h in hits_to_split:
                pulse_i = h['found_in_pulse']
                pulse = self.event.pulses[pulse_i]

                # Get the pulse waveform in ADC counts above baseline (because it's what build_hits expect)
                baseline_to_subtract = self.config[
                    'digitizer_reference_baseline'] - pulse.baseline
                w = baseline_to_subtract - pulse.raw_data.astype(np.float64)

                # Use the hitfinder's build_hits to compute the properties of these hits
                # Damn this is ugly... but at least we don't have duplicate property computation code
                hits_buffer = np.zeros(2, dtype=datastructure.Hit.get_dtype())
                adc_to_pe = dsputils.adc_to_pe(self.config, h['channel'])
                hit_bounds = np.array([[h['left'], x], [x + 1, h['right']]],
                                      dtype=np.int64)
                hit_bounds -= pulse.left  # build_hits expects hit bounds relative to pulse start
                build_hits(
                    w,
                    hit_bounds=hit_bounds,
                    hits_buffer=hits_buffer,
                    adc_to_pe=adc_to_pe,
                    channel=h['channel'],
                    noise_sigma_pe=pulse.noise_sigma * adc_to_pe,
                    dt=self.config['sample_duration'],
                    start=pulse.left,
                    pulse_i=pulse_i,
                    saturation_threshold=self.
                    config['digitizer_reference_baseline'] - pulse.baseline -
                    0.5,
                    central_bounds=hit_bounds
                )  # TODO: Recompute central bounds in an intelligent way...

                # Remove hits with 0 or negative area (very rare, but possible due to rigid integration bound)
                hits_buffer = hits_buffer[hits_buffer['area'] > 0]

                new_hits.append(hits_buffer)

            # Now remake the hits list, then go on to the next peak.
            hits = np.concatenate(new_hits)

        # Next, split the peaks, sorting hits to the right peak by their maximum index.
        # Iterate over left, right bounds of the new peaks
        boundaries = list(
            zip([0] + [y + 1 for y in split_points],
                split_points + [float('inf')]))
        for left, right in boundaries:
            # Convert to index in event
            left += peak.left
            right += peak.left

            # Select hits which have their maximum within this peak bounds
            # The last new peak must also contain hits at the right bound (though this is unlikely to happen)
            hs = hits[(hits['index_of_maximum'] >= left)
                      & (hits['index_of_maximum'] <= right)]

            if not len(hs):
                # Hits have probably been removed by area > 0 condition
                self.log.info(
                    "Localminimumclustering requested creation of peak %s-%s without hits. "
                    "This is a possible outcome if there are large oscillations in one channel, "
                    "but it should be very rare." % (left, right))
                continue

            right = right if right < float('inf') else peak.right

            if not len(hs):
                raise RuntimeError(
                    "Attempt to create a peak without hits in LocalMinimumClustering!"
                )

            yield self.build_peak(hits=hs,
                                  detector=peak.detector,
                                  left=left,
                                  right=right)