Returns a list of tuples each containing a SEQRES Mutation object and an ATOM Mutation object representing the
           mutations from pdb_id_1, chain_1 to pdb_id_2, chain_2.

           SequenceMaps are constructed in this function between the chains based on the alignment.

           PDBMutationPair are returned as they are hashable and amenable to Set construction to eliminate duplicates.

def get_chain_mutations(self, pdb_id_1, chain_1, pdb_id_2, chain_2):
        '''Returns a list of tuples each containing a SEQRES Mutation object and an ATOM Mutation object representing the
           mutations from pdb_id_1, chain_1 to pdb_id_2, chain_2.

           SequenceMaps are constructed in this function between the chains based on the alignment.

           PDBMutationPair are returned as they are hashable and amenable to Set construction to eliminate duplicates.
           '''

        # Set up the objects
        p1 = self.add_pdb(pdb_id_1)
        p2 = self.add_pdb(pdb_id_2)
        sifts_1, pdb_1 = p1['sifts'], p1['pdb']
        sifts_2, pdb_2 = p2['sifts'], p2['pdb']

        # Set up the sequences
        #pprint.pprint(sifts_1.seqres_to_atom_sequence_maps)
        seqres_to_atom_sequence_maps_1 = sifts_1.seqres_to_atom_sequence_maps.get(chain_1, {}) # this is not guaranteed to exist e.g. 2ZNW chain A
        seqres_1, atom_1 = pdb_1.seqres_sequences.get(chain_1), pdb_1.atom_sequences.get(chain_1)
        seqres_2, atom_2 = pdb_2.seqres_sequences.get(chain_2), pdb_2.atom_sequences.get(chain_2)
        if not seqres_1: raise Exception('No SEQRES sequence for chain {0} of {1}.'.format(chain_1, pdb_1))
        if not atom_1: raise Exception('No ATOM sequence for chain {0} of {1}.'.format(chain_1, pdb_1))
        if not seqres_2: raise Exception('No SEQRES sequence for chain {0} of {1}.'.format(chain_2, pdb_2))
        if not atom_2: raise Exception('No ATOM sequence for chain {0} of {1}.'.format(chain_2, pdb_2))
        seqres_str_1 = str(seqres_1)
        seqres_str_2 = str(seqres_2)

        # Align the SEQRES sequences
        sa = SequenceAligner()
        sa.add_sequence('{0}_{1}'.format(pdb_id_1, chain_1), seqres_str_1)
        sa.add_sequence('{0}_{1}'.format(pdb_id_2, chain_2), seqres_str_2)
        sa.align()
        seqres_residue_mapping, seqres_match_mapping = sa.get_residue_mapping()
        #colortext.pcyan(sa.alignment_output)

        # Create a SequenceMap
        seqres_sequence_map = SequenceMap()
        assert(sorted(seqres_residue_mapping.keys()) == sorted(seqres_match_mapping.keys()))
        for k, v in seqres_residue_mapping.iteritems():
            seqres_sequence_map.add(k, v, seqres_match_mapping[k])
        self.seqres_sequence_maps[(pdb_id_1, chain_1)][(pdb_id_2, chain_2)] = seqres_sequence_map

        # Determine the mutations between the SEQRES sequences and use these to generate a list of ATOM mutations
        mutations = []
        clustal_symbols = SubstitutionScore.clustal_symbols

        #print(pdb_id_1, chain_1, pdb_id_2, chain_2)
        #print(seqres_to_atom_sequence_maps_1)

        for seqres_res_id, v in seqres_match_mapping.iteritems():
            # Look at all positions which differ. seqres_res_id is 1-indexed, following the SEQRES and UniProt convention. However, so our our Sequence objects.
            if clustal_symbols[v.clustal] != '*':

                # Get the wildtype Residue objects
                seqres_wt_residue = seqres_1[seqres_res_id]
                #print(seqres_wt_residue)
                seqres_mutant_residue = seqres_2[seqres_residue_mapping[seqres_res_id]] # todo: this will probably fail for some cases where there is no corresponding mapping

                # If there is an associated ATOM record for the wildtype residue, get its residue ID
                atom_res_id = None
                atom_chain_res_id = seqres_to_atom_sequence_maps_1.get(seqres_res_id)
                try:
                    if atom_chain_res_id:
                        assert(atom_chain_res_id[0] == chain_1)
                        atom_residue = atom_1[atom_chain_res_id]
                        atom_res_id = atom_chain_res_id[1:]
                        assert(atom_residue.ResidueAA == seqres_wt_residue.ResidueAA)
                        assert(atom_residue.ResidueID == atom_res_id)
                except:
                    atom_res_id = None
                    if seqres_wt_residue.ResidueAA != 'X':
                        # we do not seem to keep ATOM records for unknown/non-canonicals: see 2BTF chain A -> 2PBD chain A
                        raise

                # Create two mutations - one for the SEQRES residue and one for the corresponding (if any) ATOM residue
                # We create both so that the user is informed whether there is a mutation between the structures which is
                # not captured by the coordinates.
                # If there are no ATOM coordinates, there is no point creating an ATOM mutation object so we instead use
                # the None type. This also fits with the approach in the SpiderWeb framework.
                seqres_mutation = ChainMutation(seqres_wt_residue.ResidueAA, seqres_res_id,seqres_mutant_residue.ResidueAA, Chain = chain_1)
                atom_mutation = None
                if atom_res_id:
                    atom_mutation = ChainMutation(seqres_wt_residue.ResidueAA, atom_res_id, seqres_mutant_residue.ResidueAA, Chain = chain_1)

                mutations.append(PDBMutationPair(seqres_mutation, atom_mutation))

        return mutations