Skip to main content

problemreductions/rules/unitdiskmapping/ksg/
mapping.rs

1//! KSG (King's SubGraph) mapping functions for graphs to grid graphs.
2//!
3//! This module provides functions to map arbitrary graphs to King's SubGraph
4//! (8-connected grid graphs). It supports both unweighted and weighted mapping modes.
5
6use super::super::copyline::{create_copylines, mis_overhead_copyline, CopyLine};
7use super::super::grid::MappingGrid;
8use super::super::pathdecomposition::{
9    pathwidth, vertex_order_from_layout, PathDecompositionMethod,
10};
11use super::gadgets::{
12    apply_crossing_gadgets, apply_simplifier_gadgets, tape_entry_mis_overhead, KsgPattern,
13    KsgTapeEntry,
14};
15use super::gadgets_weighted::{
16    apply_weighted_crossing_gadgets, apply_weighted_simplifier_gadgets,
17    weighted_tape_entry_mis_overhead, WeightedKsgPattern, WeightedKsgTapeEntry,
18};
19use super::{PADDING, SPACING};
20use crate::rules::unitdiskmapping::{mapping_integer_overflow, mapping_invalid};
21use crate::rules::ReductionError;
22use crate::topology::{Graph, KingsSubgraph, TriangularSubgraph};
23use serde::{Deserialize, Serialize};
24use std::collections::{HashMap, HashSet};
25use std::fmt;
26
27/// The kind of grid lattice used in a mapping result.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum GridKind {
30    /// Square lattice (King's SubGraph connectivity, radius 1.5).
31    Kings,
32    /// Triangular lattice (radius 1.1).
33    Triangular,
34}
35
36/// Result of mapping a graph to a grid graph.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct MappingResult<T = KsgTapeEntry> {
39    /// Integer grid positions (row, col) for each node.
40    pub positions: Vec<(i64, i64)>,
41    /// Weight of each node.
42    pub node_weights: Vec<i64>,
43    /// Grid dimensions (rows, cols).
44    pub grid_dimensions: (usize, usize),
45    /// The kind of grid lattice.
46    pub kind: GridKind,
47    /// Copy lines used in the mapping.
48    pub lines: Vec<CopyLine>,
49    /// Padding used.
50    pub padding: usize,
51    /// Spacing used.
52    pub spacing: usize,
53    /// MIS overhead from the mapping.
54    pub mis_overhead: i64,
55    /// Tape entries recording gadget applications (for unapply during solution extraction).
56    pub tape: Vec<T>,
57    /// Doubled cells (where two copy lines overlap) for map_config_back.
58    #[serde(default)]
59    pub doubled_cells: HashSet<(usize, usize)>,
60}
61
62impl<T> MappingResult<T> {
63    /// Get the number of vertices in the original graph.
64    pub fn num_original_vertices(&self) -> usize {
65        self.lines.len()
66    }
67
68    /// Compute edges based on grid kind.
69    pub fn edges(&self) -> Vec<(usize, usize)> {
70        match self.kind {
71            GridKind::Kings => self.to_kings_subgraph().edges(),
72            GridKind::Triangular => self.to_triangular_subgraph().edges(),
73        }
74    }
75
76    /// Compute the number of edges based on grid kind.
77    pub fn num_edges(&self) -> usize {
78        match self.kind {
79            GridKind::Kings => self.to_kings_subgraph().num_edges(),
80            GridKind::Triangular => self.to_triangular_subgraph().num_edges(),
81        }
82    }
83
84    /// Print a configuration on the grid, highlighting selected nodes.
85    ///
86    /// Characters:
87    /// - `.` = empty cell (no grid node at this position)
88    /// - `*` = selected node (config != 0)
89    /// - `o` = unselected node (config == 0)
90    pub fn print_config(&self, config: &[Vec<usize>]) {
91        print!("{}", self.format_config(config));
92    }
93
94    /// Format a 2D configuration as a string.
95    pub fn format_config(&self, config: &[Vec<usize>]) -> String {
96        let (rows, cols) = self.grid_dimensions;
97
98        // Build position to node index map
99        let mut pos_to_node: HashMap<(i64, i64), usize> = HashMap::new();
100        for (idx, &(r, c)) in self.positions.iter().enumerate() {
101            pos_to_node.insert((r, c), idx);
102        }
103
104        let mut lines = Vec::new();
105
106        for r in 0..rows {
107            let row = i64::try_from(r).expect("mapping grid rows are validated against i64");
108            let mut line = String::new();
109            for c in 0..cols {
110                let is_selected = config
111                    .get(r)
112                    .and_then(|row| row.get(c))
113                    .copied()
114                    .unwrap_or(0)
115                    > 0;
116                let column =
117                    i64::try_from(c).expect("mapping grid columns are validated against i64");
118                let has_node = pos_to_node.contains_key(&(row, column));
119
120                let s = if has_node {
121                    if is_selected {
122                        "*"
123                    } else {
124                        "o"
125                    }
126                } else {
127                    "."
128                };
129                line.push_str(s);
130                line.push(' ');
131            }
132            // Remove trailing space
133            line.pop();
134            lines.push(line);
135        }
136
137        lines.join("\n")
138    }
139
140    /// Print a flat configuration vector on the grid.
141    pub fn print_config_flat(&self, config: &[usize]) {
142        print!("{}", self.format_config_flat(config));
143    }
144
145    /// Format a flat configuration vector as a string.
146    pub fn format_config_flat(&self, config: &[usize]) -> String {
147        self.format_grid_with_config(Some(config))
148    }
149
150    /// Create a [`KingsSubgraph`] from this mapping result, extracting positions
151    /// and discarding weights.
152    pub fn to_kings_subgraph(&self) -> KingsSubgraph {
153        KingsSubgraph::new(self.positions.clone())
154    }
155
156    /// Create a [`TriangularSubgraph`] from this mapping result, extracting positions
157    /// and discarding weights.
158    pub fn to_triangular_subgraph(&self) -> TriangularSubgraph {
159        TriangularSubgraph::new(self.positions.clone())
160    }
161
162    /// Format the grid, optionally with a configuration overlay.
163    ///
164    /// Without config: shows weight values (single-char) or `●` for multi-char weights.
165    /// With config: shows `●` for selected nodes, `○` for unselected.
166    /// Empty cells show `⋅`.
167    fn format_grid_with_config(&self, config: Option<&[usize]>) -> String {
168        if self.positions.is_empty() {
169            return String::from("(empty grid graph)");
170        }
171
172        let (rows, cols) = self.grid_dimensions;
173
174        let mut pos_to_idx: HashMap<(i64, i64), usize> = HashMap::new();
175        for (idx, &(r, c)) in self.positions.iter().enumerate() {
176            pos_to_idx.insert((r, c), idx);
177        }
178
179        let mut lines = Vec::new();
180
181        for r in 0..rows {
182            let r = i64::try_from(r).expect("mapping grid rows are validated against i64");
183            let mut line = String::new();
184            for c in 0..cols {
185                let c = i64::try_from(c).expect("mapping grid columns are validated against i64");
186                let s = if let Some(&idx) = pos_to_idx.get(&(r, c)) {
187                    if let Some(cfg) = config {
188                        if cfg.get(idx).copied().unwrap_or(0) > 0 {
189                            "●".to_string()
190                        } else {
191                            "○".to_string()
192                        }
193                    } else {
194                        let w = self.node_weights[idx];
195                        let ws = format!("{}", w);
196                        if ws.len() == 1 {
197                            ws
198                        } else {
199                            "●".to_string()
200                        }
201                    }
202                } else {
203                    "⋅".to_string()
204                };
205                line.push_str(&s);
206                line.push(' ');
207            }
208            line.pop();
209            lines.push(line);
210        }
211
212        lines.join("\n")
213    }
214}
215
216impl MappingResult<KsgTapeEntry> {
217    /// Map a configuration back from grid to original graph.
218    ///
219    /// This follows the algorithm:
220    /// 1. Convert flat grid config to 2D matrix
221    /// 2. Unapply gadgets in reverse order (modifying config matrix)
222    /// 3. Extract vertex configs from copyline locations
223    ///
224    /// # Arguments
225    /// * `grid_config` - Configuration on the grid graph (0 = not selected, 1 = selected)
226    ///
227    /// # Returns
228    /// A vector where `result[v]` is 1 if vertex `v` is selected, 0 otherwise.
229    pub fn map_config_back(
230        &self,
231        grid_config: &[usize],
232    ) -> crate::rules::ExtractionResult<Vec<usize>> {
233        self.map_config_back_internal(grid_config)
234            .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))
235    }
236
237    fn map_config_back_internal(
238        &self,
239        grid_config: &[usize],
240    ) -> Result<Vec<usize>, ReductionError> {
241        if grid_config.len() != self.positions.len() {
242            return Err(mapping_invalid(
243                "grid configuration length must match the mapped vertex count",
244            ));
245        }
246        // Step 1: Convert flat config to 2D matrix
247        let (rows, cols) = self.grid_dimensions;
248        let mut config_2d = vec![vec![0usize; cols]; rows];
249
250        for (idx, &(row, col)) in self.positions.iter().enumerate() {
251            let row = usize::try_from(row)
252                .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?;
253            let col = usize::try_from(col)
254                .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?;
255            if row >= rows || col >= cols {
256                return Err(mapping_invalid(
257                    "mapping result contains a position outside its grid dimensions",
258                ));
259            }
260            config_2d[row][col] = grid_config[idx];
261        }
262
263        // Step 2: Unapply gadgets in reverse order
264        unapply_gadgets(&self.tape, &mut config_2d)?;
265
266        // Step 3: Extract vertex configs from copylines
267        map_config_copyback(
268            &self.lines,
269            self.padding,
270            self.spacing,
271            &config_2d,
272            &self.doubled_cells,
273        )
274    }
275}
276
277impl MappingResult<WeightedKsgTapeEntry> {
278    /// Map a configuration back from grid to original graph (weighted version).
279    pub fn map_config_back(
280        &self,
281        grid_config: &[usize],
282    ) -> crate::rules::ExtractionResult<Vec<usize>> {
283        self.map_config_back_internal(grid_config)
284            .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))
285    }
286
287    fn map_config_back_internal(
288        &self,
289        grid_config: &[usize],
290    ) -> Result<Vec<usize>, ReductionError> {
291        if grid_config.len() != self.positions.len() {
292            return Err(mapping_invalid(
293                "grid configuration length must match the mapped vertex count",
294            ));
295        }
296        // Step 1: Convert flat config to 2D matrix
297        let (rows, cols) = self.grid_dimensions;
298        let mut config_2d = vec![vec![0usize; cols]; rows];
299
300        for (idx, &(row, col)) in self.positions.iter().enumerate() {
301            let row = usize::try_from(row)
302                .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?;
303            let col = usize::try_from(col)
304                .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?;
305            if row >= rows || col >= cols {
306                return Err(mapping_invalid(
307                    "mapping result contains a position outside its grid dimensions",
308                ));
309            }
310            config_2d[row][col] = grid_config[idx];
311        }
312
313        // Step 2: Unapply gadgets in reverse order
314        unapply_weighted_gadgets(&self.tape, &mut config_2d)?;
315
316        // Step 3: Extract vertex configs from copylines
317        map_config_copyback(
318            &self.lines,
319            self.padding,
320            self.spacing,
321            &config_2d,
322            &self.doubled_cells,
323        )
324    }
325}
326
327impl<T> fmt::Display for MappingResult<T> {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        write!(f, "{}", self.format_grid_with_config(None))
330    }
331}
332
333/// Extract original vertex configurations from copyline locations.
334///
335/// For each copyline, count selected nodes handling doubled cells specially:
336/// - For doubled cells: count 1 if value is 2, or if value is 1 and both neighbors are 0
337/// - For regular cells: just add the value
338/// - Result is `count - (len(locs) / 2)`
339pub(crate) fn map_config_copyback(
340    lines: &[CopyLine],
341    padding: usize,
342    spacing: usize,
343    config: &[Vec<usize>],
344    doubled_cells: &HashSet<(usize, usize)>,
345) -> Result<Vec<usize>, ReductionError> {
346    let mut result = vec![0usize; lines.len()];
347
348    for line in lines {
349        let locs = line.copyline_locations(padding, spacing);
350        let n = locs.len();
351        let mut count = 0i64;
352
353        for (iloc, &(row, col, weight)) in locs.iter().enumerate() {
354            let ci = config
355                .get(row)
356                .and_then(|r| r.get(col))
357                .copied()
358                .ok_or(mapping_invalid(
359                    "copy line lies outside the configuration grid",
360                ))?;
361
362            // Check if this cell is doubled in the grid (two copylines overlap here)
363            if doubled_cells.contains(&(row, col)) {
364                // Doubled cell - handle specially
365                if ci == 2 {
366                    count = count
367                        .checked_add(1)
368                        .ok_or(mapping_integer_overflow("summing copy-back values"))?;
369                } else if ci == 1 {
370                    // Check if both neighbors are 0
371                    let prev_zero =
372                        if iloc > 0 {
373                            let (pr, pc, _) = locs[iloc - 1];
374                            config.get(pr).and_then(|r| r.get(pc)).copied().ok_or(
375                                mapping_invalid(
376                                    "copy-line neighbor lies outside the configuration grid",
377                                ),
378                            )? == 0
379                        } else {
380                            true
381                        };
382                    let next_zero =
383                        if iloc + 1 < n {
384                            let (nr, nc, _) = locs[iloc + 1];
385                            config.get(nr).and_then(|r| r.get(nc)).copied().ok_or(
386                                mapping_invalid(
387                                    "copy-line neighbor lies outside the configuration grid",
388                                ),
389                            )? == 0
390                        } else {
391                            true
392                        };
393                    if prev_zero && next_zero {
394                        count = count
395                            .checked_add(1)
396                            .ok_or(mapping_integer_overflow("summing copy-back values"))?;
397                    }
398                }
399                // ci == 0: count += 0 (nothing)
400            } else if weight >= 1 {
401                // Regular non-empty cell
402                let value = i64::try_from(ci)
403                    .map_err(|_| mapping_integer_overflow("converting a copy-back value to i64"))?;
404                count = count
405                    .checked_add(value)
406                    .ok_or(mapping_integer_overflow("summing copy-back values"))?;
407            }
408            // weight == 0 or empty: skip
409        }
410
411        // Subtract overhead: MIS overhead for copyline is len/2
412        let overhead = i64::try_from(n / 2)
413            .map_err(|_| mapping_integer_overflow("converting copy-back overhead to i64"))?;
414        // Result is count - overhead, clamped to non-negative
415        let adjusted = count
416            .checked_sub(overhead)
417            .ok_or(mapping_integer_overflow("subtracting copy-back overhead"))?;
418        let adjusted = adjusted.max(0);
419        result[line.vertex] = usize::try_from(adjusted)
420            .map_err(|_| mapping_integer_overflow("converting a copy-back result to usize"))?;
421    }
422
423    Ok(result)
424}
425
426/// Unapply gadgets from tape in reverse order, converting mapped configs to source configs.
427pub(crate) fn unapply_gadgets(
428    tape: &[KsgTapeEntry],
429    config: &mut [Vec<usize>],
430) -> Result<(), ReductionError> {
431    // Iterate tape in REVERSE order
432    for entry in tape.iter().rev() {
433        let pattern = KsgPattern::from_tape_idx(entry.pattern_idx).ok_or(mapping_invalid(
434            "tape contains an unknown unweighted KSG gadget index",
435        ))?;
436        pattern.map_config_back(entry.row, entry.col, config)?;
437    }
438    Ok(())
439}
440
441/// Unapply weighted gadgets from tape in reverse order.
442pub(crate) fn unapply_weighted_gadgets(
443    tape: &[WeightedKsgTapeEntry],
444    config: &mut [Vec<usize>],
445) -> Result<(), ReductionError> {
446    // Iterate tape in REVERSE order
447    for entry in tape.iter().rev() {
448        let pattern = WeightedKsgPattern::from_tape_idx(entry.pattern_idx).ok_or(
449            mapping_invalid("tape contains an unknown weighted KSG gadget index"),
450        )?;
451        pattern.map_config_back(entry.row, entry.col, config)?;
452    }
453    Ok(())
454}
455
456/// Internal function that creates both the mapping grid and copylines.
457fn embed_graph_internal(
458    num_vertices: usize,
459    edges: &[(usize, usize)],
460    vertex_order: &[usize],
461) -> Result<(MappingGrid, Vec<CopyLine>), ReductionError> {
462    if num_vertices == 0 {
463        return Err(mapping_invalid("num_vertices must be positive"));
464    }
465
466    let copylines = create_copylines(num_vertices, edges, vertex_order)?;
467
468    // Calculate grid dimensions
469    let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1);
470
471    let padding_twice = PADDING
472        .checked_mul(2)
473        .ok_or(mapping_integer_overflow("computing grid padding"))?;
474    let extent = |slots: usize| {
475        slots
476            .checked_mul(SPACING)
477            .and_then(|value| value.checked_add(2))
478            .and_then(|value| value.checked_add(padding_twice))
479            .ok_or(mapping_integer_overflow("computing grid dimensions"))
480    };
481    let rows = extent(max_hslot)?;
482    let cols = extent(num_vertices - 1)?;
483
484    let mut grid = MappingGrid::with_padding(rows, cols, SPACING, PADDING);
485
486    // Add copy line nodes using dense locations (all cells along the L-shape)
487    for line in &copylines {
488        for (row, col, weight) in line.copyline_locations(PADDING, SPACING) {
489            let weight = i64::try_from(weight)
490                .map_err(|_| mapping_integer_overflow("converting a grid weight to i64"))?;
491            grid.add_node(row, col, weight);
492        }
493    }
494
495    // Mark edge connections
496    for &(u, v) in edges {
497        let u_line = &copylines[u];
498        let v_line = &copylines[v];
499
500        let (smaller_line, larger_line) = if u_line.vslot < v_line.vslot {
501            (u_line, v_line)
502        } else {
503            (v_line, u_line)
504        };
505        let (row, col) = grid.cross_at(smaller_line.vslot, larger_line.vslot, smaller_line.hslot);
506
507        // Mark connected cells
508        if col > 0 {
509            grid.connect(row, col - 1);
510        }
511        if row > 0 && grid.is_occupied(row - 1, col) {
512            grid.connect(row - 1, col);
513        } else if row + 1 < grid.size().0 && grid.is_occupied(row + 1, col) {
514            grid.connect(row + 1, col);
515        }
516    }
517
518    Ok((grid, copylines))
519}
520
521/// Embed a graph into a mapping grid.
522///
523/// # Errors
524///
525/// Returns [`ReductionError`] if the vertex order, graph, or generated dimensions are invalid.
526#[cfg(test)]
527pub(crate) fn embed_graph(
528    num_vertices: usize,
529    edges: &[(usize, usize)],
530    vertex_order: &[usize],
531) -> Result<MappingGrid, ReductionError> {
532    embed_graph_internal(num_vertices, edges, vertex_order).map(|(grid, _)| grid)
533}
534
535// ============================================================================
536// Unweighted Mapping Functions
537// ============================================================================
538
539/// Map a graph to a KSG grid graph using automatic path decomposition.
540///
541/// Uses exact branch-and-bound for small graphs (≤30 vertices) and greedy for larger.
542pub fn map_unweighted(
543    num_vertices: usize,
544    edges: &[(usize, usize)],
545) -> Result<MappingResult<KsgTapeEntry>, ReductionError> {
546    map_unweighted_with_method(num_vertices, edges, PathDecompositionMethod::Auto)
547}
548
549/// Map a graph using a specific path decomposition method (unweighted).
550///
551/// # Arguments
552/// * `num_vertices` - Number of vertices in the graph
553/// * `edges` - List of edges as (u, v) pairs
554/// * `method` - The path decomposition method to use for vertex ordering
555pub fn map_unweighted_with_method(
556    num_vertices: usize,
557    edges: &[(usize, usize)],
558    method: PathDecompositionMethod,
559) -> Result<MappingResult<KsgTapeEntry>, ReductionError> {
560    let layout = pathwidth(num_vertices, edges, method);
561    let vertex_order = vertex_order_from_layout(&layout);
562    map_unweighted_with_order(num_vertices, edges, &vertex_order)
563}
564
565/// Map a graph with a specific vertex ordering (unweighted).
566///
567/// # Errors
568///
569/// Returns [`ReductionError`] if the vertex order, graph, or generated dimensions are invalid.
570pub fn map_unweighted_with_order(
571    num_vertices: usize,
572    edges: &[(usize, usize)],
573    vertex_order: &[usize],
574) -> Result<MappingResult<KsgTapeEntry>, ReductionError> {
575    let (mut grid, copylines) = embed_graph_internal(num_vertices, edges, vertex_order)?;
576
577    // Extract doubled cells BEFORE applying gadgets
578    let doubled_cells = grid.doubled_cells();
579
580    // Apply crossing gadgets to resolve line intersections
581    let crossing_tape = apply_crossing_gadgets(&mut grid, &copylines);
582
583    // Apply simplifier gadgets to clean up the grid
584    let simplifier_tape = apply_simplifier_gadgets(&mut grid, 2);
585
586    // Combine tape entries
587    let mut tape = crossing_tape;
588    tape.extend(simplifier_tape);
589
590    // Calculate MIS overhead from copylines
591    let copyline_overhead = copylines.iter().try_fold(0_i64, |total, line| {
592        total
593            .checked_add(mis_overhead_copyline(line, SPACING, PADDING)?)
594            .ok_or(mapping_integer_overflow("summing copy-line MIS overhead"))
595    })?;
596
597    // Add MIS overhead from gadgets
598    let gadget_overhead = tape.iter().try_fold(0_i64, |total, entry| {
599        total
600            .checked_add(tape_entry_mis_overhead(entry)?)
601            .ok_or(mapping_integer_overflow("summing gadget MIS overhead"))
602    })?;
603    let mis_overhead = copyline_overhead
604        .checked_add(gadget_overhead)
605        .ok_or(mapping_integer_overflow("computing total MIS overhead"))?;
606
607    if grid.has_unresolved_cells() {
608        return Err(mapping_invalid(
609            "mapping left doubled or connected cells unresolved",
610        ));
611    }
612
613    // Extract positions from occupied cells.
614    // In unweighted mode, all node weights are 1 — matching Julia's behavior where
615    // `node(::Type{<:UnWeightedNode}, i, j, w) = Node(i, j)` ignores the weight parameter.
616    let positions: Vec<(i64, i64)> = grid
617        .occupied_coords()
618        .into_iter()
619        .filter_map(|(row, col)| {
620            grid.get(row, col)
621                .filter(|cell| cell.weight() > 0)
622                .map(|_| {
623                    Ok((
624                        i64::try_from(row).map_err(|_| {
625                            mapping_integer_overflow("converting a grid row to i64")
626                        })?,
627                        i64::try_from(col).map_err(|_| {
628                            mapping_integer_overflow("converting a grid column to i64")
629                        })?,
630                    ))
631                })
632        })
633        .collect::<Result<_, ReductionError>>()?;
634    let node_weights = vec![1i64; positions.len()];
635
636    Ok(MappingResult {
637        positions,
638        node_weights,
639        grid_dimensions: grid.size(),
640        kind: GridKind::Kings,
641        lines: copylines,
642        padding: PADDING,
643        spacing: SPACING,
644        mis_overhead,
645        tape,
646        doubled_cells,
647    })
648}
649
650// ============================================================================
651// Weighted Mapping Functions
652// ============================================================================
653
654/// Map a graph to a KSG grid graph using optimal path decomposition (weighted mode).
655///
656/// Weighted mode uses gadgets with appropriate weight values that preserve
657/// the MWIS (Maximum Weight Independent Set) correspondence.
658pub fn map_weighted(
659    num_vertices: usize,
660    edges: &[(usize, usize)],
661) -> Result<MappingResult<WeightedKsgTapeEntry>, ReductionError> {
662    map_weighted_with_method(num_vertices, edges, PathDecompositionMethod::Auto)
663}
664
665/// Map a graph using a specific path decomposition method (weighted).
666///
667/// # Arguments
668/// * `num_vertices` - Number of vertices in the graph
669/// * `edges` - List of edges as (u, v) pairs
670/// * `method` - The path decomposition method to use for vertex ordering
671pub fn map_weighted_with_method(
672    num_vertices: usize,
673    edges: &[(usize, usize)],
674    method: PathDecompositionMethod,
675) -> Result<MappingResult<WeightedKsgTapeEntry>, ReductionError> {
676    let layout = pathwidth(num_vertices, edges, method);
677    let vertex_order = vertex_order_from_layout(&layout);
678    map_weighted_with_order(num_vertices, edges, &vertex_order)
679}
680
681/// Map a graph with a specific vertex ordering (weighted).
682///
683/// # Errors
684///
685/// Returns [`ReductionError`] if the vertex order, graph, or generated dimensions are invalid.
686pub fn map_weighted_with_order(
687    num_vertices: usize,
688    edges: &[(usize, usize)],
689    vertex_order: &[usize],
690) -> Result<MappingResult<WeightedKsgTapeEntry>, ReductionError> {
691    let (mut grid, copylines) = embed_graph_internal(num_vertices, edges, vertex_order)?;
692
693    // Extract doubled cells BEFORE applying gadgets
694    let doubled_cells = grid.doubled_cells();
695
696    // Apply weighted crossing gadgets to resolve line intersections
697    let crossing_tape = apply_weighted_crossing_gadgets(&mut grid, &copylines);
698
699    // Apply weighted simplifier gadgets to clean up the grid
700    let simplifier_tape = apply_weighted_simplifier_gadgets(&mut grid, 2);
701
702    // Combine tape entries
703    let mut tape = crossing_tape;
704    tape.extend(simplifier_tape);
705
706    // Calculate MIS overhead from copylines (weighted: multiply by 2)
707    let copyline_overhead = copylines.iter().try_fold(0_i64, |total, line| {
708        let overhead = mis_overhead_copyline(line, SPACING, PADDING)?;
709        let overhead = overhead.checked_mul(2).ok_or(mapping_integer_overflow(
710            "doubling weighted copy-line MIS overhead",
711        ))?;
712        total.checked_add(overhead).ok_or(mapping_integer_overflow(
713            "summing weighted copy-line MIS overhead",
714        ))
715    })?;
716
717    // Add MIS overhead from weighted gadgets
718    let gadget_overhead = tape.iter().try_fold(0_i64, |total, entry| {
719        total
720            .checked_add(weighted_tape_entry_mis_overhead(entry)?)
721            .ok_or(mapping_integer_overflow(
722                "summing weighted gadget MIS overhead",
723            ))
724    })?;
725    let mis_overhead =
726        copyline_overhead
727            .checked_add(gadget_overhead)
728            .ok_or(mapping_integer_overflow(
729                "computing total weighted MIS overhead",
730            ))?;
731
732    if grid.has_unresolved_cells() {
733        return Err(mapping_invalid(
734            "weighted mapping left doubled or connected cells unresolved",
735        ));
736    }
737
738    // Extract positions and weights from occupied cells
739    let positions_and_weights = grid
740        .occupied_coords()
741        .into_iter()
742        .filter_map(|(row, col)| {
743            grid.get(row, col)
744                .filter(|cell| cell.weight() > 0)
745                .map(|cell| {
746                    Ok((
747                        (
748                            i64::try_from(row).map_err(|_| {
749                                mapping_integer_overflow("converting a grid row to i64")
750                            })?,
751                            i64::try_from(col).map_err(|_| {
752                                mapping_integer_overflow("converting a grid column to i64")
753                            })?,
754                        ),
755                        cell.weight(),
756                    ))
757                })
758        })
759        .collect::<Result<Vec<_>, ReductionError>>()?;
760    let (positions, node_weights): (Vec<_>, Vec<_>) = positions_and_weights.into_iter().unzip();
761
762    Ok(MappingResult {
763        positions,
764        node_weights,
765        grid_dimensions: grid.size(),
766        kind: GridKind::Kings,
767        lines: copylines,
768        padding: PADDING,
769        spacing: SPACING,
770        mis_overhead,
771        tape,
772        doubled_cells,
773    })
774}
775
776#[cfg(test)]
777#[path = "../../../unit_tests/rules/unitdiskmapping/ksg/mapping.rs"]
778mod tests;