sim_lib_discrete_graph/assignment/
verify.rs1use 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
12pub 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}