Skip to main content

oxiz_sat/
community_partition.rs

1use crate::clause::Clause;
2use crate::community::{Communities, CommunityOrdering, LouvainDetector, VariableIncidenceGraph};
3/// Community-based clause database partitioning for improved cache locality.
4///
5/// This module integrates community detection with clause database management to partition
6/// clauses based on their variable communities. This improves cache locality and reduces
7/// memory access overhead during solving.
8#[allow(unused_imports)]
9use crate::prelude::*;
10
11/// Partitioned clause database organized by variable communities.
12#[derive(Debug, Clone)]
13pub struct CommunityPartition {
14    /// Clauses grouped by their primary community (community with most variables)
15    partitions: Vec<Vec<usize>>,
16    /// Community structure
17    communities: Communities,
18    /// Number of communities
19    num_communities: usize,
20}
21
22impl CommunityPartition {
23    /// Creates a new community partition from clauses.
24    pub fn from_clauses(num_vars: usize, clauses: &[Clause]) -> Self {
25        // Build VIG and detect communities
26        let vig = VariableIncidenceGraph::from_clauses(num_vars, clauses);
27        let detector = LouvainDetector::default();
28        let communities = detector.detect(&vig);
29        let num_communities = communities.num_communities();
30
31        // Partition clauses by their dominant community
32        let mut partitions: Vec<Vec<usize>> = vec![Vec::new(); num_communities];
33
34        for (idx, clause) in clauses.iter().enumerate() {
35            if let Some(primary_comm) = Self::get_primary_community(clause, &communities)
36                && primary_comm < num_communities
37            {
38                partitions[primary_comm].push(idx);
39            }
40        }
41
42        Self {
43            partitions,
44            communities,
45            num_communities,
46        }
47    }
48
49    /// Gets the primary community for a clause (community with most variables in clause).
50    fn get_primary_community(clause: &Clause, communities: &Communities) -> Option<usize> {
51        if clause.lits.is_empty() {
52            return None;
53        }
54
55        let mut comm_counts: HashMap<usize, usize> = HashMap::new();
56
57        for lit in &clause.lits {
58            let var = lit.var().index();
59            let comm = communities.community(var);
60            *comm_counts.entry(comm).or_insert(0) += 1;
61        }
62
63        comm_counts
64            .into_iter()
65            .max_by_key(|(_, count)| *count)
66            .map(|(comm, _)| comm)
67    }
68
69    /// Returns the clause indices in a specific community partition.
70    pub fn get_partition(&self, community: usize) -> &[usize] {
71        if community < self.partitions.len() {
72            &self.partitions[community]
73        } else {
74            &[]
75        }
76    }
77
78    /// Returns the number of communities.
79    pub fn num_communities(&self) -> usize {
80        self.num_communities
81    }
82
83    /// Returns the community ID for a variable.
84    pub fn community(&self, var: usize) -> usize {
85        self.communities.community(var)
86    }
87
88    /// Returns all partition sizes.
89    pub fn partition_sizes(&self) -> Vec<usize> {
90        self.partitions.iter().map(|p| p.len()).collect()
91    }
92
93    /// Returns the total number of clauses across all partitions.
94    pub fn total_clauses(&self) -> usize {
95        self.partitions.iter().map(|p| p.len()).sum()
96    }
97
98    /// Returns the communities structure.
99    pub fn communities(&self) -> &Communities {
100        &self.communities
101    }
102
103    /// Creates a community-aware variable ordering.
104    pub fn create_ordering(&self) -> CommunityOrdering {
105        CommunityOrdering::new(self.communities.clone())
106    }
107}
108
109/// Statistics for community-based partitioning.
110#[derive(Debug, Clone, Default)]
111pub struct PartitionStats {
112    /// Number of communities
113    pub num_communities: usize,
114    /// Total clauses partitioned
115    pub total_clauses: usize,
116    /// Partition sizes
117    pub partition_sizes: Vec<usize>,
118    /// Average partition size
119    pub avg_partition_size: f64,
120    /// Largest partition size
121    pub max_partition_size: usize,
122    /// Smallest partition size
123    pub min_partition_size: usize,
124    /// Modularity of the partition
125    pub modularity: f64,
126}
127
128impl PartitionStats {
129    /// Creates statistics from a community partition.
130    pub fn from_partition(partition: &CommunityPartition) -> Self {
131        let sizes = partition.partition_sizes();
132        let total = partition.total_clauses();
133        let avg = if !sizes.is_empty() {
134            total as f64 / sizes.len() as f64
135        } else {
136            0.0
137        };
138
139        Self {
140            num_communities: partition.num_communities(),
141            total_clauses: total,
142            partition_sizes: sizes.clone(),
143            avg_partition_size: avg,
144            max_partition_size: sizes.iter().copied().max().unwrap_or(0),
145            min_partition_size: sizes.iter().copied().min().unwrap_or(0),
146            modularity: partition.communities().modularity(),
147        }
148    }
149
150    /// Displays the statistics.
151    pub fn display(&self) -> String {
152        format!(
153            "Community Partition Statistics:\n\
154             - Communities: {}\n\
155             - Total Clauses: {}\n\
156             - Avg Partition Size: {:.2}\n\
157             - Size Range: [{}, {}]\n\
158             - Modularity: {:.4}",
159            self.num_communities,
160            self.total_clauses,
161            self.avg_partition_size,
162            self.min_partition_size,
163            self.max_partition_size,
164            self.modularity
165        )
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::community::{LouvainDetector, VariableIncidenceGraph};
173    use crate::literal::{Lit, Var};
174
175    fn make_lit(var: usize, sign: bool) -> Lit {
176        let v = Var::new(var as u32);
177        if sign { Lit::pos(v) } else { Lit::neg(v) }
178    }
179
180    #[test]
181    fn test_partition_creation() {
182        let clauses = vec![
183            Clause::original(vec![make_lit(0, false), make_lit(1, false)]),
184            Clause::original(vec![make_lit(0, false), make_lit(1, true)]),
185            Clause::original(vec![make_lit(2, false), make_lit(3, false)]),
186            Clause::original(vec![make_lit(2, true), make_lit(3, false)]),
187        ];
188
189        let partition = CommunityPartition::from_clauses(4, &clauses);
190
191        assert!(partition.num_communities() > 0);
192        assert_eq!(partition.total_clauses(), 4);
193    }
194
195    #[test]
196    fn test_get_partition() {
197        let clauses = vec![
198            Clause::original(vec![make_lit(0, false), make_lit(1, false)]),
199            Clause::original(vec![make_lit(2, false), make_lit(3, false)]),
200        ];
201
202        let partition = CommunityPartition::from_clauses(4, &clauses);
203
204        for i in 0..partition.num_communities() {
205            let part = partition.get_partition(i);
206            assert!(part.len() <= 2);
207        }
208    }
209
210    #[test]
211    fn test_primary_community() {
212        // Create a simple VIG to get communities
213        let test_clauses = vec![
214            Clause::original(vec![make_lit(0, false), make_lit(0, true)]),
215            Clause::original(vec![make_lit(1, false)]),
216        ];
217
218        let vig = VariableIncidenceGraph::from_clauses(2, &test_clauses);
219        let detector = LouvainDetector::default();
220        let communities = detector.detect(&vig);
221
222        // Now test with a clause where variable 0 appears twice
223        let clause = Clause::original(vec![
224            make_lit(0, false),
225            make_lit(0, true),
226            make_lit(1, false),
227        ]);
228
229        let primary = CommunityPartition::get_primary_community(&clause, &communities);
230        assert!(primary.is_some());
231        // The primary community should be var 0's community since it appears twice
232        assert_eq!(primary, Some(communities.community(0)));
233    }
234
235    #[test]
236    fn test_partition_sizes() {
237        let clauses = vec![
238            Clause::original(vec![make_lit(0, false), make_lit(1, false)]),
239            Clause::original(vec![make_lit(2, false), make_lit(3, false)]),
240            Clause::original(vec![make_lit(4, false), make_lit(5, false)]),
241        ];
242
243        let partition = CommunityPartition::from_clauses(6, &clauses);
244        let sizes = partition.partition_sizes();
245
246        assert_eq!(sizes.iter().sum::<usize>(), 3);
247    }
248
249    #[test]
250    fn test_create_ordering() {
251        let clauses = vec![
252            Clause::original(vec![make_lit(0, false), make_lit(1, false)]),
253            Clause::original(vec![make_lit(2, false), make_lit(3, false)]),
254        ];
255
256        let partition = CommunityPartition::from_clauses(4, &clauses);
257        let ordering = partition.create_ordering();
258
259        assert_eq!(ordering.ordering().len(), 4);
260    }
261
262    #[test]
263    fn test_partition_stats() {
264        let clauses = vec![
265            Clause::original(vec![make_lit(0, false), make_lit(1, false)]),
266            Clause::original(vec![make_lit(2, false), make_lit(3, false)]),
267            Clause::original(vec![make_lit(4, false), make_lit(5, false)]),
268        ];
269
270        let partition = CommunityPartition::from_clauses(6, &clauses);
271        let stats = PartitionStats::from_partition(&partition);
272
273        assert_eq!(stats.total_clauses, 3);
274        assert!(stats.avg_partition_size > 0.0);
275        assert!(stats.modularity >= 0.0);
276    }
277
278    #[test]
279    fn test_empty_partition() {
280        let partition = CommunityPartition::from_clauses(5, &[]);
281
282        assert_eq!(partition.total_clauses(), 0);
283        assert_eq!(partition.num_communities(), 5);
284    }
285
286    #[test]
287    fn test_single_clause_partition() {
288        let clauses = vec![Clause::original(vec![
289            make_lit(0, false),
290            make_lit(1, false),
291        ])];
292
293        let partition = CommunityPartition::from_clauses(2, &clauses);
294
295        assert_eq!(partition.total_clauses(), 1);
296    }
297
298    #[test]
299    fn test_community_lookup() {
300        let clauses = vec![
301            Clause::original(vec![make_lit(0, false), make_lit(1, false)]),
302            Clause::original(vec![make_lit(2, false), make_lit(3, false)]),
303        ];
304
305        let partition = CommunityPartition::from_clauses(4, &clauses);
306
307        // All variables should be assigned to some community
308        for var in 0..4 {
309            let comm = partition.community(var);
310            assert!(comm < partition.num_communities());
311        }
312    }
313}