bibd/
bibd.rs

1//! A model for the balanced incomplete block design problem. For a formal definition of the
2//! problem, see:
3//! - https://w.wiki/9F4h
4//! - https://mathworld.wolfram.com/BlockDesign.html
5//!
6//! Informally, the `BIBD(v, b, r, k, l)` problem looks for a binary `v * b` matrix such that all
7//! rows sum to `r`, all columns sum to `k`, and the dot product between any two distinct rows is at
8//! most `l` (any two pairs of rows have at most `l` overlapping 1s in their columns).
9//!
10//! The parameters are not independent, but satisfy the following conditions:
11//! - `bk = vr`
12//! - `l(v - 1) = r(k - 1)`
13//!
14//! Hence, the problem is defined in terms of v, k, and l.
15
16use pumpkin_solver::constraints;
17use pumpkin_solver::results::ProblemSolution;
18use pumpkin_solver::results::SatisfactionResult;
19use pumpkin_solver::termination::Indefinite;
20use pumpkin_solver::variables::DomainId;
21use pumpkin_solver::Solver;
22
23struct Bibd {
24    /// The number of rows in the matrix.
25    rows: u32,
26    /// The number of columns in the matrix.
27    columns: u32,
28    /// The sum each row should equal.
29    row_sum: u32,
30    /// The sum each column should equal.
31    column_sum: u32,
32    /// The maximum dot product between any distinct pair of rows.
33    max_dot_product: u32,
34}
35
36impl Bibd {
37    fn from_args() -> Option<Bibd> {
38        let args = std::env::args()
39            .skip(1)
40            .map(|arg| arg.parse::<u32>())
41            .collect::<Result<Vec<u32>, _>>()
42            .ok()?;
43
44        if args.len() != 3 {
45            return None;
46        }
47
48        let v = args[0];
49        let k = args[1];
50        let l = args[2];
51
52        let r = l * (v - 1) / (k - 1);
53        let b = v * r / k;
54
55        Some(Self {
56            rows: v,
57            columns: b,
58            row_sum: r,
59            column_sum: k,
60            max_dot_product: l,
61        })
62    }
63}
64
65fn create_matrix(solver: &mut Solver, bibd: &Bibd) -> Vec<Vec<DomainId>> {
66    (0..bibd.rows)
67        .map(|_| {
68            (0..bibd.columns)
69                .map(|_| solver.new_bounded_integer(0, 1))
70                .collect::<Vec<_>>()
71        })
72        .collect::<Vec<_>>()
73}
74
75fn main() {
76    env_logger::init();
77
78    let Some(bibd) = Bibd::from_args() else {
79        eprintln!("Usage: {} <v> <k> <l>", std::env::args().next().unwrap());
80        return;
81    };
82
83    println!(
84        "bibd: (v = {}, b = {}, r = {}, k = {}, l = {})",
85        bibd.rows, bibd.columns, bibd.row_sum, bibd.column_sum, bibd.max_dot_product
86    );
87
88    let mut solver = Solver::default();
89
90    // Creates a dummy constraint tag; since this example does not support proof logging the
91    // constraint tag does not matter.
92    let constraint_tag = solver.new_constraint_tag();
93
94    // Create 0-1 integer variables that make up the matrix.
95    let matrix = create_matrix(&mut solver, &bibd);
96
97    // Enforce the row sum.
98    for row in matrix.iter() {
99        let _ = solver
100            .add_constraint(constraints::equals(
101                row.clone(),
102                bibd.row_sum as i32,
103                constraint_tag,
104            ))
105            .post();
106    }
107
108    // Enforce the column sum.
109    for row in transpose(&matrix) {
110        let _ = solver
111            .add_constraint(constraints::equals(
112                row,
113                bibd.column_sum as i32,
114                constraint_tag,
115            ))
116            .post();
117    }
118
119    // Enforce the dot product constraint.
120    // pairwise_product[r1][r2][col] = matrix[r1][col] * matrix[r2][col]
121    let pairwise_product = (0..bibd.rows)
122        .map(|_| create_matrix(&mut solver, &bibd))
123        .collect::<Vec<_>>();
124
125    for r1 in 0..bibd.rows as usize {
126        for r2 in r1 + 1..bibd.rows as usize {
127            for col in 0..bibd.columns as usize {
128                let _ = solver
129                    .add_constraint(constraints::times(
130                        matrix[r1][col],
131                        matrix[r2][col],
132                        pairwise_product[r1][r2][col],
133                        constraint_tag,
134                    ))
135                    .post();
136            }
137
138            let _ = solver
139                .add_constraint(constraints::less_than_or_equals(
140                    pairwise_product[r1][r2].clone(),
141                    bibd.max_dot_product as i32,
142                    constraint_tag,
143                ))
144                .post();
145        }
146    }
147
148    let mut brancher = solver.default_brancher();
149    match solver.satisfy(&mut brancher, &mut Indefinite) {
150        SatisfactionResult::Satisfiable(satisfiable) => {
151            let solution = satisfiable.solution();
152
153            let row_separator = format!("{}+", "+---".repeat(bibd.columns as usize));
154
155            for row in matrix.iter() {
156                let line = row
157                    .iter()
158                    .map(|var| {
159                        if solution.get_integer_value(*var) == 1 {
160                            String::from("| * ")
161                        } else {
162                            String::from("|   ")
163                        }
164                    })
165                    .collect::<String>();
166
167                println!("{row_separator}\n{line}|");
168            }
169
170            println!("{row_separator}");
171        }
172        SatisfactionResult::Unsatisfiable(_, _) => {
173            println!("UNSATISFIABLE")
174        }
175        SatisfactionResult::Unknown(_, _) => {
176            println!("UNKNOWN")
177        }
178    };
179}
180
181fn transpose<T: Clone, Inner: AsRef<[T]>>(matrix: &[Inner]) -> Vec<Vec<T>> {
182    let rows = matrix.len();
183    let cols = matrix[0].as_ref().len();
184
185    (0..cols)
186        .map(|col| {
187            (0..rows)
188                .map(|row| matrix[row].as_ref()[col].clone())
189                .collect()
190        })
191        .collect()
192}