clipsexpert-system

Expert system for feasible / actual cooking receipe, managing ingredients (available or feasible)


I would like to implement an expert system with CLIPS for cooking receipe. Basically, from available ingredients I have in stock (e.g. tomato, floor, oil, tomatoSauce, etc.) and "crafted" ingredients/receipe I can do (e.g. dough from floor and oil, tomatoSauce from tomato), I would like the expert system to infere which receipe are feasible. And If I tell him to perform a receipe, then the stock are maintened up to date.

I find it actually very hard (for me) to implement. Maybe because I kind of do forward chaining inference, where CLIPS is more a production system with production rules (IMHO).

I tried the code bellow. Whithin this code, I set some comments to explain the ideas/proposals I had, but which are not working (invalid syntax).

Your help, or even direct/indirect advices are welcome.

bonus question : is-it possible to ask the expert system "give me all the steps, available ingredients, and ingredients I need to purchase to create N kg of this ingredient" ? I guess this is a backward logic problem more suitable for prolog or equivalent.


(defrule at-start
=>
(set-dynamic-constraint-checking TRUE)
)

(deftemplate Ingredient
(slot name)
(slot quantityKg (type FLOAT) (default 0.0) (range 0.0 ?VARIABLE))
(slot status) ;see my "not working" proposals bellow
)

(deftemplate DoAction
(slot name) ;see my "not working" proposals bellow
)

(defrule feasible_recipe_dough
    ?f0 <- (Ingredient (name flour) (quantityKg ?q0))
    (test (>= ?q0 0.5))
    ?f1 <- (Ingredient (name butter | oil) (quantityKg ?q1))
    (test (>= ?q1 0.15))    
=>
    (modify ?f0 (quantityKg (- ?q0 0.5)))
    (modify ?f1 (quantityKg (- ?q1 0.15)))
    
    ;<proposal "available" / "feasible" not working>
    ;(if (DoAction (name make_dough)) 
    ;then (bind ?s available)
    ;else (bind ?s feasible)
    ;)
    ;(assert (Ingredient (name dough) (quantityKg 0.5) (status ?s)))
    ;</proposal "available" / "feasible" not working>

    ;<other needed feature : increment quantity of already available ingredient>
    ;pseudo code : 
    ;(if (?i <- (Ingredient (name dough) (quantityKg ?curKg)))
    ;then ((modify ?i (quantityKg (+ ?curKg 0.5))))
    ;else (assert (Ingredient (name dough) (quantityKg 0.5)))
    ;)
    ;</other needed feature : increment quantity of already available ingredient>


    (assert (Ingredient (name dough) (quantityKg 0.5))) ; this is working, but this is not implementing the features I need ...
)

(defrule feasible_recipe_tomato_sauce
    ?f0 <- (Ingredient (name tomato) (quantityKg ?q0))
    (test (>= ?q0 0.5))
=>
    (modify ?f0 (quantityKg (- ?q0 0.5)))
    (assert (Ingredient (name tomatoSauce) (quantityKg 0.5)))
)

(defrule feasible_recipe_pizza_margarita
    ?f0 <- (Ingredient (name dough) (quantityKg ?q0))
    (test (>= ?q0 0.5))
    ?f1 <- (Ingredient (name tomatoSauce) (quantityKg ?q1))
    (test (>= ?q1 0.15))   
    ?f2 <- (Ingredient (name mozzarela) (quantityKg ?q2))
    (test (>= ?q2 0.3))       
=>
    (modify ?f0 (quantityKg (- ?q0 0.5)))
    (modify ?f1 (quantityKg (- ?q1 0.15)))
    (modify ?f2 (quantityKg (- ?q2 0.3)))    
    (assert (Ingredient (name pizzaMargarita) (quantityKg 0.75)))
)

My trials and proposals are in the comments in the code.


Solution

  • Rather than write a separate rule for every recipe, I'd represent the recipes as facts.

            CLIPS (6.4.1 4/8/23)
    CLIPS> 
    (deftemplate do-action
       (slot action)
       (slot name)
       (multislot arguments))
    CLIPS> 
    (deftemplate recipe
       (slot name)
       (multislot needed)
       (slot quantityKg))
    CLIPS>    
    (deftemplate recipe-ingredient
       (slot recipe)
       (slot needed)
       (slot ingredient)
       (slot quantityKg))
    CLIPS> 
    (deftemplate ingredient
       (slot name)
       (slot quantityKg))
    CLIPS>    
    (deffacts recipes
       (recipe (name tomatoSauce)
               (needed tomato)
               (quantityKg 0.5))  
       (recipe-ingredient
               (recipe tomatoSauce)
               (needed tomato)
               (ingredient tomato)
               (quantityKg 0.5))           
       (recipe (name dough)
               (needed flour butter-or-oil)
               (quantityKg 0.5))  
       (recipe-ingredient
               (recipe dough)
               (needed flour)
               (ingredient flour)
               (quantityKg 0.5))  
       (recipe-ingredient
               (recipe dough)
               (needed butter-or-oil)
               (ingredient butter)
               (quantityKg 0.15))  
       (recipe-ingredient
               (recipe dough)
               (needed butter-or-oil)
               (ingredient oil)
               (quantityKg 0.15))
       (recipe (name pizzaMargarita)
               (needed dough tomatoSauce mozzarela)
               (quantityKg 0.75))  
       (recipe-ingredient
               (recipe pizzaMargarita)
               (needed dough)
               (ingredient dough)
               (quantityKg 0.5))   
       (recipe-ingredient
               (recipe pizzaMargarita)
               (needed tomatoSauce)
               (ingredient tomatoSauce)
               (quantityKg 0.15))  
       (recipe-ingredient
               (recipe pizzaMargarita)
               (needed mozzarela)
               (ingredient mozzarela)
               (quantityKg 0.3)) )
    CLIPS> 
    (deffacts ingredients   
       (ingredient (name tomato)
                   (quantityKg 1.0))
       (ingredient (name flour)
                   (quantityKg 1.0))
       (ingredient (name butter)
                   (quantityKg 0.5))
       (ingredient (name oil)
                   (quantityKg 1.0))
       (ingredient (name mozzarela)
                   (quantityKg 1.0)))
    CLIPS> 
    

    You can then write some generic rules that handle any recipe.

    CLIPS>                      
    (defrule feasible
       (do-action (action feasible))
       (recipe (name ?recipe))
       (forall (recipe (name ?recipe)
                       (needed $? ?needed $?))
               (exists (recipe-ingredient (recipe ?recipe)
                                          (needed ?needed)
                                          (ingredient ?ingredient)
                                          (quantityKg ?neededKg))
                       (ingredient (name ?ingredient)
                                   (quantityKg ?quantity&:(>= ?quantity ?neededKg)))))
       =>
       (println "Making " ?recipe " is feasible."))             
    CLIPS>           
    (defrule make
       ?d <- (do-action (action make)
                        (name ?recipe))
       (recipe (name ?recipe)
               (needed $?needed-list)
               (quantityKg ?quantity))
       (forall (recipe (name ?recipe)
                       (needed $? ?needed $?))
               (exists (recipe-ingredient (recipe ?recipe)
                                          (needed ?needed)
                                          (ingredient ?ingredient)
                                          (quantityKg ?neededKg))
                       (ingredient (name ?ingredient)
                                   (quantityKg ?iquantity&:(>= ?iquantity ?neededKg)))))
       =>
       (retract ?d)
       (assert (ingredient (name ?recipe) 
                           (quantityKg ?quantity)))
       (assert (do-action (action update-quantities) 
                          (name ?recipe) 
                          (arguments $?needed-list)))) 
    CLIPS>    
    (defrule update-quantities
       ?d <- (do-action (action update-quantities)
                        (name ?recipe)
                        (arguments $?b ?needed $?e))
       ;; Find the ingredient to update the quantity
       (recipe-ingredient (recipe ?recipe)
                          (needed ?needed)
                          (ingredient ?ingredient)
                          (quantityKg ?neededKg))
       ?i <- (ingredient (name ?ingredient)
                         (quantityKg ?quantity&:(>= ?quantity ?neededKg)))
       ;; Verify there's not an alternate ingredient with a large quantity
       (not (and (recipe-ingredient (recipe ?recipe)
                                    (needed ?needed)
                                    (ingredient ?ingredient2&~?ingredient)
                                    (quantityKg ?neededKg2))
                 (ingredient (name ?ingredient2)
                             (quantityKg ?quantity2&:(>= ?quantity2 ?neededKg2)
                                                   &:(> ?quantity2 ?quantity)))))
       =>
       (modify ?d (arguments ?b ?e))
       (modify ?i (quantityKg (- ?quantity ?neededKg))))
    CLIPS>    
    (defrule action-done
       (declare (salience -10))
       ?d <- (do-action)
       =>
       (retract ?d))
    CLIPS> 
    

    The conditions of the rules are more complex, but you only have to write general rules for each task and you can write the rule conditions to do things like pick the ingredient with the largest quantity (for example, if you can use either oil or butter to make dough, pick the one that has the most remaining).

    CLIPS> (reset)
    CLIPS> (facts 11)
    f-11    (ingredient (name tomato) (quantityKg 1.0))
    f-12    (ingredient (name flour) (quantityKg 1.0))
    f-13    (ingredient (name butter) (quantityKg 0.5))
    f-14    (ingredient (name oil) (quantityKg 1.0))
    f-15    (ingredient (name mozzarela) (quantityKg 1.0))
    For a total of 5 facts.
    CLIPS> (assert (do-action (action feasible)))
    <Fact-16>
    CLIPS> (run)
    Making dough is feasible.
    Making tomatoSauce is feasible.
    CLIPS> (assert (do-action (action make) (name dough)))
    <Fact-17>
    CLIPS> (run)
    CLIPS> (facts 11)
    f-11    (ingredient (name tomato) (quantityKg 1.0))
    f-12    (ingredient (name flour) (quantityKg 0.5))
    f-13    (ingredient (name butter) (quantityKg 0.5))
    f-14    (ingredient (name oil) (quantityKg 0.85))
    f-15    (ingredient (name mozzarela) (quantityKg 1.0))
    f-18    (ingredient (name dough) (quantityKg 0.5))
    For a total of 6 facts.
    CLIPS> 
    

    For planning tasks, like buying ingredients, you just need to split your tasks into subtasks as I did in the prior rules for updating the quantities. For example, you can determine the number of pizzas you need to make by subtracting the number you have from the number you want. Based on that you can determine the amount of dough you need and from that you can determine the amount of flour and oil or butter.

    Regarding questions of the nature which language is most suitable for implementing a specific type of problem, I think it's a mistake to try to answer this question before you have a good specification of conceptually how you're going to solve the problem. You need to identify an algorithm that is suitable for your requirements first, and then you can figure out which language would be best for implementing that algorithm.

    For example, let's say all your recipes have a fixed set of ingredients (no substitutions) and you want to determine the total list of ingredients needed to make a specific recipe. In Java, you could create objects to represent each recipe/ingredient. Each recipe object would have a value indicating the quantity produced by the recipe and a HashMap containing the list of ingredients needed to create it where the HashMap key would be the ingredient and the value would be the quantity needed. Each recipe object would be added to a HashMap where the key is the recipe name and the value is the recipe object.

    To calculate the ingredients needed for a recipe, you'd add a method for the recipe class where you pass in the quantity needed and an empty HashMap. The recipe object method adds the ingredients to the HashMap. So if you are calculating for 1.5 Kgs of Margarita pizza the HashMap would containing the key/values of { tomatoSauce: 0.3, dough: 1.0, mozzarela: 0.6 }.

    Next the method is recursively called for each ingredient. So 0.5 Kgs of tomatoSauce requires 0.5 Kgs to tomatoes, so 0.3 Kgs of tomatoes would produce 0.3 Kgs of tomatoSauce and the HashMap would be updated to { tomato: 0.3, dough: 1.0, mozzarela: 0.6 }.

    Similarly for dough (assuming the recipe uses butter, not oil) the HashMap would be updated to { tomato: 0.3, flour: 1.0, oil: 0.3, mozzarela: 0.6 } and this would be the value returned.

    Even though I'm talking about Java in this case, I'm really just walking through an algorithm where I'm starting with the high level recipe and then incrementally replacing the ingredients with the lower level recipes. I could also implement this same approach with C, C++, C#, Python, or CLIPS; the algorithm would stay the same its just the specific implementation details that would change from language to language.

    Now let's say you want to add a method that determines whether its feasible to make a recipe with available ingredients and your recipes can use substitutions. Let's say that you've got 0.15 Kg of butter and 0.25 Kgs of oil in store and further that one ingredient of the recipe requires 0.15 Kg of oil and another ingredient requires either 0.15 Kg of butter or 0.15 Kg of oil.

    If we want to adapt the prior method to determine feasibility of making the recipe, we'll have to resolve some issues. First, we'll need to pass in a HashMap containing the list of available ingredients we have. We can update this while we're computing the required ingredients and then HashMap can be used to determine feasibility: if there are any negative quantities, the recipe can't be made; otherwise the inventories can be updated with the values in this HashMap.

    Let's say that our method processes the ingredient that can either use butter or oil first, and that we pick oil because we've got more oil than butter. But if we do that, we've now got 0.1 Kg of oil and when we get to the ingredient requiring 0.15 Kg of oil, we don't have enough. So we needed to pick butter in the first place and that suggests that we need to be able to backtrack.

    So now our algorithm needs to recurse in a manner that we save that states of the quantities whenever there is more than one choice that can be made, so that we can restore those states and pick another choice if the last choice failed. Again, we can implement this in any number of languages, it's just that the specific details are going to change in the actual implementation.