Skip to main content

plot3d/
graph.rs

1//! Graph partitioning utilities for structured multi-block grids.
2//!
3//! The `partition_from_face_matches` function requires the `metis-partition` Cargo feature.
4
5use std::collections::HashMap;
6use std::io::Write;
7use std::path::Path;
8
9use crate::face_record::FaceMatch;
10
11/// Strategy for combining weights when multiple faces connect the same
12/// pair of blocks.
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14pub enum WeightAggregate {
15    Sum,
16    Max,
17    Min,
18}
19
20/// Adjacency and edge-weight data for a block graph.
21#[derive(Clone, Debug)]
22pub struct BlockGraph {
23    /// Adjacency list: `adj_list[u]` is a sorted, deduplicated list of
24    /// neighbors for block `u`.
25    pub adj_list: Vec<Vec<usize>>,
26    /// Edge weights: `edge_weights[u][v]` = communication cost between `u` and `v`.
27    pub edge_weights: Vec<HashMap<usize, i64>>,
28}
29
30/// CSR (Compressed Sparse Row) representation of a block graph.
31#[derive(Clone, Debug)]
32pub struct CsrGraph {
33    pub xadj: Vec<usize>,
34    pub adjncy: Vec<usize>,
35    pub eweights: Vec<i64>,
36}
37
38/// Build a weighted graph from face-match data.
39///
40/// Each face match contributes a weight equal to the product of the face
41/// dimension ranges (`dI * dJ * dK`), representing the communication cost
42/// (number of shared nodes).
43pub fn build_weighted_graph_from_face_matches(
44    face_matches: &[FaceMatch],
45    n_blocks: usize,
46    aggregate: WeightAggregate,
47    ignore_self_matches: bool,
48) -> BlockGraph {
49    let mut pair_weight: HashMap<(usize, usize), i64> = HashMap::new();
50
51    for m in face_matches {
52        let i = m.block1.block_index;
53        let j = m.block2.block_index;
54        if ignore_self_matches && i == j {
55            continue;
56        }
57
58        let di = (m.block1.ih as i64 - m.block1.il as i64)
59            .unsigned_abs()
60            .max(1);
61        let dj = (m.block1.jh as i64 - m.block1.jl as i64)
62            .unsigned_abs()
63            .max(1);
64        let dk = (m.block1.kh as i64 - m.block1.kl as i64)
65            .unsigned_abs()
66            .max(1);
67        let w = (di * dj * dk) as i64;
68
69        let (a, b) = if i < j { (i, j) } else { (j, i) };
70        let entry = pair_weight.entry((a, b)).or_insert(0);
71        match aggregate {
72            WeightAggregate::Sum => *entry += w,
73            WeightAggregate::Max => *entry = (*entry).max(w),
74            WeightAggregate::Min => {
75                if *entry == 0 {
76                    *entry = w;
77                } else {
78                    *entry = (*entry).min(w);
79                }
80            }
81        }
82    }
83
84    let mut adj_list: Vec<Vec<usize>> = vec![Vec::new(); n_blocks];
85    let mut edge_weights: Vec<HashMap<usize, i64>> = vec![HashMap::new(); n_blocks];
86
87    for (&(a, b), &w) in &pair_weight {
88        adj_list[a].push(b);
89        adj_list[b].push(a);
90        edge_weights[a].insert(b, w);
91        edge_weights[b].insert(a, w);
92    }
93
94    // Sort and deduplicate adjacency lists
95    for list in &mut adj_list {
96        list.sort_unstable();
97        list.dedup();
98    }
99
100    BlockGraph {
101        adj_list,
102        edge_weights,
103    }
104}
105
106/// Convert a [`BlockGraph`] to CSR arrays suitable for METIS.
107pub fn csr_from_block_graph(graph: &BlockGraph) -> CsrGraph {
108    let mut xadj: Vec<usize> = vec![0];
109    let mut adjncy: Vec<usize> = Vec::new();
110    let mut eweights: Vec<i64> = Vec::new();
111
112    let mut count: usize = 0;
113    for u in 0..graph.adj_list.len() {
114        for &v in &graph.adj_list[u] {
115            adjncy.push(v);
116            let w = graph.edge_weights[u].get(&v).copied().unwrap_or(1);
117            eweights.push(w);
118            count += 1;
119        }
120        xadj.push(count);
121    }
122
123    CsrGraph {
124        xadj,
125        adjncy,
126        eweights,
127    }
128}
129
130/// Partition blocks using METIS graph partitioning.
131///
132/// Requires the `metis-partition` Cargo feature to be enabled.
133///
134/// Returns `(parts, graph)` where `parts[i]` is the 0-based partition ID
135/// for block `i`.
136#[cfg(feature = "metis-partition")]
137pub fn partition_from_face_matches(
138    face_matches: &[FaceMatch],
139    block_sizes: &[usize],
140    nparts: usize,
141    favor_blocksize: bool,
142    aggregate: WeightAggregate,
143    ignore_self_matches: bool,
144) -> Result<(Vec<i32>, BlockGraph), String> {
145    let n_blocks = block_sizes.len();
146    let graph = build_weighted_graph_from_face_matches(
147        face_matches,
148        n_blocks,
149        aggregate,
150        ignore_self_matches,
151    );
152    let csr = csr_from_block_graph(&graph);
153
154    // Build metis-rs Graph
155    let mut g = metis_rs::Graph::new(n_blocks, csr.xadj, csr.adjncy).with_adjwgt(csr.eweights);
156
157    if favor_blocksize {
158        let vwgt: Vec<i64> = block_sizes.iter().map(|&s| s as i64).collect();
159        g = g.with_vwgt(vwgt);
160    }
161
162    let (_edge_cut, part) = metis_rs::partition(&g, nparts);
163
164    Ok((part.iter().map(|&p| p as i32).collect(), graph))
165}
166
167/// Write ddcmp.dat and ddcmp_info.txt files for domain decomposition.
168///
169/// `parts` are 0-based internally, written 1-based in the file.
170pub fn write_ddcmp(
171    parts: &[i32],
172    block_sizes: &[usize],
173    graph: &BlockGraph,
174    filename: &str,
175) -> std::io::Result<()> {
176    let n_blocks = parts.len();
177    let n_proc = parts.iter().copied().max().unwrap_or(-1) + 1;
178    let n_isp = n_proc;
179
180    // Write ddcmp.dat
181    let path = Path::new(filename);
182    if let Some(parent) = path.parent() {
183        if !parent.as_os_str().is_empty() {
184            std::fs::create_dir_all(parent)?;
185        }
186    }
187
188    let mut f = std::fs::File::create(path)?;
189    writeln!(f, "{}", n_proc)?;
190    writeln!(f, "{}", n_isp)?;
191    writeln!(f, "{}", n_blocks)?;
192    for (b_idx, part) in parts.iter().enumerate().take(n_blocks) {
193        writeln!(f, "{} {}", b_idx + 1, part + 1)?;
194    }
195    for isp in 0..n_isp {
196        writeln!(f, "{} {}", isp + 1, isp)?;
197    }
198
199    // Compute statistics
200    let np = n_proc as usize;
201    let mut communication_work = vec![0i64; np];
202    let mut partition_edge_weights = vec![0i64; np];
203    let mut volume_nodes = vec![0usize; np];
204
205    for (b, &bsize) in block_sizes.iter().enumerate() {
206        let pid = parts[b] as usize;
207        volume_nodes[pid] += bsize;
208    }
209
210    for b in 0..block_sizes.len() {
211        let pid = parts[b] as usize;
212        for &nbr in &graph.adj_list[b] {
213            let nbr_pid = parts[nbr] as usize;
214            if nbr_pid != pid {
215                communication_work[pid] += 1;
216                partition_edge_weights[pid] +=
217                    graph.edge_weights[b].get(&nbr).copied().unwrap_or(1);
218            }
219        }
220    }
221
222    // Write ddcmp_info.txt
223    let info_path = path.with_file_name("ddcmp_info.txt");
224    let mut fi = std::fs::File::create(&info_path)?;
225    for i in 0..np {
226        let block_count = parts.iter().filter(|&&p| p == i as i32).count();
227        writeln!(fi, "Parition {} has {} blocks", i, block_count)?;
228    }
229    writeln!(fi, "Number of partitions/processors {}", n_proc)?;
230    for i in 0..np {
231        writeln!(
232            fi,
233            "Parition or processor {} has communication work {} edge_work {} volume_nodes {}",
234            i, communication_work[i], partition_edge_weights[i], volume_nodes[i]
235        )?;
236    }
237    let total_comm: i64 = communication_work.iter().sum();
238    let total_edge: i64 = partition_edge_weights.iter().sum();
239    writeln!(
240        fi,
241        "Total communication work {} Total edge_work {}",
242        total_comm, total_edge
243    )?;
244
245    Ok(())
246}