docs: def _index_chains(chains):
          '''
          Given a list of LiftOverChain objects, creates a
           dict: source_name --> 
              IntervalTree: <source_from, source_to> -->
                  (target_from, target_to, chain)
          Returns the resulting dict.
          Throws an exception on any errors or inconsistencies among chains (e.g. different sizes specified for the same chromosome in various chains).
          '''
          chain_index = {}
          source_size = {}
          target_size = {}
          for c in chains:
              # Verify that sizes of chromosomes are consistent over all chains
              source_size.setdefault(c.source_name, c.source_size)
              if source_size[c.source_name] != c.source_size:
                  raise Exception("Chains have inconsistent specification of source chromosome size for %s (%d vs %d)" % (c.source_name, source_size[c.source_name], c.source_size))
              target_size.setdefault(c.target_name, c.target_size)
              if target_size[c.target_name] != c.target_size:
                  raise Exception("Chains have inconsistent specification of target chromosome size for %s (%d vs %d)" % (c.target_name, target_size[c.target_name], c.target_size))
              chain_index.setdefault(c.source_name, IntervalTree(0, c.source_size))
              # Register all blocks from the chain in the corresponding interval tree
              tree = chain_index[c.source_name]
              for (sfrom, sto, tfrom, tto) in c.blocks:
                  tree.add_interval(sfrom, sto, (tfrom, tto, c))
  
          # Sort all interval trees
          for k in chain_index:
              chain_index[k].sort()
          return chain_index