Skip to main content

conjure_cp_core/
instantiate.rs

1use crate::{
2    Model,
3    ast::{DeclarationKind, DeclarationPtr, Literal, declaration::Declaration, eval_constant},
4};
5use anyhow::anyhow;
6
7/// Instantiate a problem model with values from a parameter model.
8///
9/// For each `given` declaration in `problem_model`, this looks for a corresponding value `letting`
10/// in `param_model`, checks it is a constant and within the given domain, and replaces the `given`
11/// with a value-letting in the returned model.
12pub fn instantiate_model(mut problem_model: Model, param_model: Model) -> anyhow::Result<Model> {
13    let symbol_table = problem_model.symbols_ptr_unchecked().write();
14    let param_table = param_model.symbols_ptr_unchecked().write();
15    let mut pending_givens = symbol_table
16        .iter_local()
17        .filter_map(|(name, decl)| decl.as_given().map(|_| name.clone()))
18        .collect::<Vec<_>>();
19
20    while !pending_givens.is_empty() {
21        let mut next_pending = Vec::new();
22        let mut made_progress = false;
23
24        for name in pending_givens {
25            let mut decl = symbol_table
26                .lookup_local(&name)
27                .ok_or_else(|| anyhow!("Given declaration `{name}` not found in problem model"))?;
28
29            let Some(domain) = decl.as_given() else {
30                continue;
31            };
32
33            let param_decl = param_table.lookup(&name);
34            let expr = param_decl
35                .as_ref()
36                .and_then(DeclarationPtr::as_value_letting)
37                .ok_or_else(|| {
38                    anyhow!(
39                        "Given declaration `{name}` does not have corresponding letting in parameter file"
40                    )
41                })?;
42
43            let expr_value = eval_constant(&expr)
44                .ok_or_else(|| anyhow!("Letting expression `{expr}` cannot be evaluated"))?;
45
46            let Ok(ground_domain) = domain.resolve() else {
47                next_pending.push(name);
48                continue;
49            };
50
51            if !ground_domain.contains(&expr_value)? {
52                return Err(anyhow!(
53                    "Domain of given statement `{name}` does not contain letting value"
54                ));
55            }
56
57            // The given domain is a validity check, but after instantiation the parameter is a
58            // constant. Keep the tighter domain inferred from its value when possible so bounds
59            // derived from instantiated parameters (for example optimisation auxiliaries) stay
60            // finite.
61            let instantiated_domain = expr.domain_of().unwrap_or_else(|| domain.clone());
62            let new_decl = Declaration::new(
63                name.clone(),
64                DeclarationKind::ValueLetting(expr.clone(), Some(instantiated_domain)),
65            );
66            drop(domain);
67            decl.replace(new_decl);
68            made_progress = true;
69
70            tracing::info!("Replaced {name} given with letting.");
71        }
72
73        if next_pending.is_empty() {
74            break;
75        }
76
77        if !made_progress {
78            return Err(anyhow!(
79                "Domain of given statement `{}` cannot be resolved",
80                next_pending[0]
81            ));
82        }
83
84        pending_givens = next_pending;
85    }
86
87    drop(symbol_table);
88    ground_collection_valued_domains(&mut problem_model);
89    validate_instantiation_conditions(&mut problem_model)?;
90    Ok(problem_model)
91}
92
93/// Evaluate and remove all top-level `where` conditions after parameter instantiation.
94pub fn validate_instantiation_conditions(model: &mut Model) -> anyhow::Result<()> {
95    for condition in model.take_instantiation_conditions() {
96        match eval_constant(&condition) {
97            Some(Literal::Bool(true)) => {}
98            Some(Literal::Bool(false)) => {
99                return Err(anyhow!(
100                    "invalid instance: where condition `{condition}` evaluated to false"
101                ));
102            }
103            Some(value) => {
104                return Err(anyhow!(
105                    "where condition `{condition}` evaluated to non-boolean value `{value}`"
106                ));
107            }
108            None => {
109                return Err(anyhow!(
110                    "could not evaluate where condition `{condition}` after parameter instantiation"
111                ));
112            }
113        }
114    }
115    Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::ast::{
122        Atom, Domain, Expression, GroundDomain, IntVal, Metadata, Name, Objective,
123        OptimiseDirection, Range, Reference,
124    };
125
126    #[test]
127    fn instantiated_given_uses_the_tighter_value_domain() {
128        let name = Name::user("n");
129        let mut problem = Model::default();
130        problem
131            .add_symbol(DeclarationPtr::new_given(
132                name.clone(),
133                Domain::int(vec![Range::UnboundedR(1)]),
134            ))
135            .unwrap();
136
137        let mut parameters = Model::default();
138        parameters
139            .add_symbol(DeclarationPtr::new_value_letting(
140                name.clone(),
141                crate::ast::Expression::Atomic(Metadata::new(), Atom::Literal(Literal::Int(7))),
142            ))
143            .unwrap();
144
145        let instantiated = instantiate_model(problem, parameters).unwrap();
146        let declaration = instantiated
147            .symbols()
148            .lookup(&name)
149            .expect("instantiated parameter should exist");
150
151        assert_eq!(
152            declaration.domain().unwrap().resolve().unwrap().as_ref(),
153            &GroundDomain::Int(vec![Range::Single(7)])
154        );
155    }
156
157    #[test]
158    fn instantiation_invalidates_cached_expression_domains() {
159        let parameter_name = Name::user("n");
160        let parameter = DeclarationPtr::new_given(
161            parameter_name.clone(),
162            Domain::int(vec![Range::UnboundedR(1)]),
163        );
164        let variable = DeclarationPtr::new_find(
165            Name::user("x"),
166            Domain::int(vec![Range::Bounded(
167                IntVal::Const(1),
168                IntVal::Reference(Reference::new(parameter.clone())),
169            )]),
170        );
171        let reference = Expression::Atomic(Metadata::new(), Atom::new_ref(variable.clone()));
172
173        let mut problem = Model::default();
174        problem.add_symbol(parameter).unwrap();
175        problem.add_symbol(variable).unwrap();
176        problem.objective = Some(Objective {
177            direction: OptimiseDirection::Minimising,
178            expression: reference,
179        });
180
181        let cached_domain = problem
182            .objective
183            .as_ref()
184            .unwrap()
185            .expression
186            .domain_of()
187            .unwrap();
188        assert!(cached_domain.resolve().is_err());
189
190        let mut parameters = Model::default();
191        parameters
192            .add_symbol(DeclarationPtr::new_value_letting(
193                parameter_name,
194                Expression::Atomic(Metadata::new(), Atom::Literal(Literal::Int(7))),
195            ))
196            .unwrap();
197
198        let instantiated = instantiate_model(problem, parameters).unwrap();
199        let objective_domain = instantiated
200            .objective
201            .as_ref()
202            .unwrap()
203            .expression
204            .domain_of()
205            .unwrap();
206
207        assert_eq!(
208            objective_domain.resolve().unwrap().as_ref(),
209            &GroundDomain::Int(vec![Range::Bounded(1, 7)])
210        );
211    }
212}
213
214/// Grounds declaration domains that take their values from a collection expression.
215///
216/// `int([i | i <- nums])` stays an expression until the parameters arrive, and resolving it means
217/// evaluating the collection afresh -- which the rewriter would otherwise do on every domain
218/// query, per node per rule attempt. Doing it once here is the difference between a second and
219/// twenty minutes.
220///
221/// Only these domains are grounded. Resolving every domain is not safe to do blindly: a full-width
222/// `int` resolves to an enormous ground domain.
223fn ground_collection_valued_domains(model: &mut Model) {
224    for (_, decl) in model.symbols_mut().iter_local_mut() {
225        // Only decision variables, and reached through `as_find_mut` rather than `domain()`:
226        // the latter computes a domain for every declaration, which for a value letting over a
227        // large expression is itself expensive.
228        let Some(mut var) = decl.as_find_mut() else {
229            continue;
230        };
231        if !crate::ast::domain_has_int_from_values(&var.domain) {
232            continue;
233        }
234        let Ok(ground) = var.domain.resolve() else {
235            continue;
236        };
237        var.domain = ground.into();
238    }
239}