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.

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)