Skip to content

fragment

GhostFragment

GhostFragment(guide_coords: ndarray, i_type: int, j_type: int, k_type: int, ijk_rmsd: float, aligned_fragment: Fragment)

Stores the mapping from a Fragment to a "paired" Fragment. Handles spatial manipulation for the mapped pair

Parameters:

  • guide_coords (ndarray) –

    The array of shape (3, 3) with the set of x, y, z coordinates to map this instance to the standard fragment reference frame at the origin

  • i_type (int) –

    The particular index which identifies the aligned fragment type, i.e. the aligned_fragment

  • j_type (int) –

    The particular index which identifies the paired fragment type, i.e. this instance

  • k_type (int) –

    The particular index which identifies the spatial orientation of the i and j fragment types

  • ijk_rmsd (float) –

    The root-mean-square deviation which applies to all members particular i,j,k index type

  • aligned_fragment (Fragment) –

    The Fragment instance which this instance is paired with

Source code in symdesign/structure/fragment/__init__.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(self, guide_coords: np.ndarray, i_type: int, j_type: int, k_type: int, ijk_rmsd: float,
             aligned_fragment: Fragment):
    """Construct the instance

    Args:
        guide_coords: The array of shape (3, 3) with the set of x, y, z coordinates to map this instance to the
            standard fragment reference frame at the origin
        i_type: The particular index which identifies the aligned fragment type, i.e. the aligned_fragment
        j_type: The particular index which identifies the paired fragment type, i.e. this instance
        k_type: The particular index which identifies the spatial orientation of the i and j fragment types
        ijk_rmsd: The root-mean-square deviation which applies to all members particular i,j,k index type
        aligned_fragment: The Fragment instance which this instance is paired with
    """
    self._guide_coords = guide_coords
    self.i_type = i_type
    self.j_type = self.frag_type = j_type
    self.k_type = k_type
    self.rmsd = ijk_rmsd
    self.aligned_fragment = aligned_fragment
    # Assign both attributes for API compatibility with Fragment
    self.fragment_db = self._fragment_db = aligned_fragment.fragment_db

rmsd instance-attribute

rmsd: float = ijk_rmsd

The deviation from the representative ghost fragment

aligned_fragment instance-attribute

aligned_fragment: Fragment = aligned_fragment

The Fragment instance that this GhostFragment is aligned too. Must support .chain_id, .number, .index, .rotation, .translation, and .transformation attributes

type property

type: int

The secondary structure of the Fragment

ijk property

ijk: tuple[int, int, int]

The Fragment cluster index information

Returns:

  • tuple[int, int, int]

    I cluster index, J cluster index, K cluster index

aligned_chain_and_residue property

aligned_chain_and_residue: tuple[str, int]

Return the Fragment identifiers that the GhostFragment was mapped to

Returns:

  • tuple[str, int]

    aligned chain_id, aligned residue_number

number property

number: int

The Residue number of the aligned Fragment

index property

index: int

The Residue index of the aligned Fragment

guide_coords property

guide_coords: ndarray

Return the guide coordinates of the GhostFragment

rotation property

rotation: ndarray

The rotation of the aligned Fragment from the Fragment Database

translation property

translation: ndarray

The translation of the aligned Fragment from the Fragment Database

transformation property

transformation: tuple[ndarray, ndarray]

The transformation of the aligned Fragment from the Fragment Database

Returns:

  • tuple[ndarray, ndarray]

    The rotation (3, 3) and the translation (3,)

representative property

representative: 'structure.model.Model'

Access the Representative GhostFragment StructureBase

write

write(out_path: bytes | str = os.getcwd(), file_handle: IO = None, header: str = None, **kwargs) -> AnyStr | None

Write the GhostFragment to a file specified by out_path or with a passed file_handle

If a file_handle is passed, no header information will be written. Arguments are mutually exclusive

Parameters:

  • out_path (bytes | str, default: getcwd() ) –

    The location where the StructureBase object should be written to disk

  • file_handle (IO, default: None ) –

    Used to write to an open FileObject

  • header (str, default: None ) –

    A string that is desired at the top of the file

Other Parameters:

  • chain_id

    str = None - The chain ID to use

  • atom_offset

    int = 0 - How much to offset the atom number by. Default returns one-indexed

Returns:

  • AnyStr | None

    The name of the written file if out_path is used

Source code in symdesign/structure/fragment/__init__.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def write(
    self, out_path: bytes | str = os.getcwd(), file_handle: IO = None, header: str = None, **kwargs
) -> AnyStr | None:
    """Write the GhostFragment to a file specified by out_path or with a passed file_handle

    If a file_handle is passed, no header information will be written. Arguments are mutually exclusive

    Args:
        out_path: The location where the StructureBase object should be written to disk
        file_handle: Used to write to an open FileObject
        header: A string that is desired at the top of the file

    Keyword Args:
        chain_id: str = None - The chain ID to use
        atom_offset: int = 0 - How much to offset the atom number by. Default returns one-indexed

    Returns:
        The name of the written file if out_path is used
    """
    representative = self.representative
    if file_handle:
        file_handle.write(f'{representative.get_atom_record(**kwargs)}\n')
        return None
    else:  # out_path always has default argument current working directory
        _header = representative.format_header(**kwargs)
        if header is not None:
            if not isinstance(header, str):
                header = str(header)
            _header += (header if header[-2:] == '\n' else f'{header}\n')

        with open(out_path, 'w') as outfile:
            outfile.write(_header)
            outfile.write(f'{representative.get_atom_record(**kwargs)}\n')
        return out_path

Fragment

Fragment(fragment_type: int = None, fragment_db: FragmentDatabase = None, **kwargs)

Bases: ABC

Parameters:

  • fragment_type (int, default: None ) –

    The particular index for the class of fragment this instance belongs to

  • fragment_db (FragmentDatabase, default: None ) –

    The FragmentDatabase from where this instance was derived

  • **kwargs
Source code in symdesign/structure/fragment/__init__.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def __init__(self, fragment_type: int = None, fragment_db: db.FragmentDatabase = None, **kwargs):
    """Construct the instance

    Args:
        fragment_type: The particular index for the class of fragment this instance belongs to
        fragment_db: The FragmentDatabase from where this instance was derived
        **kwargs:
    """
    self._fragment_coords = None
    self.ghost_fragments = None
    self.i_type = fragment_type
    self.rotation = utils.symmetry.identity_matrix
    self.translation = utils.symmetry.origin
    self._fragment_db = fragment_db

    super().__init__(**kwargs)  # Fragment

fragment_db property writable

fragment_db: FragmentDatabase

The FragmentDatabase that the Fragment was created from

frag_type property writable

frag_type: int | None

The secondary structure of the Fragment

aligned_chain_and_residue property

aligned_chain_and_residue: tuple[str, int]

Return the Fragment identifiers that the Fragment was mapped to

Returns:

  • tuple[str, int]

    aligned chain_id, aligned residue_number

chain_id abstractmethod property

chain_id: str

Return the Fragment identifiers that the Fragment was mapped to

Returns:

  • str

    The aligned Residue.chain_id attribute

number abstractmethod property

number: int

Return the Fragment identifiers that the Fragment was mapped to

Returns:

  • int

    The aligned Residue.number attribute

index abstractmethod property

index: int

Return the Fragment identifiers that the Fragment was mapped to

Returns:

  • int

    The aligned Residue.index attribute

guide_coords property

guide_coords: ndarray

Return the guide coordinates of the mapped Fragment

transformation property

transformation: tuple[ndarray, ndarray]

The transformation of the Fragment from the FragmentDatabase to its current position

find_ghost_fragments

find_ghost_fragments(clash_tree: BinaryTreeType = None, clash_dist: float = 2.1) -> list[GhostFragment]

Find all the GhostFragments associated with the Fragment

Parameters:

  • clash_tree (BinaryTreeType, default: None ) –

    Allows clash prevention during search. Typical use is the backbone and CB atoms of the ContainsAtomsMixin that the Fragment is assigned

  • clash_dist (float, default: 2.1 ) –

    The distance to check for backbone clashes

Source code in symdesign/structure/fragment/__init__.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def find_ghost_fragments(self, clash_tree: BinaryTreeType = None, clash_dist: float = 2.1) -> list[GhostFragment]:
    """Find all the GhostFragments associated with the Fragment

    Args:
        clash_tree: Allows clash prevention during search. Typical use is the backbone and CB atoms of the
            ContainsAtomsMixin that the Fragment is assigned
        clash_dist: The distance to check for backbone clashes
    """
    try:
        ghost_i_type_arrays = self._fragment_db.indexed_ghosts.get(self.i_type, None)
    except AttributeError:  # _fragment_db is None
        raise NotImplementedError(
            f"Can't {self.find_ghost_fragments.__name__} without first setting '.fragment_db' to a "
            f"{db.FragmentDatabase.__name__}"
        )
    if ghost_i_type_arrays is None:
        self.ghost_fragments = {}
        return []

    stacked_bb_coords, stacked_guide_coords, ijk_types, rmsd_array = ghost_i_type_arrays
    # No need to transform stacked_guide_coords as these will be transformed upon .guide_coords access
    if clash_tree is None:
        # Ensure no slice occurs
        viable_indices = Ellipsis
    else:
        # Ensure that the backbone coords are transformed to the Fragment reference frame
        transformed_bb_coords = transform_coordinate_sets(stacked_bb_coords, *self.transformation)
        # with .reshape(), we query on a np.view saving memory
        neighbors = clash_tree.query_radius(transformed_bb_coords.reshape(-1, 3), clash_dist)
        neighbor_counts = np.array([neighbor.size for neighbor in neighbors.tolist()])
        # reshape to original size then query for existence of any neighbors for each fragment individually
        clashing_indices = neighbor_counts.reshape(len(transformed_bb_coords), -1).any(axis=1)
        viable_indices = ~clashing_indices

    # self.ghost_fragments = [GhostFragment(*info) for info in zip(list(transformed_guide_coords[viable_indices]),
    selected_ijk_types = ijk_types[viable_indices].tolist()
    ghost_fragments = [
        GhostFragment(*info_)
        for info_ in zip(
            list(stacked_guide_coords[viable_indices]), *zip(*selected_ijk_types),
            rmsd_array[viable_indices].tolist(), repeat(self))
    ]
    self.ghost_fragments = {ijk: frag for ijk, frag in zip(selected_ijk_types, ghost_fragments)}

    return ghost_fragments

get_ghost_fragments

get_ghost_fragments(**kwargs) -> list | list[GhostFragment]

Retrieve the GhostFragments associated with the Fragment. Will generate if none are available, otherwise, will return the already generated instances.

Optionally, check clashing with the original structure backbone by passing clash_tree

Other Parameters:

  • clash_tree

    BinaryTreeType = None - Allows clash prevention during search. Typical use is the backbone and CB coordinates of the ContainsAtomsMixin that the Fragment is assigned

  • clash_dist

    float = 2.1 - The distance to check for backbone clashes

Returns:

  • list | list[GhostFragment]

    The ghost fragments associated with the fragment

Source code in symdesign/structure/fragment/__init__.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def get_ghost_fragments(self, **kwargs) -> list | list[GhostFragment]:
    """Retrieve the GhostFragments associated with the Fragment. Will generate if none are available, otherwise,
    will return the already generated instances.

    Optionally, check clashing with the original structure backbone by passing clash_tree

    Keyword Args:
        clash_tree: BinaryTreeType = None - Allows clash prevention during search.
            Typical use is the backbone and CB coordinates of the ContainsAtomsMixin that the Fragment is assigned
        clash_dist: float = 2.1 - The distance to check for backbone clashes

    Returns:
        The ghost fragments associated with the fragment
    """
    #         Args:
    #             indexed_ghost_fragments: The paired fragment database to match to the Fragment instance
    # self.find_ghost_fragments(**kwargs)
    if self.ghost_fragments is None:
        ghost_fragments = self.find_ghost_fragments(**kwargs)
    else:
        # This routine is necessary when the ghost_fragments are already generated on a residue,
        # but that residue is copied.
        logger.debug('Using previously generated ghost fragments. Updating their .aligned_fragment attribute')
        ghost_fragments = list(self.ghost_fragments.values())
        for ghost in ghost_fragments:
            ghost.aligned_fragment = self

    return ghost_fragments

MonoFragment

MonoFragment(residues: Sequence['structure.base.Residue'], **kwargs)

Bases: Fragment

Used to represent Fragment information when treated as a continuous Fragment of length fragment_length

**kwargs:
Source code in symdesign/structure/fragment/__init__.py
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
def __init__(self, residues: Sequence['structure.base.Residue'], **kwargs):
    """

    Args:
        residues: The Residue instances which comprise the MonoFragment
        **kwargs:
    """
    super().__init__(**kwargs)  # MonoFragment

    fragment_db = self.fragment_db
    try:
        fragment_length = fragment_db.fragment_length
    except AttributeError:  # fragment_db is None
        raise ValueError(
            f"Can't construct {self.__class__.__name__} without passing 'fragment_db'")

    if not residues:
        raise ValueError(
            f"Can't find {self.__class__.__name__} without passing {fragment_length} Residue instances")
    self.central_residue = residues[int(fragment_length / 2)]

    try:
        fragment_representatives = fragment_db.representatives
    except AttributeError:
        raise TypeError(
            f"The 'fragment_db' is not of the required type '{db.FragmentDatabase.__name__}'")

    fragment_ca_coords = np.array([residue.ca_coords for residue in residues])
    min_rmsd = float('inf')

    for fragment_type, representative in fragment_representatives.items():
        rmsd, rot, tx = superposition3d(fragment_ca_coords, representative.ca_coords)
        if rmsd <= self.rmsd_thresh and rmsd <= min_rmsd:
            self.i_type = fragment_type
            min_rmsd, self.rotation, self.translation = rmsd, rot, tx

    if self.i_type:
        # self.guide_coords = \
        #     np.matmul(self.template_coords, np.transpose(self.rotation)) + self.translation
        self._fragment_coords = fragment_representatives[self.i_type].backbone_coords

chain_id property

chain_id: str

The Residue chainID

number property

number: int

The Residue number

index property

index: int

Return the Fragment identifiers that the Fragment was mapped to

Returns:

  • int

    The aligned Residue.index attribute

ResidueFragment

ResidueFragment(**kwargs)

Bases: Fragment, ABC

Represent Fragment information for a single Residue

Source code in symdesign/structure/fragment/__init__.py
465
466
def __init__(self, **kwargs):
    super().__init__(**kwargs)  # ResidueFragment

backbone_coords abstractmethod property

backbone_coords: ndarray

transformation property

transformation: tuple[ndarray, ndarray]

The transformation of the ResidueFragment from the FragmentDatabase to its current position

find_fragment_overlap

find_fragment_overlap(fragments1: Iterable[Fragment], fragments2: Sequence[Fragment], clash_coords: ndarray = None, min_match_value: float = 2.0, **kwargs) -> list[tuple[GhostFragment, Fragment, float]]

From two sets of Residues, score the fragment overlap according to Nanohedra's fragment matching

Parameters:

  • fragments1 (Iterable[Fragment]) –

    The Fragment instances that will be used to search for GhostFragment instances

  • fragments2 (Sequence[Fragment]) –

    The Fragment instances to pair against fragments1 GhostFragment instances

  • clash_coords (ndarray, default: None ) –

    The coordinates to use for checking for GhostFragment clashes

  • min_match_value (float, default: 2.0 ) –

    The minimum value which constitutes an acceptable fragment z_score

Returns:

  • list[tuple[GhostFragment, Fragment, float]]

    The GhostFragment, Fragment pairs, along with their match score

Source code in symdesign/structure/fragment/__init__.py
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
def find_fragment_overlap(fragments1: Iterable[Fragment], fragments2: Sequence[Fragment],
                          clash_coords: np.ndarray = None, min_match_value: float = 2.,  # .2,
                          **kwargs) -> list[tuple[GhostFragment, Fragment, float]]:
    #           entity1, entity2, entity1_interface_residue_numbers, entity2_interface_residue_numbers, max_z_value=2):
    """From two sets of Residues, score the fragment overlap according to Nanohedra's fragment matching

    Args:
        fragments1: The Fragment instances that will be used to search for GhostFragment instances
        fragments2: The Fragment instances to pair against fragments1 GhostFragment instances
        clash_coords: The coordinates to use for checking for GhostFragment clashes
        min_match_value: The minimum value which constitutes an acceptable fragment z_score

    Returns:
        The GhostFragment, Fragment pairs, along with their match score
    """
    # min_match_value: The minimum value which constitutes an acceptable fragment match score
    # 0.2 with match score, 2 with z_score
    # Todo memoize this variable into a function default... The load time is kinda significant and shouldn't be used
    #  until needed. Getting the factory everytime is a small overhead that is really unnecessary. Perhaps this function
    #  should be refactored to structure.fragment.db or something and imported upon usage...

    # logger.debug('Starting Ghost Frag Lookup')
    if clash_coords is not None:
        clash_tree = BallTree(clash_coords)
    else:
        clash_tree = None

    ghost_frags1: list[GhostFragment] = []
    for fragment in fragments1:
        ghost_frags1.extend(fragment.get_ghost_fragments(clash_tree=clash_tree))

    logger.debug(f'Residues 1 has {len(ghost_frags1)} ghost fragments')

    # Get fragment guide coordinates
    residue1_ghost_guide_coords = np.array([ghost_frag.guide_coords for ghost_frag in ghost_frags1])
    residue2_guide_coords = np.array([fragment.guide_coords for fragment in fragments2])
    # interface_surf_frag_guide_coords = np.array([residue.guide_coords for residue in interface_residues2])

    # Check for matching Euler angles
    # Todo create a stand alone function
    # logger.debug('Starting Euler Lookup')
    euler_lookup = fragments2[0].fragment_db.euler_lookup

    overlapping_ghost_indices, overlapping_frag_indices = \
        euler_lookup.check_lookup_table(residue1_ghost_guide_coords, residue2_guide_coords)
    # logger.debug('Finished Euler Lookup')
    logger.debug(f'Found {len(overlapping_ghost_indices)} overlapping fragments in the same Euler rotational space')
    # filter array by matching type for surface (i) and ghost (j) frags
    ghost_type_array = np.array([ghost_frags1[idx].frag_type for idx in overlapping_ghost_indices.tolist()])
    mono_type_array = np.array([fragments2[idx].frag_type for idx in overlapping_frag_indices.tolist()])
    ij_type_match = mono_type_array == ghost_type_array

    passing_ghost_indices = overlapping_ghost_indices[ij_type_match]
    passing_frag_indices = overlapping_frag_indices[ij_type_match]
    logger.debug(f'Found {len(passing_ghost_indices)} overlapping fragments in the same i/j type')

    passing_ghost_coords = residue1_ghost_guide_coords[passing_ghost_indices]
    passing_frag_coords = residue2_guide_coords[passing_frag_indices]
    # # Todo keep without euler_lookup?
    # ghost_type_array = np.array([ghost_frag.frag_type for ghost_frag in ghost_frags1])
    # mono_type_array = np.array([residue.frag_type for residue in fragments2])
    # # Using only ij_type_match, no euler_lookup
    # int_ghost_shape = len(ghost_frags1)
    # int_surf_shape = len(fragments2)
    # # maximum_number_of_pairs = int_ghost_shape*int_surf_shape
    # ghost_indices_repeated = np.repeat(ghost_type_array, int_surf_shape)
    # surf_indices_tiled = np.tile(mono_type_array, int_ghost_shape)
    # # ij_type_match = ij_type_match_lookup_table[ghost_indices_repeated, surf_indices_tiled]
    # # ij_type_match = np.where(ghost_indices_repeated == surf_indices_tiled, True, False)
    # # ij_type_match = ghost_indices_repeated == surf_indices_tiled
    # ij_type_match_lookup_table = (ghost_indices_repeated == surf_indices_tiled).reshape(int_ghost_shape, -1)
    # ij_type_match = ij_type_match_lookup_table[ghost_indices_repeated, surf_indices_tiled]
    # # possible_fragments_pairs = ghost_indices_repeated.shape[0]
    # passing_ghost_indices = ghost_indices_repeated[ij_type_match]
    # passing_surf_indices = surf_indices_tiled[ij_type_match]
    # # passing_ghost_coords = residue1_ghost_guide_coords[ij_type_match]
    # # passing_frag_coords = residue2_guide_coords[ij_type_match]
    # passing_ghost_coords = residue1_ghost_guide_coords[passing_ghost_indices]
    # passing_frag_coords = residue2_guide_coords[passing_surf_indices]
    # Precalculate the reference_rmsds for each ghost fragment
    reference_rmsds = np.array([ghost_frags1[ghost_idx].rmsd for ghost_idx in passing_ghost_indices.tolist()])
    # # Todo keep without euler_lookup?
    # reference_rmsds = np.array([ghost_frag.rmsd for ghost_frag in ghost_frags1])[passing_ghost_indices]

    # logger.debug('Calculating passing fragment overlaps by RMSD')
    # all_fragment_match = metrics.calculate_match(passing_ghost_coords, passing_frag_coords, reference_rmsds)
    # passing_overlaps_indices = np.flatnonzero(all_fragment_match > min_match_value)
    all_fragment_z_score = metrics.rmsd_z_score(passing_ghost_coords, passing_frag_coords, reference_rmsds)
    passing_overlaps_indices = np.flatnonzero(all_fragment_z_score < min_match_value)
    # logger.debug('Finished calculating fragment overlaps')
    # logger.debug(f'Found {len(passing_overlaps_indices)} overlapping fragments over the {min_match_value} threshold')
    logger.debug(f'Found {len(passing_overlaps_indices)} overlapping fragments under the {min_match_value} threshold')

    # interface_ghostfrags = [ghost_frags1[idx] for idx in passing_ghost_indices[passing_overlap_indices].tolist()]
    # interface_monofrags2 = [fragments2[idx] for idx in passing_surf_indices[passing_overlap_indices].tolist()]
    # passing_z_values = all_fragment_overlap[passing_overlap_indices]
    # match_scores = utils.match_score_from_z_value(all_fragment_overlap[passing_overlap_indices])

    return list(zip([ghost_frags1[idx] for idx in passing_ghost_indices[passing_overlaps_indices].tolist()],
                    [fragments2[idx] for idx in passing_frag_indices[passing_overlaps_indices].tolist()],
                    # all_fragment_match[passing_overlaps_indices].tolist()))
                    metrics.match_score_from_z_value(all_fragment_z_score[passing_overlaps_indices]).tolist()))

create_fragment_info_from_pairs

create_fragment_info_from_pairs(ghostfrag_frag_pairs: list[tuple[GhostFragment, Fragment, float]]) -> list[FragmentInfo]

From a ghost fragment/surface fragment pair and corresponding match score, return the pertinent interface information

Parameters:

  • ghostfrag_frag_pairs (list[tuple[GhostFragment, Fragment, float]]) –

    Observed ghost and surface fragment overlaps and their match score

Returns:

  • list[FragmentInfo]

    The formatted fragment information for each pair {'mapped': int, 'paired': int, 'match': float, 'cluster': tuple(int, int, int)}

Source code in symdesign/structure/fragment/__init__.py
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def create_fragment_info_from_pairs(
    ghostfrag_frag_pairs: list[tuple[GhostFragment, Fragment, float]]
) -> list[db.FragmentInfo]:
    """From a ghost fragment/surface fragment pair and corresponding match score, return the pertinent interface
    information

    Args:
        ghostfrag_frag_pairs: Observed ghost and surface fragment overlaps and their match score

    Returns:
        The formatted fragment information for each pair
            {'mapped': int, 'paired': int, 'match': float, 'cluster': tuple(int, int, int)}
    """
    fragment_matches = [db.FragmentInfo(mapped=ghost_frag.index, paired=surf_frag.index,
                                        match=match_score, cluster=ghost_frag.ijk)
                        for ghost_frag, surf_frag, match_score in ghostfrag_frag_pairs]

    logger.debug(f'Fragments for Entity1 found at indices: {[frag.mapped for frag in fragment_matches]}')
    logger.debug(f'Fragments for Entity2 found at indices: {[frag.paired for frag in fragment_matches]}')

    return fragment_matches