1use super::*;
2use crate::{
3 constraint_type::{ConstraintCollection, ConstraintType},
4 ATol, Evaluate,
5};
6use std::{collections::BTreeMap, ops::Neg};
7
8fn convert_objective_pair(sense: &mut Sense, objective: &mut Function, target: Sense) -> bool {
9 if *sense == target {
10 false
11 } else {
12 *sense = target;
13 *objective = std::mem::take(objective).neg();
14 true
15 }
16}
17
18impl Instance {
19 pub fn convert_active_objective(&mut self, target: Sense) -> bool {
67 if self.sense == target {
68 return false;
69 }
70 self.capture_output_objective();
71 convert_objective_pair(&mut self.sense, &mut self.objective, target)
72 }
73
74 pub fn as_minimization_problem(&mut self) -> bool {
114 self.convert_problem_objective(Sense::Minimize)
115 }
116
117 pub fn as_maximization_problem(&mut self) -> bool {
153 self.convert_problem_objective(Sense::Maximize)
154 }
155
156 fn convert_problem_objective(&mut self, target: Sense) -> bool {
157 let active_converted = convert_objective_pair(&mut self.sense, &mut self.objective, target);
158 let output_converted = if let Some(output) = &mut self.output_objective {
159 convert_objective_pair(&mut output.sense, &mut output.function, target)
160 } else {
161 false
162 };
163 active_converted || output_converted
164 }
165}
166
167impl From<Instance> for ParametricInstance {
168 fn from(
169 Instance {
170 sense,
171 objective,
172 output_objective,
173 decision_variables,
174 constraint_collection,
175 indicator_constraint_collection,
176 one_hot_constraint_collection,
177 sos1_constraint_collection,
178 decision_variable_dependency,
179 description,
180 annotations,
181 named_functions,
182 ..
183 }: Instance,
184 ) -> Self {
185 ParametricInstance {
186 sense,
187 objective,
188 output_objective,
189 decision_variables,
190 parameters: ParameterTable::default(),
191 constraint_collection,
192 indicator_constraint_collection,
193 one_hot_constraint_collection,
194 sos1_constraint_collection,
195 decision_variable_dependency,
196 description,
197 annotations,
198 named_functions,
199 }
200 }
201}
202
203fn materialize_constraint_collection_parameters<T: ConstraintType>(
204 collection: &mut ConstraintCollection<T>,
205 state: &crate::v1::State,
206 atol: ATol,
207) -> crate::Result<()> {
208 let mut active_replacements = BTreeMap::new();
209 for (&id, constraint) in collection.active() {
210 let mut constraint = constraint.clone();
211 constraint.partial_evaluate(state, atol).inspect_err(|e| {
212 tracing::error!(?id, error = %e, "failed to partial_evaluate active constraint");
213 })?;
214 active_replacements.insert(id, constraint);
215 }
216
217 let mut removed_replacements = BTreeMap::new();
218 for (&id, (constraint, _reason)) in collection.removed() {
219 let mut constraint = constraint.clone();
220 constraint.partial_evaluate(state, atol).inspect_err(|e| {
221 tracing::error!(?id, error = %e, "failed to partial_evaluate removed constraint");
222 })?;
223 removed_replacements.insert(id, constraint);
224 }
225
226 collection.replace_rows_preserving_lifecycle(active_replacements, removed_replacements)
227}
228
229impl ParametricInstance {
230 pub fn with_parameters(self, parameters: crate::v1::Parameters) -> crate::Result<Instance> {
272 use std::collections::BTreeSet;
273
274 let param_map: BTreeMap<VariableID, f64> = parameters
276 .entries
277 .iter()
278 .map(|(k, v)| (VariableID::from(*k), *v))
279 .collect();
280
281 let required_ids: BTreeSet<VariableID> = self.parameters.keys().cloned().collect();
283 let given_ids: BTreeSet<VariableID> = param_map.keys().cloned().collect();
284
285 if !required_ids.is_subset(&given_ids) {
286 let missing_ids: Vec<VariableID> =
287 required_ids.difference(&given_ids).cloned().collect();
288 crate::bail!(
289 { ?missing_ids },
290 "Missing parameters: required IDs {required_ids:?}, got {given_ids:?}",
291 );
292 }
293
294 let state = crate::v1::State {
296 entries: parameters.entries.clone(),
297 };
298 let atol = ATol::default();
299
300 let mut objective = self.objective;
303 objective.partial_evaluate(&state, atol)?;
304 let mut output_objective = self.output_objective;
305 if let Some(output_objective) = &mut output_objective {
306 output_objective.function.partial_evaluate(&state, atol)?;
307 }
308
309 let mut constraint_collection = self.constraint_collection;
314 materialize_constraint_collection_parameters(&mut constraint_collection, &state, atol)?;
315
316 let mut indicator_constraint_collection = self.indicator_constraint_collection;
320 materialize_constraint_collection_parameters(
321 &mut indicator_constraint_collection,
322 &state,
323 atol,
324 )?;
325
326 let mut named_functions = self.named_functions;
327 named_functions.partial_evaluate(&state, atol)?;
328
329 let mut decision_variable_dependency = self.decision_variable_dependency;
334 decision_variable_dependency.partial_evaluate(&state, atol)?;
335
336 Ok(Instance {
337 sense: self.sense,
338 objective,
339 output_objective,
340 decision_variables: self.decision_variables,
341 constraint_collection,
342 indicator_constraint_collection,
343 one_hot_constraint_collection: self.one_hot_constraint_collection,
349 sos1_constraint_collection: self.sos1_constraint_collection,
350 named_functions,
351 decision_variable_dependency,
352 parameters: Some(parameters),
353 description: self.description,
354 annotations: self.annotations,
355 })
356 }
357}
358
359#[cfg(test)]
360mod output_objective_tests {
361 use super::*;
362 use crate::{linear, v1::State, ATol, DecisionVariable, Evaluate, Sampled};
363 use std::collections::{BTreeMap, HashMap};
364
365 fn maximizing_binary_instance() -> Instance {
366 Instance::builder()
367 .sense(Sense::Maximize)
368 .objective(Function::from(linear!(1)))
369 .decision_variables(BTreeMap::from([(
370 VariableID::from(1),
371 DecisionVariable::binary(),
372 )]))
373 .constraints(BTreeMap::new())
374 .build()
375 .unwrap()
376 }
377
378 fn assert_evaluation(instance: &Instance, sense: Sense, objective: f64) {
379 let state = State::from(HashMap::from([(1, 1.0)]));
380 let solution = instance.evaluate(&state, ATol::default()).unwrap();
381 assert_eq!(*solution.sense(), Some(sense));
382 assert_eq!(*solution.objective(), objective);
383
384 let sample_set = instance
385 .evaluate_samples(&Sampled::from(state), ATol::default())
386 .unwrap();
387 assert_eq!(*sample_set.sense(), sense);
388 let sample_id = sample_set.sample_ids().into_iter().next().unwrap();
389 assert_eq!(sample_set.objectives().get(sample_id), Some(&objective));
390 }
391
392 #[test]
393 fn conversions_preserve_false_optimality_transport() {
394 let mut instance = maximizing_binary_instance();
395 let original_objective = instance.objective().clone();
396 instance.output_objective = Some(OutputObjective::new(
397 Sense::Maximize,
398 original_objective.clone(),
399 false,
400 ));
401
402 assert!(instance.convert_active_objective(Sense::Minimize));
403 let output = instance.output_objective().unwrap();
404 assert_eq!(output.sense(), Sense::Maximize);
405 assert_eq!(output.function(), &original_objective);
406 assert!(!output.preserves_optimality());
407 assert_evaluation(&instance, Sense::Maximize, 1.0);
408
409 assert!(instance.as_minimization_problem());
412 let output = instance.output_objective().unwrap();
413 assert_eq!(output.sense(), Sense::Minimize);
414 assert_eq!(output.function(), instance.objective());
415 assert!(!output.preserves_optimality());
416 assert_evaluation(&instance, Sense::Minimize, -1.0);
417
418 assert!(instance.as_maximization_problem());
419 let output = instance.output_objective().unwrap();
420 assert_eq!(output.sense(), Sense::Maximize);
421 assert_eq!(output.function(), instance.objective());
422 assert!(!output.preserves_optimality());
423 assert_evaluation(&instance, Sense::Maximize, 1.0);
424 }
425
426 #[test]
427 fn with_parameters_specializes_parameterized_output_objective() {
428 let output_function = Function::from((linear!(1) + linear!(100)).unwrap());
429 let mut parametric = ParametricInstance::new(
430 Sense::Minimize,
431 output_function.clone().neg(),
432 BTreeMap::from([(VariableID::from(1), DecisionVariable::continuous())]),
433 ParameterTable::from_ids([VariableID::from(100)].into_iter().collect()),
434 BTreeMap::new(),
435 )
436 .unwrap();
437 parametric.output_objective =
438 Some(OutputObjective::new(Sense::Maximize, output_function, true));
439
440 let materialized = parametric
441 .with_parameters(crate::v1::Parameters {
442 entries: HashMap::from([(100, 2.0)]),
443 })
444 .unwrap();
445
446 let output = materialized.output_objective().unwrap();
447 assert_eq!(
448 output.function(),
449 &Function::from((linear!(1) + crate::coeff!(2.0)).unwrap())
450 );
451 assert_eq!(
452 output.function().required_ids(),
453 VariableIDSet::from([VariableID::from(1)])
454 );
455 assert!(output.preserves_optimality());
456 let solution = materialized
457 .evaluate(&State::from(HashMap::from([(1, 3.0)])), ATol::default())
458 .unwrap();
459 assert_eq!(*solution.sense(), Some(Sense::Maximize));
460 assert_eq!(*solution.objective(), 5.0);
461 }
462
463 #[test]
464 fn with_parameters_preserves_output_objective_when_it_matches_active() {
465 let mut parametric = ParametricInstance::new(
466 Sense::Minimize,
467 Function::from(linear!(1)),
468 BTreeMap::from([(VariableID::from(1), DecisionVariable::continuous())]),
469 ParameterTable::from_ids([VariableID::from(100)].into_iter().collect()),
470 BTreeMap::new(),
471 )
472 .unwrap();
473 parametric.output_objective = Some(OutputObjective::new(
474 Sense::Minimize,
475 Function::from((linear!(1) + linear!(100)).unwrap()),
476 true,
477 ));
478
479 let materialized = parametric
480 .with_parameters(crate::v1::Parameters {
481 entries: HashMap::from([(100, 0.0)]),
482 })
483 .unwrap();
484
485 let output = materialized.output_objective().unwrap();
486 assert_eq!(output.sense(), materialized.sense());
487 assert_eq!(output.function(), materialized.objective());
488 assert!(output.preserves_optimality());
489 assert!(materialized.to_v1_bytes().is_err());
490 }
491}
492
493#[cfg(test)]
494mod with_parameters_tests {
495 use super::*;
496 use crate::{coeff, linear, Equality, Function};
497 use maplit::btreemap;
498
499 fn parameters(ids: impl IntoIterator<Item = VariableID>) -> ParameterTable {
500 ParameterTable::from_ids(ids.into_iter().collect())
501 }
502
503 #[test]
509 fn decision_variable_dependency_rhs_is_substituted() {
510 use crate::AcyclicAssignments;
511 let x = VariableID::from(1);
512 let dep = VariableID::from(2);
513 let p = VariableID::from(100);
514 let assignments =
516 AcyclicAssignments::new(vec![(dep, Function::from(linear!(1) + linear!(100)))])
517 .unwrap();
518
519 let parametric = ParametricInstance::builder()
520 .sense(Sense::Minimize)
521 .objective(Function::Zero)
522 .decision_variables(btreemap! {
523 x => DecisionVariable::binary(),
524 dep => DecisionVariable::binary(),
525 })
526 .parameters(parameters([p]))
527 .constraints(BTreeMap::new())
528 .decision_variable_dependency(assignments)
529 .build()
530 .unwrap();
531
532 let params = crate::v1::Parameters {
533 entries: std::collections::HashMap::from([(100, 1.0)]),
534 };
535 let instance = parametric.with_parameters(params).unwrap();
536
537 let dep_rhs = instance
538 .decision_variable_dependency()
539 .get(&dep)
540 .expect("dependency entry survives materialization");
541 let rhs_required: VariableIDSet = dep_rhs.required_ids();
542 assert!(
543 !rhs_required.contains(&p),
544 "parameter id {p:?} survived in dependency RHS: {rhs_required:?}",
545 );
546 assert!(
547 rhs_required.contains(&x),
548 "decision variable id {x:?} should remain in dependency RHS: {rhs_required:?}",
549 );
550 }
551
552 #[test]
558 fn removed_regular_constraint_body_is_substituted() {
559 let x = VariableID::from(1);
560 let p = VariableID::from(100);
561 let c_active = Constraint::equal_to_zero(Function::from(linear!(1)));
562 let c_removed = Constraint::equal_to_zero(Function::from(linear!(1) + linear!(100)));
563
564 let parametric = ParametricInstance::builder()
565 .sense(Sense::Minimize)
566 .objective(Function::Zero)
567 .decision_variables(btreemap! {
568 x => DecisionVariable::binary(),
569 })
570 .parameters(parameters([p]))
571 .constraints(btreemap! {
572 ConstraintID::from(0) => c_active,
573 })
574 .removed_constraints(btreemap! {
575 ConstraintID::from(1) => (
576 c_removed,
577 crate::constraint::RemovedReason {
578 reason: "test".to_string(),
579 parameters: Default::default(),
580 },
581 ),
582 })
583 .build()
584 .unwrap();
585
586 let params = crate::v1::Parameters {
587 entries: std::collections::HashMap::from([(100, 1.0)]),
588 };
589 let instance = parametric.with_parameters(params).unwrap();
590
591 let (rc, _r) = instance
592 .removed_constraints()
593 .get(&ConstraintID::from(1))
594 .unwrap();
595 let body_required: VariableIDSet = rc.stage.function.required_ids();
596 assert!(
597 !body_required.contains(&p),
598 "parameter id {p:?} survived in removed-constraint body: {body_required:?}",
599 );
600 }
601
602 #[test]
608 fn removed_indicator_function_body_is_substituted() {
609 let y = VariableID::from(1);
610 let x = VariableID::from(2);
611 let p = VariableID::from(100);
612 let indicator = crate::IndicatorConstraint::new(
613 y,
614 Equality::EqualToZero,
615 Function::from(linear!(2) + linear!(100)),
616 );
617
618 let parametric = ParametricInstance::builder()
619 .sense(Sense::Minimize)
620 .objective(Function::Zero)
621 .decision_variables(btreemap! {
622 y => DecisionVariable::binary(),
623 x => DecisionVariable::binary(),
624 })
625 .parameters(parameters([p]))
626 .constraints(BTreeMap::new())
627 .removed_indicator_constraints(btreemap! {
628 crate::IndicatorConstraintID::from(0) => (
629 indicator,
630 crate::constraint::RemovedReason {
631 reason: "test".to_string(),
632 parameters: Default::default(),
633 },
634 ),
635 })
636 .build()
637 .unwrap();
638
639 let params = crate::v1::Parameters {
640 entries: std::collections::HashMap::from([(100, 1.0)]),
641 };
642 let instance = parametric.with_parameters(params).unwrap();
643
644 let (ic, _r) = instance
645 .removed_indicator_constraints()
646 .get(&crate::IndicatorConstraintID::from(0))
647 .expect("removed indicator survives materialization");
648 let body_required: VariableIDSet = ic.stage.function.required_ids();
649 assert!(
650 !body_required.contains(&p),
651 "parameter id {p:?} survived in removed-indicator body: {body_required:?}",
652 );
653 }
654
655 #[test]
661 fn indicator_function_body_is_substituted() {
662 let y = VariableID::from(1);
665 let x = VariableID::from(2);
666 let p = VariableID::from(100);
667 let indicator = crate::IndicatorConstraint::new(
668 y,
669 Equality::EqualToZero,
670 Function::from(((linear!(2) + linear!(100)).unwrap() + coeff!(-1.0)).unwrap()),
671 );
672
673 let parametric = ParametricInstance::builder()
674 .sense(Sense::Minimize)
675 .objective(Function::Zero)
676 .decision_variables(btreemap! {
677 y => DecisionVariable::binary(),
678 x => DecisionVariable::binary(),
679 })
680 .parameters(parameters([p]))
681 .constraints(BTreeMap::new())
682 .indicator_constraints(btreemap! {
683 crate::IndicatorConstraintID::from(0) => indicator,
684 })
685 .build()
686 .unwrap();
687
688 let params = crate::v1::Parameters {
689 entries: std::collections::HashMap::from([(100, 1.0)]),
690 };
691 let instance = parametric.with_parameters(params).unwrap();
692
693 let materialized = instance
696 .indicator_constraints()
697 .get(&crate::IndicatorConstraintID::from(0))
698 .unwrap();
699 let body_required: VariableIDSet = materialized.stage.function.required_ids();
700 assert!(
701 !body_required.contains(&p),
702 "parameter id {p:?} survived in indicator body after with_parameters: {body_required:?}",
703 );
704 assert!(
705 body_required.contains(&x),
706 "decision variable id {x:?} should remain in indicator body: {body_required:?}",
707 );
708 }
709}