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 = ©lines[v];
31 let line_w = ©lines[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 ©lines {
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 = ©lines[u];
148 let v_line = ©lines[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 ©lines,
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, ©lines, 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;