problemreductions/topology/
triangular_subgraph.rs1use super::graph::Graph;
7use super::unit_disk_graph::UnitDiskGraph;
8use crate::registry::ConstructionError;
9use crate::types::i64_to_exact_f64;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct TriangularSubgraph {
25 positions: Vec<(i64, i64)>,
27}
28
29const TRIANGULAR_RADIUS: f64 = 1.1;
31
32impl TriangularSubgraph {
33 pub fn new(positions: Vec<(i64, i64)>) -> Self {
35 Self { positions }
36 }
37
38 pub fn positions(&self) -> &[(i64, i64)] {
40 &self.positions
41 }
42
43 pub fn num_positions(&self) -> usize {
45 self.positions.len()
46 }
47
48 #[allow(unknown_lints, clippy::manual_is_multiple_of)]
54 fn physical_position(row: i64, col: i64) -> Result<(f64, f64), ConstructionError> {
55 let row = i64_to_exact_f64(row)?;
56 let offset = if col % 2 == 0 { 0.5 } else { 0.0 };
57 let col = i64_to_exact_f64(col)?;
58 let y = col * (3.0_f64.sqrt() / 2.0);
59 let x = row + offset;
60 Ok((x, y))
61 }
62
63 fn are_adjacent(p1: (i64, i64), p2: (i64, i64)) -> bool {
64 let column_delta = (i128::from(p1.1) - i128::from(p2.1)).abs();
65 if column_delta > 1 {
66 return false;
67 }
68 let x1 = 2 * i128::from(p1.0) + i128::from(p1.1.rem_euclid(2) == 0);
69 let x2 = 2 * i128::from(p2.0) + i128::from(p2.1.rem_euclid(2) == 0);
70 let x_delta = (x1 - x2).abs();
71 x_delta <= 2 && x_delta * x_delta + 3 * column_delta * column_delta <= 4
72 }
73
74 pub(crate) fn try_to_unit_disk_graph(&self) -> Result<UnitDiskGraph, ConstructionError> {
75 let positions = self
76 .positions
77 .iter()
78 .map(|&(row, column)| Self::physical_position(row, column))
79 .collect::<Result<Vec<_>, _>>()?;
80 let graph = UnitDiskGraph::new(positions, TRIANGULAR_RADIUS)?;
81 for first in 0..self.positions.len() {
82 for second in (first + 1)..self.positions.len() {
83 if Self::are_adjacent(self.positions[first], self.positions[second])
84 != graph.has_edge(first, second)
85 {
86 return Err(ConstructionError::Conversion(format!(
87 "triangular-subgraph coordinates at indices {first} and {second} cannot be represented in UnitDiskGraph without changing adjacency"
88 )));
89 }
90 }
91 }
92 Ok(graph)
93 }
94}
95
96impl Graph for TriangularSubgraph {
97 const NAME: &'static str = "TriangularSubgraph";
98
99 fn num_vertices(&self) -> usize {
100 self.positions.len()
101 }
102
103 fn num_edges(&self) -> usize {
104 let n = self.positions.len();
105 let mut count = 0;
106 for i in 0..n {
107 for j in (i + 1)..n {
108 if Self::are_adjacent(self.positions[i], self.positions[j]) {
109 count += 1;
110 }
111 }
112 }
113 count
114 }
115
116 fn edges(&self) -> Vec<(usize, usize)> {
117 let n = self.positions.len();
118 let mut edges = Vec::new();
119 for i in 0..n {
120 for j in (i + 1)..n {
121 if Self::are_adjacent(self.positions[i], self.positions[j]) {
122 edges.push((i, j));
123 }
124 }
125 }
126 edges
127 }
128
129 fn has_edge(&self, u: usize, v: usize) -> bool {
130 if u >= self.positions.len() || v >= self.positions.len() || u == v {
131 return false;
132 }
133 Self::are_adjacent(self.positions[u], self.positions[v])
134 }
135
136 fn neighbors(&self, v: usize) -> Vec<usize> {
137 if v >= self.positions.len() {
138 return Vec::new();
139 }
140 (0..self.positions.len())
141 .filter(|&u| u != v && Self::are_adjacent(self.positions[v], self.positions[u]))
142 .collect()
143 }
144}
145
146impl crate::variant::VariantParam for TriangularSubgraph {
147 const CATEGORY: &'static str = "graph";
148 const VALUE: &'static str = "TriangularSubgraph";
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::types::MAX_EXACT_F64_INTEGER;
155
156 #[test]
157 fn adjacency_handles_full_i64_coordinate_range() {
158 let graph = TriangularSubgraph::new(vec![(i64::MAX, 0), (i64::MAX, 1), (i64::MIN, 0)]);
159
160 assert!(graph.has_edge(0, 1));
161 assert!(!graph.has_edge(0, 2));
162 }
163
164 #[test]
165 fn integer_adjacency_matches_euclidean_definition() {
166 for row_a in -4..=4 {
167 for column_a in -4..=4 {
168 for row_b in -4..=4 {
169 for column_b in -4..=4 {
170 let a = TriangularSubgraph::physical_position(row_a, column_a).unwrap();
171 let b = TriangularSubgraph::physical_position(row_b, column_b).unwrap();
172 let euclidean = (a.0 - b.0).hypot(a.1 - b.1) < TRIANGULAR_RADIUS;
173 assert_eq!(
174 TriangularSubgraph::are_adjacent((row_a, column_a), (row_b, column_b)),
175 euclidean
176 );
177 }
178 }
179 }
180 }
181 }
182
183 #[test]
184 fn unit_disk_conversion_rejects_inexact_coordinates() {
185 let graph = TriangularSubgraph::new(vec![(MAX_EXACT_F64_INTEGER + 1, 0)]);
186
187 assert!(matches!(
188 graph.try_to_unit_disk_graph(),
189 Err(ConstructionError::InexactFloatConversion(_))
190 ));
191 }
192
193 #[test]
194 fn unit_disk_conversion_rejects_changed_adjacency() {
195 let graph =
196 TriangularSubgraph::new(vec![(MAX_EXACT_F64_INTEGER, 0), (MAX_EXACT_F64_INTEGER, 1)]);
197
198 assert!(matches!(
199 graph.try_to_unit_disk_graph(),
200 Err(ConstructionError::Conversion(_))
201 ));
202 }
203}