Skip to main content

sim_lib_discrete_graph/assignment/
verify.rs

1use core::cmp::Ordering;
2
3use super::{
4    Assignment, AssignmentCertificate, AssignmentCost, AssignmentOperation, AssignmentPolicy,
5    CostMatrix, DoublingPolicy, VoiceCrossingPolicy, add, certificate_error, flow, ordered,
6};
7use crate::{
8    GraphError,
9    cost::{compare, validate},
10};
11
12/// Verifies assignment feasibility, exact cost, and the supplied optimality
13/// certificate.
14pub fn verify_assignment<C: AssignmentCost>(
15    costs: &CostMatrix<C>,
16    policy: &AssignmentPolicy<C>,
17    assignment: &Assignment<C>,
18) -> Result<(), GraphError> {
19    validate_inputs(costs, policy)?;
20    assignment.receipt.validate()?;
21    validate_operations(costs, policy, assignment)?;
22    match (&policy.voice_crossing, &assignment.certificate) {
23        (VoiceCrossingPolicy::Allow, AssignmentCertificate::MinCostFlow { potentials }) => {
24            flow::verify(costs, policy, assignment, potentials)
25        }
26        (VoiceCrossingPolicy::Forbid, AssignmentCertificate::OrderPreserving { suffix_costs }) => {
27            ordered::verify(costs, policy, assignment, suffix_costs)
28        }
29        _ => Err(GraphError::CertificateInvalid(
30            "certificate kind does not match voice-crossing policy".to_owned(),
31        )),
32    }
33}
34
35pub(super) fn validate_inputs<C: AssignmentCost>(
36    costs: &CostMatrix<C>,
37    policy: &AssignmentPolicy<C>,
38) -> Result<(), GraphError> {
39    if policy.insertion_costs.len() != costs.columns() {
40        return Err(GraphError::InvalidAssignment(format!(
41            "received {} insertion costs for {} targets",
42            policy.insertion_costs.len(),
43            costs.columns()
44        )));
45    }
46    if policy.deletion_costs.len() != costs.rows() {
47        return Err(GraphError::InvalidAssignment(format!(
48            "received {} deletion costs for {} sources",
49            policy.deletion_costs.len(),
50            costs.rows()
51        )));
52    }
53    if let DoublingPolicy::Allow { per_source } = &policy.doubling
54        && per_source.len() != costs.rows()
55    {
56        return Err(GraphError::InvalidAssignment(format!(
57            "received {} doubling costs for {} sources",
58            per_source.len(),
59            costs.rows()
60        )));
61    }
62    let zero = C::zero();
63    for cost in costs.values().iter().flatten() {
64        validate_non_negative(cost, &zero, "assignment matrix")?;
65    }
66    for cost in &policy.insertion_costs {
67        validate_non_negative(cost, &zero, "assignment insertion")?;
68    }
69    for cost in &policy.deletion_costs {
70        validate_non_negative(cost, &zero, "assignment deletion")?;
71    }
72    if let DoublingPolicy::Allow { per_source } = &policy.doubling {
73        for cost in per_source {
74            validate_non_negative(cost, &zero, "assignment doubling")?;
75        }
76    }
77    Ok(())
78}
79
80fn validate_operations<C: AssignmentCost>(
81    costs: &CostMatrix<C>,
82    policy: &AssignmentPolicy<C>,
83    assignment: &Assignment<C>,
84) -> Result<(), GraphError> {
85    let mut source_targets = vec![Vec::new(); costs.rows()];
86    let mut target_owner = vec![None; costs.columns()];
87    let mut deleted = vec![false; costs.rows()];
88    let mut total = C::zero();
89
90    for operation in &assignment.operations {
91        let operation_cost = match operation {
92            AssignmentOperation::Match {
93                source,
94                target,
95                cost,
96            } => {
97                register_pair(
98                    *source,
99                    *target,
100                    false,
101                    cost,
102                    costs,
103                    policy,
104                    &mut source_targets,
105                    &mut target_owner,
106                )?;
107                cost
108            }
109            AssignmentOperation::Double {
110                source,
111                target,
112                cost,
113            } => {
114                register_pair(
115                    *source,
116                    *target,
117                    true,
118                    cost,
119                    costs,
120                    policy,
121                    &mut source_targets,
122                    &mut target_owner,
123                )?;
124                cost
125            }
126            AssignmentOperation::Insert { target, cost } => {
127                if *target >= costs.columns() || target_owner[*target].replace(None).is_some() {
128                    return certificate_error("target is assigned more than once or out of range");
129                }
130                if cost != &policy.insertion_costs[*target] {
131                    return certificate_error("insertion operation has the wrong cost");
132                }
133                cost
134            }
135            AssignmentOperation::Delete { source, cost } => {
136                if *source >= costs.rows() || core::mem::replace(&mut deleted[*source], true) {
137                    return certificate_error("source is deleted more than once or out of range");
138                }
139                if cost != &policy.deletion_costs[*source] {
140                    return certificate_error("deletion operation has the wrong cost");
141                }
142                cost
143            }
144        };
145        total = add(&total, operation_cost, "assignment operation total")?;
146    }
147
148    if target_owner.iter().any(Option::is_none) {
149        return certificate_error("not every target is assigned");
150    }
151    for source in 0..costs.rows() {
152        let count = source_targets[source].len();
153        if (count == 0) != deleted[source] {
154            return certificate_error("source must be either used or deleted exactly once");
155        }
156        if count > 1 && matches!(policy.doubling, DoublingPolicy::Forbid) {
157            return certificate_error("assignment doubles a source under a forbid policy");
158        }
159        if count > 0 {
160            let matches = assignment
161                .operations
162                .iter()
163                .filter(|operation| {
164                    matches!(operation, AssignmentOperation::Match { source: found, .. } if *found == source)
165                })
166                .count();
167            if matches != 1 {
168                return certificate_error("each used source must have exactly one base match");
169            }
170        }
171    }
172    if policy.voice_crossing == VoiceCrossingPolicy::Forbid {
173        let mut pairs = source_targets
174            .iter()
175            .enumerate()
176            .flat_map(|(source, targets)| targets.iter().map(move |target| (source, *target)))
177            .collect::<Vec<_>>();
178        pairs.sort_unstable();
179        if pairs.windows(2).any(|pair| pair[0].1 >= pair[1].1) {
180            return certificate_error("assignment violates source/target order");
181        }
182    }
183    if total != assignment.total_cost {
184        return certificate_error("assignment total does not equal its operation costs");
185    }
186    Ok(())
187}
188
189#[allow(clippy::too_many_arguments)]
190fn register_pair<C: AssignmentCost>(
191    source: usize,
192    target: usize,
193    doubled: bool,
194    operation_cost: &C,
195    costs: &CostMatrix<C>,
196    policy: &AssignmentPolicy<C>,
197    source_targets: &mut [Vec<usize>],
198    target_owner: &mut [Option<Option<usize>>],
199) -> Result<(), GraphError> {
200    if source >= costs.rows() || target >= costs.columns() {
201        return certificate_error("match endpoint is out of range");
202    }
203    if !costs.allowed(source, target) {
204        return certificate_error("match uses a forbidden assignment edge");
205    }
206    if target_owner[target].replace(Some(source)).is_some() {
207        return certificate_error("target is assigned more than once");
208    }
209    let expected = if doubled {
210        let doubling = policy
211            .doubling_cost(source)
212            .ok_or_else(|| GraphError::CertificateInvalid("doubling is forbidden".to_owned()))?;
213        add(costs.value(source, target), doubling, "doubling operation")?
214    } else {
215        costs.value(source, target).clone()
216    };
217    if operation_cost != &expected {
218        return certificate_error("match operation has the wrong cost");
219    }
220    source_targets[source].push(target);
221    Ok(())
222}
223
224fn validate_non_negative<C: AssignmentCost>(
225    cost: &C,
226    zero: &C,
227    context: &str,
228) -> Result<(), GraphError> {
229    validate(cost, context)?;
230    if compare(cost, zero, context)? == Ordering::Less {
231        return Err(GraphError::InvalidAssignment(
232            "assignment costs must be non-negative".to_owned(),
233        ));
234    }
235    Ok(())
236}