Learn » Examples » Example Jacobi 1D Program

The full code for this example can be found here: examples/collection/jacobi1d_vt.cc.

//
// This code applies a few steps of the Jacobi iteration to
// the linear system  A x = 0
// where A is a tridiagonal matrix with pattern [-1 2 -1]
// The initial guess for x is a made-up non-zero vector.
// The exact solution is the vector 0.
//
// The matrix A is square and invertible.
// The number of rows is ((number of objects) * (number of rows per object))
//
// Such a matrix A is obtained when using 2nd-order finite difference
// for discretizing (-d^2 u /dx^2 = f) on [0, 1] with homogeneous
// Dirichlet condition (u(0) = u(1) = 0) using a uniform grid
// with grid size 1 / ((number of objects) * (number of rows per object) + 1)
//


#include <vt/transport.h>

#include <cstdlib>

static constexpr std::size_t const default_nrow_object = 8;
static constexpr std::size_t const default_num_objs = 4;
static constexpr double const default_tol = 1.0e-02;

struct NodeObj {
  bool is_finished_ = false;
  void workFinishedHandler() { is_finished_ = true; }
  bool isWorkFinished() { return is_finished_; }
};
using NodeObjProxy = vt::objgroup::proxy::Proxy<NodeObj>;

struct LinearPb1DJacobi : vt::Collection<LinearPb1DJacobi,vt::Index1D> {

private:

  std::vector<double> tcur_, told_;
  std::vector<double> rhs_;
  size_t iter_ = 0;
  size_t msgReceived_ = 0, totalReceive_ = 0;
  size_t numObjs_ = 1;
  size_t numRowsPerObject_ = 1;
  size_t maxIter_ = 8;
  NodeObjProxy objProxy_;

public:

  explicit LinearPb1DJacobi() :
    tcur_(), told_(), rhs_(), iter_(0),
    msgReceived_(0), totalReceive_(0),
    numObjs_(1), numRowsPerObject_(1), maxIter_(8)
  { }


  using BlankMsg = vt::CollectionMessage<LinearPb1DJacobi>;

  struct LPMsg : vt::CollectionMessage<LinearPb1DJacobi> {

    size_t numObjects = 0;
    size_t nRowPerObject = 0;
    size_t iterMax = 0;
    NodeObjProxy objProxy;

    LPMsg() = default;

    LPMsg(const size_t nobjs, const size_t nrow, const size_t itMax, NodeObjProxy proxy) :
      numObjects(nobjs), nRowPerObject(nrow), iterMax(itMax), objProxy(proxy)
    { }

  };

  void checkCompleteCB(double normRes) {
    //
    // Only one object for the reduction will visit
    // this function
    //

    auto const iter_max_reached = iter_ > maxIter_;
    auto const norm_res_done = normRes < default_tol;

    if (iter_max_reached or norm_res_done) {
      auto const to_print = iter_max_reached ?
        "\n Maximum Number of Iterations Reached. \n\n" :
        fmt::format("\n Max-Norm Residual Reduced by {} \n\n", default_tol);

      fmt::print(to_print);

      // Notify all nodes that computation is finished
      objProxy_.broadcast<&NodeObj::workFinishedHandler>();
    } else {
      fmt::print(" ## ITER {} >> Residual Norm = {} \n", iter_, normRes);
    }
  }

  void doIteration() {

    iter_ += 1;

    //
    //--- Copy extremal values
    //
    tcur_[0] = told_[0];
    tcur_[numRowsPerObject_+1] = told_[numRowsPerObject_+1];

    //
    //---- Jacobi iteration step
    //---- A tridiagonal matrix = "tridiag" ( [-1.0  2.0  -1.0] )
    //---- rhs_ right hand side vector
    //
    for (size_t ii = 1; ii <= numRowsPerObject_; ++ii) {
      tcur_[ii] = 0.5*(rhs_[ii] + told_[ii-1] + told_[ii+1]);
    }

    std::copy(tcur_.begin(), tcur_.end(), told_.begin());

    //
    // Compute the maximum entries among the rows on this object
    // We do not take into account the "ghost" entries
    // as they may be "out of date".
    //

    double maxNorm = 0.0;

    for (size_t ii = 1; ii < tcur_.size()-1; ++ii) {
      double val = tcur_[ii];
      maxNorm = (maxNorm > std::fabs(val)) ? maxNorm : std::fabs(val);
    }

    auto proxy = this->getCollectionProxy();
    proxy.reduce<&LinearPb1DJacobi::checkCompleteCB, vt::collective::MaxOp>(
      proxy[0], maxNorm
    );

  }


  struct VecMsg : vt::CollectionMessage<LinearPb1DJacobi> {
    using MessageParentType = vt::CollectionMessage<LinearPb1DJacobi>;
    vt_msg_serialize_if_needed_by_parent_or_type1(vt::IdxBase);

    VecMsg() = default;

    VecMsg(vt::IdxBase const& in_index, double const& ref) :
      vt::CollectionMessage<LinearPb1DJacobi>(),
      from_index(in_index), val(ref)
    { }

    template <typename Serializer>
    void serialize(Serializer& s) {
      MessageParentType::serialize(s);
      s | from_index;
      s | val;
    }

    vt::IdxBase from_index = 0;
    double val = 0.0;
  };

  void exchange(VecMsg *msg) {
    // Receive and treat the message from a neighboring object.

    const vt::IdxBase myIdx = getIndex().x();

    if (myIdx > msg->from_index) {
      this->told_[0] = msg->val;
      msgReceived_ += 1;
    }

    if (myIdx < msg->from_index) {
      this->told_[numRowsPerObject_ + 1] = msg->val;
      msgReceived_ += 1;
    }

    // Check whether this 'object' has received all the expected messages.
    if (msgReceived_ == totalReceive_) {
      msgReceived_ = 0;
      doIteration();
    }

  }

  void doIter([[maybe_unused]] BlankMsg *msg) {

    //
    // Treat the particular case of 1 object
    // where no communication is needed.
    // Without this treatment, the code would not iterate.
    //

    if (numObjs_ == 1) {
      doIteration();
      return;
    }
    //---------------------------------------

    //
    // Routine to send information to a different object
    //

    vt::IdxBase const myIdx = getIndex().x();

    //--- Send the values to the left
    auto proxy = this->getCollectionProxy();
    if (myIdx > 0) {
      proxy[myIdx - 1].send<VecMsg, &LinearPb1DJacobi::exchange>(
        myIdx, told_[1]
      );
    }

    //--- Send values to the right
    if (size_t(myIdx) < numObjs_ - 1) {
      proxy[myIdx + 1].send<VecMsg, &LinearPb1DJacobi::exchange>(
        myIdx, told_[numRowsPerObject_]
      );
    }
  }


  void init() {

    tcur_.assign(numRowsPerObject_ + 2, 0.0);
    told_.assign(numRowsPerObject_ + 2, 0.0);
    rhs_.assign(numRowsPerObject_ + 2, 0.0);

    double h = 1.0 / (numRowsPerObject_ * numObjs_ + 1.0);
    int nf = 3 * int(numRowsPerObject_ * numObjs_ + 1) / 4;

    size_t const myIdx = getIndex().x();

    for (size_t ii = 0; ii < tcur_.size(); ++ii) {
      double x0 = ( numRowsPerObject_ * myIdx + ii) * h;
      tcur_[ii] = sin(nf * M_PI * x0 * x0);
    }

    totalReceive_ = 2;

    if (myIdx == 0) {
      tcur_[0] = 0.0;
      totalReceive_ -= 1;
    }

    if (myIdx == numObjs_ - 1) {
      tcur_[numRowsPerObject_+1] = 0.0;
      totalReceive_ -= 1;
    }

    std::copy(tcur_.begin(), tcur_.end(), told_.begin());

  }


  void init(LPMsg* msg) {
    numObjs_ = msg->numObjects;
    numRowsPerObject_ = msg->nRowPerObject;
    maxIter_ = msg->iterMax;
    objProxy_ = msg->objProxy;

    // Initialize the starting vector
    init();
  }

};

bool isWorkDone( vt::objgroup::proxy::Proxy<NodeObj> const& proxy){
  auto const this_node = vt::theContext()->getNode();
  return proxy[this_node].invoke<&NodeObj::isWorkFinished>();
}

int main(int argc, char** argv) {

  size_t num_objs = default_num_objs;
  size_t numRowsPerObject = default_nrow_object;
  size_t maxIter = 8;

  std::string name(argv[0]);

  vt::initialize(argc, argv);

  vt::NodeType this_node = vt::theContext()->getNode();
  vt::NodeType num_nodes = vt::theContext()->getNumNodes();

  if (argc == 1) {
    if (this_node == 0) {
      fmt::print(
        stderr, "{}: using default arguments since none provided\n", name
      );
    }
    num_objs = default_num_objs * num_nodes;
  } else if (argc == 2) {
    num_objs = static_cast<size_t>(strtol(argv[1], nullptr, 10));
  }
  else if (argc == 3) {
    num_objs = static_cast<size_t>(strtol(argv[1], nullptr, 10));
    numRowsPerObject = static_cast<size_t>(strtol(argv[2], nullptr, 10));
  }
  else if (argc == 4) {
    num_objs = static_cast<size_t>(strtol(argv[1], nullptr, 10));
    numRowsPerObject = static_cast<size_t>(strtol(argv[2], nullptr, 10));
    maxIter = static_cast<size_t>(strtol(argv[3], nullptr, 10));
  }
  else {
    fmt::print(
      stderr, "usage: {} <num-objects> <num-rows-per-object> <maxiter>\n",
      name
    );
    return 1;
  }

  // Object group of all nodes that take part in computation
  // Used to determine whether the computation is finished
  auto grp_proxy = vt::theObjGroup()->makeCollective<NodeObj>("examples_jacobi1d");

  // Create the decomposition into objects
  using BaseIndexType = typename vt::Index1D::DenseIndexType;
  auto range = vt::Index1D(static_cast<BaseIndexType>(num_objs));

  auto col_proxy = vt::makeCollection<LinearPb1DJacobi>("examples_jacobi1d")
    .bounds(range)
    .bulkInsert()
    .wait();

  vt::runInEpochCollective([col_proxy, grp_proxy, num_objs, numRowsPerObject, maxIter]{
    col_proxy.broadcastCollective<LinearPb1DJacobi::LPMsg, &LinearPb1DJacobi::init>(
      num_objs, numRowsPerObject, maxIter, grp_proxy
    );
  });

  while(!isWorkDone(grp_proxy)){
    vt::runInEpochCollective([col_proxy]{
      col_proxy.broadcastCollective<
        LinearPb1DJacobi::BlankMsg, &LinearPb1DJacobi::doIter
      >();
    });

    vt::thePhase()->nextPhaseCollective();
  }

  vt::finalize();

  return 0;
}