docs: def add_backbone_atoms_linearly_from_loop_filepaths(self, loop_json_filepath, fasta_filepath, residue_ids):
          '''A utility wrapper around add_backbone_atoms_linearly. Adds backbone atoms in a straight line from the first to
             the last residue of residue_ids.
  
             loop_json_filepath is a path to a JSON file using the JSON format for Rosetta loops files. This file identifies
             the insertion points of the sequence.
  
             fasta_filepath is a path to a FASTA file with one sequence. This sequence will be used as the sequence for
             the inserted residues (between the start and stop residues defined in loop_json_filepath).
  
             residue_ids is a list of PDB chain residues (columns 22-27 of ATOM lines in the PDB format). It is assumed that
             they are sequential although the logic does not depend on that. This list should have the length length as the
             sequence identified in the FASTA file.
          '''
  
          # Parse the loop file
          loop_def = json.loads(read_file(loop_json_filepath))
          assert(len(loop_def['LoopSet']) == 1)
          start_res = loop_def['LoopSet'][0]['start']
          end_res = loop_def['LoopSet'][0]['stop']
          start_res = PDB.ChainResidueID2String(start_res['chainID'], (str(start_res['resSeq']) + start_res['iCode']).strip())
          end_res = PDB.ChainResidueID2String(end_res['chainID'], (str(end_res['resSeq']) + end_res['iCode']).strip())
          assert(start_res in residue_ids)
          assert(end_res in residue_ids)
  
          # Parse the FASTA file and extract the sequence
          f = FASTA(read_file(fasta_filepath), strict = False)
          assert(len(f.get_sequences()) == 1)
          insertion_sequence = f.sequences[0][2]
          if not len(residue_ids) == len(insertion_sequence):
              raise Exception('The sequence in the FASTA file must have the same length as the list of residues.')
  
          # Create the insertion sequence (a sub-sequence of the FASTA sequence)
          # The post-condition is that the start and end residues are the first and last elements of kept_residues respectively
          kept_residues = []
          insertion_residue_map = {}
          in_section = False
          found_end = False
          for x in range(len(residue_ids)):
              residue_id = residue_ids[x]
              if residue_id == start_res:
                  in_section = True
              if in_section:
                  kept_residues.append(residue_id)
                  insertion_residue_map[residue_id] = insertion_sequence[x]
                  if residue_id == end_res:
                      found_end = True
                      break
          if not kept_residues:
              raise Exception('The insertion sequence is empty (check the start and end residue ids).')
          if not found_end:
              raise Exception('The end residue was not encountered when iterating over the insertion sequence (check the start and end residue ids).')
  
          # Identify the start and end Residue objects
          try:
              start_res = self.residues[start_res[0]][start_res[1:]]
              end_res = self.residues[end_res[0]][end_res[1:]]
          except Exception, e:
              raise Exception('The start or end residue could not be found in the PDB file.')
  
          return self.add_backbone_atoms_linearly(start_res, end_res, kept_residues, insertion_residue_map)