Exemple #1
0
class ReconstructedPosition(StrictModel):
    """Reconstructed position

    Each reconstruction algorithm creates one of these.
    """
    x = float('nan')  #: x position (cm)
    y = float('nan')  #: y position (cm)

    #: For 3d-position reconstruction algorithms, the z-position (cm)
    #: This is NOT related to drift time, which is an interaction-level quantity!
    z = float('nan')

    #: Goodness-of-fit of hitpattern to position (provided by PosRecTopPatternFit)
    #: For PosRecThreedPatternFit, the 3d position goodness-of-fit.
    goodness_of_fit = float('nan')

    #: Number of degrees of freedom used in goodness-of-fit calculation
    ndf = float('nan')

    #: Name of algorithm which provided this position
    algorithm = 'none'

    #: Confidence_levels
    # error_matrix = np.array([], dtype=np.float64)
    confidence_tuples = ListField(ConfidenceTuple)

    # For convenience: cylindrical coordinates
    # Must be properties so InterpolatingDetectorMap can transparently use
    # cylindrical coordinates
    @property
    def r(self):
        """Radial position"""
        return np.sqrt(self.x ** 2 + self.y ** 2)

    #: phi position, i.e. angle wrt the x=0 axis in the xy plane (radians)
    @property
    def phi(self):
        """Angular position (radians, origin at positive x-axis)"""
        return np.arctan2(self.y, self.x)
Exemple #2
0
class Event(StrictModel):
    """Object holding high-level information about a triggered event,
    and list of objects (such as Peak, Hit and Pulse) containing lower-level information.
    """
    #: The name of the dataset this event belongs to
    dataset_name = 'Unknown'

    #: A nonnegative integer that uniquely identifies the event within the dataset.
    event_number = 0

    #: Internal number used for multiprocessing, no physical meaning.
    block_id = -1

    #: Total number of channels in the event (whether or not they see anything).
    #: Has to be the same as n_channels in config, provided here for deserialization ease.
    n_channels = INT_NAN

    #: Integer start time of the event in nanoseconds since January 1, 1970.
    #: This is the time that the first sample starts.
    #: NB: don't do floating-point arithmetic on 64-bit integers such as these,
    #: floats have rounding that result in loss of precision.
    start_time = long(0)

    #: Integer stop time of the event in nanoseconds since January 1, 1970.
    #: This is the time that the last sample ends.
    #: NB: don't do floating-point arithmetic on 64-bit integers such as these,
    #: floats have rounding that result in loss of precision.
    stop_time = long(0)

    #: Time duration of a sample (in ns).
    #: For V1724 digitizers (e.g. XENON), this is 10 nanoseconds.
    #: This is also in config, but we need it here too, to convert between event duration and length in samples
    #: Must be an int for same reason as start_time and stop_time
    #: DO NOT set to 10 ns as default, otherwise no way to check if it was given to constructor!
    sample_duration = 0

    #: A list of :class:`pax.datastructure.Interaction` objects.
    interactions = ListField(Interaction)

    #: A list of :class:`pax.datastructure.Peak` objects.
    peaks = ListField(Peak)

    #: Array of trigger signals contained in the event
    trigger_signals = np.array([], dtype=TriggerSignal.get_dtype())

    #: Array of all hits found in event
    #: These will get grouped into peaks during clustering. New hits will be added when peaks are split.
    #: NEVER rely upon the order of hits in this field! It depends on lunar phase and ambient pressure.
    #: This is usually emptied before output (but not in LED mode)
    all_hits = np.array([], dtype=Hit.get_dtype())

    #: A list :class:`pax.datastructure.SumWaveform` objects.
    sum_waveforms = ListField(SumWaveform)

    #: A list of :class:`pax.datastructure.Interaction` objects.
    #: A pulse holds a stream of samples in one channel provided by the digitizer.
    #: To save space, only the pulses contributing hits to S1s are kept in the output (but not in LED mode)
    #: The order of this field cannot be changed after the hitfinder, since hits have a found_in_pulse field
    #: referring to the index of a pulse in this field.
    pulses = ListField(Pulse)

    #: Number of pulses per channel
    n_pulses_per_channel = np.array([], dtype=np.int16)

    #: Total number of pulses
    n_pulses = 0

    #: Number of noise pulses (pulses without any hits found) per channel
    noise_pulses_in = np.array([], dtype=np.int16)

    #: Number of lone hits per channel BEFORE suspicious channel hit rejection.
    #: lone_hit is a peak type (sorry, confusing...) indicating just one contributing channel.
    #: Use this to check / calibrate the suspicious channel hit rejection.
    lone_hits_per_channel_before = np.array([], dtype=np.int16)

    #: Number of lone hits per channel AFTER suspicious channel hit rejection.
    #: lone_hit is a peak type (sorry, confusing...) indicating just one contributing channel
    lone_hits_per_channel = np.array([], dtype=np.int16)

    #: Was channel flagged as suspicious?
    is_channel_suspicious = np.array([], dtype=np.bool)

    #: Number of hits rejected per channel in the suspicious channel algorithm
    n_hits_rejected = np.array([], dtype=np.int16)

    def __init__(self, n_channels, start_time, **kwargs):

        # Start time is mandatory, so it is not in kwargs
        kwargs['start_time'] = start_time
        kwargs['n_channels'] = n_channels

        # Model's init must be called first, else we can't store attributes
        # This will store all of the kwargs as attrs
        # We don't pass length, it's not an attribute that can be set
        StrictModel.__init__(self, **{k: v for k, v in kwargs.items() if k != 'length'})

        # Cheat to init stop_time from length and duration
        if 'length' in kwargs and self.sample_duration and not self.stop_time:
            self.stop_time = int(self.start_time + kwargs['length'] * self.sample_duration)

        if not self.stop_time or not self.sample_duration:
            raise ValueError("Cannot initialize an event with an unknown length: " +
                             "pass sample_duration and either stop_time or length")

        if self.duration() <= 0:
            raise ValueError("Nonpositive event duration %s!" % self.duration())

        # Initialize numpy arrays -- need to have n_channels and self.length
        self.n_pulses_per_channel = np.zeros(n_channels, dtype=np.int16)
        self.noise_pulses_in = np.zeros(n_channels, dtype=np.int16)
        self.n_hits_rejected = np.zeros(n_channels, dtype=np.int16)
        self.is_channel_suspicious = np.zeros(n_channels, dtype=np.bool)
        self.lone_hits_per_channel_before = np.zeros(n_channels, dtype=np.int16)
        self.lone_hits_per_channel = np.zeros(n_channels, dtype=np.int16)

    @classmethod
    def empty_event(cls):
        """Returns an empty example event: for testing purposes only!!
        """
        return Event(n_channels=1, start_time=10, length=1, sample_duration=int(10 * units.ns))

    def duration(self):
        """Duration of event window in units of ns
        """
        return self.stop_time - self.start_time

    def get_sum_waveform_names(self):
        """Get list of the names of sum waveform objects
        Deprecated -- for Xerawdp matching only
        """
        return [sw.name for sw in self.sum_waveforms]

    def get_sum_waveform(self, name):
        """Get sum waveform object by name
        Deprecated -- for Xerawdp matching only
        """
        for sw in self.sum_waveforms:
            if sw.name == name:
                return sw

        raise RuntimeError("SumWaveform %s not found" % name)

    def length(self):
        """Number of samples in the event
        """
        return int(self.duration() / self.sample_duration)

    def s1s(self, detector='tpc', sort_key=('tight_coincidence', 'area'), reverse=True):  # noqa
        """List of S1 (scintillation) signals in this event
        In the ROOT class output, this returns a list of integer indices in event.peaks
        Inside pax, returns a list of :class:`pax.datastructure.Peak` objects
          whose type is 's1', and
          who are in the detector specified by the 'detector' argument (unless detector='all')
        The returned list is sorted DESCENDING (i.e. reversed!) by the key sort_key (default area)
        unless you pass reverse=False, then it is ascending.
        """
        return self.get_peaks_by_type('s1', sort_key=sort_key, reverse=reverse, detector=detector)

    def S1s(self, *args, **kwargs):
        """See s1s"""
        return self.s1s(*args, **kwargs)

    def s2s(self, detector='tpc', sort_key='area', reverse=True):  # noqa
        """List of S2 (ionization) signals in this event
        In the ROOT class output, this returns a list of integer indices in event.peaks.
        Inside pax, returns a list of :class:`pax.datastructure.Peak` objects
          whose type is 's2', and
          who are in the detector specified by the 'detector' argument (unless detector='all')
        The returned list is sorted DESCENDING (i.e. reversed!) by the key sort_key (default area)
        unless you pass reverse=False, then it is ascending.
        """
        return self.get_peaks_by_type(desired_type='s2', sort_key=sort_key, reverse=reverse, detector=detector)

    def S2s(self, *args, **kwargs):
        """See s2s"""
        return self.s2s(*args, **kwargs)

    @property
    def main_s1(self):
        """Return the S1 of the primary interaction, or if that does not exist, the largest S1 in the tpc.
        Returns None if neither exist"""
        if self.interactions:
            return self.peaks[self.interactions[0].s1]
        else:
            try:
                return self.s1s()[0]
            except IndexError:
                return None

    @property
    def main_s2(self):
        """Return the S2 of the primary interaction, or if that does not exist, the largest S2 in the tpc.
        Returns None if neither exist"""
        if self.interactions:
            return self.peaks[self.interactions[0].s2]
        else:
            try:
                return self.s2s()[0]
            except IndexError:
                return None

    def get_peaks_by_type(self, desired_type='all', detector='tpc', sort_key='area', reverse=True):
        """Helper function for retrieving only certain types of peaks
        Returns a list of :class:`pax.datastructure.Peak` objects
          whose type is desired_type, and
          who are in the detector specified by the 'detector' argument (unless detector='all')
        The returned list is sorted DESCENDING (i.e. reversed!) by the key sort_key (default area)
        unless you pass reverse=False, then it is ascending (normal sort order).
        """
        # Extract only peaks of a certain type
        peaks = []
        for peak in self.peaks:
            if detector != 'all':
                if peak.detector != detector:
                    continue
            if desired_type != 'all' and peak.type.lower() != desired_type:
                continue
            peaks.append(peak)

        # Sort the peaks by your sort key
        if isinstance(sort_key, (str, bytes)):
            sort_key = [sort_key]
        peaks = sorted(peaks,
                       key=operator.attrgetter(*sort_key),
                       reverse=reverse)

        return peaks
Exemple #3
0
class Peak(StrictModel):
    """A group of nearby hits across one or more channels.
    Peaks will be classified as e.g. s1, s2, lone_hit, unknown, coincidence
    """
    #: Type of peak (e.g., 's1', 's2', ...):
    #: NB 'lone_hit' incicates one or more hits in a single channel. Use lone_hit_channel to retrieve that channel.
    type = 'unknown'

    #: Detector in which the peak was found, e.g. tpc or veto
    detector = 'none'

    ##
    #  Hit, area, and saturation data
    ##

    #: The hits that make up this peak. To save space, we usually only store the hits for s1s in the root file.
    #: Do not rely on the order of hits in this field!!
    #: For the root output, this gets converted back to a list of Hit classes (then to a vector of c++ Hit objects)
    hits = np.array([], dtype=Hit.get_dtype())

    #: Total areas of all hits per PMT (pe).
    area_per_channel = np.array([], dtype='float64')

    # contribution to tight coincidence, 1 or 0 for each channel
    coincidence_per_channel = np.array([], dtype=np.int16)

    #: Total area of all hits across all PMTs (pes).
    #: In XerawdpImitation mode, rightmost sample is not included in area integral.
    area = 0.0

    #: Fraction of area in the top PMTs
    area_fraction_top = 0.0

    #: Multiplicative correction on S2 due to LCE variations
    s2_spatial_correction = 1.0

    #: Multiplicative correction on S2 top due to LCE variations
    s2_top_spatial_correction = 1.0

    #: Multiplicative correction on S2 bottom due to LCE variations
    s2_bottom_spatial_correction = 1.0

    #: Multiplicative correction on S2 due to saturation
    s2_saturation_correction = 1.0

    #: Number of hits in the peak, per channel (that is, it's an array with index = channel number)
    hits_per_channel = np.array([], dtype=np.int16)

    #: Number of channels which contribute to the peak
    n_contributing_channels = 0

    #: Number of channels in the top array contributing to the peak
    n_contributing_channels_top = 0

    #: Total number of hits in the peak
    n_hits = 0

    #: Fraction of hits in the top array
    hits_fraction_top = 0.0

    #: Number of samples with ADC saturation in this peak, per channel
    n_saturated_per_channel = np.array([], dtype=np.int16)

    @property
    def is_channel_saturated(self):
        """Boolean array of n_channels which indicates if there was ADC saturation in any hit
        in that channel during the peak"""
        return self.n_saturated_per_channel > 0

    @property
    def saturated_channels(self):
        """List of channels which contribute hits with saturated channels in this peak"""
        return np.where(self.n_saturated_per_channel > 0)[0]

    #: Total number of samples with ADC saturation threshold in all channels in this peak
    n_saturated_samples = 0

    #: Total number of channels in the peakw hich have at least one saturated hit
    n_saturated_channels = 0

    #: If the peak is a lone_hit: the channel the hit is / hits are in
    lone_hit_channel = INT_NAN

    # Area of the largest hit in the peak
    largest_hit_area = float('nan')

    # Channel of the largest hit in the peak
    largest_hit_channel = INT_NAN

    @property
    def does_channel_contribute(self):
        """Boolean array of n_channels which tells you if the channel contributes any hit"""
        return self.area_per_channel > 0

    @property
    def contributing_channels(self):
        """List of channels which contribute one or more hits to this peak"""
        return np.where(self.does_channel_contribute)[0]

    #: Number of channels that have a hit maximum within a short (configurable) window around the peak's sum
    #: waveform maximum.
    tight_coincidence = INT_NAN
    tight_coincidence_thresholds = np.array([], dtype=np.int16)  # various tight coincidence levels

    ##
    # Time distribution information
    ##

    left = 0                 #: Index/sample of left bound (inclusive) in event.
    right = 0                #: Index/sample of right bound (INCLUSIVE) in event.

    #: Weighted (by hit area) mean of hit times (since event start) [ns]
    hit_time_mean = 0.0

    #: Weighted (by hit area) std of hit times [ns]
    hit_time_std = 0.0

    #: Central range of peak (hit-only) sum waveform which includes a given decile (0-10) of area [ns].
    #: e.g. range_area_decile[5] = range of 50% area = distance (in time) between point
    #: of 25% area and 75% area (with boundary samples added fractionally).
    #: First element (0) is always zero, last element (10) is the full range of the peak.
    range_area_decile = np.zeros(11, dtype=np.float)

    #: Time (ns) from the area decile point to the area midpoint.
    #: If you want to know the time until some other point (say the sum waveform maximum),
    #: just add the difference between that point and the area midpoint.
    area_decile_from_midpoint = np.zeros(11, dtype=np.float)

    @property
    def range_50p_area(self):
        return self.range_area_decile[5]

    @property
    def range_90p_area(self):
        return self.range_area_decile[9]

    @property
    def full_range(self):
        return self.range_area_decile[10]

    #: Time at which the peak reaches 50% of its area (with the central sample considered fractionally)
    area_midpoint = 0.0

    ##
    # Spatial pattern information
    ##

    #: List of reconstructed positions (instances of :class:`pax.datastructure.ReconstructedPosition`)
    reconstructed_positions = ListField(ReconstructedPosition)

    def get_reconstructed_position_from_algorithm(self, algorithm):
        """Return reconstructed position found by algorithm, or None if the peak doesn't have one"""
        for rp in self.reconstructed_positions:
            if rp.algorithm == algorithm:
                return rp
        return None

    def get_position_from_preferred_algorithm(self, algorithm_list):
        """Return reconstructed position by the first algorithm in list,
        unless it doesn't exist or is a nan position, then moves on to further algorithms."""
        for algo in algorithm_list:
            rp = self.get_reconstructed_position_from_algorithm(algo)
            if rp is not None and not np.isnan(rp.x):
                return rp
        else:
            raise ValueError("Could not find any position from the chosen algorithms: %s" % algorithm_list)

    #: Weighted-average distance of top array hits from weighted mean hitpattern center on top array (cm)
    top_hitpattern_spread = float('nan')

    #: Weighted-average distance of bottom array hits from weighted mean hitpattern center on bottom array (cm)
    bottom_hitpattern_spread = float('nan')

    ##
    # Signal / noise info
    ##

    #: Weighted (by area) mean hit amplitude / noise level in that hit's channel
    mean_amplitude_to_noise = 0.0

    #: Number of pulses without hits in the event overlapping (in time; at least partially) with this peak.
    #: Includes channels from other detectors (since veto and tpc cables could influence each other)
    n_noise_pulses = 0

    ##
    # Sum-waveform properties
    ##

    #: Cut-out of the peak's sum waveform in pe/bin
    #: The peak's center of gravity is always in the center of the array.
    sum_waveform = np.array([], dtype=np.float32)

    #: For tpc peaks, the peak's sum waveform in the top array only. Aligned with the sum waveform.
    sum_waveform_top = np.array([], dtype=np.float32)

    #: Index/sample in the event's sum waveform at which this peak has its maximum.
    index_of_maximum = 0

    #: Time since start of the event at which the peak's sum waveform has its center of gravity [ns].
    center_time = 0.0

    #: Height of sum waveform (in pe/bin)
    height = 0.0

    ##
    # Clustering record
    ##

    #: Best goodness of split observed inside the peak
    interior_split_goodness = float('nan')

    #: Area fraction of the smallest of the two halves considered in the best split inside the peak
    #: (i.e. the one corresponding to interior_split_goodness)
    interior_split_fraction = float('nan')

    #: Goodness of split of last split that was used to construct this peak (if split did occur).
    birthing_split_goodness = float('nan')

    #: Area of this peak / area of parent peak it was split from (if split did occur)
    birthing_split_fraction = float('nan')