docs: 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