|
2 | 2 | #include "../dist_graph.hpp" |
3 | 3 | #include <vector> |
4 | 4 | #include <map> |
| 5 | +#include <set> |
| 6 | +#include <array> |
5 | 7 | #include <algorithm> |
6 | 8 | #include <mpi.h> |
7 | 9 | #include <iostream> |
@@ -370,7 +372,80 @@ class AtomicData { |
370 | 372 | r_edges, |
371 | 373 | type_norb); |
372 | 374 | } |
373 | | - |
| 375 | + |
| 376 | + // Distributed construction from a CALLER-GIVEN partition (doc/design/42 §4). |
| 377 | + // Each rank passes ONLY its owned atoms — there is no rank-0 gather of the |
| 378 | + // geometry and no ParMETIS: the partition is whatever the caller chose (e.g. |
| 379 | + // a spatial slab/RCB aligned with the grid). Global ids are assigned |
| 380 | + // contiguously by rank (Allgather of counts -> Exscan), so rank r owns |
| 381 | + // [offset_r, offset_r + n_r). This form Allgathers positions/Z (O(N), transient) |
| 382 | + // so every rank builds the edges of ITS OWNED atoms locally; the persistent |
| 383 | + // storage is owned + ghost only. ``my_input_index[li]`` is the original |
| 384 | + // input-order index of owned atom ``li`` (fills AtomicData.atom_index). |
| 385 | + static AtomicData* from_distributed( |
| 386 | + const std::vector<double>& my_pos, const std::vector<int>& my_z, |
| 387 | + const std::vector<int>& my_input_index, |
| 388 | + const std::vector<double>& cell, const std::vector<bool>& pbc, |
| 389 | + const std::vector<double>& r_max_per_type, const std::vector<int>& type_norb_in, |
| 390 | + MPI_Comm comm) { |
| 391 | + int rank = 0, size = 1, initialized = 0; |
| 392 | + MPI_Initialized(&initialized); |
| 393 | + if (initialized) { MPI_Comm_rank(comm, &rank); MPI_Comm_size(comm, &size); } |
| 394 | + const int my_n = static_cast<int>(my_z.size()); |
| 395 | + |
| 396 | + // 1. Contiguous global-id layout: rank r owns [offset_r, offset_r + n_r). |
| 397 | + std::vector<int> counts(size, my_n), displs(size + 1, 0); |
| 398 | + if (initialized) MPI_Allgather(&my_n, 1, MPI_INT, counts.data(), 1, MPI_INT, comm); |
| 399 | + for (int i = 0; i < size; ++i) displs[i + 1] = displs[i] + counts[i]; |
| 400 | + const int total = displs[size]; |
| 401 | + const int my_offset = displs[rank]; |
| 402 | + if (total == 0) return new AtomicData(comm); |
| 403 | + |
| 404 | + // 2. Allgather all positions + atomic numbers (global-id == rank order). |
| 405 | + std::vector<int> pos_counts(size), pos_displs(size + 1, 0); |
| 406 | + for (int i = 0; i < size; ++i) { pos_counts[i] = counts[i] * 3; pos_displs[i + 1] = pos_displs[i] + pos_counts[i]; } |
| 407 | + std::vector<double> all_pos(static_cast<size_t>(total) * 3); |
| 408 | + std::vector<int> all_z(total); |
| 409 | + if (initialized) { |
| 410 | + MPI_Allgatherv(my_pos.data(), my_n * 3, MPI_DOUBLE, all_pos.data(), pos_counts.data(), pos_displs.data(), MPI_DOUBLE, comm); |
| 411 | + MPI_Allgatherv(my_z.data(), my_n, MPI_INT, all_z.data(), counts.data(), displs.data(), MPI_INT, comm); |
| 412 | + } else { all_pos = my_pos; all_z = my_z; } |
| 413 | + |
| 414 | + // 3. Global z -> type (sorted-unique z; identical on every rank). |
| 415 | + std::vector<int> uz = all_z; std::sort(uz.begin(), uz.end()); |
| 416 | + uz.erase(std::unique(uz.begin(), uz.end()), uz.end()); |
| 417 | + std::map<int, int> z2t; for (size_t i = 0; i < uz.size(); ++i) z2t[uz[i]] = static_cast<int>(i); |
| 418 | + std::vector<int> all_types(total); for (int g = 0; g < total; ++g) all_types[g] = z2t[all_z[g]]; |
| 419 | + std::vector<int> type_norb = type_norb_in; |
| 420 | + if (type_norb.empty()) type_norb.assign(uz.size(), 1); |
| 421 | + std::vector<int> my_types(my_n); for (int li = 0; li < my_n; ++li) my_types[li] = all_types[my_offset + li]; |
| 422 | + |
| 423 | + // 4. Edges of MY owned atoms (NeighborList over all positions; same cutoff |
| 424 | + // + distance test as process_input_rank0, so the graph is identical to |
| 425 | + // from_points for the same geometry). r_edges: 5 ints {gi, gj, rx, ry, rz}. |
| 426 | + double max_r = 0; for (double r : r_max_per_type) max_r = std::max(max_r, r); |
| 427 | + NeighborList nl; nl.build(all_pos, cell, pbc, max_r * 2.0); |
| 428 | + std::vector<int> r_edges; |
| 429 | + for (int li = 0; li < my_n; ++li) { |
| 430 | + const int gi = my_offset + li, ti = all_types[gi]; |
| 431 | + for (const auto& nb : nl.get_neighbors(gi)) { |
| 432 | + const int gj = nb.index, tj = all_types[gj]; |
| 433 | + const double rc = r_max_per_type[ti] + r_max_per_type[tj]; |
| 434 | + const double dx = all_pos[3 * gj] - all_pos[3 * gi] + nb.rx * cell[0] + nb.ry * cell[3] + nb.rz * cell[6]; |
| 435 | + const double dy = all_pos[3 * gj + 1] - all_pos[3 * gi + 1] + nb.rx * cell[1] + nb.ry * cell[4] + nb.rz * cell[7]; |
| 436 | + const double dz = all_pos[3 * gj + 2] - all_pos[3 * gi + 2] + nb.rx * cell[2] + nb.ry * cell[5] + nb.rz * cell[8]; |
| 437 | + if (std::sqrt(dx * dx + dy * dy + dz * dz) > rc + 1e-9) continue; |
| 438 | + r_edges.push_back(gi); r_edges.push_back(gj); |
| 439 | + r_edges.push_back(nb.rx); r_edges.push_back(nb.ry); r_edges.push_back(nb.rz); |
| 440 | + } |
| 441 | + } |
| 442 | + |
| 443 | + // 5. Build the distributed object (owned atoms + owned-src edges; the ctor |
| 444 | + // derives ghosts from the edge dsts). Same final assembly as from_points. |
| 445 | + return construct_final_object(comm, rank, size, cell, pbc, my_n, |
| 446 | + my_input_index, my_z, my_types, my_pos, r_edges, type_norb); |
| 447 | + } |
| 448 | + |
374 | 449 | static AtomicData* from_file(const std::string& filename, const std::vector<double>& r_max_per_type, std::vector<int> type_norb, MPI_Comm comm, const std::string& format="") { |
375 | 450 | int rank; |
376 | 451 | int initialized = 0; |
@@ -749,10 +824,144 @@ class AtomicData { |
749 | 824 | } |
750 | 825 |
|
751 | 826 | new_graph->construct_distributed(owned_indices, my_block_sizes, matrix_adj); |
752 | | - |
| 827 | + |
753 | 828 | return new_graph; |
754 | 829 | } |
755 | 830 |
|
| 831 | + // Build a full ``AtomicData`` whose neighbour graph IS the 3-centre (graph3b) structure, |
| 832 | + // with the SAME atom distribution as ``this`` — the proper-AtomicData sibling of |
| 833 | + // ``get_graph3b`` (doc/design/41). Unlike ``get_graph3b`` (which returns a shift-less |
| 834 | + // ``DistGraph`` and so forces the union-graph ``ImageContainer`` ctor + a hand-built |
| 835 | + // explicit shift set), this tracks each 3-centre block's integer lattice shift, so the |
| 836 | + // normal ``ImageContainer(AtomicData*)`` ctor builds the per-R image graphs directly. |
| 837 | + // |
| 838 | + // For an owned centre i with reduced neighbours (j, R_j) and (k, R_k) (within the |
| 839 | + // ``r_max_left[itype] + r_max_right[jtype]`` cut, same as get_graph3b), the 3-centre block |
| 840 | + // (gid_j -> gid_k) lives at shift R_k - R_j and is owned by gid_j's row owner; the routing |
| 841 | + // is the same one-Alltoallv idiom as get_graph3b, carrying 5 ints (gj, gk, dRx, dRy, dRz) |
| 842 | + // per block. The self-onsite (j==k, R=0) is dropped — the ImageContainer ctor re-adds the |
| 843 | + // R=0 diagonal. The resulting (gi, gj, R) edge set equals the graph3b pair set the V_nl / |
| 844 | + // force / DM consumers enumerate, so an ImageContainer on this AtomicData has exactly their |
| 845 | + // blocks (and ``build_dmr_images`` can read its pairs straight from the graph). |
| 846 | + AtomicData* get_atomicdata3b(const std::vector<double>& r_max_left, |
| 847 | + const std::vector<double>& r_max_right) { |
| 848 | + int initialized = 0; |
| 849 | + MPI_Initialized(&initialized); |
| 850 | + |
| 851 | + // 1. Reduced neighbour list WITH integer shift (do NOT collapse periodic images). |
| 852 | + std::vector<std::vector<std::pair<int, std::array<int, 3>>>> rnbr(n_atom); |
| 853 | + for (int i = 0; i < n_atom; ++i) { |
| 854 | + const int itype = atom_type[i]; |
| 855 | + for (int edge_idx : iconn[i]) { |
| 856 | + const int j = edges[edge_idx].dst; |
| 857 | + double rx, ry, rz; |
| 858 | + get_edge_vec(edge_idx, &rx, &ry, &rz); |
| 859 | + const double r = std::sqrt(rx * rx + ry * ry + rz * rz); |
| 860 | + const int jtype = atom_type[j]; |
| 861 | + if (r <= r_max_left[itype] + r_max_right[jtype] + 1e-9) { |
| 862 | + int sx, sy, sz; |
| 863 | + get_edge_shift_vec(edge_idx, &sx, &sy, &sz); |
| 864 | + rnbr[i].push_back({get_gid(j), {sx, sy, sz}}); |
| 865 | + } |
| 866 | + } |
| 867 | + } |
| 868 | + |
| 869 | + // 2. Each ordered neighbour pair (j,Rj),(k,Rk) of i -> block (gid_j -> gid_k) at Rk-Rj, |
| 870 | + // routed to gid_j's owner (5 ints each). |
| 871 | + std::map<int, std::vector<std::array<int, 5>>> send_map; |
| 872 | + for (int i = 0; i < n_atom; ++i) { |
| 873 | + const auto& nb = rnbr[i]; |
| 874 | + for (const auto& a : nb) { |
| 875 | + const int owner_j = graph->find_owner(a.first); |
| 876 | + for (const auto& b : nb) { |
| 877 | + send_map[owner_j].push_back({a.first, b.first, |
| 878 | + b.second[0] - a.second[0], b.second[1] - a.second[1], b.second[2] - a.second[2]}); |
| 879 | + } |
| 880 | + } |
| 881 | + } |
| 882 | + for (auto& kv : send_map) { |
| 883 | + std::sort(kv.second.begin(), kv.second.end()); |
| 884 | + kv.second.erase(std::unique(kv.second.begin(), kv.second.end()), kv.second.end()); |
| 885 | + } |
| 886 | + |
| 887 | + std::vector<int> send_counts(size, 0); |
| 888 | + for (auto& kv : send_map) send_counts[kv.first] = static_cast<int>(kv.second.size()) * 5; |
| 889 | + std::vector<int> recv_counts(size); |
| 890 | + if (initialized) { |
| 891 | + MPI_Alltoall(send_counts.data(), 1, MPI_INT, recv_counts.data(), 1, MPI_INT, comm); |
| 892 | + } else { |
| 893 | + recv_counts = send_counts; |
| 894 | + } |
| 895 | + std::vector<int> sdispls(size + 1, 0), rdispls(size + 1, 0); |
| 896 | + for (int i = 0; i < size; ++i) { |
| 897 | + sdispls[i + 1] = sdispls[i] + send_counts[i]; |
| 898 | + rdispls[i + 1] = rdispls[i] + recv_counts[i]; |
| 899 | + } |
| 900 | + std::vector<int> send_buf(sdispls[size]); |
| 901 | + for (auto& kv : send_map) { |
| 902 | + int off = sdispls[kv.first]; |
| 903 | + for (const auto& e : kv.second) for (int t = 0; t < 5; ++t) send_buf[off++] = e[t]; |
| 904 | + } |
| 905 | + std::vector<int> recv_buf(rdispls[size]); |
| 906 | + if (initialized) { |
| 907 | + MPI_Alltoallv(send_buf.data(), send_counts.data(), sdispls.data(), MPI_INT, |
| 908 | + recv_buf.data(), recv_counts.data(), rdispls.data(), MPI_INT, comm); |
| 909 | + } else { |
| 910 | + recv_buf = send_buf; |
| 911 | + } |
| 912 | + |
| 913 | + // 3. Dedup received 3-centre edges (gid_j owned here); drop the self-onsite. |
| 914 | + std::set<std::array<int, 5>> edge_set; |
| 915 | + for (size_t p = 0; p + 4 < recv_buf.size(); p += 5) { |
| 916 | + if (recv_buf[p] == recv_buf[p + 1] && |
| 917 | + recv_buf[p + 2] == 0 && recv_buf[p + 3] == 0 && recv_buf[p + 4] == 0) continue; |
| 918 | + edge_set.insert({recv_buf[p], recv_buf[p + 1], recv_buf[p + 2], recv_buf[p + 3], recv_buf[p + 4]}); |
| 919 | + } |
| 920 | + |
| 921 | + // graph3b ⊇ the 2-body S/T graph: add this rank's owned 2-body edges (i -> j, R_j) WITH |
| 922 | + // shift (owned locally, no routing). The 3-centre enumeration alone covers only pairs |
| 923 | + // sharing a common centre, so the pure 2-body pairs must be added explicitly — mirrors |
| 924 | + // get_graph3b's "add two-body and onsite edges". The R=0 onsite is re-added by the |
| 925 | + // ImageContainer ctor, so it is skipped here. |
| 926 | + for (int i = 0; i < n_atom; ++i) { |
| 927 | + const int gi = get_gid(i); |
| 928 | + for (int edge_idx : iconn[i]) { |
| 929 | + const int j = edges[edge_idx].dst; |
| 930 | + int sx, sy, sz; |
| 931 | + get_edge_shift_vec(edge_idx, &sx, &sy, &sz); |
| 932 | + if (gi == get_gid(j) && sx == 0 && sy == 0 && sz == 0) continue; |
| 933 | + edge_set.insert({gi, get_gid(j), sx, sy, sz}); |
| 934 | + } |
| 935 | + } |
| 936 | + |
| 937 | + std::vector<int> edge_index, edge_shift; |
| 938 | + edge_index.reserve(edge_set.size() * 2); |
| 939 | + edge_shift.reserve(edge_set.size() * 3); |
| 940 | + for (const auto& e : edge_set) { |
| 941 | + edge_index.push_back(e[0]); edge_index.push_back(e[1]); |
| 942 | + edge_shift.push_back(e[2]); edge_shift.push_back(e[3]); edge_shift.push_back(e[4]); |
| 943 | + } |
| 944 | + int n_edge3 = static_cast<int>(edge_set.size()); |
| 945 | + int N_edge3 = n_edge3; |
| 946 | + if (initialized) MPI_Allreduce(&n_edge3, &N_edge3, 1, MPI_INT, MPI_SUM, comm); |
| 947 | + |
| 948 | + // 4. Owned per-atom arrays copied from this AtomicData. |
| 949 | + std::vector<int> at_index(n_atom), at_type(n_atom), at_num(n_atom); |
| 950 | + std::vector<double> pos(3 * n_atom); |
| 951 | + for (int i = 0; i < n_atom; ++i) { |
| 952 | + at_index[i] = atom_index[i]; |
| 953 | + at_type[i] = atom_type[i]; |
| 954 | + at_num[i] = atomic_numbers[i]; |
| 955 | + pos[3 * i] = x[i]; pos[3 * i + 1] = y[i]; pos[3 * i + 2] = z[i]; |
| 956 | + } |
| 957 | + |
| 958 | + return new AtomicData( |
| 959 | + static_cast<size_t>(n_atom), static_cast<size_t>(N_atom), static_cast<size_t>(atom_offset), |
| 960 | + static_cast<size_t>(n_edge3), static_cast<size_t>(N_edge3), |
| 961 | + at_index.data(), at_type.data(), edge_index.data(), type_norb.data(), edge_shift.data(), |
| 962 | + cell.data(), pos.data(), at_num.data(), comm, pbc); |
| 963 | + } |
| 964 | + |
756 | 965 | private: |
757 | 966 | static std::vector<bool> infer_pbc_from_edge_shifts(size_t n_edge, const int *edge_shift_vec_in, int initialized, MPI_Comm comm) { |
758 | 967 | int local_flags[3] = {0, 0, 0}; |
|
0 commit comments