problemreductions/rules/unitdiskmapping/triangular/
mapping.rs

1//! Mapping functions for weighted triangular lattice.
2//!
3//! This module provides functions to map arbitrary graphs to weighted triangular
4//! lattice grid graphs using the copy-line technique.
5
6use super::super::copyline::{create_copylines, CopyLine};
7use super::super::grid::MappingGrid;
8use super::super::ksg::mapping::MappingResult;
9use super::super::ksg::KsgTapeEntry as TapeEntry;
10use super::super::pathdecomposition::{
11    pathwidth, vertex_order_from_layout, PathDecompositionMethod,
12};
13use super::gadgets::{apply_crossing_gadgets, apply_simplifier_gadgets, tape_entry_mis_overhead};
14use crate::rules::unitdiskmapping::ksg::mapping::GridKind;
15
16/// Spacing between copy lines on triangular lattice.
17pub const SPACING: usize = 6;
18
19/// Padding around the grid for triangular lattice.
20pub const PADDING: usize = 2;
21
22/// Calculate crossing point for two copylines on triangular lattice.
23fn crossat(
24    copylines: &[CopyLine],
25    v: usize,
26    w: usize,
27    spacing: usize,
28    padding: usize,
29) -> (usize, usize) {
30    let line_v = &copylines[v];
31    let line_w = &copylines[w];
32
33    // Use vslot to determine order
34    let (line_first, line_second) = if line_v.vslot < line_w.vslot {
35        (line_v, line_w)
36    } else {
37        (line_w, line_v)
38    };
39
40    let hslot = line_first.hslot;
41    let max_vslot = line_second.vslot;
42
43    // 0-indexed coordinates (subtract 1 from Julia's 1-indexed formula)
44    let row = (hslot - 1) * spacing + 1 + padding; // 0-indexed
45    let col = (max_vslot - 1) * spacing + padding; // 0-indexed
46
47    (row, col)
48}
49
50/// Map a graph to a weighted triangular lattice grid graph using optimal path decomposition.
51///
52/// This is the main entry point for triangular lattice mapping. It uses
53/// automatic path decomposition (exact for ≤30 vertices, greedy for larger).
54///
55/// # Arguments
56/// * `num_vertices` - Number of vertices in the original graph
57/// * `edges` - Edge list as (u, v) pairs
58///
59/// # Returns
60/// A `MappingResult` containing the grid graph and mapping metadata.
61///
62/// # Panics
63/// Panics if `num_vertices == 0`.
64///
65/// # Example
66/// ```rust
67/// use problemreductions::rules::unitdiskmapping::triangular::mapping::map_weighted;
68/// use problemreductions::topology::Graph;
69///
70/// let edges = vec![(0, 1), (1, 2)];
71/// let result = map_weighted(3, &edges);
72/// assert!(result.to_triangular_subgraph().num_vertices() > 0);
73/// ```
74pub fn map_weighted(num_vertices: usize, edges: &[(usize, usize)]) -> MappingResult {
75    map_weighted_with_method(num_vertices, edges, PathDecompositionMethod::Auto)
76}
77
78/// Map a graph to weighted triangular lattice using a specific path decomposition method.
79///
80/// # Arguments
81/// * `num_vertices` - Number of vertices in the original graph
82/// * `edges` - Edge list as (u, v) pairs
83/// * `method` - Path decomposition method to use
84///
85/// # Returns
86/// A `MappingResult` containing the grid graph and mapping metadata.
87pub fn map_weighted_with_method(
88    num_vertices: usize,
89    edges: &[(usize, usize)],
90    method: PathDecompositionMethod,
91) -> MappingResult {
92    let layout = pathwidth(num_vertices, edges, method);
93    let vertex_order = vertex_order_from_layout(&layout);
94    map_weighted_with_order(num_vertices, edges, &vertex_order)
95}
96
97/// Map a graph to weighted triangular lattice with specific vertex ordering.
98///
99/// This is the most flexible mapping function, allowing custom vertex ordering
100/// for cases where a specific layout is desired.
101///
102/// # Arguments
103/// * `num_vertices` - Number of vertices in the original graph
104/// * `edges` - Edge list as (u, v) pairs
105/// * `vertex_order` - Custom vertex ordering
106///
107/// # Returns
108/// A `MappingResult` containing the grid graph and mapping metadata.
109///
110/// # Panics
111/// Panics if `num_vertices == 0` or if any edge vertex is not in `vertex_order`.
112pub fn map_weighted_with_order(
113    num_vertices: usize,
114    edges: &[(usize, usize)],
115    vertex_order: &[usize],
116) -> MappingResult {
117    assert!(num_vertices > 0, "num_vertices must be > 0");
118
119    let spacing = SPACING;
120    let padding = PADDING;
121
122    let copylines = create_copylines(num_vertices, edges, vertex_order);
123
124    // Calculate grid dimensions
125    // Julia formula: N = (n-1)*col_spacing + 2 + 2*padding
126    //                M = nrow*row_spacing + 2 + 2*padding
127    // where nrow = max(hslot, vstop) and n = num_vertices
128    let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1);
129    let max_vstop = copylines.iter().map(|l| l.vstop).max().unwrap_or(1);
130
131    let rows = max_hslot.max(max_vstop) * spacing + 2 + 2 * padding;
132    // Use (num_vertices - 1) for cols, matching Julia's (n-1) formula
133    let cols = (num_vertices - 1) * spacing + 2 + 2 * padding;
134
135    let mut grid = MappingGrid::with_padding(rows, cols, spacing, padding);
136
137    // Add copy line nodes using triangular dense locations
138    // (includes the endpoint node for triangular weighted mode)
139    for line in &copylines {
140        for (row, col, weight) in line.copyline_locations_triangular(padding, spacing) {
141            grid.add_node(row, col, weight as i32);
142        }
143    }
144
145    // Mark edge connections at crossing points
146    for &(u, v) in edges {
147        let u_line = &copylines[u];
148        let v_line = &copylines[v];
149
150        let (smaller_line, larger_line) = if u_line.vslot < v_line.vslot {
151            (u_line, v_line)
152        } else {
153            (v_line, u_line)
154        };
155
156        let (row, col) = crossat(
157            &copylines,
158            smaller_line.vertex,
159            larger_line.vertex,
160            spacing,
161            padding,
162        );
163
164        // Mark connected cells at crossing point
165        if col > 0 {
166            grid.connect(row, col - 1);
167        }
168        if row > 0 && grid.is_occupied(row - 1, col) {
169            grid.connect(row - 1, col);
170        } else if row + 1 < grid.size().0 && grid.is_occupied(row + 1, col) {
171            grid.connect(row + 1, col);
172        }
173    }
174
175    // Apply crossing gadgets (iterates ALL pairs, not just edges)
176    let mut triangular_tape = apply_crossing_gadgets(&mut grid, &copylines, spacing, padding);
177
178    // Apply simplifier gadgets (weighted DanglingLeg pattern)
179    // Julia's triangular mode uses: weighted.(default_simplifier_ruleset(UnWeighted()))
180    // which applies the weighted DanglingLeg pattern to reduce grid complexity.
181    let simplifier_tape = apply_simplifier_gadgets(&mut grid, 10);
182    triangular_tape.extend(simplifier_tape);
183
184    // Calculate MIS overhead from copylines using the dedicated function
185    // which matches Julia's mis_overhead_copyline(TriangularWeighted(), ...)
186    let copyline_overhead: i32 = copylines
187        .iter()
188        .map(|line| super::super::copyline::mis_overhead_copyline_triangular(line, spacing))
189        .sum();
190
191    // Add gadget overhead (crossing gadgets + simplifiers)
192    let gadget_overhead: i32 = triangular_tape.iter().map(tape_entry_mis_overhead).sum();
193    let mis_overhead = copyline_overhead + gadget_overhead;
194
195    // Convert triangular tape entries to generic tape entries
196    let tape: Vec<TapeEntry> = triangular_tape
197        .into_iter()
198        .map(|entry| TapeEntry {
199            pattern_idx: entry.gadget_idx,
200            row: entry.row,
201            col: entry.col,
202        })
203        .collect();
204
205    // Extract doubled cells before extracting positions
206    let doubled_cells = grid.doubled_cells();
207
208    // Extract positions and weights from occupied cells
209    let (positions, node_weights): (Vec<(i32, i32)>, Vec<i32>) = grid
210        .occupied_coords()
211        .into_iter()
212        .filter_map(|(row, col)| {
213            grid.get(row, col)
214                .map(|cell| ((row as i32, col as i32), cell.weight()))
215        })
216        .filter(|&(_, w)| w > 0)
217        .unzip();
218
219    MappingResult {
220        positions,
221        node_weights,
222        grid_dimensions: grid.size(),
223        kind: GridKind::Triangular,
224        lines: copylines,
225        padding,
226        spacing,
227        mis_overhead,
228        tape,
229        doubled_cells,
230    }
231}
232
233/// Get the weighted triangular crossing ruleset.
234///
235/// This returns the list of weighted triangular gadgets used for resolving
236/// crossings in the mapping process. Matches Julia's `crossing_ruleset_triangular_weighted`.
237///
238/// # Returns
239/// A vector of `WeightedTriangularGadget` enum variants.
240pub fn weighted_ruleset() -> Vec<super::super::weighted::WeightedTriangularGadget> {
241    super::super::weighted::triangular_weighted_ruleset()
242}
243
244/// Trace center locations through gadget transformations.
245///
246/// Returns the final center location for each original vertex after all
247/// gadget transformations have been applied.
248///
249/// This matches Julia's `trace_centers` function which:
250/// 1. Gets initial center locations with (0, 1) offset
251/// 2. Applies `move_center` for each gadget in the tape
252///
253/// # Arguments
254/// * `result` - The mapping result from `map_weighted`
255///
256/// # Returns
257/// A vector of (row, col) positions for each original vertex.
258pub fn trace_centers(result: &MappingResult) -> Vec<(usize, usize)> {
259    super::super::weighted::trace_centers(result)
260}
261
262/// Map source vertex weights to grid graph weights.
263///
264/// This function takes weights for each original vertex and maps them to
265/// the corresponding nodes in the grid graph.
266///
267/// # Arguments
268/// * `result` - The mapping result from `map_weighted`
269/// * `source_weights` - Weights for each original vertex (should be in [0, 1])
270///
271/// # Returns
272/// A vector of weights for each node in the grid graph.
273///
274/// # Panics
275/// Panics if any weight is outside the range [0, 1] or if the number of
276/// weights doesn't match the number of vertices.
277pub fn map_weights(result: &MappingResult, source_weights: &[f64]) -> Vec<f64> {
278    super::super::weighted::map_weights(result, source_weights)
279}
280
281#[cfg(test)]
282#[path = "../../../unit_tests/rules/unitdiskmapping/triangular/mapping.rs"]
283mod tests;