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