From f69d94d758ecc60da8b8b646acc1356f62c26bb0 Mon Sep 17 00:00:00 2001 From: artus40 Date: Tue, 19 Feb 2019 15:57:27 +0100 Subject: [PATCH] thinking about rules implementation... --- planner/src/constraint.rs | 18 ++++++- planner/src/rules.rs | 40 ++++++++++---- planner/src/solver.rs | 107 +++++++++++++++++++++++++++----------- 3 files changed, 124 insertions(+), 41 deletions(-) diff --git a/planner/src/constraint.rs b/planner/src/constraint.rs index 70a8585..1ed97eb 100644 --- a/planner/src/constraint.rs +++ b/planner/src/constraint.rs @@ -8,6 +8,22 @@ //! by a key of type `K`. //! A constraint owns references to actual values assigned, //! used to perform checks. +//! +//! +//! The problem is to clarify the way Constraints operate. +//! Do they compute their status from some data on demand ? +//! Do they keep their status updated by watching the Variables +//! they act on ? +//! Worse, do they have superpowers ? +//! Could they filter on a variable domain, according to some other variable +//! state ? This would mean that constraints won't judge a result, but guide +//! the solving process to avoid erroring paths, like a constraint-driven +//! solving. This would be powerfull but maybe far too complex... +//! +//! On the other hand, we can implement a simple Observer pattern, with strong +//! coupling to [`Problem`](crate::solver::Problem). +//! Because of this, we can safely play with private fields of Problem, and in +//! return, provide a public api to build specific constraints. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Status { @@ -93,7 +109,7 @@ mod tests { use super::Constraint; let domain = Domain::new(vec![1, 2, 3]); - let mut problem = Problem::build() + let problem = Problem::build() .add_variable("Left", domain.all(), None) .add_variable("Right", domain.all(), None) .add_constraint(Constraint::new(vec![&"Left", &"Right"])) diff --git a/planner/src/rules.rs b/planner/src/rules.rs index b7f24eb..145c7d7 100644 --- a/planner/src/rules.rs +++ b/planner/src/rules.rs @@ -1,16 +1,36 @@ //! Rules used by the `planner` //! A rule is a constraint on valid solutions, but also provides insights //! and eventually inferences to optimize the solving process. +//! +//! * Basic repartition +//! * All different meals +//! * Map recipes categories to each meals +//! * (Eating a dish over two days (leftovers)) +//! * Nutritional values +//! * Per day : according to user profile (man: 2000kcal, woman: 1800kcal) +//! * Per meal : some meals should have higher nutrional values than others +//! +//! * Ingredients +//! * Per week : should use most of a limited set of ingredients (excluding +//! condiments, ...) +//! * To consume : must use a small set of ingredients (leftovers) +//! +//! +//! Price +//! - Per week : should restrict ingredients cost to a given amount -// Nutritional values -// - Per day : according to user profile (man: 2000kcal, woman: 1800kcal) -// - Per meal : some meals should have higher nutrional values than others +trait Rule { + type State; -// Ingredients -// - Per week : should use most of a limited set of ingredients (excluding -// condiments, ...) -// - To consume : must use a small set of ingredients (leftovers) -// + fn check(&self, state: Self::State); + fn insights(&self, state: Self::State) -> Insight; +}; -// Price -// - Per week : should restrict ingredients cost to a given amount +struct AllDifferentMeals; +struct FilterRecipeByMeals; // Essentially work on domain + +struct NutritionalByDayAverageReq; +struct NutritionalByMealAverageValues; + +struct IngredientsInFridge; +struct IngredientsMustUse; diff --git a/planner/src/solver.rs b/planner/src/solver.rs index d72aa1c..49b107f 100644 --- a/planner/src/solver.rs +++ b/planner/src/solver.rs @@ -105,6 +105,46 @@ impl fmt::Debug for Domain { } } +/// Or we can have a much more complex version of Domain. +/// We want to retrieve a filtered domain for each variable. +/// Filters will be static (filter by category,...) or dynamic +/// (inserted by rules updates). +/// +/// For every variable, we can retrieve its filtered values (values, +/// filtered by all globals, filtered by one local). +/// Plus, set a dynamic filter that will apply to all other variables. +/// Of course, it also affects this variable, but considering that dynamic +/// filters are cleared and repopulated on every assign, this side-effect +/// can never occur. +struct SDomain { + values: Vec, + global_filters: Vec, // Globals are dynamic Filters + local_filters: Vec, // Locals are static Filters +} + +impl SDomain { + fn new(values: Vec) -> Self { + Self { + values, + global_filters: Vec::new(), + local_filters: Vec::new(), + } + } + + /// Returns the current domain values for a variable by index + fn get(&self, idx: usize) -> DomainValues { + self.values + .iter() + .collect() + } + + /// Adds a dynamic filter to globals, identified by its setter's id + /// /!\ Previously set filters are overriden, hence dynamic + fn set_global(&mut self, setter: usize, filter: F) { + self.global_filters[setter] = filter; + } +} + //pub type Constraint<'a,V> = fn(&Variables<'a,V>) -> bool; @@ -116,7 +156,7 @@ impl fmt::Debug for Domain { pub struct Problem<'p, V, K> { keys: Vec, - /// The initial assignements map + /// The initial assignements variables: Variables<'p, V>, /// Each variable has its associated domain domains: Vec>, @@ -139,18 +179,18 @@ impl<'p, V: PartialEq, K: Eq + Hash + Clone> Problem<'p, V, K> { /// Returns all possible Updates for next assignements, prepended with /// a Clear to ensure the variable is unset before when leaving the branch. fn _push_updates(&self) -> Option>> { - // TODO: should be able to inject a choosing strategy - if let Some(key) = self._next_assign() { + if let Some(idx) = self._next_assign() { let domain_values = self.domains - .get(key) + .get(idx) .expect("No domain for variable !"); + // TODO: handle case of empty domain.values assert!(!domain_values.is_empty()); // Push a clear assignment first, just before going up the stack. - let mut updates = vec![Assignment::Clear(key.clone())]; + let mut updates = vec![Assignment::Clear(idx.clone())]; // TODO: should be able to filter domain values (inference, pertinence) domain_values.iter().for_each(|value| { updates.push( - Assignment::Update(key, *value) + Assignment::Update(idx, *value) ); }); Some(updates) @@ -160,6 +200,7 @@ impl<'p, V: PartialEq, K: Eq + Hash + Clone> Problem<'p, V, K> { } fn _next_assign(&self) -> Option { + // TODO: should be able to inject a choosing strategy self.variables.iter() .enumerate() .find_map(|(idx, val)| { @@ -180,6 +221,28 @@ impl<'p, V: PartialEq, K: Eq + Hash + Clone> Problem<'p, V, K> { &self.keys[idx] } + fn _get_solution(&self) -> Solution<'p, V, K> { + // Returns the current state wrapped in a Solution type. + self.keys.iter().cloned() + .zip(self.variables.iter().cloned()) + .collect() + } + + /// Assigns a new value to the given index, then calls update + /// on every constraints. + fn _assign(&mut self, idx: usize, value: Option<&'p V>) { + self.variables[idx] = value; + let var_key = &self.keys[idx]; + self.constraints.iter_mut() + .for_each(|c| { + c.update(&var_key, value); + }); + // Some thoughts: what if we used a stack of filters, + // for each variable, that is cleared, on every assign, + // and repopulated by aggregating update calls on constraints. + // Domain would then be filtered by ALL filters in these stacks. + } + fn _solve(&mut self, limit: Option) -> Vec> { let mut solutions: Vec> = vec![]; let mut stack: Vec> = vec![]; @@ -204,35 +267,19 @@ impl<'p, V: PartialEq, K: Eq + Hash + Clone> Problem<'p, V, K> { match node.unwrap() { Assignment::Update(idx, val) => { // Assign the variable and open new branches, if any. - self.variables[idx] = Some(val); - { - let v_key = &self.keys[idx]; - self.constraints.iter_mut().for_each(|cons| { - cons.update(&v_key, Some(val)); - }); - } - // TODO: handle case of empty domain.values + self._assign(idx, Some(val)); if let Some(mut nodes) = self._push_updates() { stack.append(&mut nodes); } else { // Assignements are completed if self._is_valid() { - solutions.push( - // Builds Solution - self.keys.iter().cloned() - .zip(self.variables.iter().cloned()) - .collect() - ); + solutions.push(self._get_solution()); }; }; }, Assignment::Clear(idx) => { // We are closing this branch, unset the variable - self.variables[idx] = None; - let v_key = &self.keys[idx]; - self.constraints.iter_mut().for_each(|cons| { - cons.update(&v_key, None); - }); + self._assign(idx, None); }, }; }; @@ -268,11 +315,11 @@ impl<'p, V, K: Eq + Hash + Clone> ProblemBuilder<'p, V, K> { }) } - pub fn add_variable(mut self, name: K, domain: Vec<&'p V>, value: Option<&'p V>) -> Self + pub fn add_variable(mut self, name: K, static_filter: Vec<&'p V>, initial: Option<&'p V>) -> Self { self.0.keys.push(name); - self.0.variables.push(value); - self.0.domains.push(domain); + self.0.variables.push(initial); + self.0.domains.push(static_filter); self } @@ -293,7 +340,7 @@ mod tests { fn test_solver_find_pairs() { use super::*; let domain = Domain::new(vec![1,2,3]); - let mut problem: Problem<_, _> = Problem::build() + let problem: Problem<_, _> = Problem::build() .add_variable(String::from("Left"), domain.all(), None) .add_variable(String::from("Right"), domain.all(), None) .finish(); @@ -311,7 +358,7 @@ mod tests { fn test_solver_find_pairs_with_initial() { use super::*; let domain = Domain::new(vec![1,2,3]); - let mut problem: Problem<_, _> = Problem::build() + let problem: Problem<_, _> = Problem::build() .add_variable("Left".to_string(), domain.all(), None) .add_variable("Right".to_string(), domain.all(), Some(&2)) .finish();