problemreductions/models/misc/
maximum_likelihood_ranking.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
10use crate::traits::Problem;
11use crate::types::Min;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "MaximumLikelihoodRanking",
17 display_name: "Maximum Likelihood Ranking",
18 aliases: &[],
19 dimensions: &[],
20 category: crate::registry::ProblemCategory::Misc,
21 module_path: module_path!(),
22 description: "Find a ranking minimizing total pairwise disagreement cost",
23 fields: &[
24 FieldInfo { name: "matrix", type_name: "Vec<Vec<i64>>", description: "Antisymmetric comparison matrix A (a_ij + a_ji = c, a_ii = 0)" },
25 ],
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(try_from = "MaximumLikelihoodRankingData")]
59pub struct MaximumLikelihoodRanking {
60 matrix: Vec<Vec<i64>>,
61}
62
63#[derive(Deserialize)]
64struct MaximumLikelihoodRankingData {
65 matrix: Vec<Vec<i64>>,
66}
67
68impl TryFrom<MaximumLikelihoodRankingData> for MaximumLikelihoodRanking {
69 type Error = crate::registry::ConstructionError;
70
71 fn try_from(data: MaximumLikelihoodRankingData) -> Result<Self, Self::Error> {
72 Self::try_new(data.matrix)
73 }
74}
75
76impl MaximumLikelihoodRanking {
77 pub fn new(matrix: Vec<Vec<i64>>) -> Self {
84 Self::try_new(matrix).unwrap_or_else(|error| panic!("{error}"))
85 }
86
87 fn try_new(matrix: Vec<Vec<i64>>) -> Result<Self, crate::registry::ConstructionError> {
88 let n = matrix.len();
89 for (i, row) in matrix.iter().enumerate() {
90 if row.len() != n {
91 return Err(format!(
92 "matrix must be square: row {i} has length {} but expected {n}",
93 row.len()
94 )
95 .into());
96 }
97 if row[i] != 0 {
98 return Err(format!(
99 "diagonal entries must be zero: matrix[{i}][{i}] = {}",
100 row[i]
101 )
102 .into());
103 }
104 }
105
106 let mut comparison_count = None;
107 for (i, row) in matrix.iter().enumerate() {
108 for (j, &entry) in row.iter().enumerate().skip(i + 1) {
109 let pair_sum = entry.checked_add(matrix[j][i]).ok_or_else(|| {
110 crate::registry::ConstructionError::IntegerOverflow(
111 "computing the pairwise comparison count".into(),
112 )
113 })?;
114 match comparison_count {
115 None => comparison_count = Some(pair_sum),
116 Some(expected) => {
117 if pair_sum != expected {
118 return Err(format!("all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}").into());
119 }
120 }
121 }
122 }
123 }
124
125 Ok(Self { matrix })
126 }
127
128 pub fn matrix(&self) -> &Vec<Vec<i64>> {
130 &self.matrix
131 }
132
133 pub fn num_items(&self) -> usize {
135 self.matrix.len()
136 }
137
138 pub fn comparison_count(&self) -> i64 {
140 if self.matrix.len() < 2 {
141 0
142 } else {
143 self.matrix[0][1] + self.matrix[1][0]
144 }
145 }
146}
147
148impl Problem for MaximumLikelihoodRanking {
149 const NAME: &'static str = "MaximumLikelihoodRanking";
150 type Solution = Vec<usize>;
151 type Value = Min<i64>;
152
153 crate::problem_parameters![("num_items", num_items),];
154
155 fn variant() -> Vec<(&'static str, &'static str)> {
156 crate::variant_params![]
157 }
158
159 fn evaluate(
160 &self,
161 config: &Self::Solution,
162 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
163 Ok({
164 let n = self.num_items();
165
166 if config.len() != n {
168 return Err(crate::traits::EvaluationError::InvalidConfiguration(
169 "ranking length does not match the number of alternatives".into(),
170 ));
171 }
172
173 if config.iter().any(|&rank| rank >= n) {
174 return Err(crate::traits::EvaluationError::InvalidConfiguration(
175 "ranking contains an out-of-range position".into(),
176 ));
177 }
178
179 let mut seen = vec![false; n];
181 for &rank in config {
182 if rank >= n || seen[rank] {
183 return Ok(Min(None));
184 }
185 seen[rank] = true;
186 }
187
188 let mut cost: i64 = 0;
192 for a in 0..n {
193 for b in 0..n {
194 if a != b && config[a] > config[b] {
195 cost = cost.checked_add(self.matrix[a][b]).ok_or_else(|| {
196 crate::traits::EvaluationError::IntegerOverflow(
197 "summing ranking disagreement costs".into(),
198 )
199 })?;
200 }
201 }
202 }
203
204 Min(Some(cost))
205 })
206 }
207}
208
209impl crate::solvers::BruteForceProblem for MaximumLikelihoodRanking {
210 fn dimensions(&self) -> Vec<usize> {
211 let n = self.num_items();
212 vec![n; n]
213 }
214}
215
216crate::declare_variants! {
217 default MaximumLikelihoodRanking => "num_items * num_items * 2^num_items",
218}
219
220crate::register_brute_force! {
221 MaximumLikelihoodRanking,
222}
223
224#[cfg(feature = "example-db")]
225pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
226 let matrix = vec![
233 vec![0, 4, 3, 5],
234 vec![1, 0, 4, 3],
235 vec![2, 1, 0, 4],
236 vec![0, 2, 1, 0],
237 ];
238 vec![crate::example_db::specs::ModelExampleSpec {
239 id: "maximum_likelihood_ranking",
240 instance: Box::new(MaximumLikelihoodRanking::new(matrix)),
241 optimal_config: serde_json::json!(vec![0, 1, 2, 3]),
242 optimal_value: serde_json::json!(7),
243 }]
244}
245
246#[cfg(test)]
247#[path = "../../unit_tests/models/misc/maximum_likelihood_ranking.rs"]
248mod tests;