problemreductions/rules/unitdiskmapping/triangular/
mod.rs

1//! Triangular lattice mapping module.
2//!
3//! Maps arbitrary graphs to weighted triangular lattice graphs.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use problemreductions::rules::unitdiskmapping::triangular;
9//!
10//! let edges = vec![(0, 1), (1, 2), (0, 2)];
11//!
12//! // Weighted triangular mapping
13//! let result = triangular::map_weighted(3, &edges);
14//! ```
15
16pub mod gadgets;
17pub mod mapping;
18
19// Re-export all public items from gadgets for convenient access
20pub use gadgets::{
21    apply_crossing_gadgets, apply_simplifier_gadgets, tape_entry_mis_overhead, SourceCell,
22    WeightedTriBranch, WeightedTriBranchFix, WeightedTriBranchFixB, WeightedTriCross,
23    WeightedTriEndTurn, WeightedTriTConDown, WeightedTriTConLeft, WeightedTriTConUp,
24    WeightedTriTapeEntry, WeightedTriTrivialTurnLeft, WeightedTriTrivialTurnRight, WeightedTriTurn,
25    WeightedTriWTurn, WeightedTriangularGadget,
26};
27
28// Re-export all public items from mapping for convenient access
29pub use mapping::{
30    map_weighted, map_weighted_with_method, map_weighted_with_order, map_weights, trace_centers,
31    weighted_ruleset,
32};
33
34/// Spacing between copy lines for triangular mapping.
35pub const SPACING: usize = 6;
36
37/// Padding around the grid for triangular mapping.
38pub const PADDING: usize = 2;
39
40// ============================================================================
41// Legacy exports for backward compatibility
42// ============================================================================
43
44use super::copyline::create_copylines;
45use super::grid::MappingGrid;
46use super::ksg::mapping::MappingResult;
47use super::ksg::KsgTapeEntry as TapeEntry;
48use super::pathdecomposition::{pathwidth, vertex_order_from_layout, PathDecompositionMethod};
49use crate::rules::unitdiskmapping::ksg::mapping::GridKind;
50use serde::{Deserialize, Serialize};
51
52pub const TRIANGULAR_SPACING: usize = 6;
53pub const TRIANGULAR_PADDING: usize = 2;
54
55/// Tape entry recording a triangular gadget application.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TriangularTapeEntry {
58    /// Index of the gadget in the ruleset (0-12).
59    pub gadget_idx: usize,
60    /// Row where gadget was applied.
61    pub row: usize,
62    /// Column where gadget was applied.
63    pub col: usize,
64}
65
66/// Calculate crossing point for two copylines on triangular lattice.
67fn crossat_triangular(
68    copylines: &[super::copyline::CopyLine],
69    v: usize,
70    w: usize,
71    spacing: usize,
72    padding: usize,
73) -> (usize, usize) {
74    let line_v = &copylines[v];
75    let line_w = &copylines[w];
76
77    // Use vslot to determine order
78    let (line_first, line_second) = if line_v.vslot < line_w.vslot {
79        (line_v, line_w)
80    } else {
81        (line_w, line_v)
82    };
83
84    let hslot = line_first.hslot;
85    let max_vslot = line_second.vslot;
86
87    // 0-indexed coordinates (subtract 1 from Julia's 1-indexed formula)
88    let row = (hslot - 1) * spacing + 1 + padding; // 0-indexed
89    let col = (max_vslot - 1) * spacing + padding; // 0-indexed
90
91    (row, col)
92}
93
94/// Cell type for source matrix pattern matching (legacy).
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum LegacySourceCell {
97    Empty,
98    Occupied,
99    Connected,
100}
101
102/// Trait for triangular lattice gadgets (simplified interface).
103///
104/// Note: source_graph returns explicit edges (like Julia's simplegraph),
105/// while mapped_graph locations should use unit disk edges.
106#[allow(dead_code)]
107#[allow(clippy::type_complexity)]
108pub trait TriangularGadget {
109    fn size(&self) -> (usize, usize);
110    fn cross_location(&self) -> (usize, usize);
111    fn is_connected(&self) -> bool;
112    /// Returns (locations, edges, pins) - edges are explicit, not unit disk.
113    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>);
114    /// Returns (locations, pins) - use unit disk for edges on triangular lattice.
115    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>);
116    fn mis_overhead(&self) -> i32;
117
118    /// Returns 1-indexed node indices that should be Connected (matching Julia).
119    fn connected_nodes(&self) -> Vec<usize> {
120        vec![]
121    }
122
123    /// Returns source node weights. Default is weight 2 for all nodes.
124    fn source_weights(&self) -> Vec<i32> {
125        let (locs, _, _) = self.source_graph();
126        vec![2; locs.len()]
127    }
128
129    /// Returns mapped node weights. Default is weight 2 for all nodes.
130    fn mapped_weights(&self) -> Vec<i32> {
131        let (locs, _) = self.mapped_graph();
132        vec![2; locs.len()]
133    }
134
135    /// Generate source matrix for pattern matching.
136    /// Returns LegacySourceCell::Connected for nodes in connected_nodes() when is_connected() is true.
137    fn source_matrix(&self) -> Vec<Vec<LegacySourceCell>> {
138        let (rows, cols) = self.size();
139        let (locs, _, _) = self.source_graph();
140        let mut matrix = vec![vec![LegacySourceCell::Empty; cols]; rows];
141
142        // Build set of connected node indices (1-indexed in Julia)
143        let connected_set: std::collections::HashSet<usize> = if self.is_connected() {
144            self.connected_nodes().into_iter().collect()
145        } else {
146            std::collections::HashSet::new()
147        };
148
149        for (idx, (r, c)) in locs.iter().enumerate() {
150            if *r > 0 && *c > 0 && *r <= rows && *c <= cols {
151                let cell_type = if connected_set.contains(&(idx + 1)) {
152                    LegacySourceCell::Connected
153                } else {
154                    LegacySourceCell::Occupied
155                };
156                matrix[r - 1][c - 1] = cell_type;
157            }
158        }
159        matrix
160    }
161
162    /// Generate mapped matrix for gadget application.
163    fn mapped_matrix(&self) -> Vec<Vec<bool>> {
164        let (rows, cols) = self.size();
165        let (locs, _) = self.mapped_graph();
166        let mut matrix = vec![vec![false; cols]; rows];
167        for (r, c) in locs {
168            if r > 0 && c > 0 && r <= rows && c <= cols {
169                matrix[r - 1][c - 1] = true;
170            }
171        }
172        matrix
173    }
174}
175
176/// Triangular cross gadget - matches Julia's Cross gadget with weights.
177///
178/// This uses the same structure as Julia's base Cross gadget, with all nodes
179/// having weight 2 (the standard weighted mode).
180/// mis_overhead = base_overhead * 2 = -1 * 2 = -2
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182pub struct TriCross<const CON: bool>;
183
184impl TriangularGadget for TriCross<true> {
185    fn size(&self) -> (usize, usize) {
186        (6, 4)
187    }
188
189    fn cross_location(&self) -> (usize, usize) {
190        (2, 2)
191    }
192
193    fn is_connected(&self) -> bool {
194        true
195    }
196
197    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
198        // Julia: locs = Node.([(2,1), (2,2), (2,3), (2,4), (1,2), (2,2), (3,2), (4,2), (5,2), (6,2)])
199        // Note: Julia has duplicate (2,2) at indices 2 and 6
200        let locs = vec![
201            (2, 1),
202            (2, 2),
203            (2, 3),
204            (2, 4),
205            (1, 2),
206            (2, 2),
207            (3, 2),
208            (4, 2),
209            (5, 2),
210            (6, 2),
211        ];
212        // Julia: g = simplegraph([(1,2), (2,3), (3,4), (5,6), (6,7), (7,8), (8,9), (9,10), (1,5)])
213        // 0-indexed: [(0,1), (1,2), (2,3), (4,5), (5,6), (6,7), (7,8), (8,9), (0,4)]
214        let edges = vec![
215            (0, 1),
216            (1, 2),
217            (2, 3),
218            (4, 5),
219            (5, 6),
220            (6, 7),
221            (7, 8),
222            (8, 9),
223            (0, 4),
224        ];
225        // Julia: pins = [1,5,10,4] -> 0-indexed: [0,4,9,3]
226        let pins = vec![0, 4, 9, 3];
227        (locs, edges, pins)
228    }
229
230    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
231        // Julia: locs = Node.([(1,2), (2,1), (2,2), (2,3), (1,4), (3,3), (4,2), (4,3), (5,1), (6,1), (6,2)])
232        let locs = vec![
233            (1, 2),
234            (2, 1),
235            (2, 2),
236            (2, 3),
237            (1, 4),
238            (3, 3),
239            (4, 2),
240            (4, 3),
241            (5, 1),
242            (6, 1),
243            (6, 2),
244        ];
245        // Julia: pins = [2,1,11,5] -> 0-indexed: [1,0,10,4]
246        let pins = vec![1, 0, 10, 4];
247        (locs, pins)
248    }
249
250    fn mis_overhead(&self) -> i32 {
251        1
252    }
253
254    fn connected_nodes(&self) -> Vec<usize> {
255        // Julia: connected_nodes = [1,5] (1-indexed, keep as-is for source_matrix)
256        vec![1, 5]
257    }
258
259    fn source_weights(&self) -> Vec<i32> {
260        // Julia: sw = [2,2,2,2,2,2,2,2,2,2]
261        vec![2; 10]
262    }
263
264    fn mapped_weights(&self) -> Vec<i32> {
265        // Julia: mw = [3,2,3,3,2,2,2,2,2,2,2]
266        vec![3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2]
267    }
268}
269
270impl TriangularGadget for TriCross<false> {
271    fn size(&self) -> (usize, usize) {
272        (6, 6)
273    }
274
275    fn cross_location(&self) -> (usize, usize) {
276        (2, 4)
277    }
278
279    fn is_connected(&self) -> bool {
280        false
281    }
282
283    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
284        // Julia: locs = Node.([(2,2), (2,3), (2,4), (2,5), (2,6), (1,4), (2,4), (3,4), (4,4), (5,4), (6,4), (2,1)])
285        // Note: Julia has duplicate (2,4) at indices 3 and 7
286        let locs = vec![
287            (2, 2),
288            (2, 3),
289            (2, 4),
290            (2, 5),
291            (2, 6),
292            (1, 4),
293            (2, 4),
294            (3, 4),
295            (4, 4),
296            (5, 4),
297            (6, 4),
298            (2, 1),
299        ];
300        // Julia: g = simplegraph([(1,2), (2,3), (3,4), (4,5), (6,7), (7,8), (8,9), (9,10), (10,11), (12,1)])
301        // 0-indexed: [(0,1), (1,2), (2,3), (3,4), (5,6), (6,7), (7,8), (8,9), (9,10), (11,0)]
302        let edges = vec![
303            (0, 1),
304            (1, 2),
305            (2, 3),
306            (3, 4),
307            (5, 6),
308            (6, 7),
309            (7, 8),
310            (8, 9),
311            (9, 10),
312            (11, 0),
313        ];
314        // Julia: pins = [12,6,11,5] -> 0-indexed: [11,5,10,4]
315        let pins = vec![11, 5, 10, 4];
316        (locs, edges, pins)
317    }
318
319    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
320        // Julia: locs = Node.([(1,4), (2,2), (2,3), (2,4), (2,5), (2,6), (3,2), (3,3), (3,4), (3,5), (4,2), (4,3), (5,2), (6,3), (6,4), (2,1)])
321        let locs = vec![
322            (1, 4),
323            (2, 2),
324            (2, 3),
325            (2, 4),
326            (2, 5),
327            (2, 6),
328            (3, 2),
329            (3, 3),
330            (3, 4),
331            (3, 5),
332            (4, 2),
333            (4, 3),
334            (5, 2),
335            (6, 3),
336            (6, 4),
337            (2, 1),
338        ];
339        // Julia: pins = [16,1,15,6] -> 0-indexed: [15,0,14,5]
340        let pins = vec![15, 0, 14, 5];
341        (locs, pins)
342    }
343
344    fn mis_overhead(&self) -> i32 {
345        3
346    }
347
348    fn source_weights(&self) -> Vec<i32> {
349        vec![2; 12]
350    }
351
352    fn mapped_weights(&self) -> Vec<i32> {
353        vec![3, 3, 2, 4, 2, 2, 2, 4, 3, 2, 2, 2, 2, 2, 2, 2]
354    }
355}
356
357/// Triangular turn gadget - matches Julia's TriTurn gadget.
358///
359/// Julia TriTurn (from triangular.jl):
360/// - size = (3, 4)
361/// - cross_location = (2, 2)
362/// - 4 source nodes, 4 mapped nodes
363/// - mis_overhead = -2 (weighted)
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
365pub struct TriTurn;
366
367impl TriangularGadget for TriTurn {
368    fn size(&self) -> (usize, usize) {
369        (3, 4)
370    }
371
372    fn cross_location(&self) -> (usize, usize) {
373        (2, 2)
374    }
375
376    fn is_connected(&self) -> bool {
377        false
378    }
379
380    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
381        // Julia: locs = Node.([(1,2), (2,2), (2,3), (2,4)])
382        // Julia: g = simplegraph([(1,2), (2,3), (3,4)])
383        let locs = vec![(1, 2), (2, 2), (2, 3), (2, 4)];
384        let edges = vec![(0, 1), (1, 2), (2, 3)];
385        // Julia: pins = [1,4] -> 0-indexed: [0,3]
386        let pins = vec![0, 3];
387        (locs, edges, pins)
388    }
389
390    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
391        // Julia: locs = Node.([(1,2), (2,2), (3,3), (2,4)])
392        let locs = vec![(1, 2), (2, 2), (3, 3), (2, 4)];
393        // Julia: pins = [1,4] -> 0-indexed: [0,3]
394        let pins = vec![0, 3];
395        (locs, pins)
396    }
397
398    fn mis_overhead(&self) -> i32 {
399        0
400    }
401
402    fn source_weights(&self) -> Vec<i32> {
403        vec![2; 4]
404    }
405
406    fn mapped_weights(&self) -> Vec<i32> {
407        vec![2; 4]
408    }
409}
410
411/// Triangular branch gadget - matches Julia's Branch gadget with weights.
412///
413/// Julia Branch:
414/// - size = (5, 4)
415/// - cross_location = (3, 2)
416/// - 8 source nodes, 6 mapped nodes
417/// - mis_overhead = -1 (base), -2 (weighted)
418/// - For weighted mode: source node 4 has weight 3, mapped node 2 has weight 3
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
420pub struct TriBranch;
421
422impl TriangularGadget for TriBranch {
423    fn size(&self) -> (usize, usize) {
424        (6, 4)
425    }
426
427    fn cross_location(&self) -> (usize, usize) {
428        (2, 2)
429    }
430
431    fn is_connected(&self) -> bool {
432        false
433    }
434
435    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
436        // Julia: locs = Node.([(1,2),(2,2),(2,3),(2,4),(3,3),(3,2),(4,2),(5,2),(6,2)])
437        let locs = vec![
438            (1, 2),
439            (2, 2),
440            (2, 3),
441            (2, 4),
442            (3, 3),
443            (3, 2),
444            (4, 2),
445            (5, 2),
446            (6, 2),
447        ];
448        // Julia: g = simplegraph([(1,2), (2,3), (3, 4), (3,5), (5,6), (6,7), (7,8), (8,9)])
449        // 0-indexed: [(0,1), (1,2), (2,3), (2,4), (4,5), (5,6), (6,7), (7,8)]
450        let edges = vec![
451            (0, 1),
452            (1, 2),
453            (2, 3),
454            (2, 4),
455            (4, 5),
456            (5, 6),
457            (6, 7),
458            (7, 8),
459        ];
460        // Julia: pins = [1, 4, 9] -> 0-indexed: [0, 3, 8]
461        let pins = vec![0, 3, 8];
462        (locs, edges, pins)
463    }
464
465    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
466        // Julia: locs = Node.([(1,2),(2,2),(2,4),(3,3),(4,2),(4,3),(5,1),(6,1),(6,2)])
467        let locs = vec![
468            (1, 2),
469            (2, 2),
470            (2, 4),
471            (3, 3),
472            (4, 2),
473            (4, 3),
474            (5, 1),
475            (6, 1),
476            (6, 2),
477        ];
478        // Julia: pins = [1,3,9] -> 0-indexed: [0,2,8]
479        let pins = vec![0, 2, 8];
480        (locs, pins)
481    }
482
483    fn mis_overhead(&self) -> i32 {
484        0
485    }
486
487    fn source_weights(&self) -> Vec<i32> {
488        // Julia: sw = [2,2,3,2,2,2,2,2,2]
489        vec![2, 2, 3, 2, 2, 2, 2, 2, 2]
490    }
491
492    fn mapped_weights(&self) -> Vec<i32> {
493        // Julia: mw = [2,2,2,3,2,2,2,2,2]
494        vec![2, 2, 2, 3, 2, 2, 2, 2, 2]
495    }
496}
497
498/// Triangular T-connection left gadget - matches Julia's TCon gadget with weights.
499///
500/// Julia TCon:
501/// - size = (3, 4)
502/// - cross_location = (2, 2)
503/// - 4 source nodes, 4 mapped nodes, 3 pins
504/// - connected_nodes = [1, 2] -> [0, 1]
505/// - mis_overhead = 0 (both base and weighted)
506/// - For weighted mode: source node 2 has weight 1, mapped node 2 has weight 1
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
508pub struct TriTConLeft;
509
510impl TriangularGadget for TriTConLeft {
511    fn size(&self) -> (usize, usize) {
512        (6, 5)
513    }
514
515    fn cross_location(&self) -> (usize, usize) {
516        (2, 2)
517    }
518
519    fn is_connected(&self) -> bool {
520        true
521    }
522
523    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
524        // Julia: locs = Node.([(1,2), (2,1), (2,2), (3,2), (4,2), (5,2), (6,2)])
525        let locs = vec![(1, 2), (2, 1), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2)];
526        // Julia: g = simplegraph([(1,2), (1,3), (3,4), (4,5), (5,6), (6,7)])
527        // 0-indexed: [(0,1), (0,2), (2,3), (3,4), (4,5), (5,6)]
528        let edges = vec![(0, 1), (0, 2), (2, 3), (3, 4), (4, 5), (5, 6)];
529        // Julia: pins = [1,2,7] -> 0-indexed: [0,1,6]
530        let pins = vec![0, 1, 6];
531        (locs, edges, pins)
532    }
533
534    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
535        // Julia: locs = Node.([(1,2), (2,1), (2,2), (2,3), (2,4), (3,3), (4,2), (4,3), (5,1), (6,1), (6,2)])
536        let locs = vec![
537            (1, 2),
538            (2, 1),
539            (2, 2),
540            (2, 3),
541            (2, 4),
542            (3, 3),
543            (4, 2),
544            (4, 3),
545            (5, 1),
546            (6, 1),
547            (6, 2),
548        ];
549        // Julia: pins = [1,2,11] -> 0-indexed: [0,1,10]
550        let pins = vec![0, 1, 10];
551        (locs, pins)
552    }
553
554    fn mis_overhead(&self) -> i32 {
555        4
556    }
557
558    fn connected_nodes(&self) -> Vec<usize> {
559        // Julia: connected_nodes = [1,2] (1-indexed, keep as-is for source_matrix)
560        vec![1, 2]
561    }
562
563    fn source_weights(&self) -> Vec<i32> {
564        // Julia: sw = [2,1,2,2,2,2,2]
565        vec![2, 1, 2, 2, 2, 2, 2]
566    }
567
568    fn mapped_weights(&self) -> Vec<i32> {
569        // Julia: mw = [3,2,3,3,1,3,2,2,2,2,2]
570        vec![3, 2, 3, 3, 1, 3, 2, 2, 2, 2, 2]
571    }
572}
573
574/// Triangular T-connection down gadget.
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
576pub struct TriTConDown;
577
578impl TriangularGadget for TriTConDown {
579    fn size(&self) -> (usize, usize) {
580        (3, 3)
581    }
582
583    fn cross_location(&self) -> (usize, usize) {
584        (2, 2)
585    }
586
587    fn is_connected(&self) -> bool {
588        true
589    }
590
591    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
592        // Julia: locs = Node.([(2,1), (2,2), (2,3), (3,2)])
593        // Julia: g = simplegraph([(1,2), (2,3), (1,4)])
594        // 0-indexed: [(0,1), (1,2), (0,3)]
595        let locs = vec![(2, 1), (2, 2), (2, 3), (3, 2)];
596        let edges = vec![(0, 1), (1, 2), (0, 3)];
597        // Julia: pins = [1,4,3] -> 0-indexed: [0,3,2]
598        let pins = vec![0, 3, 2];
599        (locs, edges, pins)
600    }
601
602    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
603        // Julia: locs = Node.([(2,2), (3,1), (3,2), (3,3)])
604        let locs = vec![(2, 2), (3, 1), (3, 2), (3, 3)];
605        // Julia: pins = [2,3,4] -> 0-indexed: [1,2,3]
606        let pins = vec![1, 2, 3];
607        (locs, pins)
608    }
609
610    fn mis_overhead(&self) -> i32 {
611        0
612    }
613
614    fn connected_nodes(&self) -> Vec<usize> {
615        // Julia: connected_nodes = [1, 4] (1-indexed, keep as-is for source_matrix)
616        vec![1, 4]
617    }
618
619    fn source_weights(&self) -> Vec<i32> {
620        vec![2, 2, 2, 1]
621    }
622
623    fn mapped_weights(&self) -> Vec<i32> {
624        vec![2, 2, 3, 2]
625    }
626}
627
628/// Triangular T-connection up gadget.
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
630pub struct TriTConUp;
631
632impl TriangularGadget for TriTConUp {
633    fn size(&self) -> (usize, usize) {
634        (3, 3)
635    }
636
637    fn cross_location(&self) -> (usize, usize) {
638        (2, 2)
639    }
640
641    fn is_connected(&self) -> bool {
642        true
643    }
644
645    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
646        // Julia: locs = Node.([(1,2), (2,1), (2,2), (2,3)])
647        // Julia: g = simplegraph([(1,2), (2,3), (3,4)])
648        // 0-indexed: [(0,1), (1,2), (2,3)]
649        let locs = vec![(1, 2), (2, 1), (2, 2), (2, 3)];
650        let edges = vec![(0, 1), (1, 2), (2, 3)];
651        // Julia: pins = [2,1,4] -> 0-indexed: [1,0,3]
652        let pins = vec![1, 0, 3];
653        (locs, edges, pins)
654    }
655
656    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
657        // Julia: locs = Node.([(1,2), (2,1), (2,2), (2,3)])
658        let locs = vec![(1, 2), (2, 1), (2, 2), (2, 3)];
659        // Julia: pins = [2,1,4] -> 0-indexed: [1,0,3]
660        let pins = vec![1, 0, 3];
661        (locs, pins)
662    }
663
664    fn mis_overhead(&self) -> i32 {
665        0
666    }
667
668    fn connected_nodes(&self) -> Vec<usize> {
669        // Julia: connected_nodes = [1, 2] (1-indexed, keep as-is for source_matrix)
670        vec![1, 2]
671    }
672
673    fn source_weights(&self) -> Vec<i32> {
674        vec![1, 2, 2, 2]
675    }
676
677    fn mapped_weights(&self) -> Vec<i32> {
678        vec![3, 2, 2, 2]
679    }
680}
681
682/// Triangular trivial turn left gadget.
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
684pub struct TriTrivialTurnLeft;
685
686impl TriangularGadget for TriTrivialTurnLeft {
687    fn size(&self) -> (usize, usize) {
688        (2, 2)
689    }
690
691    fn cross_location(&self) -> (usize, usize) {
692        (2, 2)
693    }
694
695    fn is_connected(&self) -> bool {
696        true
697    }
698
699    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
700        // Julia: locs = Node.([(1,2), (2,1)])
701        let locs = vec![(1, 2), (2, 1)];
702        let edges = vec![(0, 1)];
703        let pins = vec![0, 1];
704        (locs, edges, pins)
705    }
706
707    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
708        // Julia: locs = Node.([(1,2),(2,1)])
709        let locs = vec![(1, 2), (2, 1)];
710        let pins = vec![0, 1];
711        (locs, pins)
712    }
713
714    fn mis_overhead(&self) -> i32 {
715        0
716    }
717
718    fn connected_nodes(&self) -> Vec<usize> {
719        // Julia: connected_nodes = [1, 2] (1-indexed, keep as-is for source_matrix)
720        vec![1, 2]
721    }
722
723    fn source_weights(&self) -> Vec<i32> {
724        vec![1, 1]
725    }
726
727    fn mapped_weights(&self) -> Vec<i32> {
728        vec![1, 1]
729    }
730}
731
732/// Triangular trivial turn right gadget.
733#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
734pub struct TriTrivialTurnRight;
735
736impl TriangularGadget for TriTrivialTurnRight {
737    fn size(&self) -> (usize, usize) {
738        (2, 2)
739    }
740
741    fn cross_location(&self) -> (usize, usize) {
742        (1, 2)
743    }
744
745    fn is_connected(&self) -> bool {
746        true
747    }
748
749    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
750        // Julia: locs = Node.([(1,1), (2,2)])
751        let locs = vec![(1, 1), (2, 2)];
752        let edges = vec![(0, 1)];
753        let pins = vec![0, 1];
754        (locs, edges, pins)
755    }
756
757    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
758        // Julia: locs = Node.([(2,1),(2,2)])
759        let locs = vec![(2, 1), (2, 2)];
760        let pins = vec![0, 1];
761        (locs, pins)
762    }
763
764    fn mis_overhead(&self) -> i32 {
765        0
766    }
767
768    fn connected_nodes(&self) -> Vec<usize> {
769        // Julia: connected_nodes = [1, 2] (1-indexed, keep as-is for source_matrix)
770        vec![1, 2]
771    }
772
773    fn source_weights(&self) -> Vec<i32> {
774        vec![1, 1]
775    }
776
777    fn mapped_weights(&self) -> Vec<i32> {
778        vec![1, 1]
779    }
780}
781
782/// Triangular end turn gadget - matches Julia's EndTurn gadget with weights.
783///
784/// Julia EndTurn:
785/// - size = (3, 4)
786/// - cross_location = (2, 2)
787/// - 3 source nodes, 1 mapped node, 1 pin
788/// - mis_overhead = -1 (base), -2 (weighted)
789/// - For weighted mode: source node 3 has weight 1, mapped node 1 has weight 1
790#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
791pub struct TriEndTurn;
792
793impl TriangularGadget for TriEndTurn {
794    fn size(&self) -> (usize, usize) {
795        (3, 4)
796    }
797
798    fn cross_location(&self) -> (usize, usize) {
799        (2, 2)
800    }
801
802    fn is_connected(&self) -> bool {
803        false
804    }
805
806    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
807        // Julia: locs = Node.([(1,2), (2,2), (2,3)])
808        // Julia: g = simplegraph([(1,2), (2,3)])
809        let locs = vec![(1, 2), (2, 2), (2, 3)];
810        let edges = vec![(0, 1), (1, 2)];
811        // Julia: pins = [1] -> 0-indexed: [0]
812        let pins = vec![0];
813        (locs, edges, pins)
814    }
815
816    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
817        // Julia: locs = Node.([(1,2)])
818        let locs = vec![(1, 2)];
819        // Julia: pins = [1] -> 0-indexed: [0]
820        let pins = vec![0];
821        (locs, pins)
822    }
823
824    fn mis_overhead(&self) -> i32 {
825        -2
826    }
827
828    fn source_weights(&self) -> Vec<i32> {
829        vec![2, 2, 1]
830    }
831
832    fn mapped_weights(&self) -> Vec<i32> {
833        vec![1]
834    }
835}
836
837/// Triangular W-turn gadget - matches Julia's WTurn gadget with weights.
838///
839/// Julia WTurn:
840/// - size = (4, 4)
841/// - cross_location = (2, 2)
842/// - 5 source nodes, 3 mapped nodes
843/// - mis_overhead = -1 (base), -2 (weighted)
844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
845pub struct TriWTurn;
846
847impl TriangularGadget for TriWTurn {
848    fn size(&self) -> (usize, usize) {
849        (4, 4)
850    }
851
852    fn cross_location(&self) -> (usize, usize) {
853        (2, 2)
854    }
855
856    fn is_connected(&self) -> bool {
857        false
858    }
859
860    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
861        // Julia: locs = Node.([(2,3), (2,4), (3,2),(3,3),(4,2)])
862        let locs = vec![(2, 3), (2, 4), (3, 2), (3, 3), (4, 2)];
863        // Julia: g = simplegraph([(1,2), (1,4), (3,4),(3,5)])
864        // 0-indexed: [(0,1), (0,3), (2,3), (2,4)]
865        let edges = vec![(0, 1), (0, 3), (2, 3), (2, 4)];
866        // Julia: pins = [2, 5] -> 0-indexed: [1, 4]
867        let pins = vec![1, 4];
868        (locs, edges, pins)
869    }
870
871    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
872        // Julia: locs = Node.([(1,4), (2,3), (3,2), (3,3), (4,2)])
873        let locs = vec![(1, 4), (2, 3), (3, 2), (3, 3), (4, 2)];
874        // Julia: pins = [1, 5] -> 0-indexed: [0, 4]
875        let pins = vec![0, 4];
876        (locs, pins)
877    }
878
879    fn mis_overhead(&self) -> i32 {
880        0
881    }
882
883    fn source_weights(&self) -> Vec<i32> {
884        vec![2; 5]
885    }
886
887    fn mapped_weights(&self) -> Vec<i32> {
888        vec![2; 5]
889    }
890}
891
892/// Triangular branch fix gadget.
893#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
894pub struct TriBranchFix;
895
896impl TriangularGadget for TriBranchFix {
897    fn size(&self) -> (usize, usize) {
898        (4, 4)
899    }
900
901    fn cross_location(&self) -> (usize, usize) {
902        (2, 2)
903    }
904
905    fn is_connected(&self) -> bool {
906        false
907    }
908
909    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
910        // Julia: locs = Node.([(1,2), (2,2), (2,3),(3,3),(3,2),(4,2)])
911        // Julia: g = simplegraph([(1,2), (2,3), (3,4),(4,5), (5,6)])
912        let locs = vec![(1, 2), (2, 2), (2, 3), (3, 3), (3, 2), (4, 2)];
913        // 0-indexed: [(0,1), (1,2), (2,3), (3,4), (4,5)]
914        let edges = vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)];
915        // Julia: pins = [1, 6] -> 0-indexed: [0, 5]
916        let pins = vec![0, 5];
917        (locs, edges, pins)
918    }
919
920    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
921        // Julia: locs = Node.([(1,2),(2,2),(3,2),(4,2)])
922        let locs = vec![(1, 2), (2, 2), (3, 2), (4, 2)];
923        // Julia: pins = [1, 4] -> 0-indexed: [0, 3]
924        let pins = vec![0, 3];
925        (locs, pins)
926    }
927
928    fn mis_overhead(&self) -> i32 {
929        -2
930    }
931
932    fn source_weights(&self) -> Vec<i32> {
933        vec![2; 6]
934    }
935
936    fn mapped_weights(&self) -> Vec<i32> {
937        vec![2; 4]
938    }
939}
940
941/// Triangular branch fix B gadget.
942#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
943pub struct TriBranchFixB;
944
945impl TriangularGadget for TriBranchFixB {
946    fn size(&self) -> (usize, usize) {
947        (4, 4)
948    }
949
950    fn cross_location(&self) -> (usize, usize) {
951        (2, 2)
952    }
953
954    fn is_connected(&self) -> bool {
955        false
956    }
957
958    fn source_graph(&self) -> (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<usize>) {
959        // Julia: locs = Node.([(2,3),(3,2),(3,3),(4,2)])
960        // Julia: g = simplegraph([(1,3), (2,3), (2,4)])
961        let locs = vec![(2, 3), (3, 2), (3, 3), (4, 2)];
962        // 0-indexed: [(0,2), (1,2), (1,3)]
963        let edges = vec![(0, 2), (1, 2), (1, 3)];
964        // Julia: pins = [1, 4] -> 0-indexed: [0, 3]
965        let pins = vec![0, 3];
966        (locs, edges, pins)
967    }
968
969    fn mapped_graph(&self) -> (Vec<(usize, usize)>, Vec<usize>) {
970        // Julia: locs = Node.([(3,2),(4,2)])
971        let locs = vec![(3, 2), (4, 2)];
972        // Julia: pins = [1, 2] -> 0-indexed: [0, 1]
973        let pins = vec![0, 1];
974        (locs, pins)
975    }
976
977    fn mis_overhead(&self) -> i32 {
978        -2
979    }
980
981    fn source_weights(&self) -> Vec<i32> {
982        vec![2; 4]
983    }
984
985    fn mapped_weights(&self) -> Vec<i32> {
986        vec![2; 2]
987    }
988}
989
990/// Check if a triangular gadget pattern matches at position (i, j) in the grid.
991/// i, j are 0-indexed row/col offsets (pattern top-left corner).
992///
993/// For weighted triangular mode, this also checks that weights match the expected
994/// source_weights from the gadget. This matches Julia's behavior where WeightedGadget
995/// source matrices include weights and match() uses == comparison.
996#[allow(clippy::needless_range_loop)]
997fn pattern_matches_triangular<G: TriangularGadget>(
998    gadget: &G,
999    grid: &MappingGrid,
1000    i: usize,
1001    j: usize,
1002) -> bool {
1003    use super::grid::CellState;
1004
1005    let source = gadget.source_matrix();
1006    let (m, n) = gadget.size();
1007
1008    // First pass: check cell states (empty/occupied/connected)
1009    for r in 0..m {
1010        for c in 0..n {
1011            let grid_r = i + r;
1012            let grid_c = j + c;
1013            let expected = source[r][c];
1014            let actual = grid.get(grid_r, grid_c);
1015
1016            match expected {
1017                LegacySourceCell::Empty => {
1018                    // Grid cell should be empty
1019                    if actual.map(|c| !c.is_empty()).unwrap_or(false) {
1020                        return false;
1021                    }
1022                }
1023                LegacySourceCell::Occupied => {
1024                    // Grid cell should be occupied (but not necessarily connected)
1025                    if !actual.map(|c| !c.is_empty()).unwrap_or(false) {
1026                        return false;
1027                    }
1028                }
1029                LegacySourceCell::Connected => {
1030                    // Grid cell should be Connected specifically
1031                    match actual {
1032                        Some(CellState::Connected { .. }) => {}
1033                        _ => return false,
1034                    }
1035                }
1036            }
1037        }
1038    }
1039
1040    // Second pass: check weights for weighted triangular mode
1041    // Julia's WeightedGadget stores source_weights and match() compares cells including weight
1042    let (locs, _, _) = gadget.source_graph();
1043    let weights = gadget.source_weights();
1044
1045    for (idx, (loc_r, loc_c)) in locs.iter().enumerate() {
1046        // source_graph locations are 1-indexed, convert to grid position
1047        let grid_r = i + loc_r - 1;
1048        let grid_c = j + loc_c - 1;
1049        let expected_weight = weights[idx];
1050
1051        if let Some(cell) = grid.get(grid_r, grid_c) {
1052            if cell.weight() != expected_weight {
1053                return false;
1054            }
1055        } else {
1056            return false;
1057        }
1058    }
1059
1060    true
1061}
1062
1063/// Apply a triangular gadget pattern at position (i, j).
1064/// i, j are 0-indexed row/col offsets (pattern top-left corner).
1065#[allow(clippy::needless_range_loop)]
1066fn apply_triangular_gadget<G: TriangularGadget>(
1067    gadget: &G,
1068    grid: &mut MappingGrid,
1069    i: usize,
1070    j: usize,
1071) {
1072    use super::grid::CellState;
1073
1074    let source = gadget.source_matrix();
1075    let (m, n) = gadget.size();
1076
1077    // First, clear source pattern cells (any non-empty cell)
1078    for r in 0..m {
1079        for c in 0..n {
1080            if source[r][c] != LegacySourceCell::Empty {
1081                grid.set(i + r, j + c, CellState::Empty);
1082            }
1083        }
1084    }
1085
1086    // Then, add mapped pattern cells with proper weights
1087    // locs are 1-indexed within the pattern's bounding box
1088    let (locs, _) = gadget.mapped_graph();
1089    let weights = gadget.mapped_weights();
1090    for (idx, (r, c)) in locs.iter().enumerate() {
1091        if *r > 0 && *c > 0 && *r <= m && *c <= n {
1092            let weight = weights.get(idx).copied().unwrap_or(2);
1093            // Convert 1-indexed pattern pos to 0-indexed grid pos
1094            grid.add_node(i + r - 1, j + c - 1, weight);
1095        }
1096    }
1097}
1098
1099/// Apply all triangular crossing gadgets to resolve crossings.
1100/// Returns the tape of applied gadgets.
1101///
1102/// This matches Julia's `apply_crossing_gadgets!` which iterates ALL pairs (i,j)
1103/// and tries to match patterns at each crossing point.
1104pub fn apply_triangular_crossing_gadgets(
1105    grid: &mut MappingGrid,
1106    copylines: &[super::copyline::CopyLine],
1107    spacing: usize,
1108    padding: usize,
1109) -> Vec<TriangularTapeEntry> {
1110    use std::collections::HashSet;
1111
1112    let mut tape = Vec::new();
1113    let mut processed = HashSet::new();
1114    let n = copylines.len();
1115
1116    // Iterate ALL pairs (matching Julia's for j=1:n, for i=1:n)
1117    for j in 0..n {
1118        for i in 0..n {
1119            let (cross_row, cross_col) = crossat_triangular(copylines, i, j, spacing, padding);
1120
1121            // Skip if this crossing point has already been processed
1122            // (avoids double-applying trivial gadgets for symmetric pairs like (i,j) and (j,i))
1123            if processed.contains(&(cross_row, cross_col)) {
1124                continue;
1125            }
1126
1127            // Try each gadget in the ruleset at this crossing point
1128            if let Some(entry) = try_match_triangular_gadget(grid, cross_row, cross_col) {
1129                tape.push(entry);
1130                processed.insert((cross_row, cross_col));
1131            }
1132        }
1133    }
1134
1135    tape
1136}
1137
1138/// Try to match and apply a triangular gadget at the crossing point.
1139fn try_match_triangular_gadget(
1140    grid: &mut MappingGrid,
1141    cross_row: usize,
1142    cross_col: usize,
1143) -> Option<TriangularTapeEntry> {
1144    // Macro to reduce repetition
1145    macro_rules! try_gadget {
1146        ($gadget:expr, $idx:expr) => {{
1147            let g = $gadget;
1148            let (cr, cc) = g.cross_location();
1149            if cross_row >= cr && cross_col >= cc {
1150                let x = cross_row - cr + 1;
1151                let y = cross_col - cc + 1;
1152                if pattern_matches_triangular(&g, grid, x, y) {
1153                    apply_triangular_gadget(&g, grid, x, y);
1154                    return Some(TriangularTapeEntry {
1155                        gadget_idx: $idx,
1156                        row: x,
1157                        col: y,
1158                    });
1159                }
1160            }
1161        }};
1162    }
1163
1164    // Try gadgets in order (matching Julia's triangular_crossing_ruleset)
1165    // TriCross<true> must be tried BEFORE TriCross<false> because it's more specific
1166    // (requires Connected cells). If we try TriCross<false> first, it will match
1167    // even when there are Connected cells since it doesn't check for them.
1168    try_gadget!(TriCross::<true>, 1);
1169    try_gadget!(TriCross::<false>, 0);
1170    try_gadget!(TriTConLeft, 2);
1171    try_gadget!(TriTConUp, 3);
1172    try_gadget!(TriTConDown, 4);
1173    try_gadget!(TriTrivialTurnLeft, 5);
1174    try_gadget!(TriTrivialTurnRight, 6);
1175    try_gadget!(TriEndTurn, 7);
1176    try_gadget!(TriTurn, 8);
1177    try_gadget!(TriWTurn, 9);
1178    try_gadget!(TriBranchFix, 10);
1179    try_gadget!(TriBranchFixB, 11);
1180    try_gadget!(TriBranch, 12);
1181
1182    None
1183}
1184
1185/// Get MIS overhead for a triangular tape entry.
1186/// For triangular mode, crossing gadgets use their native overhead,
1187/// but simplifiers (DanglingLeg) use weighted overhead = unweighted * 2.
1188/// Julia: mis_overhead(w::WeightedGadget) = mis_overhead(w.gadget) * 2
1189pub fn triangular_tape_entry_mis_overhead(entry: &TriangularTapeEntry) -> i32 {
1190    match entry.gadget_idx {
1191        0 => TriCross::<false>.mis_overhead(),
1192        1 => TriCross::<true>.mis_overhead(),
1193        2 => TriTConLeft.mis_overhead(),
1194        3 => TriTConUp.mis_overhead(),
1195        4 => TriTConDown.mis_overhead(),
1196        5 => TriTrivialTurnLeft.mis_overhead(),
1197        6 => TriTrivialTurnRight.mis_overhead(),
1198        7 => TriEndTurn.mis_overhead(),
1199        8 => TriTurn.mis_overhead(),
1200        9 => TriWTurn.mis_overhead(),
1201        10 => TriBranchFix.mis_overhead(),
1202        11 => TriBranchFixB.mis_overhead(),
1203        12 => TriBranch.mis_overhead(),
1204        // Simplifier gadgets (100+): weighted overhead = -1 * 2 = -2
1205        idx if idx >= 100 => -2,
1206        _ => 0,
1207    }
1208}
1209
1210// ============================================================================
1211// Triangular Simplifier Gadgets
1212// ============================================================================
1213
1214/// Apply simplifier gadgets to the triangular grid.
1215/// This matches Julia's `apply_simplifier_gadgets!` for TriangularWeighted mode.
1216///
1217/// The weighted DanglingLeg pattern matches 3 nodes in a line where:
1218/// - The end node (closest to center) has weight 1
1219/// - The other two nodes have weight 2
1220///   After simplification, only 1 node remains with weight 1.
1221#[allow(dead_code)]
1222pub fn apply_triangular_simplifier_gadgets(
1223    grid: &mut MappingGrid,
1224    nrepeat: usize,
1225) -> Vec<TriangularTapeEntry> {
1226    #[allow(unused)]
1227    use super::grid::CellState;
1228
1229    let mut tape = Vec::new();
1230    let (rows, cols) = grid.size();
1231
1232    for _ in 0..nrepeat {
1233        // Try all 4 directions at each position
1234        // Pattern functions handle bounds checking internally
1235        for j in 0..cols {
1236            for i in 0..rows {
1237                // Down pattern (4x3): needs i+3 < rows, j+2 < cols
1238                if try_apply_dangling_leg_down(grid, i, j) {
1239                    tape.push(TriangularTapeEntry {
1240                        gadget_idx: 100, // DanglingLeg down
1241                        row: i,
1242                        col: j,
1243                    });
1244                }
1245                // Up pattern (4x3): needs i+3 < rows, j+2 < cols
1246                if try_apply_dangling_leg_up(grid, i, j) {
1247                    tape.push(TriangularTapeEntry {
1248                        gadget_idx: 101, // DanglingLeg up
1249                        row: i,
1250                        col: j,
1251                    });
1252                }
1253                // Right pattern (3x4): needs i+2 < rows, j+3 < cols
1254                if try_apply_dangling_leg_right(grid, i, j) {
1255                    tape.push(TriangularTapeEntry {
1256                        gadget_idx: 102, // DanglingLeg right
1257                        row: i,
1258                        col: j,
1259                    });
1260                }
1261                // Left pattern (3x4): needs i+2 < rows, j+3 < cols
1262                if try_apply_dangling_leg_left(grid, i, j) {
1263                    tape.push(TriangularTapeEntry {
1264                        gadget_idx: 103, // DanglingLeg left
1265                        row: i,
1266                        col: j,
1267                    });
1268                }
1269            }
1270        }
1271    }
1272
1273    tape
1274}
1275
1276/// Try to apply DanglingLeg pattern going downward.
1277#[allow(dead_code)]
1278fn try_apply_dangling_leg_down(grid: &mut MappingGrid, i: usize, j: usize) -> bool {
1279    use super::grid::CellState;
1280
1281    let (rows, cols) = grid.size();
1282
1283    // Need at least 4 rows and 3 cols from position (i, j)
1284    if i + 3 >= rows || j + 2 >= cols {
1285        return false;
1286    }
1287
1288    // Helper to check if cell at (row, col) is empty
1289    let is_empty = |row: usize, col: usize| -> bool { !grid.is_occupied(row, col) };
1290
1291    // Helper to check if cell has specific weight
1292    let has_weight = |row: usize, col: usize, w: i32| -> bool {
1293        grid.get(row, col).is_some_and(|c| c.weight() == w)
1294    };
1295
1296    // Row i (row 1 of pattern): all 3 cells must be empty
1297    if !is_empty(i, j) || !is_empty(i, j + 1) || !is_empty(i, j + 2) {
1298        return false;
1299    }
1300
1301    // Row i+1 (row 2): empty, occupied(w=1), empty
1302    if !is_empty(i + 1, j) || !has_weight(i + 1, j + 1, 1) || !is_empty(i + 1, j + 2) {
1303        return false;
1304    }
1305
1306    // Row i+2 (row 3): empty, occupied(w=2), empty
1307    if !is_empty(i + 2, j) || !has_weight(i + 2, j + 1, 2) || !is_empty(i + 2, j + 2) {
1308        return false;
1309    }
1310
1311    // Row i+3 (row 4): empty, occupied(w=2), empty
1312    if !is_empty(i + 3, j) || !has_weight(i + 3, j + 1, 2) || !is_empty(i + 3, j + 2) {
1313        return false;
1314    }
1315
1316    // Apply transformation: remove top 2 nodes, bottom node gets weight 1
1317    grid.set(i + 1, j + 1, CellState::Empty);
1318    grid.set(i + 2, j + 1, CellState::Empty);
1319    grid.set(i + 3, j + 1, CellState::Occupied { weight: 1 });
1320
1321    true
1322}
1323
1324/// Try to apply DanglingLeg pattern going upward.
1325#[allow(dead_code)]
1326fn try_apply_dangling_leg_up(grid: &mut MappingGrid, i: usize, j: usize) -> bool {
1327    use super::grid::CellState;
1328
1329    let (rows, cols) = grid.size();
1330
1331    // Need at least 4 rows and 3 cols from position (i, j)
1332    if i + 3 >= rows || j + 2 >= cols {
1333        return false;
1334    }
1335
1336    let is_empty = |row: usize, col: usize| -> bool { !grid.is_occupied(row, col) };
1337
1338    let has_weight = |row: usize, col: usize, w: i32| -> bool {
1339        grid.get(row, col).is_some_and(|c| c.weight() == w)
1340    };
1341
1342    // Row i: empty, occupied(w=2), empty
1343    if !is_empty(i, j) || !has_weight(i, j + 1, 2) || !is_empty(i, j + 2) {
1344        return false;
1345    }
1346
1347    // Row i+1: empty, occupied(w=2), empty
1348    if !is_empty(i + 1, j) || !has_weight(i + 1, j + 1, 2) || !is_empty(i + 1, j + 2) {
1349        return false;
1350    }
1351
1352    // Row i+2: empty, occupied(w=1), empty [dangling end]
1353    if !is_empty(i + 2, j) || !has_weight(i + 2, j + 1, 1) || !is_empty(i + 2, j + 2) {
1354        return false;
1355    }
1356
1357    // Row i+3: all 3 cells must be empty
1358    if !is_empty(i + 3, j) || !is_empty(i + 3, j + 1) || !is_empty(i + 3, j + 2) {
1359        return false;
1360    }
1361
1362    // Apply transformation: remove dangling end and middle, base gets weight 1
1363    grid.set(i + 1, j + 1, CellState::Empty);
1364    grid.set(i + 2, j + 1, CellState::Empty);
1365    grid.set(i, j + 1, CellState::Occupied { weight: 1 });
1366
1367    true
1368}
1369
1370/// Try to apply DanglingLeg pattern going right.
1371#[allow(dead_code)]
1372fn try_apply_dangling_leg_right(grid: &mut MappingGrid, i: usize, j: usize) -> bool {
1373    use super::grid::CellState;
1374
1375    let (rows, cols) = grid.size();
1376
1377    // Need at least 3 rows and 4 cols from position (i, j)
1378    if i + 2 >= rows || j + 3 >= cols {
1379        return false;
1380    }
1381
1382    let is_empty = |row: usize, col: usize| -> bool { !grid.is_occupied(row, col) };
1383
1384    let has_weight = |row: usize, col: usize, w: i32| -> bool {
1385        grid.get(row, col).is_some_and(|c| c.weight() == w)
1386    };
1387
1388    // Row i: all 4 cells must be empty
1389    if !is_empty(i, j) || !is_empty(i, j + 1) || !is_empty(i, j + 2) || !is_empty(i, j + 3) {
1390        return false;
1391    }
1392
1393    // Row i+1: occupied(w=2), occupied(w=2), occupied(w=1), empty
1394    if !has_weight(i + 1, j, 2)
1395        || !has_weight(i + 1, j + 1, 2)
1396        || !has_weight(i + 1, j + 2, 1)
1397        || !is_empty(i + 1, j + 3)
1398    {
1399        return false;
1400    }
1401
1402    // Row i+2: all 4 cells must be empty
1403    if !is_empty(i + 2, j)
1404        || !is_empty(i + 2, j + 1)
1405        || !is_empty(i + 2, j + 2)
1406        || !is_empty(i + 2, j + 3)
1407    {
1408        return false;
1409    }
1410
1411    // Apply transformation: remove dangling and middle, base gets weight 1
1412    grid.set(i + 1, j + 1, CellState::Empty);
1413    grid.set(i + 1, j + 2, CellState::Empty);
1414    grid.set(i + 1, j, CellState::Occupied { weight: 1 });
1415
1416    true
1417}
1418
1419/// Try to apply DanglingLeg pattern going left.
1420#[allow(dead_code)]
1421fn try_apply_dangling_leg_left(grid: &mut MappingGrid, i: usize, j: usize) -> bool {
1422    use super::grid::CellState;
1423
1424    let (rows, cols) = grid.size();
1425
1426    // Need at least 3 rows and 4 cols from position (i, j)
1427    if i + 2 >= rows || j + 3 >= cols {
1428        return false;
1429    }
1430
1431    let is_empty = |row: usize, col: usize| -> bool { !grid.is_occupied(row, col) };
1432
1433    let has_weight = |row: usize, col: usize, w: i32| -> bool {
1434        grid.get(row, col).is_some_and(|c| c.weight() == w)
1435    };
1436
1437    // Row i: all 4 cells must be empty
1438    if !is_empty(i, j) || !is_empty(i, j + 1) || !is_empty(i, j + 2) || !is_empty(i, j + 3) {
1439        return false;
1440    }
1441
1442    // Row i+1: empty, occupied(w=1), occupied(w=2), occupied(w=2)
1443    if !is_empty(i + 1, j)
1444        || !has_weight(i + 1, j + 1, 1)
1445        || !has_weight(i + 1, j + 2, 2)
1446        || !has_weight(i + 1, j + 3, 2)
1447    {
1448        return false;
1449    }
1450
1451    // Row i+2: all 4 cells must be empty
1452    if !is_empty(i + 2, j)
1453        || !is_empty(i + 2, j + 1)
1454        || !is_empty(i + 2, j + 2)
1455        || !is_empty(i + 2, j + 3)
1456    {
1457        return false;
1458    }
1459
1460    // Apply transformation: remove dangling and middle, base gets weight 1
1461    grid.set(i + 1, j + 1, CellState::Empty);
1462    grid.set(i + 1, j + 2, CellState::Empty);
1463    grid.set(i + 1, j + 3, CellState::Occupied { weight: 1 });
1464
1465    true
1466}
1467
1468/// Map a graph to a triangular lattice grid graph using optimal path decomposition.
1469///
1470/// # Panics
1471/// Panics if `num_vertices == 0`.
1472pub fn map_graph_triangular(num_vertices: usize, edges: &[(usize, usize)]) -> MappingResult {
1473    map_graph_triangular_with_method(num_vertices, edges, PathDecompositionMethod::Auto)
1474}
1475
1476/// Map a graph to triangular lattice using a specific path decomposition method.
1477pub fn map_graph_triangular_with_method(
1478    num_vertices: usize,
1479    edges: &[(usize, usize)],
1480    method: PathDecompositionMethod,
1481) -> MappingResult {
1482    let layout = pathwidth(num_vertices, edges, method);
1483    let vertex_order = vertex_order_from_layout(&layout);
1484    map_graph_triangular_with_order(num_vertices, edges, &vertex_order)
1485}
1486
1487/// Map a graph to triangular lattice with specific vertex ordering.
1488///
1489/// # Panics
1490/// Panics if `num_vertices == 0` or if any edge vertex is not in `vertex_order`.
1491pub fn map_graph_triangular_with_order(
1492    num_vertices: usize,
1493    edges: &[(usize, usize)],
1494    vertex_order: &[usize],
1495) -> MappingResult {
1496    assert!(num_vertices > 0, "num_vertices must be > 0");
1497
1498    let spacing = TRIANGULAR_SPACING;
1499    let padding = TRIANGULAR_PADDING;
1500
1501    let copylines = create_copylines(num_vertices, edges, vertex_order);
1502
1503    // Calculate grid dimensions
1504    // Julia formula: N = (n-1)*col_spacing + 2 + 2*padding
1505    //                M = nrow*row_spacing + 2 + 2*padding
1506    // where nrow = max(hslot, vstop) and n = num_vertices
1507    let max_hslot = copylines.iter().map(|l| l.hslot).max().unwrap_or(1);
1508    let max_vstop = copylines.iter().map(|l| l.vstop).max().unwrap_or(1);
1509
1510    let rows = max_hslot.max(max_vstop) * spacing + 2 + 2 * padding;
1511    // Use (num_vertices - 1) for cols, matching Julia's (n-1) formula
1512    let cols = (num_vertices - 1) * spacing + 2 + 2 * padding;
1513
1514    let mut grid = MappingGrid::with_padding(rows, cols, spacing, padding);
1515
1516    // Add copy line nodes using triangular dense locations
1517    // (includes the endpoint node for triangular weighted mode)
1518    for line in &copylines {
1519        for (row, col, weight) in line.copyline_locations_triangular(padding, spacing) {
1520            grid.add_node(row, col, weight as i32);
1521        }
1522    }
1523
1524    // Mark edge connections at crossing points
1525    for &(u, v) in edges {
1526        let u_line = &copylines[u];
1527        let v_line = &copylines[v];
1528
1529        let (smaller_line, larger_line) = if u_line.vslot < v_line.vslot {
1530            (u_line, v_line)
1531        } else {
1532            (v_line, u_line)
1533        };
1534
1535        let (row, col) = crossat_triangular(
1536            &copylines,
1537            smaller_line.vertex,
1538            larger_line.vertex,
1539            spacing,
1540            padding,
1541        );
1542
1543        // Mark connected cells at crossing point
1544        if col > 0 {
1545            grid.connect(row, col - 1);
1546        }
1547        if row > 0 && grid.is_occupied(row - 1, col) {
1548            grid.connect(row - 1, col);
1549        } else if row + 1 < grid.size().0 && grid.is_occupied(row + 1, col) {
1550            grid.connect(row + 1, col);
1551        }
1552    }
1553
1554    // Apply crossing gadgets (iterates ALL pairs, not just edges)
1555    let mut triangular_tape =
1556        apply_triangular_crossing_gadgets(&mut grid, &copylines, spacing, padding);
1557
1558    // Apply simplifier gadgets (weighted DanglingLeg pattern)
1559    // Julia's triangular mode uses: weighted.(default_simplifier_ruleset(UnWeighted()))
1560    // which applies the weighted DanglingLeg pattern to reduce grid complexity.
1561    let simplifier_tape = apply_triangular_simplifier_gadgets(&mut grid, 10);
1562    triangular_tape.extend(simplifier_tape);
1563
1564    // Calculate MIS overhead from copylines using the dedicated function
1565    // which matches Julia's mis_overhead_copyline(TriangularWeighted(), ...)
1566    let copyline_overhead: i32 = copylines
1567        .iter()
1568        .map(|line| super::copyline::mis_overhead_copyline_triangular(line, spacing))
1569        .sum();
1570
1571    // Add gadget overhead (crossing gadgets + simplifiers)
1572    let gadget_overhead: i32 = triangular_tape
1573        .iter()
1574        .map(triangular_tape_entry_mis_overhead)
1575        .sum();
1576    let mis_overhead = copyline_overhead + gadget_overhead;
1577
1578    // Convert triangular tape entries to generic tape entries
1579    let tape: Vec<TapeEntry> = triangular_tape
1580        .into_iter()
1581        .map(|entry| TapeEntry {
1582            pattern_idx: entry.gadget_idx,
1583            row: entry.row,
1584            col: entry.col,
1585        })
1586        .collect();
1587
1588    // Extract doubled cells before extracting positions
1589    let doubled_cells = grid.doubled_cells();
1590
1591    // Extract positions and weights from occupied cells
1592    let (positions, node_weights): (Vec<(i32, i32)>, Vec<i32>) = grid
1593        .occupied_coords()
1594        .into_iter()
1595        .filter_map(|(row, col)| {
1596            grid.get(row, col)
1597                .map(|cell| ((row as i32, col as i32), cell.weight()))
1598        })
1599        .filter(|&(_, w)| w > 0)
1600        .unzip();
1601
1602    MappingResult {
1603        positions,
1604        node_weights,
1605        grid_dimensions: grid.size(),
1606        kind: GridKind::Triangular,
1607        lines: copylines,
1608        padding,
1609        spacing,
1610        mis_overhead,
1611        tape,
1612        doubled_cells,
1613    }
1614}
1615
1616#[cfg(test)]
1617#[path = "../../../unit_tests/rules/unitdiskmapping/triangular/mod.rs"]
1618mod tests;