Skip to main content

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;
15use crate::rules::unitdiskmapping::{mapping_integer_overflow, mapping_invalid};
16use crate::rules::ReductionError;
17use std::collections::HashMap;
18
19fn position_index(
20    result: &MappingResult,
21) -> Result<HashMap<(usize, usize), usize>, ReductionError> {
22    result
23        .positions
24        .iter()
25        .enumerate()
26        .map(|(index, &(row, column))| {
27            let row = usize::try_from(row)
28                .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?;
29            let column = usize::try_from(column)
30                .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?;
31            Ok(((row, column), index))
32        })
33        .collect()
34}
35
36/// Spacing between copy lines on triangular lattice.
37pub const SPACING: usize = 6;
38
39/// Padding around the grid for triangular lattice.
40pub const PADDING: usize = 2;
41
42/// Calculate crossing point for two copylines on triangular lattice.
43fn crossat(
44    copylines: &[CopyLine],
45    v: usize,
46    w: usize,
47    spacing: usize,
48    padding: usize,
49) -> (usize, usize) {
50    let line_v = &copylines[v];
51    let line_w = &copylines[w];
52
53    // Use vslot to determine order
54    let (line_first, line_second) = if line_v.vslot < line_w.vslot {
55        (line_v, line_w)
56    } else {
57        (line_w, line_v)
58    };
59
60    let hslot = line_first.hslot;
61    let max_vslot = line_second.vslot;
62
63    // 0-indexed coordinates (subtract 1 from Julia's 1-indexed formula)
64    let row = (hslot - 1) * spacing + 1 + padding; // 0-indexed
65    let col = (max_vslot - 1) * spacing + padding; // 0-indexed
66
67    (row, col)
68}
69
70/// Map a graph to a weighted triangular lattice grid graph using optimal path decomposition.
71///
72/// This is the main entry point for triangular lattice mapping. It uses
73/// automatic path decomposition (exact for ≤30 vertices, greedy for larger).
74///
75/// # Arguments
76/// * `num_vertices` - Number of vertices in the original graph
77/// * `edges` - Edge list as (u, v) pairs
78///
79/// # Returns
80/// A `MappingResult` containing the grid graph and mapping metadata.
81///
82/// # Errors
83/// Returns [`ReductionError`] if the input graph or generated dimensions are invalid.
84///
85/// # Example
86/// ```rust
87/// use problemreductions::rules::unitdiskmapping::triangular::mapping::map_weighted;
88/// use problemreductions::topology::Graph;
89///
90/// let edges = vec![(0, 1), (1, 2)];
91/// let result = map_weighted(3, &edges).unwrap();
92/// assert!(result.to_triangular_subgraph().num_vertices() > 0);
93/// ```
94pub fn map_weighted(
95    num_vertices: usize,
96    edges: &[(usize, usize)],
97) -> Result<MappingResult, ReductionError> {
98    map_weighted_with_method(num_vertices, edges, PathDecompositionMethod::Auto)
99}
100
101/// Map a graph to weighted triangular lattice using a specific path decomposition method.
102///
103/// # Arguments
104/// * `num_vertices` - Number of vertices in the original graph
105/// * `edges` - Edge list as (u, v) pairs
106/// * `method` - Path decomposition method to use
107///
108/// # Returns
109/// A `MappingResult` containing the grid graph and mapping metadata.
110pub fn map_weighted_with_method(
111    num_vertices: usize,
112    edges: &[(usize, usize)],
113    method: PathDecompositionMethod,
114) -> Result<MappingResult, ReductionError> {
115    let layout = pathwidth(num_vertices, edges, method);
116    let vertex_order = vertex_order_from_layout(&layout);
117    map_weighted_with_order(num_vertices, edges, &vertex_order)
118}
119
120/// Map a graph to weighted triangular lattice with specific vertex ordering.
121///
122/// This is the most flexible mapping function, allowing custom vertex ordering
123/// for cases where a specific layout is desired.
124///
125/// # Arguments
126/// * `num_vertices` - Number of vertices in the original graph
127/// * `edges` - Edge list as (u, v) pairs
128/// * `vertex_order` - Custom vertex ordering
129///
130/// # Returns
131/// A `MappingResult` containing the grid graph and mapping metadata.
132///
133/// # Errors
134/// Returns [`ReductionError`] if the vertex order, graph, or generated dimensions are invalid.
135pub fn map_weighted_with_order(
136    num_vertices: usize,
137    edges: &[(usize, usize)],
138    vertex_order: &[usize],
139) -> Result<MappingResult, ReductionError> {
140    if num_vertices == 0 {
141        return Err(mapping_invalid("num_vertices must be positive"));
142    }
143
144    let spacing = SPACING;
145    let padding = PADDING;
146
147    let copylines = create_copylines(num_vertices, edges, vertex_order)?;
148
149    // Calculate grid dimensions
150    // Julia formula: N = (n-1)*col_spacing + 2 + 2*padding
151    //                M = nrow*row_spacing + 2 + 2*padding
152    // where nrow = max(hslot, vstop) and n = num_vertices
153    let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1);
154    let max_vstop = copylines.iter().map(|l| l.vstop).max().unwrap_or(1);
155
156    let padding_twice = padding.checked_mul(2).ok_or(mapping_integer_overflow(
157        "computing triangular grid padding",
158    ))?;
159    let extent = |slots: usize| {
160        slots
161            .checked_mul(spacing)
162            .and_then(|value| value.checked_add(2))
163            .and_then(|value| value.checked_add(padding_twice))
164            .ok_or(mapping_integer_overflow(
165                "computing triangular grid dimensions",
166            ))
167    };
168    let rows = extent(max_hslot.max(max_vstop))?;
169    // Use (num_vertices - 1) for cols, matching Julia's (n-1) formula
170    let cols = extent(num_vertices - 1)?;
171
172    let mut grid = MappingGrid::with_padding(rows, cols, spacing, padding);
173
174    // Add copy line nodes using triangular dense locations
175    // (includes the endpoint node for triangular weighted mode)
176    for line in &copylines {
177        for (row, col, weight) in line.copyline_locations_triangular(padding, spacing) {
178            let weight = i64::try_from(weight).map_err(|_| {
179                mapping_integer_overflow("converting a triangular grid weight to i64")
180            })?;
181            grid.add_node(row, col, weight);
182        }
183    }
184
185    // Mark edge connections at crossing points
186    for &(u, v) in edges {
187        let u_line = &copylines[u];
188        let v_line = &copylines[v];
189
190        let (smaller_line, larger_line) = if u_line.vslot < v_line.vslot {
191            (u_line, v_line)
192        } else {
193            (v_line, u_line)
194        };
195
196        let (row, col) = crossat(
197            &copylines,
198            smaller_line.vertex,
199            larger_line.vertex,
200            spacing,
201            padding,
202        );
203
204        // Mark connected cells at crossing point
205        if col > 0 {
206            grid.connect(row, col - 1);
207        }
208        if row > 0 && grid.is_occupied(row - 1, col) {
209            grid.connect(row - 1, col);
210        } else if row + 1 < grid.size().0 && grid.is_occupied(row + 1, col) {
211            grid.connect(row + 1, col);
212        }
213    }
214
215    // Apply crossing gadgets (iterates ALL pairs, not just edges)
216    let mut triangular_tape = apply_crossing_gadgets(&mut grid, &copylines, spacing, padding);
217
218    // Apply simplifier gadgets (weighted DanglingLeg pattern)
219    // Julia's triangular mode uses: weighted.(default_simplifier_ruleset(UnWeighted()))
220    // which applies the weighted DanglingLeg pattern to reduce grid complexity.
221    let simplifier_tape = apply_simplifier_gadgets(&mut grid, 10);
222    triangular_tape.extend(simplifier_tape);
223
224    // Calculate MIS overhead from copylines using the dedicated function
225    // which matches Julia's mis_overhead_copyline(TriangularWeighted(), ...)
226    let copyline_overhead = copylines.iter().try_fold(0_i64, |total, line| {
227        total
228            .checked_add(super::super::copyline::mis_overhead_copyline_triangular(
229                line, spacing,
230            )?)
231            .ok_or(mapping_integer_overflow(
232                "summing triangular copy-line MIS overhead",
233            ))
234    })?;
235
236    // Add gadget overhead (crossing gadgets + simplifiers)
237    let gadget_overhead = triangular_tape.iter().try_fold(0_i64, |total, entry| {
238        total
239            .checked_add(tape_entry_mis_overhead(entry)?)
240            .ok_or(mapping_integer_overflow(
241                "summing triangular gadget MIS overhead",
242            ))
243    })?;
244    let mis_overhead =
245        copyline_overhead
246            .checked_add(gadget_overhead)
247            .ok_or(mapping_integer_overflow(
248                "computing total triangular MIS overhead",
249            ))?;
250
251    if grid.has_unresolved_cells() {
252        return Err(mapping_invalid(
253            "triangular mapping left doubled or connected cells unresolved",
254        ));
255    }
256
257    // Convert triangular tape entries to generic tape entries
258    let tape: Vec<TapeEntry> = triangular_tape
259        .into_iter()
260        .map(|entry| TapeEntry {
261            pattern_idx: entry.gadget_idx,
262            row: entry.row,
263            col: entry.col,
264        })
265        .collect();
266
267    // Extract doubled cells before extracting positions
268    let doubled_cells = grid.doubled_cells();
269
270    // Extract positions and weights from occupied cells
271    let positions_and_weights = grid
272        .occupied_coords()
273        .into_iter()
274        .filter_map(|(row, col)| {
275            grid.get(row, col)
276                .filter(|cell| cell.weight() > 0)
277                .map(|cell| {
278                    Ok((
279                        (
280                            i64::try_from(row).map_err(|_| {
281                                mapping_integer_overflow("converting a grid row to i64")
282                            })?,
283                            i64::try_from(col).map_err(|_| {
284                                mapping_integer_overflow("converting a grid column to i64")
285                            })?,
286                        ),
287                        cell.weight(),
288                    ))
289                })
290        })
291        .collect::<Result<Vec<_>, ReductionError>>()?;
292    let (positions, node_weights): (Vec<_>, Vec<_>) = positions_and_weights.into_iter().unzip();
293
294    Ok(MappingResult {
295        positions,
296        node_weights,
297        grid_dimensions: grid.size(),
298        kind: GridKind::Triangular,
299        lines: copylines,
300        padding,
301        spacing,
302        mis_overhead,
303        tape,
304        doubled_cells,
305    })
306}
307
308/// Read the original vertex configuration at the traced triangular centers.
309pub fn map_config_back(
310    result: &MappingResult,
311    grid_config: &[usize],
312) -> crate::rules::ExtractionResult<Vec<usize>> {
313    map_config_back_internal(result, grid_config)
314        .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))
315}
316
317fn map_config_back_internal(
318    result: &MappingResult,
319    grid_config: &[usize],
320) -> Result<Vec<usize>, ReductionError> {
321    if grid_config.len() != result.positions.len() {
322        return Err(mapping_invalid(
323            "grid configuration length must match the mapped vertex count",
324        ));
325    }
326    let positions = position_index(result)?;
327
328    super::super::weighted::trace_centers(result)?
329        .into_iter()
330        .map(|center| {
331            positions
332                .get(&center)
333                .map(|&index| grid_config[index])
334                .ok_or(mapping_invalid(
335                    "a traced center is missing from the mapped graph",
336                ))
337        })
338        .collect()
339}
340
341/// Encode unit source weights exactly in the integer target weights.
342///
343/// Multiplying the base gadget weights by `n + 1` preserves the mapping's
344/// primary objective. Adding one at each traced source center then maximizes
345/// the source independent-set size among those primary optima.
346pub fn map_unit_weights(result: &MappingResult) -> Result<Vec<i64>, ReductionError> {
347    let count = i64::try_from(result.lines.len())
348        .map_err(|_| mapping_integer_overflow("converting the source vertex count to i64"))?;
349    let scale = count.checked_add(1).ok_or(mapping_integer_overflow(
350        "computing the unit-weight encoding scale",
351    ))?;
352    let mut weights = result
353        .node_weights
354        .iter()
355        .map(|weight| {
356            weight.checked_mul(scale).ok_or(mapping_integer_overflow(
357                "scaling a triangular mapped weight",
358            ))
359        })
360        .collect::<Result<Vec<_>, _>>()?;
361    let positions = position_index(result)?;
362
363    for center in super::super::weighted::trace_centers(result)? {
364        let index = positions.get(&center).copied().ok_or(mapping_invalid(
365            "a traced center is missing from the mapped graph",
366        ))?;
367        weights[index] = weights[index]
368            .checked_add(1)
369            .ok_or(mapping_integer_overflow(
370                "adding a unit source weight to a triangular center",
371            ))?;
372    }
373    Ok(weights)
374}
375
376#[cfg(test)]
377#[path = "../../../unit_tests/rules/unitdiskmapping/triangular/mapping.rs"]
378mod tests;