problemreductions/rules/unitdiskmapping/triangular/
gadgets.rs

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