Program Listing for File ngt.hpp#

↰ Return to documentation for file (source/pele/ngt.hpp)

#ifndef _NGT_HPP_
#define _NGT_HPP_
/*
 *
 * This implements the New Graph Transformation method (NGT) described in
 *
 * David Wales, J. Chem. Phys., 2009 http://dx.doi.org/10.1063/1.3133782
 *
 * This procedure computes transition rates and committor probabilities for
 * transition network (kinetic monte carlo).
 */

#include <assert.h>
#include <cstdlib>
#include <iostream>
#include <list>
#include <memory>
#include <queue>
#include <stdexcept>

#include "graph.hpp"

using std::cout;

namespace pele {

inline bool compare_degree(node_ptr u, node_ptr v) {
  return u->in_out_degree() < v->in_out_degree();
}

class NGT {
 public:
  typedef std::map<std::pair<node_id, node_id>, double> rate_map_t;

  std::shared_ptr<Graph> _graph;
  std::set<node_ptr> _A;              // the source nodes
  std::set<node_ptr> _B;              // the sink nodes
  std::list<node_ptr> intermediates;  // this will an up to date list of nodes
                                      // sorted by the node degree
  bool debug;

  std::map<node_id, double> initial_tau;
  std::map<node_id, double> final_omPxx;
  std::map<node_id, double> final_tau;
  std::map<node_id, double> final_committors;
  std::map<node_id, double>
      weights;  // normally these are equilibrium occupation probabilities

  ~NGT() {}

  /*
   * construct the NGT from an existing graph.
   *
   * The graph will be used directly, without copying.  Any modifications
   * will be reflected in the passed graph
   */
  template <class Acontainer, class Bcontainer>
  NGT(std::shared_ptr<Graph> graph, Acontainer const &A, Bcontainer const &B)
      : _graph(graph), debug(false) {
    for (auto u : A) {
      _A.insert(_graph->get_node(u));
    }
    for (auto u : B) {
      _B.insert(_graph->get_node(u));
    }

    // make intermediates
    for (auto const &mapval : _graph->node_map_) {
      node_ptr u = mapval.second;
      if (_A.find(u) == _A.end() and _B.find(u) == _B.end()) {
        intermediates.push_back(u);
      }
    }

    //        std::cout << "number of nodes " << _graph->number_of_nodes() <<
    //        "\n"; std::cout << "A.size() " << _A.size() << "\n"; std::cout <<
    //        "B.size() " << _B.size() << "\n"; std::cout <<
    //        "intermediates.size() " << intermediates.size() << "\n";
    assert(intermediates.size() + _A.size() + _B.size() ==
           _graph->number_of_nodes());
  }

  void set_debug() { debug = true; }
  std::map<node_id, double> const &get_committors() { return final_committors; }

  /*
   * construct the NGT from a map of rate constants.
   */
  template <class Acontainer, class Bcontainer>
  NGT(rate_map_t &rate_constants, Acontainer const &A, Bcontainer const &B)
      : _graph(new Graph()), debug(false) {
    std::set<node_ptr> nodes;

    // add nodes to the graph and sum the rate constants for all out edges for
    // each node.
    std::map<node_ptr, double> sum_out_rates;
    for (auto const &mapvals : rate_constants) {
      node_ptr u = _graph->add_node(mapvals.first.first);
      node_ptr v = _graph->add_node(mapvals.first.second);
      double k = mapvals.second;
      nodes.insert(u);
      nodes.insert(v);

      try {
        sum_out_rates.at(u) += k;
      } catch (std::out_of_range &e) {
        sum_out_rates[u] = k;
      }
    }

    // set tau_x for each node
    // add edge Pxx for each node and initialize P to 0.
    for (auto x : nodes) {
      double tau_x = 1. / sum_out_rates[x];
      set_tau(x, tau_x);
      initial_tau[x->id()] = tau_x;
      edge_ptr xx = _graph->_add_edge(x, x);
      set_P(xx, 0.);
    }

    // set Puv for each edge
    for (auto const &mapval : rate_constants) {
      node_ptr u = _graph->get_node(mapval.first.first);
      node_ptr v = _graph->get_node(mapval.first.second);
      double k = mapval.second;

      edge_ptr uv = _graph->_add_edge(u, v);
      double tau_u = get_tau(u);
      double Puv = k * tau_u;
      set_P(uv, Puv);

      try {
        sum_out_rates.at(u) += k;
      } catch (std::out_of_range &e) {
        sum_out_rates[u] = k;
      }
    }

    // make the set of A and B
    for (auto a : A) {
      _A.insert(_graph->get_node(a));
    }
    for (auto b : B) {
      _B.insert(_graph->get_node(b));
    }

    // make a list of intermediates
    for (auto a : _A) {
      nodes.erase(a);
    }
    for (auto b : _B) {
      nodes.erase(b);
    }
    intermediates.assign(nodes.begin(), nodes.end());

    //        std::cout << _graph->number_of_nodes() << "\n";
    //        std::cout << _A.size() << "\n";
    //        std::cout << _B.size() << "\n";
    //        std::cout << intermediates.size() << "\n";
    //        std::cout << nodes.size() << "\n";
    assert(intermediates.size() + _A.size() + _B.size() ==
           _graph->number_of_nodes());
  }

  void set_node_occupation_probabilities(std::map<node_id, double> &Peq) {
    weights.insert(Peq.begin(), Peq.end());
  }

  /*
   * Sort the list of intermediates.
   *
   * This is done because it is faster to remove nodes with fewer connections
   * first.
   */
  void sort_intermediates() {
    node_ptr x = *intermediates.begin();
    if (debug) {
      std::cout << "smallest node degree " << x->in_out_degree() << "\n";
    }
    if (x->in_out_degree() > 4) {
      intermediates.sort(compare_degree);
    }
  }

  /*
   * accessors for graph properties P and tau attached to the edges and nodes.
   */
  inline double get_tau(node_ptr u) { return u->tau; }
  inline double get_P(edge_ptr edge) { return edge->P; }
  inline void set_tau(node_ptr u, double tau) { u->tau = tau; }
  inline void set_P(edge_ptr edge, double P) { edge->P = P; }

  /*
   * This returns P for the edge u->u.  This is slow because the edge must first
   * be found.
   */
  double get_node_P(node_ptr u) { return get_P(u->get_successor_edge(u)); }

  /*
   * This returns 1.-P for the edge u->u.
   *
   * If P is close to one compute 1.-P directly by summing P over all the out
   * edges of u. This is extremely important for numerical precision.  It is
   * this ability to deal precisely with both P and 1.-P that makes this method
   * more stable then linear algebra methods.
   */
  double get_node_one_minus_P(node_ptr u) {
    edge_ptr uu = u->get_successor_edge(u);
    double Puu = get_P(uu);
    if (Puu < 0.99) {
      return 1. - Puu;
    } else {
      // sum the contributions from all other edges
      double omPuu = 0.;
      for (auto eiter = u->out_edge_begin(); eiter != u->out_edge_end();
           ++eiter) {
        node_ptr v = (*eiter)->head();
        if (v != u) {
          omPuu += (*eiter)->P;
        }
      }
      return omPuu;
    }
  }

  /*
   * node x is being deleted, so update tau for node u
   *
   * tau_u -> tau_u + Pux * tau_x / (1-Pxx)
   */
  void update_node(edge_ptr ux, double omPxx, double tau_x) {
    node_ptr u = ux->tail();
    double Pux = get_P(ux);
    double tau_u = get_tau(u);
    double new_tau_u = tau_u + Pux * tau_x / omPxx;
    if (debug) {
      std::cout << "updating node " << u->id() << " tau " << tau_u << " -> "
                << new_tau_u << "\n";
    }
    set_tau(u, new_tau_u);
  }

  /*
   * add an edge to the graph and set P to 0
   */
  edge_ptr add_edge(node_ptr u, node_ptr v) {
    edge_ptr edge = _graph->_add_edge(u, v);
    set_P(edge, 0.);
    return edge;
  }

  /*
   * Node x is being deleted, so update P for the edge u -> v
   *
   * Puv -> Puv + Pux * Pxv / (1-Pxx)
   */
  void update_edge(node_ptr u, node_ptr v, edge_ptr ux, edge_ptr xv,
                   double omPxx) {
    edge_ptr uv = u->get_successor_edge(v);  // this is slow
    if (uv == NULL) {
      uv = add_edge(u, v);
    }

    double Pux = get_P(ux);
    double Pxv = get_P(xv);
    double Puv = get_P(uv);

    double newPuv = Puv + Pux * Pxv / omPxx;
    if (debug) {
      std::cout << "updating edge " << u->id() << " -> " << v->id() << " Puv "
                << Puv << " -> " << newPuv << " 1-Pxx " << omPxx << " Pux "
                << Pux << " Pxv " << Pxv << "\n";
    }
    set_P(uv, newPuv);
  }

  /*
   * remove node x from the graph and update its neighbors
   */
  void remove_node(node_ptr x) {
    if (debug) {
      std::cout << "removing node " << x->id() << "\n";
    }
    double taux = get_tau(x);
    //        double Pxx = get_node_P(x);
    double omPxx = get_node_one_minus_P(x);

    // update the node data for all the neighbors
    for (auto eiter = x->in_edge_begin(); eiter != x->in_edge_end(); eiter++) {
      edge_ptr edge = *eiter;
      if (edge->tail() != edge->head()) {
        update_node(edge, omPxx, taux);
      }
    }

    std::set<node_ptr> neibs = x->in_out_neighbors();
    neibs.erase(x);

    //
    for (auto uxiter = x->in_edge_begin(); uxiter != x->in_edge_end();
         ++uxiter) {
      edge_ptr ux = *uxiter;
      node_ptr u = ux->tail();
      if (u == x) continue;
      for (auto xviter = x->out_edge_begin(); xviter != x->out_edge_end();
           ++xviter) {
        edge_ptr xv = *xviter;
        node_ptr v = xv->head();
        if (v == x) continue;
        //                if (u == v){
        //                    continue;
        //                }
        update_edge(u, v, ux, xv, omPxx);
      }
    }

    // remove the node from the graph
    _graph->_remove_node(x);
  }

  /*
   * remove all intermediates from the graph
   */
  void remove_intermediates() {
    while (intermediates.size() > 0) {
      sort_intermediates();

      node_ptr x = intermediates.front();
      intermediates.pop_front();

      remove_node(x);
    }
  }

  /*
   * phase one of the rate calculation is to remove all intermediate nodes
   */
  void phase_one() { remove_intermediates(); }

  /*
   * Compute final_tau and final_omPxx for each node in to_remove
   *
   * For each node x in to_remove, this involves removing all other nodes in
   * to_remove, and getting the results from this reduced graph.
   */
  void reduce_all_in_group(std::set<node_ptr> &to_remove,
                           std::set<node_ptr> &to_keep) {
    std::list<node_id> Aids, Bids;
    // copy the ids of the nodes in to_remove into Aids
    for (auto u : to_remove) {
      Aids.push_back(u->id());
    }
    // copy the ids of the nodes in to_keep into Bids
    for (auto u : to_keep) {
      Bids.push_back(u->id());
    }

    // note: should we sort the minima in to_remove?

    if (Aids.size() > 1) {
      // make a copy of _graph called working_graph
      auto working_graph = std::make_shared<Graph>(*_graph);
      std::list<node_id> empty_list;
      // make an ngt object for working_graph
      NGT working_ngt(working_graph, std::list<node_id>(), Bids);
      while (Aids.size() > 1) {
        /*
         * Create a new graph and a new NGT object new_ngt.  Pass x as A and
         * Bids as B.  new_ngt will remove all `intermediates`, i.e. everything
         * in Aids except x.  Then save the final value of 1-Pxx and tau_x.
         */
        // choose an element x and remove it from the list
        node_id x = Aids.back();
        Aids.pop_back();
        std::list<node_id> newAids;
        newAids.push_back(x);

        // make a new graph from the old graph
        auto new_graph = std::make_shared<Graph>(*working_graph);

        // remove all nodes from new_graph except x
        NGT new_ngt(new_graph, newAids, Bids);
        new_ngt.remove_intermediates();
        node_ptr xptr = new_graph->get_node(x);
        final_omPxx[x] = new_ngt.get_node_one_minus_P(xptr);
        final_tau[x] = new_ngt.get_tau(xptr);

        // delete node x from the old_graph
        working_ngt.remove_node(working_graph->get_node(x));
      }
      // there is one node left. we can just read off the results
      assert(Aids.size() == 1);
      node_id x = Aids.back();
      Aids.pop_back();
      node_ptr xptr = working_graph->get_node(x);
      final_omPxx[x] = working_ngt.get_node_one_minus_P(xptr);
      final_tau[x] = working_ngt.get_tau(xptr);

    } else if (Aids.size() == 1) {
      // if there is only one node in A then we can just read off the results.
      node_id x = Aids.back();
      Aids.pop_back();
      node_ptr xptr = _graph->get_node(x);
      final_omPxx[x] = get_node_one_minus_P(xptr);
      final_tau[x] = get_tau(xptr);
    }
    assert(Aids.size() == 0);
  }

  /*
   * Phase two, compute final_tau and final_omPxx for each x separately in _A
   * and in _B
   */
  void phase_two() {
    reduce_all_in_group(_A, _B);
    reduce_all_in_group(_B, _A);
  }

  /*
   * do phase one and phase two of the rate calculation
   */
  void compute_rates() {
    phase_one();
    phase_two();
  }

  /*
   * compute the final rate A->B or B->A from final_tau and final_omPxx
   */
  double _get_rate_final(std::set<node_ptr> &A) {
    double rate_sum = 0.;
    double norm = 0.;
    for (auto a : A) {
      double omPxx = final_omPxx.at(a->id());
      double tau_a = final_tau.at(a->id());
      double weight = 1.;
      if (weights.size() > 0) {
        weight = weights.at(a->id());
      }
      rate_sum += weight * omPxx / tau_a;
      norm += weight;
    }
    return rate_sum / norm;
  }

  /*
   * Return the rate A->B
   */
  double get_rate_AB() { return _get_rate_final(_A); }

  /*
   * Return the rate B->A
   */
  double get_rate_BA() { return _get_rate_final(_B); }

  double _get_rate_SS(std::set<node_ptr> &A, std::set<node_ptr> &B) {
    double kAB = 0.;
    double norm = 0.;
    for (auto a : A) {
      // compute PaB the probability that this node goes directly to B
      double PaB = 0.;
      for (auto eiter = a->out_edge_begin(); eiter != a->out_edge_end();
           ++eiter) {
        edge_ptr ab = *eiter;
        node_ptr b = ab->head();
        if (B.find(b) != B.end()) {
          PaB += get_P(ab);
        }
      }
      double weight = 1.;
      if (weights.size() > 0) {
        weight = weights.at(a->id());
      }
      kAB += weight * PaB / initial_tau.at(a->id());
      norm += weight;
    }
    return kAB / norm;
  }

  /*
   * Return the steady state rate A->B
   *
   * this must be called after calling phase_one
   */
  double get_rate_AB_SS() { return _get_rate_SS(_A, _B); }

  /*
   * Return the steady state rate B->A
   *
   * this must be called after calling phase_one
   */
  double get_rate_BA_SS() { return _get_rate_SS(_B, _A); }

  /*
   * sum the probabilities of the out edges of x that end in B normalized by
   * 1-Pxx
   */
  double get_PxB(node_ptr x, std::set<node_id> &B) {
    double PxB = 0.;
    double Pxx = 0.;
    double omPxx = 0.;
    for (auto eiter = x->out_edge_begin(); eiter != x->out_edge_end();
         ++eiter) {
      edge_ptr xb = *eiter;
      node_ptr b = xb->head();
      double Pxb = get_P(xb);
      if (b == x) {
        Pxx = Pxb;
      } else {
        omPxx += Pxb;
      }
      if (B.find(b->id()) != B.end()) {
        PxB += Pxb;
      }
    }
    if (Pxx < 0.9) {
      omPxx = 1. - Pxx;
    }
    return PxB / omPxx;
  }

  /*
   * compute the committors for all intermediates
   *
   * \param to_remove a list of nodes that will be removed.  Committor values
   * will be computed for these nodes \param to_keep a list of nodes that should
   * not be deleted. \param committor_targets a list of nodes that should not be
   * deleted.  These nodes will be the targets in the committor calucation.
   *
   * All nodes should be in one of the three passed groups of nodes.  Duplicates
   * between to_keep and committor_targets are OK.
   */
  void _remove_nodes_and_compute_committors(
      std::list<node_ptr> &to_remove, std::set<node_ptr> &to_keep,
      std::set<node_ptr> const &committor_targets) {
    // make a copy of to_remove.  Store the id's
    std::list<node_id> to_remove_cp;
    for (auto u : to_remove) {
      to_remove_cp.push_back(u->id());
    }

    // copy the nodes from to_keep and committor_target into a new set Bids;
    // make a copy of committor_target
    std::set<node_id> Bids;
    std::set<node_id> targets;
    // create a set of Bids
    for (auto u : to_keep) {
      Bids.insert(u->id());
    }
    for (auto u : committor_targets) {
      Bids.insert(u->id());
      targets.insert(u->id());
    }

    // ensure there are no unaccounted for nodes
    assert(to_remove_cp.size() + Bids.size() == _graph->number_of_nodes());

    // note: should we sort the nodes in to_remove?

    while (to_remove_cp.size() > 0) {
      /*
       * Create a new graph and a new NGT object new_ngt.  Pass x as A and Bids
       * as B.  new_ngt will remove all `intermediates`, i.e. everything in
       * to_remove except x.  Then save the final value of 1-Pxx and tau_x.
       */
      // choose an element x and remove it from the list
      node_id x = to_remove_cp.back();
      to_remove_cp.pop_back();
      std::list<node_id> Aids;
      Aids.push_back(x);

      // make a copy of _graph
      auto new_graph = std::make_shared<Graph>(*_graph);

      // remove all to_remove nodes from new_graph except x
      NGT new_ngt(new_graph, Aids, Bids);
      new_ngt.remove_intermediates();
      node_ptr xptr = new_graph->get_node(x);
      final_omPxx[x] = new_ngt.get_node_one_minus_P(xptr);
      final_tau[x] = new_ngt.get_tau(xptr);
      if (!targets.empty()) {
        final_committors[x] = new_ngt.get_PxB(xptr, targets);
      }

      // delete node x from _graph
      this->remove_node(_graph->get_node(x));
    }
  }

  /*
   * Compute the rate from A->B and committor probabilities for all
   * intermediates
   *
   * This is much slower than compute_rates.
   * If you don't want committors use that function instead
   */
  void compute_rates_and_committors() {
    _remove_nodes_and_compute_committors(intermediates, _A, _B);
    intermediates.clear();

    phase_two();

    // set the committor for nodes in A to 0
    for (auto a : _A) {
      final_committors[a->id()] = 0.;
    }
    // set the committor for nodes in B to 1
    for (auto b : _B) {
      final_committors[b->id()] = 1.;
    }
  }
};

}  // namespace pele
#endif