Example #1
0
    def __init__(self, alg_name, alpha_deg, beta_deg, info=None):
        self.alpha_deg = alpha_deg
        self.beta_deg = beta_deg
        self.alg_name = alg_name
        self.info = info

        # customize data_format based on which algorithm type
        if self.alg_name.startswith('matlab'):
            self.data_format = dataformats.get_format('AlgorithmOutputMatlab')
        else:
            self.data_format = dataformats.get_format(self.class_name)
Example #2
0
    def __init__(self, alg_name, alpha_deg, beta_deg, info=None):
        self.alpha_deg = alpha_deg
        self.beta_deg = beta_deg
        self.alg_name = alg_name
        self.info = info

        # customize data_format based on which algorithm type
        if self.alg_name.startswith('matlab'):
            self.data_format = dataformats.get_format('AlgorithmOutputMatlab')
        else:
            self.data_format = dataformats.get_format(self.class_name)
Example #3
0
    def check_input(h5group, data_format):
        """
        Input HDF5 group should have 'obj_type' attribute which matches
        a class we know.
        """

        if ext_data_format is not None:
            # for testing: don't use dataformats.get_format()
            return ext_data_format

        else:
            if 'obj_type' not in h5group.attrs:
                if h5group == h5group.file:
                    raise InterfaceError(
                        'Looks like you supplied the HDF5 file object ' +
                        'instead of the HDF5 group representing the object...')
                else:
                    raise InterfaceError(
                        'HDF5 object should have an attribute, obj_type')
            obj_type = h5group.attrs['obj_type']
            data_format = dataformats.get_format(obj_type)
            return data_format
Example #4
0
    def check_input(h5group, data_format):
        """
        Input HDF5 group should have 'obj_type' attribute which matches
        a class we know.
        """

        if ext_data_format is not None:
            # for testing: don't use dataformats.get_format()
            return ext_data_format

        else:
            if 'obj_type' not in h5group.attrs:
                if h5group == h5group.file:
                    raise InterfaceError(
                        'Looks like you supplied the HDF5 file object ' +
                        'instead of the HDF5 group representing the object...')
                else:
                    raise InterfaceError(
                        'HDF5 object should have an attribute, obj_type')
            obj_type = h5group.attrs['obj_type']
            data_format = dataformats.get_format(obj_type)
            return data_format
Example #5
0
def test_io(tracks=None):
    """
    """

    import etrack.reconstruction.test_moments as tm

    if tracks is None:
        tracks = tm.get_tracklist(n_files=1)
    mom = tm.momentlist_from_tracklist(tracks, fill_nans=False)
    cl = tm.classifierlist_from_tracklist(tracks, mom)

    filename = 'testcl.h5'
    print('Writing classifiers to {}...'.format(filename))
    trackio.write_object_list_to_hdf5(filename, cl, prefix='cl_')

    print('Reading classifiers back...')
    clread = trackio.read_object_list_from_hdf5(
        filename, Classifier.from_hdf5, prefix='cl_')

    print('Checking attributes...')
    attrs = df.get_format('Classifier')
    error_ind = []
    error_list = []
    for i, c in enumerate(clread):
        if hasattr(c, 'error'):
            if c.error:
                error_ind.append(i)
                error_list.append(c.error)
                # if there's a track error, which fields are filled, are wonky
                continue
        for attr in attrs:
            if attr.name == 'g4track':
                continue
            elif getattr(cl[i], attr.name) is not None:
                assert getattr(cl[i], attr.name) == getattr(c, attr.name)

    return error_ind, error_list
Example #6
0
def test_io(tracks=None):
    """
    """

    import etrack.reconstruction.test_moments as tm

    if tracks is None:
        tracks = tm.get_tracklist(n_files=1)
    mom = tm.momentlist_from_tracklist(tracks, fill_nans=False)
    cl = tm.classifierlist_from_tracklist(tracks, mom)

    filename = 'testcl.h5'
    print('Writing classifiers to {}...'.format(filename))
    trackio.write_object_list_to_hdf5(filename, cl, prefix='cl_')

    print('Reading classifiers back...')
    clread = trackio.read_object_list_from_hdf5(
        filename, Classifier.from_hdf5, prefix='cl_')

    print('Checking attributes...')
    attrs = df.get_format('Classifier')
    error_ind = []
    error_list = []
    for i, c in enumerate(clread):
        if hasattr(c, 'error'):
            if c.error:
                error_ind.append(i)
                error_list.append(c.error)
                # if there's a track error, which fields are filled, are wonky
                continue
        for attr in attrs:
            if attr.name == 'g4track':
                continue
            elif getattr(cl[i], attr.name) is not None:
                assert getattr(cl[i], attr.name) == getattr(c, attr.name)

    return error_ind, error_list
Example #7
0
class MatlabAlgorithmInfo(object):
    """
    An empty container to store attributes loaded from the Matlab algorithm.
    """

    __version__ = '0.1'
    class_name = 'MatlabAlgorithmInfo'
    data_format = dataformats.get_format(class_name)

    attr_list = (
        'Tind',
        'lt',
        'n_ends',
        'Eend',
        'alpha',
        'beta',
        'dalpha',
        'dbeta',
        'edgesegments_energies_kev',
        'edgesegments_coordinates_pix',
        'edgesegments_chosen_index',
        'edgesegments_start_coordinates_pix',
        'edgesegments_start_direction_indices',
        'edgesegments_low_threshold_used',
        'dedx_ref',
        'dedx_meas',
        'measurement_start_ind',
        'measurement_end_ind',
    )
    data_list = (
        'thinned_img',
        'x',
        'y',
        'w',
        'a0',
        'dE',
    )

    def __init__(self, **kwargs):
        """
        Shouldn't need to call this -- use from_h5pixnoise or from_pydict
        """

        for attr in kwargs:
            setattr(self, attr, kwargs[attr])

    @classmethod
    def from_h5pixnoise(cls, pixnoise):
        """
        Initialize MatlabAlgorithmInfo with all the attributes from a
        successful HybridTrack algorithm.

        pixnoise is the h5 group.

        This goes with the _h5matlab format.
        """

        kwargs = {}
        for attr in cls.attr_list:
            if attr in pixnoise.attrs:
                kwargs[attr] = pixnoise.attrs[attr]
            else:
                raise InputError('Missing h5 attribute: {} in {}'.format(
                    attr, str(pixnoise)))
        for attr in cls.data_list:
            if attr in pixnoise:
                kwargs[attr] = pixnoise[attr]
            else:
                raise InputError('Missing h5 dataset: {} in {}'.format(
                    attr, str(pixnoise)))
        return cls(**kwargs)

    @classmethod
    def from_pydict(cls, read_dict, pydict_to_pyobj=None):
        """
        Initialize a MatlabAlgorithmInfo object from the dictionary returned by
        trackio.read_object_from_hdf5().
        """

        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        if id(read_dict) in pydict_to_pyobj:
            return pydict_to_pyobj[id(read_dict)]

        all_attrs = cls.attr_list

        kwargs = {}
        for attr in all_attrs:
            kwargs[attr] = read_dict.get(attr)
            # read_dict.get() defaults to None, although this actually
            #   shouldn't be needed since read_object_from_hdf5 adds Nones
        constructed_object = cls(**kwargs)

        # add entry to pydict_to_pyobj
        pydict_to_pyobj[id(read_dict)] = constructed_object

        return constructed_object

    @classmethod
    def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None):
        """
        Initialize a MatlabAlgorithmInfo object from an HDF5 group.
        """

        if h5_to_pydict is None:
            h5_to_pydict = {}
        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        read_dict = trackio.read_object_from_hdf5(h5group,
                                                  h5_to_pydict=h5_to_pydict)

        constructed_object = cls.from_pydict(read_dict,
                                             pydict_to_pyobj=pydict_to_pyobj)

        return constructed_object
Example #8
0
class Track(object):
    """
    Electron track, from modeling or from experiment.
    """

    __version__ = '0.1'
    class_name = 'Track'
    data_format = dataformats.get_format(class_name)

    attr_list = (
        'is_modeled',
        'is_measured',
        'pixel_size_um',
        'noise_ev',
        'g4track',
        'energy_kev',
        'x_offset_pix',
        'y_offset_pix',
        'timestamp',
        'shutter_ind',
        'label',
    )

    attr_types = {
        'is_modeled': bool,
        'is_measured': bool,
        'pixel_size_um': float,
        'noise_ev': float,
        'g4track': object,
        'energy_kev': float,
        'x_offset_pix': float,
        'y_offset_pix': float,
        'timestamp': object,
        'shutter_ind': int,
        'label': str,
    }

    def __init__(self, image, **kwargs):
        """
        Construct a track object.

        Required input:
          image

        Either is_modeled or is_measured is required.

        Keyword inputs:
          is_modeled (bool)
          is_measured (bool)
          pixel_size_um (float)
          noise_ev (float)
          g4track (G4Track object)
          energy_kev (float)
          x_offset_pix (int)
          y_offset_pix (int)
          timestamp (datetime object)
          shutter_ind (int)
          label (string)
        """

        self.input_handling(image, **kwargs)

        self.algorithms = {}

    def input_handling(self,
                       image,
                       is_modeled=None,
                       is_measured=None,
                       pixel_size_um=None,
                       noise_ev=None,
                       g4track=None,
                       energy_kev=None,
                       x_offset_pix=None,
                       y_offset_pix=None,
                       timestamp=None,
                       shutter_ind=None,
                       label=None):

        if is_modeled is None and is_measured is None:
            raise InputError('Please specify modeled or measured!')
        elif is_modeled is True and is_measured is True:
            raise InputError('Track cannot be both modeled and measured!')
        elif is_modeled is False and is_measured is False:
            raise InputError('Track must be either modeled or measured!')
        elif is_measured is not None:
            self.is_measured = bool(is_measured)
            self.is_modeled = not bool(is_measured)
        elif is_modeled is not None:
            self.is_modeled = bool(is_modeled)
            self.is_measured = not bool(is_modeled)

        if g4track is not None and not isinstance(g4track, G4Track):
            raise InputError('g4track input must be a G4Track!')
            # or handle e.g. a g4 matrix input
        self.g4track = g4track

        self.image = np.array(image)

        if pixel_size_um is not None:
            pixel_size_um = np.float(pixel_size_um)
        self.pixel_size_um = pixel_size_um

        if noise_ev is not None:
            noise_ev = np.float(noise_ev)
        self.noise_ev = noise_ev

        if energy_kev is not None:
            energy_kev = np.float(energy_kev)
        self.energy_kev = energy_kev

        if x_offset_pix is not None:
            np.testing.assert_almost_equal(
                x_offset_pix,
                np.round(x_offset_pix),
                decimal=3,
                err_msg='x_offset_pix must be an integer')
            x_offset_pix = int(np.round(x_offset_pix))
        self.x_offset_pix = x_offset_pix

        if y_offset_pix is not None:
            np.testing.assert_almost_equal(
                y_offset_pix,
                np.round(y_offset_pix),
                decimal=3,
                err_msg='y_offset_pix must be an integer')
            y_offset_pix = int(np.round(y_offset_pix))
        self.y_offset_pix = y_offset_pix

        if shutter_ind is not None:
            np.testing.assert_almost_equal(
                shutter_ind,
                np.round(shutter_ind),
                decimal=3,
                err_msg='shutter_ind must be an integer')
            shutter_ind = int(np.round(shutter_ind))
        self.shutter_ind = shutter_ind

        if (timestamp is not None and timestamp != 'None'
                and not isinstance(timestamp, datetime.datetime)):
            raise InputError('timestamp should be a datetime object')
        self.timestamp = str(timestamp)

        if label is not None:
            label = str(label)
        self.label = label

    @classmethod
    def from_h5matlab(cls, pixnoise, g4track=None):
        """
        Construct a Track object from one pixelsize/noise of an event in an
        HDF5 file.

        HDF5 file is the December 2015 format from MATLAB.
        """

        try:
            errorcode = pixnoise.attrs['errorcode']
        except KeyError:
            # found this in HTbatch01_h5m/MultiAngle_HT_100_12.h5 track 127
            errorcode = 97
        if errorcode != 0:
            # for now, only accept tracks without errors
            return errorcode

        # algorithm_error = (errorcode == 4 or errorcode == 5)
        # good_algorithm = (errorcode == 0)
        # has_algorithm = good_algorithm or algorithm_error
        # has_ridge = good_algorithm
        # has_measurement = good_algorithm
        # has_multiple_tracks = (errorcode == 2 or errorcode == 6)
        #
        # do_pixnoise = true
        # check_pixsize = good_algorithm or has_multiple_tracks
        # check_noise = has_multiple_tracks
        # do_img = has_algorithm
        #
        # do_EtotTind = has_algorithm
        # do_nends = good_algorithm or errorcode == 4
        #
        # do_T = has_multiple_tracks
        # do_edgesegments = good_algorithm
        # do_ridge = has_ridge
        # do_measurement = has_measurement

        # track info (not algorithm info)
        try:
            data = pixnoise['img']
        except KeyError:
            errorcode = 96
            return errorcode
        img = np.zeros(data.shape)
        data.read_direct(img)

        kwargs = {}
        kwargs['is_modeled'] = True
        kwargs['g4track'] = g4track
        try:
            kwargs['pixel_size_um'] = pixnoise.attrs['pixel_size_um']
            kwargs['noise_ev'] = pixnoise.attrs['noise_ev']
            kwargs['energy_kev'] = pixnoise.attrs['Etot']
        except KeyError:
            # hypothetical
            errorcode = 98
            return errorcode

        track = Track(img, **kwargs)

        # algorithm info
        try:
            alpha = pixnoise.attrs['alpha']
            beta = pixnoise.attrs['beta']
        except KeyError:
            # found this in HTbatch01_h5m/MultiAngle_100_3.h5 track 00097
            # yes, with errorcode 0. maybe something crashed?
            errorcode = 99
            return errorcode
        try:
            info = MatlabAlgorithmInfo.from_h5pixnoise(pixnoise)
        except InputError:
            errorcode = 88
            return errorcode

        track.add_algorithm('matlab HT v1.5',
                            alpha_deg=alpha,
                            beta_deg=beta,
                            info=info)
        return track

    @classmethod
    def from_dth5(cls, pixnoise, g4track=None):
        """
        Construct a Track object from one pixelsize/noise of an event in an
        HDF5 file.

        The format is that of files from DT_to_hdf5.m / write_DT_hdf5.m.
        """

        # adapted from from_h5matlab()

        try:
            errorcode = int(pixnoise.attrs['errorcode'])
        except KeyError:
            # found this in HTbatch01_h5m/MultiAngle_HT_100_12.h5 track 127
            errorcode = 97
        if errorcode != 0:
            # for now, only accept tracks without errors
            return errorcode

        # see logbook p. 132 (2016-oct-11) for other error codes

        # since this is data directly from DT,
        #   we need to handle segmentation issues.
        if 'T1' in pixnoise.keys():
            # multiple tracks. not dealing with these.
            # assume real multiplicity events (multiple scattering) were
            #   already removed after the G4Track.
            errorcode = 2  # "Segmentation divided the track"
            return errorcode
        if 'T0' not in pixnoise.keys():
            # no track
            errorcode = 3  # "No segmented image to use"
            return errorcode

        # now, the track info.
        kwargs = {}
        kwargs['is_modeled'] = True
        kwargs['g4track'] = g4track

        try:
            data = pixnoise['T0']['img']
        except KeyError:
            errorcode = 76
            return errorcode
        img = np.zeros(data.shape)
        data.read_direct(img)

        try:
            kwargs['pixel_size_um'] = float(pixnoise.attrs['pixel_size_um'])
            kwargs['noise_ev'] = int(pixnoise.attrs['noise_ev'])
            kwargs['energy_kev'] = float(pixnoise.attrs['E'])
            pix_thresh = int(pixnoise.attrs['pixel_threshold'])
            seg_thresh_kev = float(pixnoise.attrs['segment_threshold'])
            edgeflag = pixnoise['T0'].attrs['edgeflag']
            kwargs['x_offset_pix'] = int(pixnoise['T0'].attrs['x'])
            kwargs['y_offset_pix'] = int(pixnoise['T0'].attrs['y'])
        except KeyError:
            # hypothetical
            errorcode = 78
            return errorcode

        # put extra info in the "label" attribute
        label_text = 'pix_thresh={}, seg_thresh_kev={}, edgeflag={}'.format(
            int(pix_thresh), float(seg_thresh_kev), int(edgeflag))
        kwargs['label'] = label_text

        track = Track(img, **kwargs)

        return track

    @classmethod
    def from_pydict(cls, read_dict, pydict_to_pyobj=None):
        """
        Initialize a Track object from the dictionary returned by
        trackio.read_object_from_hdf5().
        """

        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        if id(read_dict) in pydict_to_pyobj:
            return pydict_to_pyobj[id(read_dict)]

        other_attrs = ('image', 'algorithms')
        all_attrs = other_attrs + cls.attr_list

        kwargs = {}
        # keep algorithms in a separate dict because they do not go in __init__
        algorithms = {}
        for attr in all_attrs:
            if attr == 'g4track' and read_dict.get(attr) is not None:
                kwargs[attr] = G4Track.from_pydict(
                    read_dict[attr], pydict_to_pyobj=pydict_to_pyobj)
                # if g4track *is* None, then it gets assigned in "else" below
            elif attr == 'algorithms':
                if read_dict.get(attr) is not None:
                    for key, val in read_dict[attr].iteritems():
                        algorithms[key] = AlgorithmOutput.from_pydict(
                            read_dict[attr][key],
                            pydict_to_pyobj=pydict_to_pyobj)
                # else, algorithms is still {} as it should be
            else:
                kwargs[attr] = read_dict.get(attr)
            # read_dict.get() defaults to None, although this actually
            #   shouldn't be needed since read_object_from_hdf5 adds Nones

        image = kwargs.pop('image')
        constructed_object = cls(image, **kwargs)
        for key, algoutput in algorithms.iteritems():
            alpha = algoutput.alpha_deg
            beta = algoutput.beta_deg
            info = algoutput.info
            constructed_object.add_algorithm(key, alpha, beta, info)

        # add entry to pydict_to_pyobj
        pydict_to_pyobj[id(read_dict)] = constructed_object

        return constructed_object

    @classmethod
    def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None):
        """
        Initialize a Track instance from an HDF5 group.
        """

        if h5_to_pydict is None:
            h5_to_pydict = {}
        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        read_dict = trackio.read_object_from_hdf5(h5group,
                                                  h5_to_pydict=h5_to_pydict)

        constructed_object = cls.from_pydict(read_dict,
                                             pydict_to_pyobj=pydict_to_pyobj)

        return constructed_object

    @classmethod
    def generate_random(cls,
                        size=(10, 10),
                        alg_name=None,
                        a_fwhm=30,
                        a_f=0.7,
                        b_rms=20,
                        b_f=0.5):
        """
        Generate a random image. is_modeled = True.

        size: 2x1 tuple indicating dimensions of image
        alg_name: if this is a string, generate a random alpha and beta and
          assign as algorithm results to this alg_name.
          The distribution of dalpha and dbeta are controlled by
          a_fwhm, a_f, b_rms, b_f, as in evaluation.generate_random_alg_results
        """

        img = np.random.random(size=size)
        a0 = np.random.uniform(-180.0, 180.0)
        b0 = np.random.uniform(-90.0, 90.0)
        g4t = G4Track(alpha_deg=a0, beta_deg=b0)
        t = Track(img, is_modeled=True, g4track=g4t)

        if alg_name is not None and isinstance(alg_name, str):
            # alpha
            if np.random.random() < a_f:
                a1 = a0 + np.random.normal(loc=0.0, scale=(a_fwhm) / 2.355)
            else:
                a1 = np.random.uniform(-180.0, 180.0)

            # beta
            if np.random.random() < b_f:
                b1 = b0 + np.random.normal(loc=0.0, scale=b_rms)
                if b1 < 0:
                    b1 = 0
                elif b1 > 90:
                    b1 = 89.9
            else:
                b1 = 0

            t.add_algorithm(alg_name, a1, b1)

        return t

    def add_algorithm(self, alg_name, alpha_deg, beta_deg, info=None):
        """
        """

        if alg_name in self.algorithms:
            raise InputError(alg_name + " already in algorithms")
        self.algorithms[alg_name] = AlgorithmOutput(alg_name,
                                                    alpha_deg,
                                                    beta_deg,
                                                    info=info)

    def list_algorithms(self):
        """
        List AlgorithmOutput objects attached to this track.

        Like dict.keys()
        """

        return self.algorithms.keys()

    def keys(self):
        """
        Allow dictionary-like behavior on Track object for its attached
        algorithms.
        """

        return self.list_algorithms()

    def __getitem__(self, key):
        # Map dictionary lookup to algorithms dictionary.
        return self.algorithms[key]

    def __contains__(self, item):
        # Map dictionary lookup to algorithms dictionary.
        return item in self.algorithms
Example #9
0
class G4Track(object):
    """
    Electron track from Geant4.
    """

    __version__ = '0.2'
    class_name = 'G4Track'
    data_format = dataformats.get_format(class_name)

    # a lot more attributes could be added here...
    attr_list = (
        'x',
        'dE',
        'x0',
        'alpha_deg',
        'beta_deg',
        'first_step_vector',
        'energy_tot_kev',
        'energy_dep_kev',
        'energy_esc_kev',
        'energy_xray_kev',
        'energy_brems_kev',
        'depth_um',
        'is_contained',
    )

    def __init__(self, matrix=None, **kwargs):
        """
        Construct G4Track object.

        If matrix is supplied and other quantities are not, then the other
        quantities will be calculated using the matrix (not implemented yet).

          matrix
          (see attr_list class variable)
        """

        self.matrix = matrix

        for attr in self.attr_list:
            if attr in kwargs:
                setattr(self, attr, kwargs[attr])
            else:
                setattr(self, attr, None)

        if matrix is not None and ('x' not in kwargs or 'dE' not in kwargs
                                   or 'energy_tot_kev' not in kwargs
                                   or 'energy_dep_kev' not in kwargs
                                   or 'energy_esc_kev' not in kwargs
                                   or 'depth_um' not in kwargs
                                   or 'is_contained' not in kwargs):
            pass
            # self.measure_quantities()

    @classmethod
    def from_h5matlab(cls, evt):
        """
        Construct a G4Track instance from an event in an HDF5 file.

        The format of the HDF5 file is 'matlab', a.k.a. the more complete mess
        that Brian made in December 2015.
        """

        if evt.attrs['multiplicity'] > 1:
            # at this time, I am not handling multiple-scattered photons
            return None

        data = evt['trackM']
        matrix = np.zeros(data.shape)
        data.read_direct(matrix)
        matrix = np.array(matrix)

        cheat = evt['cheat']['0']

        try:
            # h5 attributes
            kwargs = {
                'energy_tot_kev': float(cheat.attrs['Etot']),
                'energy_dep_kev': float(cheat.attrs['Edep']),
                'energy_esc_kev': float(evt.attrs['Eesc']),
                'energy_xray_kev': float(cheat.attrs['Exray']),
                'energy_brems_kev': float(cheat.attrs['Ebrems']),
                'x0': cheat.attrs['x0'],
                'first_step_vector': cheat.attrs['firstStepVector'],
                'alpha_deg': float(cheat.attrs['alpha']),
                'beta_deg': float(cheat.attrs['beta'])
            }
        except KeyError:
            # found this in HTbatch01_h5m/MultiAngle_100_6.h5 track 117
            print('Unexpected missing attributes in ' + evt.name + '!')
            return None

        try:
            # h5 datasets
            data = cheat['x']
            x = np.zeros(data.shape)
            data.read_direct(x)
            kwargs['x'] = x

            data = cheat['dE']
            dE = np.zeros(data.shape)
            data.read_direct(dE)
            kwargs['dE'] = dE
        except KeyError:
            # hypothetical
            print('Unexpected missing dataset in ' + evt.name + '!')
            return None

        g4track = G4Track(matrix=matrix, **kwargs)
        return g4track

    @classmethod
    def from_dth5(cls, evt):
        """
        Construct a G4Track instance from an event in an HDF5 file.

        The format is that of files from DT_to_hdf5.m / write_DT_hdf5.m.
        evt is the h5py group corresponding to an event.
        """

        # G4Track comes from the 'cheat' subgroup, which is unchanged from
        #   h5matlab.
        return cls.from_h5matlab(evt)

    @classmethod
    def from_pydict(cls, read_dict, pydict_to_pyobj=None):
        """
        Initialize a G4Track object from the dictionary returned by
        trackio.read_object_from_hdf5().
        """

        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        if id(read_dict) in pydict_to_pyobj:
            return pydict_to_pyobj[id(read_dict)]

        other_attrs = ('matrix', )
        all_attrs = other_attrs + cls.attr_list

        kwargs = {}
        for attr in all_attrs:
            kwargs[attr] = read_dict.get(attr)
            # read_dict.get() defaults to None, although this actually
            #   shouldn't be needed since read_object_from_hdf5 adds Nones
        constructed_object = cls(**kwargs)

        # add entry to pydict_to_pyobj
        pydict_to_pyobj[id(read_dict)] = constructed_object

        return constructed_object

    @classmethod
    def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None):
        """
        Initialize a G4Track instance from an HDF5 group.
        """

        if h5_to_pydict is None:
            h5_to_pydict = {}
        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        read_dict = trackio.read_object_from_hdf5(h5group,
                                                  h5_to_pydict=h5_to_pydict)

        constructed_object = cls.from_pydict(read_dict,
                                             pydict_to_pyobj=pydict_to_pyobj)

        return constructed_object

    def measure_quantities(self):
        """
        Measure the following using the Geant4 matrix.

          alpha
          beta
          energy_tot
          energy_dep
          energy_esc
          x
          dE
          depth_um
          is_contained
        """

        # TODO
        raise NotImplementedError("haven't written this yet")

        if self.matrix is None:
            raise DataError('measure_quantities needs a geant4 matrix')
Example #10
0
class Classifier(object):
    """
    Object to handle auto classifying (ground truth) a Monte Carlo track
    """

    class_name = 'Classifier'
    data_format = df.get_format(class_name)

    def __init__(self, g4track, suppress_check=False):
        assert isinstance(g4track, G4Track), "Not a G4Track object"
        assert g4track.x.shape[0] == 3, "x has funny shape"
        self.x = np.copy(g4track.x)
        self.E = np.copy(g4track.dE.flatten())
        self.g4track = g4track

        # avoid IO errors in case of only mc_classify and not end_classify
        # or for errors in test_moments.classifierlist_from_tracklist

        self.scatterlen_um = None
        self.overlapdist_um = None
        self.scatter_type = None
        self.use2d_angle = None
        self.use2d_dist = None
        self.angle_threshold_deg = None
        self.escaped = None
        self.early_scatter = None
        self.total_scatter_angle = None
        self.overlap = None

        self.wrong_end = None
        self.n_ends = None
        self.max_end_energy = None
        self.min_end_energy = None

        self.error = None

    @classmethod
    def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None,
                  reconstruct=False):
        """
        Initialize a Classifier object from an HDF5 group.
        """

        if h5_to_pydict is None:
            h5_to_pydict = {}
        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        read_dict = trackio.read_object_from_hdf5(
            h5group, h5_to_pydict=h5_to_pydict)

        constructed_object = cls.from_pydict(
            read_dict, pydict_to_pyobj=pydict_to_pyobj,
            reconstruct=reconstruct)

        return constructed_object

    @classmethod
    def from_pydict(cls, read_dict, pydict_to_pyobj=None, reconstruct=False):
        """
        Initialize a Classifier object from a pydict.
        """

        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        if id(read_dict) in pydict_to_pyobj:
            return pydict_to_pyobj[id(read_dict)]

        # first, reconstruct g4track (if needed)
        if isinstance(read_dict['g4track'], G4Track):
            # g4track is already a G4Track object (not sure how)
            g4track = read_dict['g4track']
        elif (isinstance(read_dict['g4track'], dict) and
                id(read_dict['g4track']) in pydict_to_pyobj):
            # g4track is in the pydict table
            g4track = pydict_to_pyobj[id(read_dict['g4track'])]
        elif isinstance(read_dict['g4track'], dict):
            # g4track not in the pydict table. create and add it
            g4track = G4Track.from_pydict(
                read_dict['g4track'], pydict_to_pyobj=pydict_to_pyobj)
            pydict_to_pyobj[id(read_dict['g4track'])] = g4track
        else:
            raise Exception("Unexpected or missing 'g4track' in Classifier")

        new_obj = cls(g4track)

        # add entry to pydict_to_pyobj
        pydict_to_pyobj[id(read_dict)] = new_obj

        # fill in outputs
        if read_dict['error'] is not None:
            new_obj.error = read_dict['error']
        if read_dict['scatterlen_um'] is not None:
            # mc_classify ran
            new_obj.scatterlen_um = read_dict['scatterlen_um']
            new_obj.overlapdist_um = read_dict['overlapdist_um']
            new_obj.escaped = read_dict['escaped']
            new_obj.scatter_type = read_dict['scatter_type']
            new_obj.use2d_angle = read_dict['use2d_angle']
            new_obj.use2d_dist = read_dict['use2d_dist']
            new_obj.angle_threshold_deg = read_dict['angle_threshold_deg']
        if read_dict['early_scatter'] is not None:
            # no TrackTooShortError
            new_obj.early_scatter = read_dict['early_scatter']
            new_obj.total_scatter_angle = read_dict['total_scatter_angle']
            new_obj.overlap = read_dict['overlap']
        if read_dict['wrong_end'] is not None:
            # end_classify ran
            new_obj.wrong_end = read_dict['wrong_end']
            new_obj.n_ends = read_dict['n_ends']
            new_obj.max_end_energy = read_dict['max_end_energy']
            new_obj.min_end_energy = read_dict['min_end_energy']

        if reconstruct:
            new_obj.mc_classify()
            print(
                'Reconstructing Classifier.end_classify() not implemented yet')

        return new_obj

    def mc_classify(self, scatterlen_um=25, overlapdist_um=40, verbose=False):
        """
        Classify the Monte Carlo track as either:
          'good',
          early scatter 'scatter',
          overlapping the initial end 'overlap',
          or no result, None.

        Optional input args:
          scatterlen_um: length from initial end, in um, to look for
            high-angle scatter. (default 50 um)
          overlapdist_um: if points are
        """

        self.scatterlen_um = scatterlen_um
        self.overlapdist_um = overlapdist_um

        self.check_escape()

        self.flag_newparticle()

        self.check_early_scatter(v=verbose)
        self.check_overlap()

    def end_classify(self, track, mom=None, HT=None):
        """
        Look at the end segment selection algorithm results, and classify.
        """

        self.get_end_info(track, mom=mom, HT=HT)
        self.check_wrong_end(track, mom=mom, HT=HT)

    def get_end_info(self, track, mom=None, HT=None):
        """
        Record number of ends, minimum end energy (this is the chosen end),
        maximum end energy (may indicate escape).
        """

        if mom is not None:
            ends_energy = mom.info.ends_energy
        elif HT is not None:
            ends_energy = HT.info.ends_energy

        self.n_ends = len(ends_energy)
        if self.n_ends == 0:
            raise NoEnds('Cannot get max and min end energy')
        self.max_end_energy = np.max(ends_energy)
        self.min_end_energy = np.min(ends_energy)

    def check_wrong_end(self, track, mom=None, HT=None, maxdist=3):
        """
        See if the algorithm's end segment was correct or not.
        """

        if mom is None and HT is None:
            raise ValueError(
                'Requires either a moments object or a HybridTrack object')

        g4xfull, g4yfull = tp.get_image_xy(track)
        g4x, g4y = g4xfull[0], g4yfull[0]

        # could throw an AttributeError if there were errors in the algorithm
        if mom:
            algx, algy = mom.start_coordinates
        elif HT:
            algx, algy = HT.start_coordinates
        else:
            raise ValueError('bad value in moments or HybridTrack object')

        dist = np.sqrt((algx - g4x)**2 + (algy - g4y)**2)
        self.end_distance = dist
        if dist > maxdist:
            self.wrong_end = True
        else:
            self.wrong_end = False

        self.g4xy = g4x, g4y
        self.algxy = algx, algy

    def check_escape(self):
        """
        Check the Etot and Edep to see if track escaped. (>2 keV difference)
        Unfortunately the g4track.is_contained flag is None in this dataset.
        """

        energy_diff_kev = (
            self.g4track.energy_tot_kev - self.g4track.energy_dep_kev)
        self.escaped = (energy_diff_kev > 2.0)

    def flag_newparticle(self):
        """
        Look for particle transitions - jumping to a new electron ID in Geant4.

        Marked by >1.5um step.
        """

        if not hasattr(self, 'd'):
            self.dx = self.x[:, 1:] - self.x[:, :-1]
            self.d = np.linalg.norm(self.dx, axis=0)

        self.dx_newparticle_flag = (self.d > BIG_STEP_UM * 1.5)

    def check_early_scatter(self, v=False, scatter_type='total',
                            use2d_angle=True, use2d_dist=True,
                            angle_threshold_deg=30):
        """
        look for a >30 degree direction change within the first scatterlen_um
        of track.

        v: verbosity (True: print "Early scatter!")
        scatter_type:
          'total': compare direction at end of segment, to beginning of segment
          'discrete': look for a single scattering of more than angle
        angle_threshold_deg: threshold angle. scattering through more than
          this angle is flagged.
        use2d_angle: flag for looking at the scatter angle in the 2D plane
        use2d_dist: flag for measuring scatterlen along the track projection
        """

        angle_threshold_rad = np.float(angle_threshold_deg) / 180 * np.pi
        angle_threshold_cos = np.cos(angle_threshold_rad)

        self.scatter_type = scatter_type
        self.use2d_angle = use2d_angle
        self.use2d_dist = use2d_dist
        self.angle_threshold_deg = angle_threshold_deg

        # use only every other point, so as to bypass the zigzag issue.
        # x2: positions (every other point)
        self.x2 = self.x[:, ::2]
        # dx2: delta-position between each entry in x2
        self.dx2 = self.x2[:, 1:] - self.x2[:, :-1]

        # integrate the path length, either in 2D or 3D, to determine cutoff
        #   for scatterlen
        # d2: dx2 integrated to get path length
        # d2_2d: dx2 integrated to get path length, in 2D
        if use2d_dist:
            self.d2_2d = np.linalg.norm(self.dx2[:2, :], axis=0)
            integrated_dist = np.cumsum(self.d2_2d)
        else:
            self.d2 = np.linalg.norm(self.dx2, axis=0)
            integrated_dist = np.cumsum(self.d2)
        # ind2: the index where path length (2D or 3D) exceeds scatterlen
        # short tracks raise an IndexError here
        try:
            ind2 = np.nonzero(integrated_dist >= self.scatterlen_um)[0][0] - 1
        except IndexError:
            raise TrackTooShortError()

        # trim x2 and dx2
        self.x2 = self.x2[:, :ind2]
        self.dx2 = self.dx2[:, :ind2]

        # dx2norm: unit vectors of dx2
        self.dx2norm = self.normalize_steps(self.dx2)
        # ddir2: dot product of consecutive dx2norm's. =cos(theta)
        self.ddir2 = np.sum(
            self.dx2norm[:, 1:] * self.dx2norm[:, :-1], axis=0)

        # dx2norm_2d: 2D unit vectors of dx2
        self.dx2norm_2d = self.dx2[:2, :] / np.linalg.norm(
            self.dx2[:2, :], axis=0)
        # ddir2_2d: dot product of consecutive dx2norm_2d's. =cos(theta)
        self.ddir2_2d = np.sum(
            self.dx2norm_2d[:, 1:] * self.dx2norm_2d[:, :-1], axis=0)

        # for discrete scatters, look for large angles in dx2norm or dx2norm_2d
        # for total scatter angle, dot the unit vector with the initial
        #   direction
        if scatter_type.lower() == 'total':
            # ddir: the unit vector at each step,
            #   dotted with the initial unit vector, to get angle of deviation
            # take the maximum from along scatterlen
            if use2d_angle:
                ddir = np.array([
                    np.sum(self.dx2norm_2d[:, 0] * self.dx2norm_2d[:, i])
                    for i in xrange(1, self.dx2norm_2d.shape[1])])
            else:
                ddir = np.array([
                    np.sum(self.dx2norm[:, 0] * self.dx2norm[:, i])
                    for i in xrange(1, self.dx2norm.shape[1])])
            self.early_scatter = (np.min(ddir) < angle_threshold_cos)
            self.total_scatter_angle = np.arccos(np.min(ddir))
        elif scatter_type.lower() == 'discrete':
            if use2d_angle:
                self.early_scatter = np.any(self.ddir2 < angle_threshold_cos)
            else:
                self.early_scatter = np.any(self.ddir2 < angle_threshold_cos)
        else:
            raise ValueError(
                'scatter_type {} not recognized!'.format(scatter_type))

        if self.early_scatter and v:
            print('Early scatter!')

    def check_overlap(self):
        """
        look for a section of track overlapping with the initial scatterlen_um
        of track.
        """

        self.overlap = False    # until shown otherwise
        self.scatterlen_steps = self.scatterlen_um / BIG_STEP_UM * 2

        # see which points are within overlapdist_um of these points
        # distance matrix: distance of all points from each of the first 50

        # arrays of dimensions (self.x.shape[1], self.numsteps)
        all_x, init_x = np.meshgrid(
            self.x[0, :self.scatterlen_steps], self.x[0, :])
        all_y, init_y = np.meshgrid(
            self.x[1, :self.scatterlen_steps], self.x[1, :])

        dist_matrix = (all_x - init_x)**2 + (all_y - init_y)**2
        dist_vector = np.min(dist_matrix, axis=1)

        # don't bother with sqrt
        dist_threshold = self.overlapdist_um**2
        too_close = (dist_vector < dist_threshold) + 0  # as an int

        # the initial segment, and some points after it, are obviously
        # going to be too close.

        # first try: if at least overlapdist of consecutive points are
        # too_close, then classify the track as overlapping
        #   i.e. at least overlapdist consecutive points are within overlapdist
        #   of the initial scatterlen segment.

        # look for transitions from too_close to not too_close, and back
        dclose = too_close[1:] - too_close[:-1]

        # get indices of transitions.
        # ignore first transition away from initial segment
        going_out = np.nonzero(dclose == -1)[0]    # too_close to not too_close
        going_out = going_out[1:]
        going_in = np.nonzero(dclose == 1)[0]      # not too_close to too_close

        # get the lengths of too_close segments
        segment_len_threshold = self.overlapdist_um / BIG_STEP_UM * 2
        for i, in_ind in enumerate(going_in):
            try:
                out_ind = going_out[i]
            except IndexError:
                out_ind = len(dclose)
            if out_ind - in_ind > segment_len_threshold:
                self.overlap = True
                break

    def normalize_steps(self, dx, d=None):
        """
        normalize steps into unit vectors
        dx.shape should be (3, n)
        """

        assert dx.shape[0] == 3, "dx has funny shape in normalize_steps"
        if d is None:
            d = np.linalg.norm(dx, axis=0)

        norm_dx = dx / d
        return norm_dx

    def round_steplength(self, d):
        """
        round to nearest half-big-step (nearest 0.5 um)
        """
        return np.round(d / (BIG_STEP_UM / 2)) * (BIG_STEP_UM / 2)
Example #11
0
class MomentsReconstruction(object):

    class_name = 'MomentsReconstruction'
    data_format = df.get_format(class_name)

    def __init__(self,
                 original_image_kev,
                 pixel_size_um=10.5,
                 starting_distance_um=63):
        """
        Init: Load options only
        """

        self.original_image_kev = original_image_kev
        self.pixel_size_um = pixel_size_um
        self.options = hybridtrack.ReconstructionOptions(pixel_size_um)
        # increase walking distance from 4 pixels to 6 pixels - for now
        # hybridtrack default: 40 um
        # initial testing before 6/21/16: 63 um
        self.starting_distance_um = starting_distance_um
        self.options.ridge_starting_distance_from_track_end_um = (
            starting_distance_um)

        self.info = hybridtrack.ReconstructionInfo()

        # for writing to file - these attributes must exist
        self.edge_pixel_count = None
        self.edge_pixel_segments = None
        self.phi = None
        self.R = None
        self.alpha = None
        self.x0 = None
        self.error = None

    @classmethod
    def from_hdf5(cls,
                  h5group,
                  h5_to_pydict=None,
                  pydict_to_pyobj=None,
                  reconstruct=False):
        """
        Initialize a MomentsReconstruction object from an HDF5 group.
        """

        if h5_to_pydict is None:
            h5_to_pydict = {}
        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        read_dict = trackio.read_object_from_hdf5(h5group,
                                                  h5_to_pydict=h5_to_pydict)

        constructed_object = cls.from_pydict(read_dict,
                                             pydict_to_pyobj=pydict_to_pyobj,
                                             reconstruct=reconstruct)

        return constructed_object

    @classmethod
    def from_pydict(cls, read_dict, pydict_to_pyobj=None, reconstruct=False):
        """
        Initialize a MomentsReconstruction object from a pydict.
        """

        if pydict_to_pyobj is None:
            pydict_to_pyobj = {}

        if id(read_dict) in pydict_to_pyobj:
            return pydict_to_pyobj[id(read_dict)]

        new_obj = cls(read_dict['original_image_kev'],
                      pixel_size_um=read_dict['pixel_size_um'],
                      starting_distance_um=read_dict['starting_distance_um'])
        # fill in outputs
        new_obj.error = read_dict['error']
        if read_dict['ends_energy'] is not None:
            # (actually required by the data_formats class)
            new_obj.ends_energy = read_dict['ends_energy']
            new_obj.rough_est = read_dict['rough_est']
            new_obj.box_x = read_dict['box_x']
            new_obj.box_y = read_dict['box_y']
        if read_dict['edge_pixel_count'] is not None:
            # successfully got a segment image
            new_obj.edge_pixel_count = read_dict['edge_pixel_count']
            new_obj.edge_pixel_segments = read_dict['edge_pixel_segments']
        if read_dict['alpha'] is not None:
            # actually, even if calculation is pathological, these are np.nan
            # so they still get assigned.
            new_obj.phi = read_dict['phi']
            new_obj.R = read_dict['R']
            new_obj.alpha = read_dict['alpha']
            new_obj.x0 = read_dict['x0']

        # add entry to pydict_to_pyobj
        pydict_to_pyobj[id(read_dict)] = new_obj

        if reconstruct:
            new_obj.reconstruct()

        return new_obj

    def reconstruct(self):

        hybridtrack.choose_initial_end(self.original_image_kev, self.options,
                                       self.info)

        try:
            self.segment_initial_end()
            # get a sub-image containing the initial end
            # also need a rough estimate of the electron direction
            #   (using thinned)
        except CheckSegmentBoxError:
            self.error = 'CheckSegmentBoxError'
            return
        except RuntimeError:
            self.error = 'what the heck happened?'
            return

        # 1.
        self.get_coordlist()
        self.compute_first_moments()
        # 2ab.
        self.compute_central_moments()
        # 3ab.
        try:
            self.compute_optimal_rotation_angle()
        except MomentsError:
            self.error = 'Rotation angle conditions not met'
            return
        #  4.
        self.compute_arc_parameters()

        self.compute_direction()

        self.compute_pathology()

    @classmethod
    def from_end_segment(cls, end_segment_image, rough_est):
        """
        Run the moments on a (handpicked) track section.
        """

        mom = cls(None)
        mom.end_segment_image = end_segment_image
        mom.rough_est = rough_est

        mom.get_coordlist()
        mom.compute_first_moments()
        mom.compute_central_moments()
        mom.compute_optimal_rotation_angle()
        mom.compute_arc_parameters()
        mom.compute_direction()
        mom.compute_pathology()

        return mom

    @classmethod
    def reconstruct_arc(cls, clist, rough_est):
        """
        Run the moments on a CoordinatesList object, e.g. from generate_arc().

        Need to also provide a rough_est
        """

        mom = cls(None)
        mom.clist0 = clist
        mom.rough_est = rough_est

        mom.compute_first_moments()
        mom.compute_central_moments()
        mom.compute_optimal_rotation_angle()
        mom.compute_arc_parameters()
        mom.compute_direction()

        return mom

    def get_segment_initial_values(self):
        """
        Get start_coordinates, end_coordinates, and rough_est for segmenting.
        """

        # copied from hybridtrack.get_starting_point()
        self.ends_energy = self.info.ends_energy
        min_index = self.ends_energy.argmin()
        self.end_energy = self.ends_energy[min_index]
        self.start_coordinates = self.info.ends_xy[min_index]
        # start_coordinates are the end (extremity) of the thinned track

        self.end_coordinates = self.info.start_coordinates
        # end_coordinates are after walking up the track 40 um

        # angle from start to end
        dcoord = self.end_coordinates - self.start_coordinates
        self.rough_est = np.arctan2(dcoord[1], dcoord[0])

    def get_segment_box(self):
        """
        Get the x,y coordinates of the box containing the initial segment.
        """

        segwid = 10  # pixels
        seglen = 11  # pixels

        mod = self.rough_est % (np.pi / 2)
        if mod < np.pi / 6 or mod > np.pi / 3:
            # close enough to orthogonal
            general_dir = np.round(self.rough_est / (np.pi / 2)) * (np.pi / 2)
            self.is45 = False
        else:
            # use a diagonal box (aligned to 45 degrees)
            general_dir = np.round(self.rough_est / (np.pi / 4)) * (np.pi / 4)
            self.is45 = True

        # make box and rotate
        box_dx = np.array([0, -seglen, -seglen, 0, 0])
        box_dy = np.array(
            [-segwid / 2, -segwid / 2, segwid / 2, segwid / 2, -segwid / 2])
        box_dx_rot = (np.round(box_dx * np.cos(general_dir)) -
                      np.round(box_dy * np.sin(general_dir))).astype(int)
        box_dy_rot = (np.round(box_dx * np.sin(general_dir)) +
                      np.round(box_dy * np.cos(general_dir))).astype(int)
        self.box_x = self.end_coordinates[0] + box_dx_rot
        self.box_y = self.end_coordinates[1] + box_dy_rot

    def check_segment_box(self):
        """
        Check whether the box intersects too many hot pixels.
        That would indicate that we should draw a new box.
        """

        problem_length = 60  # microns

        # xmesh, ymesh get used in get_pixlist, also. so save into self.
        img_shape = self.original_image_kev.shape
        self.xmesh, self.ymesh = np.meshgrid(range(img_shape[0]),
                                             range(img_shape[1]),
                                             indexing='ij')
        # get the pixels along the line segment that passes through the track,
        #   by walking along from one endpoint toward the other.
        xcheck = [self.box_x[-2]]
        ycheck = [self.box_y[-2]]
        dx = np.sign(self.box_x[-1] - self.box_x[-2])
        dy = np.sign(self.box_y[-1] - self.box_y[-2])
        while xcheck[-1] != self.box_x[-1] or ycheck[-1] != self.box_y[-1]:
            xcheck.append(xcheck[-1] + dx)
            ycheck.append(ycheck[-1] + dy)
        xcheck.append(self.box_x[-1])
        ycheck.append(self.box_y[-1])
        xcheck = np.array(xcheck)
        ycheck = np.array(ycheck)
        lgbad = ((xcheck < 0) | (xcheck >= self.original_image_kev.shape[0]) |
                 (ycheck < 0) | (ycheck >= self.original_image_kev.shape[1]))
        xcheck = xcheck[np.logical_not(lgbad)]
        ycheck = ycheck[np.logical_not(lgbad)]

        # threshold from HybridTrack options
        low_threshold_kev = self.options.low_threshold_kev

        # see what pixels are over the threshold.
        over_thresh = np.array([
            self.original_image_kev[xcheck[i], ycheck[i]] > low_threshold_kev
            for i in xrange(len(xcheck))
        ])
        # in order to avoid counting pixels from a separate segment,
        #   start from end_coordinates and count outward until you hit a 0.
        over_thresh_pix = 1
        start_ind = np.nonzero((xcheck == self.end_coordinates[0])
                               & (ycheck == self.end_coordinates[1]))[0][0]
        # +dx, +dy side (start_ind+1 --> end):
        for i in xrange(start_ind + 1, len(xcheck), 1):
            if over_thresh[i]:
                over_thresh_pix += 1
            else:
                break
        # -dx, -dy side (start_ind-1 --> 0):
        for i in xrange(start_ind - 1, -1, -1):
            if over_thresh[i]:
                over_thresh_pix += 1
            else:
                break

        over_thresh_length = (over_thresh_pix * self.options.pixel_size_um *
                              np.sqrt(dx**2 + dy**2))

        if over_thresh_length > problem_length:
            # have we done this too much already?
            if self.options.ridge_starting_distance_from_track_end_um < 30:
                raise CheckSegmentBoxError("Couldn't get a clean end segment")

            # try again, with a shorter track segment
            self.options.ridge_starting_distance_from_track_end_um -= 10.5
            # now, repeat what we've done so far
            hybridtrack.choose_initial_end(self.original_image_kev,
                                           self.options, self.info)
            self.get_segment_initial_values()
            self.get_segment_box()
            self.check_segment_box()
            # recurse until ridge_starting_dist... < 30

    def get_pixlist(self):
        """
        get list of pixels that are inside the box (and within image bounds)
        """

        # logical array representing pixels in the segment
        if not self.is45:
            segment_lg = ((self.xmesh >= np.min(self.box_x)) &
                          (self.xmesh <= np.max(self.box_x)) &
                          (self.ymesh >= np.min(self.box_y)) &
                          (self.ymesh <= np.max(self.box_y)))
        else:
            # need to compose the lines which form bounding box
            pairs = ((0, 1), (1, 2), (2, 3), (3, 0))
            m = np.zeros(4)
            b = np.zeros(4)
            for i in xrange(len(pairs)):
                # generate the m, b for y = mx+b for line connecting this
                #   pair of points
                m[i] = ((self.box_y[pairs[i][1]] - self.box_y[pairs[i][0]]) /
                        (self.box_x[pairs[i][1]] - self.box_x[pairs[i][0]]))
                b[i] = self.box_y[pairs[i][0]] - m[i] * self.box_x[pairs[i][0]]
            # m should be alternating sign... (this is for testing)
            assert m[0] * m[1] == -1
            assert m[1] * m[2] == -1
            assert m[2] * m[3] == -1
            assert m[3] * m[0] == -1
            min_ind = np.zeros(2)
            max_ind = np.zeros(2)
            if b[0] < b[2]:
                min_ind[0] = 0
                max_ind[0] = 2
            else:
                min_ind[0] = 2
                max_ind[0] = 0
            if b[1] < b[3]:
                min_ind[1] = 1
                max_ind[1] = 3
            else:
                min_ind[1] = 3
                max_ind[1] = 1
            segment_lg = (
                (self.ymesh >= m[min_ind[0]] * self.xmesh + b[min_ind[0]]) &
                (self.ymesh >= m[min_ind[1]] * self.xmesh + b[min_ind[1]]) &
                (self.ymesh <= m[max_ind[0]] * self.xmesh + b[max_ind[0]]) &
                (self.ymesh <= m[max_ind[1]] * self.xmesh + b[max_ind[1]]))

        xpix = self.xmesh[segment_lg]
        ypix = self.ymesh[segment_lg]

        self.xpix = xpix
        self.ypix = ypix
        self.segment_lg = segment_lg

    def get_segment_image(self):
        """
        Produce the actual segment image using xpix and ypix
        """

        # initialize segment image
        min_x = np.min(self.xpix)
        max_x = np.max(self.xpix)
        min_y = np.min(self.ypix)
        max_y = np.max(self.ypix)
        seg_img = np.zeros((max_x - min_x + 1, max_y - min_y + 1))

        # fill segment image
        for i in xrange(len(self.xpix)):
            xi = self.xpix[i] - min_x
            yi = self.ypix[i] - min_y
            seg_img[xi, yi] = self.original_image_kev[self.xpix[i],
                                                      self.ypix[i]]

        self.end_segment_image = seg_img
        self.end_segment_offsets = np.array([min_x, min_y])

    def separate_segments(self):
        """
        Perform image segmentation on the "segment image", and remove any
        segments that aren't the right part of the track.
        """

        # binary image
        binary_segment_image = (self.end_segment_image >
                                self.options.low_threshold_kev)
        # segmentation: labeled regions, 8-connectivity
        labels = morph.label(binary_segment_image, connectivity=2)
        x1 = self.end_coordinates[0] - self.end_segment_offsets[0]
        y1 = self.end_coordinates[1] - self.end_segment_offsets[1]
        x2 = self.start_coordinates[0] - self.end_segment_offsets[0]
        y2 = self.start_coordinates[1] - self.end_segment_offsets[1]
        chosen_label = labels[x1, y1]
        if labels[x2, y2] != chosen_label:
            # this happens with 4-connectivity. need to use 8-connectivity
            raise RuntimeError('What the heck happened?')
        binary_again = (labels == chosen_label)
        # dilate this region, in order to capture information below threshold
        #  (it won't include the other regions, because there must be a gap
        #   between)
        pix_to_keep = morph.binary_dilation(binary_again)
        self.end_segment_image[np.logical_not(pix_to_keep)] = 0

    def check_segment_indicators(self):
        """
        Check the number of pixels above threshold along the edge of the
        segment image. Also measure number of separate segments of pixels
        along the edge of the segment image.
        """
        x1 = self.end_segment_offsets[0]
        x2 = x1 + self.end_segment_image.shape[0]
        y1 = self.end_segment_offsets[1]
        y2 = y1 + self.end_segment_image.shape[1]
        segment_lg = self.segment_lg[x1:x2, y1:y2]  # in end segment coords
        edge_pixels = segment_lg - morph.binary_erosion(segment_lg)

        # edge_pixel_count and edge_pixel_segments
        edge_pixels_over_thresh_image = np.zeros_like(self.end_segment_image)
        edge_pixels_over_thresh_image[edge_pixels] = (
            self.end_segment_image[edge_pixels] >
            self.options.low_threshold_kev)
        self.edge_pixel_count = np.sum(edge_pixels_over_thresh_image).astype(
            int)
        _, self.edge_pixel_segments = morph.label(
            edge_pixels_over_thresh_image, connectivity=2, return_num=True)

        # edge_avg_dist
        edge_values_image = np.zeros_like(self.end_segment_image)
        edge_values_image[edge_pixels] = self.end_segment_image[edge_pixels]
        max_edge_ind_flat = np.argmax(edge_values_image)
        max_coords = np.unravel_index(max_edge_ind_flat,
                                      edge_values_image.shape)
        # debug
        assert edge_values_image[max_coords[0],
                                 max_coords[1]] == np.max(edge_values_image)

        dist_sum = 0
        x, y = np.nonzero(edge_pixels)
        for i in xrange(len(x)):
            dist_sum += (self.end_segment_image[x[i], y[i]] *
                         np.sqrt((max_coords[0] - x[i])**2 +
                                 (max_coords[1] - y[i])**2))
        self.edge_avg_dist = dist_sum / np.sum(edge_values_image)

    def segment_initial_end(self):
        """
        Get the image segment to use for moments, and the rough direction
        estimate.

        Calls get_segment_box(), get_pixlist(), get_segment_image().
        """

        self.get_segment_initial_values()
        self.get_segment_box()
        self.check_segment_box()
        self.get_pixlist()
        self.get_segment_image()
        self.separate_segments()
        self.check_segment_indicators()

        def end_segment_coords_to_full_image_coords(xy):
            """
            Convert x,y from the coordinate frame of the end segment image
            to the coordinate frame of the full image
            """
            x, y = xy_split(xy)
            return np.array([
                x + self.end_segment_offsets[0],
                y + self.end_segment_offsets[1]
            ])

        self.segment_to_full = end_segment_coords_to_full_image_coords

    @classmethod
    def get_base_diagonal_pixlist(cls, diag_hw, diag_len):
        """
        Get the diagonal pixel list for 45 degrees.
        (To be rotated to other angles)
        """

        xlist = []
        ylist = []

        # major diagonals
        xy0 = np.array([0, 0])
        xy1 = np.array([diag_len, diag_len])
        for i in range(-diag_hw, diag_hw):
            offset = np.array([i, -i])
            xt, yt = cls.list_diagonal_pixels(xy0 + offset, xy1 + offset)
            xlist += xt
            ylist += yt

        # minor diagonals
        xy0 = np.array([0, 1])
        xy1 = np.array([diag_len - 1, diag_len])
        for i in range(-diag_hw + 1, diag_hw):
            offset = np.array([i, -i])
            xt, yt = cls.list_diagonal_pixels(xy0 + offset, xy1 + offset)
            xlist += xt
            ylist += yt

        return np.array(xlist), np.array(ylist)

    @classmethod
    def list_diagonal_pixels(cls, xy0, xy1):
        """
        Return xlist, ylist, which list all the pixels on the 45-degree
        diagonal between xy0 and xy1.
        """

        dxy = [np.sign(xy1[0] - xy0[0]), np.sign(xy1[1] - xy0[1])]
        xlist = range(xy0[0], xy1[0], dxy[0])
        ylist = range(xy0[1], xy1[1], dxy[1])

        return xlist, ylist

    def get_coordlist(self):
        self.clist0 = CoordinatesList.from_image(self.end_segment_image)

    def compute_first_moments(self):
        self.first_moments = get_moments(self.clist0, maxmoment=1)

    def compute_central_moments(self):
        self.xoffset = self.first_moments[1, 0] / self.first_moments[0, 0]
        self.yoffset = self.first_moments[0, 1] / self.first_moments[0, 0]
        self.clist1 = CoordinatesList.from_clist(self.clist0,
                                                 xoffset=self.xoffset,
                                                 yoffset=self.yoffset)

        self.central_moments = get_moments(self.clist1, maxmoment=3)

        def central_coords_to_end_segment_coords(xy):
            """
            Convert x,y from the coordinate frame of the central moments
            to the coordinate frame of the end segment image
            """
            x, y = xy_split(xy)
            return np.array([x + self.xoffset, y + self.yoffset])

        self.central_to_segment = central_coords_to_end_segment_coords

        def central_coords_to_full_image_coords(xy):
            """
            Convert x,y from the coordinate frame of the central moments
            to the coordinate frame of the end segment image
            """
            return self.segment_to_full(self.central_to_segment(xy))

        self.central_to_full = central_coords_to_full_image_coords

    def compute_optimal_rotation_angle(self):
        numerator = 2 * self.central_moments[1, 1]
        denominator = self.central_moments[2, 0] - self.central_moments[0, 2]
        theta0 = 0.5 * np.arctan(numerator / denominator)
        # four possible quadrants
        theta = np.array([0, np.pi / 2, np.pi, 3 * np.pi / 2]) + theta0
        rotated_clists = [
            CoordinatesList.from_clist(self.clist1, rotation_rad=t)
            for t in theta
        ]
        rotated_moments = [
            get_moments(this_clist, maxmoment=3)
            for this_clist in rotated_clists
        ]
        # condition A: x-axis is longer than y-axis
        condA = np.array([m[2, 0] - m[0, 2] > 0 for m in rotated_moments])
        # condition B: direction of rough estimate
        dtheta = theta - self.rough_est
        dtheta[dtheta > np.pi] -= 2 * np.pi
        dtheta[dtheta < -np.pi] += 2 * np.pi
        condB = np.abs(dtheta) <= np.pi / 2
        # choose
        cond_both = condA & condB
        if not np.any(cond_both):
            raise MomentsError('Rotation quadrant conditions not met')
        elif np.sum(cond_both) > 1:
            pass
            # should throw out this event.
            # raise MomentsError(
            #     'Rotation quadrant conditions met more than once')
        chosen_ind = np.nonzero(cond_both)[0][0]  # 1st dim, 1st entry
        self.rotation_angle = theta[chosen_ind]
        self.clist2 = rotated_clists[chosen_ind]
        self.rotated_moments = rotated_moments[chosen_ind]

        def rotated_coords_to_central_coords(xy):
            """
            Convert x,y from the coordinate frame of the rotated moments
            to the coordinate frame of the central moments
            """
            x, y = xy_split(xy)
            t = self.rotation_angle
            # rotate "forward" because CoordList rotates "backward"
            x1 = x * np.cos(t) - y * np.sin(t)
            y1 = x * np.sin(t) + y * np.cos(t)
            return np.array([x1, y1])

        self.rotated_to_central = rotated_coords_to_central_coords

        def rotated_coords_to_end_segment_coords(xy):
            """
            Convert x,y from the coordinate frame of the rotated moments
            to the coordinate frame of the end segment image.
            """
            return self.central_to_segment(self.rotated_to_central(xy))

        self.rotated_to_segment = rotated_coords_to_end_segment_coords

        def rotated_coords_to_full_image_coords(xy):
            """
            Convert x,y from the coordinate frame of the rotated moments
            to the coordinate frame of the full image.
            """
            return self.segment_to_full(self.rotated_to_segment(xy))

        self.rotated_to_full = rotated_coords_to_full_image_coords

    def compute_arc_parameters(self):
        C_fit = -8.5467
        T = self.rotated_moments
        phi = C_fit * ((np.sqrt(T[0, 0]) * T[2, 1]) / (T[2, 0] - T[0, 2])**1.5)
        q1 = np.sqrt(2 - 2 * np.cos(phi) - phi * np.sin(phi))
        q2 = np.sqrt((T[2, 0] - T[0, 2]) / T[0, 0])
        q3 = np.sqrt(T[0, 0]**3 / (T[2, 0] - T[0, 2]))
        self.R = (phi / q1 * q2)
        self.Rphi = self.R * phi
        self.eta0 = (q1 / phi**2) * q3

        self.phi = phi
        self.arc_center = np.array([T[1, 0], T[0, 1]]) / T[0, 0]

    def compute_direction(self):
        self.alpha = self.phi / 2 + self.rotation_angle
        e_theta = np.array(
            [np.cos(self.rotation_angle),
             np.sin(self.rotation_angle)])
        e2 = np.array([
            np.cos(self.rotation_angle + np.pi / 2),
            np.sin(self.rotation_angle + np.pi / 2)
        ])
        q1 = self.R * np.sin(self.phi / 2) * e_theta
        # np.sinc is a NORMALIZED sinc function - sin(pi*x)/(pi*x)
        q2 = self.R * (np.cos(self.phi / 2) - np.sin(self.phi) / self.phi) * e2
        self.x0 = self.arc_center - q1 + q2

    def compute_pathology(self):
        # Test 1: The total arc-length should be longer than 3(?) pixels
        self.arclength = self.Rphi
        # Test 2: Radius should be much greater than arc-length
        # self.phi
        # Test 3: Certain moments are expected to ... be significantly smaller
        self.pathology_ratio_3a = (self.rotated_moments[1, 2] /
                                   self.rotated_moments[2, 1])
        self.pathology_ratio_3b = (self.rotated_moments[3, 0] /
                                   self.rotated_moments[0, 3])