This function returns the PDB content for a structure with the missing backbone atoms - i.e. it adds the
           N, Ca, C atoms spaced evenly between the last existing backbone atom of start_residue and the first existing
           backbone atom of end_residue. O-atoms are not currently added although we could arbitrarily add them at 90
           degrees to the plane: If resiC_x + x = resjC_x and resiC_y + y = resjC_y, i + 1 = j, then the resiO atom could
           have coordinates (resiC_x - y,  resiC_y + x).

           Adds backbone atoms for insertion_residues in a straight line from start_residue to end_residue. This is useful
           for some computational methods which do not require the atoms to be in the correct coordinates but expect N, CA, and C backbone atoms
           to exist for all residues (O-atoms are currently ignored here).

           start_residue and end_residue are Residue objects. insertion_residues is a list of PDB residue IDs (columns 22-27
           of ATOM lines in the PDB format). insertion_residue_map is a mapping from PDB residue IDs to 1-letter amino acid
           codes. The keys of insertion_residue_map must be insertion_residues.

           start_residue and end_residue must exist in insertion_residues and the PDB file. There is no technical requirement for this;
           we just do not handle the alternate case yet. residue_ids are presumed to be ordered in sequence (N -> C) order.
           Existing N, CA, and C atoms corresponding to these two residues will be retained as long as their atoms which
           connect to the side of those residues not identified by residue_ids are present e.g.
              - if the CA atom of the first residue is present, it will be kept as long as the N atom is present and regardless of whether the C atom is present
              - if the CA atom of the last residue is present, it will be kept as long as the C atom is present and regardless of whether the N atom is present
           All O atoms of residues in residue_ids are discarded. ANISOU records corresponding to any removed ATOMS will be removed.

                   1st    2nd          n-1      n
             ... N-CA-C- N-CA-C- ... N-CA-C- N-CA-C- ..

           Note: This function currently only supports canonical amino acids.

def add_backbone_atoms_linearly(self, start_residue, end_residue, insertion_residues, insertion_residue_map):
        '''This function returns the PDB content for a structure with the missing backbone atoms - i.e. it adds the
           N, Ca, C atoms spaced evenly between the last existing backbone atom of start_residue and the first existing
           backbone atom of end_residue. O-atoms are not currently added although we could arbitrarily add them at 90
           degrees to the plane: If resiC_x + x = resjC_x and resiC_y + y = resjC_y, i + 1 = j, then the resiO atom could
           have coordinates (resiC_x - y,  resiC_y + x).

           Adds backbone atoms for insertion_residues in a straight line from start_residue to end_residue. This is useful
           for some computational methods which do not require the atoms to be in the correct coordinates but expect N, CA, and C backbone atoms
           to exist for all residues (O-atoms are currently ignored here).

           start_residue and end_residue are Residue objects. insertion_residues is a list of PDB residue IDs (columns 22-27
           of ATOM lines in the PDB format). insertion_residue_map is a mapping from PDB residue IDs to 1-letter amino acid
           codes. The keys of insertion_residue_map must be insertion_residues.

           start_residue and end_residue must exist in insertion_residues and the PDB file. There is no technical requirement for this;
           we just do not handle the alternate case yet. residue_ids are presumed to be ordered in sequence (N -> C) order.
           Existing N, CA, and C atoms corresponding to these two residues will be retained as long as their atoms which
           connect to the side of those residues not identified by residue_ids are present e.g.
              - if the CA atom of the first residue is present, it will be kept as long as the N atom is present and regardless of whether the C atom is present
              - if the CA atom of the last residue is present, it will be kept as long as the C atom is present and regardless of whether the N atom is present
           All O atoms of residues in residue_ids are discarded. ANISOU records corresponding to any removed ATOMS will be removed.

                   1st    2nd          n-1      n
             ... N-CA-C- N-CA-C- ... N-CA-C- N-CA-C- ..

           Note: This function currently only supports canonical amino acids.
        '''

        assert(sorted(insertion_residues) == sorted(insertion_residue_map.keys()))
        assert(start_residue.chain + start_residue.residue_id in insertion_residues)
        assert(end_residue.chain + end_residue.residue_id in insertion_residues)
        assert(start_residue.chain == end_residue.chain)

        atoms_to_remove = []

        discarded_atoms = []

        # Remove atoms from the segment's N-terminus residue
        # if N and CA and C, keep C else discard C
        start_res_atoms_ids = self.get_atom_serial_numbers_from_pdb_residue_ids([insertion_residues[0]])
        start_res_atoms = [self.atoms[id] for id in start_res_atoms_ids]
        start_res_atom_types = [a.name for a in start_res_atoms]
        start_atoms = [None, None, None]
        for a in start_res_atoms:
            if a.name == 'N': start_atoms[0] = a
            elif a.name == 'CA': start_atoms[1] = a
            elif a.name == 'C': start_atoms[2] = a
            else: discarded_atoms.append(a.serial_number)
        if 'C' in start_res_atom_types and 'CA' not in start_res_atom_types:
            discarded_atoms += start_atoms[2].serial_number
            start_atoms[2] = None
        if not start_atoms[0]:
            raise Exception('The N atom for the start residue must exist.')
        start_atoms = [a for a in start_atoms if a]
        start_atom = start_atoms[-1]

        # Remove atoms from the segment's C-terminus residue
        # if N and CA and C, keep N else discard N
        end_res_atoms_ids = self.get_atom_serial_numbers_from_pdb_residue_ids([insertion_residues[-1]])
        end_res_atoms = [self.atoms[id] for id in end_res_atoms_ids]
        end_res_atom_types = [a.name for a in end_res_atoms]
        end_atoms = [None, None, None]
        for a in end_res_atoms:
            if a.name == 'N': end_atoms[0] = a
            elif a.name == 'CA': end_atoms[1] = a
            elif a.name == 'C': end_atoms[2] = a
            else: discarded_atoms.append(a.serial_number)
        if 'N' in end_res_atom_types and 'CA' not in end_res_atom_types:
            discarded_atoms += end_atoms[0].serial_number
            end_atoms[0] = None
        if not end_atoms[-1]:
            raise Exception('The C atom for the end residue must exist.')
        end_atoms = [a for a in end_atoms if a]
        end_atom = end_atoms[0]

        # Remove all atoms from the remainder of the segment
        discarded_atoms += self.get_atom_serial_numbers_from_pdb_residue_ids(insertion_residues[1:-1])

        # Remove the atoms from the PDB
        bonsai_pdb_content, cutting_pdb_content, PSE_file, PSE_script = self.prune(set(discarded_atoms), generate_pymol_session = False)
        self.__init__(bonsai_pdb_content, buffer = self.buffer, bin_size = self.bin_size, safe_mode = self.safe_mode)

        # Create a list of all N, CA, C atoms for the insertion residues not including those present in the start and end residue
        # Find last of N CA C of first residue
        # Find last of N CA C of first residue
        new_atoms = []
        assert(len(start_atoms) >= 1) # N is guaranteed to exist
        if len(start_atoms) == 2:
            # add a C atom
            residue_id = insertion_residues[0]
            residue_type = insertion_residue_map[residue_id]
            assert(residue_type != 'X' and residue_type in residue_type_1to3_map)
            new_atoms.append((residue_id, residue_type_1to3_map[residue_type], 'C'))
        for insertion_residue in insertion_residues[1:-1]:
            # add an N, CA, C atoms
            residue_type = insertion_residue_map[insertion_residue]
            assert(residue_type != 'X' and residue_type in residue_type_1to3_map)
            residue_type = residue_type_1to3_map[residue_type]
            new_atoms.append((insertion_residue, residue_type, 'N'))
            new_atoms.append((insertion_residue, residue_type, 'CA'))
            new_atoms.append((insertion_residue, residue_type, 'C'))
        assert(len(end_atoms) >= 1) # C is guaranteed to exist
        if len(end_atoms) == 2:
            # add an N atom
            residue_id = insertion_residues[-1]
            residue_type = insertion_residue_map[residue_id]
            assert(residue_type != 'X' and residue_type in residue_type_1to3_map)
            new_atoms.append((residue_id, residue_type_1to3_map[residue_type], 'N'))

        return self.add_atoms_linearly(start_atom, end_atom, new_atoms)