docs: def extract_xyz_matrix_from_pdb_residue_range(pdb_lines, start_pdb_residue_id = None, stop_pdb_residue_id = None, atoms_of_interest = backbone_atoms, expected_num_residues = None, expected_num_residue_atoms = None, break_on_chain_end = True, include_all_columns = False):
          '''Creates a pandas dataframe of X, Y, Z coordinates for the residues identified in the range from
             start_pdb_residue_id to stop_pdb_residue_id inclusive. The dataframe is indexed by a string identifying the
             PDB residue and atom type.
  
             pdb_lines should be an array of lines from a PDB file (only ATOM lines are currently considered).
             The residue IDs should be 6-character strings correspond to columns 22-27 (inclusive where columns are 1-indexed)
             of the PDB file. The start and stop residues are both optional - omitting either removes the lower and upper
             bound of the residue range respectively.
             The returned coordinates will be restricted to atoms_of_interest.
             expected_num_residues can be set if the number of residues is known in advance and the user wishes to assert this.
             If expected_num_residue_atoms is set then each residue will be checked to ensure that this number of atoms was read.
  
             This method does not handle MODELs e.g. from PDB files determined via NMR. To handle those files, an intermediate
             function should be written which identifies the lines for one MODEL and then calls this function.'''
          atoms = {}
          found_start, found_end = start_pdb_residue_id == None, None
          res_id_list, x_list, y_list, z_list = [], [], [], []
          if include_all_columns:
              chain_id_list, plain_res_id_list, atom_type_list, loc_list, residue_aa_list = [], [], [], [], []
          for l in pdb_lines:
              res_id = None
              atom_type = None
              alt_loc = None
  
              if (break_on_chain_end and l.strip() == 'TER') or l.startswith('MODEL'):
                  # Do not cross over into other chains (unless specified) or models
                  break
  
              if l.startswith('ATOM  '):
                  res_id = l[21:27]
                  atom_type = l[12:16].strip()
                  alt_loc = l[16]
                  if res_id == start_pdb_residue_id:
                      # Saw the first residue - enter parsing
                      found_start = True
                  if (stop_pdb_residue_id != None) and (res_id == stop_pdb_residue_id):
                      assert(found_start)
                      found_end = True
  
              if found_end and res_id != stop_pdb_residue_id:
                  # Passed the last residue - exit parsing
                  break
  
              if found_start and l.startswith('ATOM  ') and (not(atoms_of_interest) or (atom_type in atoms_of_interest)):
                  if expected_num_residues or expected_num_residue_atoms:
                      # Only build the atoms dict if the assertions are turned on
                      assert(res_id and atom_type and not(atoms.get(res_id, {}).get(atom_type)))
                      atoms[res_id] = atoms.get(res_id, {})
                      atoms[res_id][atom_type] = True
  
                  # Add the dataframe elements
                  res_id_list.append('{0}_{1}_{2}'.format(res_id.strip(), atom_type, alt_loc))
                  x_list.append(float(l[30:38]))
                  y_list.append(float(l[38:46]))
                  z_list.append(float(l[46:54]))
                  if include_all_columns:
                      chain_id_list.append(l[21])
                      plain_res_id_list.append(l[22:27])
                      atom_type_list.append(l[12:16])
                      loc_list.append(l[16])
                      residue_aa_list.append(l[17:20])
  
          assert(found_start and ((stop_pdb_residue_id == None) or found_end))
  
          if expected_num_residues != None:
              assert(len(atoms) == expected_num_residues)
          if expected_num_residue_atoms != None:
              for res_id, atom_details in atoms.iteritems():
                  assert(len(atom_details) == expected_num_residue_atoms)
  
          if include_all_columns:
              return pandas.DataFrame(dict(chain = chain_id_list, res_id = plain_res_id_list, atom = atom_type_list, altloc = loc_list, AA = residue_aa_list, X = x_list, Y = y_list, Z = z_list), index = res_id_list)
          else:
              return pandas.DataFrame(dict(X = x_list, Y = y_list, Z = z_list), index = res_id_list)