Skip to main content

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