def _map_residues(self):
        '''For each pair of equivalence classes, match the residues of a chain in the first class to the residues of appropriate chains in the second class.

            Note: we do a lot of repeated work here. Some of the lookups e.g. atom_sequences/seqres_sequences here could be cached.'''

        pdbs = self.pdbs
        pdb_names = self.pdb_names
        partition_by_sequence = self.partition_by_sequence
        representative_pdbs = self.representative_pdbs
        representative_ids = sorted(representative_pdbs.keys())

        # Map the SEQRES sequences to the ATOM sequences
        # Note: The correct way to do this for RCSB files would be to use the SIFTS information like the ResidueRelatrix
        # does. However, we have to consider the case where users upload PDB files which have not yet been deposited in
        # the PDB so we have to resort to automatic sequence alignments. Ideally, we would store these alignments in a
        # database and then do a lookup at this point. This would not only speed up the computation here but also allow
        # us to manually fix misalignments (which will probably only occur due to gaps rather than mismatches).
        seqres_to_atom_maps = {}
        atom_to_seqres_maps = {}
        for x in range(len(representative_ids)):
            representative_id = representative_ids[x]
            pdb_object = representative_pdbs[representative_id]
            seqres_to_atom_map, atom_to_seqres_map = pdb_object.construct_seqres_to_atom_residue_map()

            # todo: I tested the remainder of this class on PDBs with no SEQRES records so any code related to these maps is untested
            #       when these assertions fail, remove them and fix the code below accordingly

            seqres_to_atom_maps[representative_id] = seqres_to_atom_map
            atom_to_seqres_maps[representative_id] = atom_to_seqres_map


        # Iterate over all pairs of representative PDBs and determine the residue mapping and sets of differing ATOM residues

        # self.residue_id_mapping maps tuples of representative ids e.g. (0, 1) to residue_id_mapping where
        #            residue_id_mapping is a mapping: 'ATOM' -> chain_1_id -> residue_1_id -> tuple(chain_2_id, residue_2_id)
        # where chain_x_id and residue_x_id are associated to representative_id_x

        # self.differing_atom_residue_ids maps tuples of representative ids e.g. (0, 1) to PDB residues IDs which differ between
        # the two representatives
        for x in range(len(representative_ids) - 1):
            for y in range(x + 1, len(representative_ids)):
                representative_pdb_id_1 = representative_ids[x]
                representative_pdb_id_2 = representative_ids[y]
                rpdb_object_1 = representative_pdbs[representative_pdb_id_1]
                rpdb_object_2 = representative_pdbs[representative_pdb_id_2]

                mapping_key = (representative_pdb_id_1, representative_pdb_id_2)
                reverse_mapping_key = mapping_key[::-1]

                residue_id_mapping = {'ATOM' : {}, 'SEQRES' : {}} # todo: add the other types of mapping here e.g. FASTA and Rosetta
                pdb1_differing_atom_residue_ids = []
                pdb2_differing_atom_residue_ids = []

                for pdb1_chain, pdb2_chains in self.get_representative_chain_mapping(mapping_key[0], mapping_key[1]).iteritems():
                    # e.g. pdb1_chain = 'A', pdb2_chains = ['A', 'E']

                    residue_id_mapping['ATOM'][pdb1_chain] = {}
                    residue_id_mapping['SEQRES'][pdb1_chain] = {}

                    # Use the SEQRES or ATOM sequence appropriately
                    pdb1_chain_sequence_type, pdb1_chain_sequence = rpdb_object_1.get_annotated_chain_sequence_string(pdb1_chain, self.use_seqres_sequences_if_possible)

                    for pdb2_chain in pdb2_chains:
                        # Get the mapping between the sequences
                        # Note: sequences and mappings are 1-based following the UniProt convention
                        # The mapping returned from sa.get_residue_mapping is an abstract mapping between *sequences of characters*
                        # and knows nothing about residue identifiers e.g. ATOM residue IDs or whether the sequences are
                        # SEQRES or ATOM sequences

                        sa = SequenceAligner()
                        pdb2_chain_sequence_type, pdb2_chain_sequence = rpdb_object_2.get_annotated_chain_sequence_string(pdb2_chain, self.use_seqres_sequences_if_possible)

                        sa.add_sequence('%s_%s' % (representative_pdb_id_1, pdb1_chain), str(pdb1_chain_sequence))
                        sa.add_sequence('%s_%s' % (representative_pdb_id_2, pdb2_chain), str(pdb2_chain_sequence))
                        mapping, match_mapping = sa.get_residue_mapping()

                        # Since the mapping is only between sequences and we wish to use the original residue identifiers of
                        # the sequence e.g. the PDB/ATOM residue ID, we look this information up in the order mapping of the
                        # Sequence objects
                        for pdb1_residue_index, pdb2_residue_index in mapping.iteritems():
                            pdb1_residue_id = pdb1_chain_sequence.order[pdb1_residue_index - 1] # order is a 0-based list
                            pdb2_residue_id = pdb2_chain_sequence.order[pdb2_residue_index - 1] # order is a 0-based list
                            pdb1_atom_residue_id, pdb2_atom_residue_id = None, None

                            if pdb1_chain_sequence_type == 'SEQRES' and pdb2_chain_sequence_type == 'SEQRES':
                                residue_id_mapping['SEQRES'][pdb1_chain][pdb1_residue_id] = residue_id_mapping['SEQRES'][pdb1_chain].get(pdb1_residue_id, [])
                                residue_id_mapping['SEQRES'][pdb1_chain][pdb1_residue_id].append((pdb2_chain, pdb2_residue_id))

                                pdb1_atom_residue_id = seqres_to_atom_maps.get(representative_pdb_id_1, {}).get(pdb1_chain, {}).get(pdb1_residue_id)
                                pdb2_atom_residue_id = seqres_to_atom_maps.get(representative_pdb_id_2, {}).get(pdb2_chain, {}).get(pdb2_residue_id)
                                if pdb1_atom_residue_id != None and pdb2_atom_residue_id != None:
                                    residue_id_mapping['ATOM'][pdb1_chain][pdb1_atom_residue_id] = residue_id_mapping['ATOM'][pdb1_chain].get(pdb1_atom_residue_id, [])
                                    residue_id_mapping['ATOM'][pdb1_chain][pdb1_atom_residue_id].append(pdb2_atom_residue_id)

                            elif pdb1_chain_sequence_type == 'SEQRES' and pdb2_chain_sequence_type == 'ATOM':
                                pdb1_atom_residue_id = seqres_to_atom_maps.get(representative_pdb_id_1, {}).get(pdb1_chain, {}).get(pdb1_residue_id)
                                if pdb1_atom_residue_id != None:
                                    residue_id_mapping['ATOM'][pdb1_chain][pdb1_atom_residue_id] = residue_id_mapping['ATOM'][pdb1_chain].get(pdb1_atom_residue_id, [])
                                    residue_id_mapping['ATOM'][pdb1_chain][pdb1_atom_residue_id].append(pdb2_residue_id)

                            elif pdb1_chain_sequence_type == 'ATOM' and pdb2_chain_sequence_type == 'SEQRES':
                                pdb2_atom_residue_id = seqres_to_atom_maps.get(representative_pdb_id_2, {}).get(pdb2_chain, {}).get(pdb2_residue_id)
                                if pdb2_atom_residue_id != None:
                                    residue_id_mapping['ATOM'][pdb1_chain][pdb1_residue_id] = residue_id_mapping['ATOM'][pdb1_chain].get(pdb1_residue_id, [])
                                    residue_id_mapping['ATOM'][pdb1_chain][pdb1_residue_id].append(pdb2_atom_residue_id)

                            elif pdb1_chain_sequence_type == 'ATOM' and pdb2_chain_sequence_type == 'ATOM':
                                residue_id_mapping['ATOM'][pdb1_chain][pdb1_residue_id] = residue_id_mapping['ATOM'][pdb1_chain].get(pdb1_residue_id, [])
                                residue_id_mapping['ATOM'][pdb1_chain][pdb1_residue_id].append(pdb2_residue_id)
                            else:
                                raise Exception('An exception occurred.') # this should not happen

                            # We store a *list* of corresponding residues i.e. if pdb1_chain matches pdb2_chain_1 and pdb2_chain_2
                            # then we may map a residue in pdb1_chain to a residue in each of those chains
                            #residue_id_mapping[pdb1_chain][pdb1_residue_id] = residue_id_mapping[pdb1_chain].get(pdb1_residue_id, [])
                            #residue_id_mapping[pdb1_chain][pdb1_residue_id].append(pdb2_residue_id)

                        # Determine which residues of each sequence differ between the sequences
                        # We ignore leading and trailing residues from both sequences
                        pdb1_residue_indices = mapping.keys()
                        pdb2_residue_indices = mapping.values()
                        differing_pdb1_indices = []
                        differing_pdb2_indices = []
                        for pdb1_residue_index, match_details in match_mapping.iteritems():
                            if match_details.clustal == 0 or match_details.clustal == -1 or match_details.clustal == -2:
                                # The residues differed
                                differing_pdb1_indices.append(pdb1_residue_index)
                                differing_pdb2_indices.append(mapping[pdb1_residue_index])

                        # Convert the different sequence indices into PDB ATOM residue IDs. Sometimes there may not be a
                        # mapping from SEQRES residues to the ATOM residues e.g. missing density
                        for idx in differing_pdb1_indices:
                            if pdb1_chain_sequence_type == 'SEQRES':
                                pdb1_seqres_residue_id = pdb1_chain_sequence.order[idx - 1]
                                pdb1_atom_residue_id = seqres_to_atom_maps.get(representative_pdb_id_1, {}).get(pdb1_chain, {}).get(pdb1_seqres_residue_id)
                                if pdb1_atom_residue_id != None:
                                    pdb1_differing_atom_residue_ids.append(pdb1_atom_residue_id)
                            elif pdb1_chain_sequence_type == 'ATOM':
                                pdb1_differing_atom_residue_ids.append(pdb1_chain_sequence.order[idx - 1])
                        for idx in differing_pdb2_indices:
                            if pdb2_chain_sequence_type == 'SEQRES':
                                pdb2_seqres_residue_id = pdb2_chain_sequence.order[idx - 1]
                                pdb2_atom_residue_id = seqres_to_atom_maps.get(representative_pdb_id_2, {}).get(pdb2_chain, {}).get(pdb2_seqres_residue_id)
                                if pdb2_atom_residue_id != None:
                                    pdb2_differing_atom_residue_ids.append(pdb2_atom_residue_id)
                            elif pdb2_chain_sequence_type == 'ATOM':
                                pdb2_differing_atom_residue_ids.append(pdb2_chain_sequence.order[idx - 1])

                self.residue_id_mapping[mapping_key] = residue_id_mapping
                self.differing_atom_residue_ids[mapping_key] = pdb1_differing_atom_residue_ids
                self.differing_atom_residue_ids[reverse_mapping_key] = pdb2_differing_atom_residue_ids

        for k, v in sorted(self.differing_atom_residue_ids.iteritems()):
            self.differing_atom_residue_ids[k] = sorted(set(v)) # the list of residues may not be unique in general so we make it unique here

        self.seqres_to_atom_maps = seqres_to_atom_maps
        self.atom_to_seqres_maps = atom_to_seqres_maps