This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new 340c493  draft blog post
340c493 is described below

commit 340c493970b3dcb416e23b886385b86d6049da36
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Mar 14 08:21:57 2024 +1000

    draft blog post
---
 site/src/site/blog/img/GoogleSheetsDietData.png    | Bin 0 -> 49776 bytes
 .../site/blog/img/GoogleSheetsDietOpenSolver.png   | Bin 0 -> 38878 bytes
 ...g-simple-optimization-problems-with-groovy.adoc | 374 +++++++++++++++++++++
 3 files changed, 374 insertions(+)

diff --git a/site/src/site/blog/img/GoogleSheetsDietData.png 
b/site/src/site/blog/img/GoogleSheetsDietData.png
new file mode 100644
index 0000000..4bfbaaf
Binary files /dev/null and b/site/src/site/blog/img/GoogleSheetsDietData.png 
differ
diff --git a/site/src/site/blog/img/GoogleSheetsDietOpenSolver.png 
b/site/src/site/blog/img/GoogleSheetsDietOpenSolver.png
new file mode 100644
index 0000000..7c80bec
Binary files /dev/null and 
b/site/src/site/blog/img/GoogleSheetsDietOpenSolver.png differ
diff --git 
a/site/src/site/blog/solving-simple-optimization-problems-with-groovy.adoc 
b/site/src/site/blog/solving-simple-optimization-problems-with-groovy.adoc
new file mode 100644
index 0000000..8ae9954
--- /dev/null
+++ b/site/src/site/blog/solving-simple-optimization-problems-with-groovy.adoc
@@ -0,0 +1,374 @@
+= Solving simple optimization problems with Groovy using Commons Math, Choco, 
JaCoP, Ojalgo, OptaPlanner, Timefold, and OR-Tools
+Paul King
+:revdate: 2024-03-10T21:45:00+00:00
+:draft: true
+:keywords: groovy, optaplanner, timefold, ojalgo, jacop, or-tools, choco, 
commons math, hipparchus, linear programming
+:description: This post looks at solving simple optimization problems using 
Groovy.
+
+== Introduction
+
+There are many problems involving optimization.
+We'll explore a case study which can be solved using linear optimization,
+also known as
+https://en.wikipedia.org/wiki/Linear_programming[linear programming].
+Linear programming problems optimize a particular linear objective
+function subject to one or more linear equality and inequality constraints.
+
+We'll look at such a problem and some libraries
+and tools which can be used to solve them.
+
+== Case Study: Diet Optimization
+
+Let's look at a case study where we try to minimize the cost of food
+items in our diet, while still maintaining some overall constraints
+which we might set for health or dietary preference reasons.
+The example is inspired by
+https://documentation.sas.com/doc/en/orcdc/14.2/ormpug/ormpug_lpsolver_examples01.htm[this
 SAS example],
+but see the <<Further Information>> section for a more elaborate linear 
programming example,
+the classic Stigler diet problem, solved using Google OR-Tools.
+
+First, here are six foods with their costs and nutritional
+values that make up our diet:
+
+[width=300]
+|===
+|                   | Bread 🍞 | Milk 🥛 | Cheese 🧀 | Potato 🥔 | Fish 🐟 | Yogurt 
🍶
+| Cost              | 2.0   | 3.5  | 8.0    | 1.5    | 11.0 | 1.0
+| Protein (g)       | 4.0   | 8.0  | 7.0    | 1.3    | 8.0  | 9.2
+| Fat (g)           | 1.0   | 5.0  | 9.0    | 0.1    | 7.0  | 1.0
+| Carbohydrates (g) | 15.0  | 11.7 | 0.4    | 22.6   | 0.0  | 17.0
+| Calories          | 90    | 120  | 106    | 97     | 130  | 180
+|===
+
+We want to minimize cost, while maintaining optimal nutrition,
+where optimal will be defined as meeting the following criteria:
+
+* Must be at least 300 calories
+* Not more than 10 grams of fat
+* Not less than 10 grams of carbohydrates
+* Not less than 8 grams of fat
+* At least 0.5 units of fish
+* No more than 1 unit of milk
+
+Note, we don't recommend this simplified set of constraints
+as a real diet, it is only for illustrative purposes for our case study.
+
+Relating this back to our earlier definition of linear programming,
+our list above represent our linear constraints. Our object function
+is cost which is determined by the amount of each food multiplied
+by its cost.
+
+== Solving with a spreadsheet solver
+
+This kind of problem is so common that solvers exist even within spreadsheet 
applications. We'll show a solution using the
+https://opensolver.org/opensolver-for-google-sheets/[OpenSolver Add-on] for
+https://www.google.com.au/sheets/about/[Google Sheets].
+But you can do the same thing using
+https://speakerdeck.com/paulk/groovy-constraint-programming?slide=77[Microsoft 
Excel] if you prefer.
+
+First, we fill in the data for our problem.
+It will be similar to the figure shown below, but initially,
+the variable cells (blue) and objective cell (yellow) will be blank.
+
+image:img/GoogleSheetsDietData.png[Data]
+
+Then, using the OpenSolver extension, we identify by way
+of cell ranges, our data (blue) and objective (yellow) cells,
+as well as the constraints.
+
+image:img/GoogleSheetsDietOpenSolver.png[Solver,width=25%]
+
+Then we click "Solve" and it calculates our optimized value.
+
+Let's look at solving this programmatically using Groovy.
+Groovy provides a particularly nice environment for
+scripting solutions to such problems, but for
+the libraries we are using, it should be possible to use
+most JVM languages.
+
+== Using Apache Commons Math or Hipparchus
+
+Let's now look at solving this problem using a simplex solver.
+We'll use the `SimplexSolver` class from Apache Commons
+Math which is essentially the same as the one from Hipparchus
+(a commons math fork).
+
+We'll start with a little helper method for defining our constraints:
+
+[source,groovy]
+----
+static scalar(coeffs, rel, double val) {
+    new LinearConstraint(coeffs as double[], rel, val)
+}
+----
+
+Next we define our individual constraints and the combined set:
+
+[source,groovy]
+----
+var milk_max   = scalar([0, 1, 0, 0, 0, 0], LEQ, 1)
+var fish_min   = scalar([0, 0, 0, 0, 1, 0], GEQ, 0.5)
+var protein    = scalar([4.0, 8.0, 7.0, 1.3, 8.0, 9.2],     LEQ, 10)
+var fat        = scalar([1.0, 5.0, 9.0, 0.1, 7.0, 1.0],     GEQ, 8)
+var carbs      = scalar([15.0, 11.7, 0.4, 22.6, 0.0, 17.0], GEQ, 10)
+var calories   = scalar([90, 120, 106, 97, 130, 180],       GEQ, 300)
+
+LinearConstraintSet constraints = [milk_max, fish_min, protein, fat, carbs, 
calories]
+----
+
+Each individual constraint has a coefficient for each food,
+a relationship, and a value.
+
+Next, we define our cost function, and an additional constraint
+to indicate that we can't buy a negative amount of any food.
+The `zeroOrMore` constraint saves us from doing the long-hand
+equivalent, like `fish_min` but with a minimum of `0`, for each food.
+
+[source,groovy]
+----
+var cost = new LinearObjectiveFunction([2.0, 3.5, 8.0, 1.5, 11.0, 1.0] as 
double[], 0)
+
+var zeroOrMore = new NonNegativeConstraint(true)
+----
+
+Now, our solution is found by creating a new solver, and asking
+it to optimize using our cost function and the constraints.
+We then print our solution out:
+
+[source,groovy]
+----
+var solution = new SimplexSolver().optimize(cost, constraints, zeroOrMore)
+
+static pretty(int idx, double d) {
+    d ? [sprintf('%s %.2f', ['🍞', '🥛', '🧀', '🥔', '🐟', '🍶'][idx], d)] : []
+}
+
+if (solution != null) {
+    printf "Cost: %.2f%n", solution.value
+    println solution.point.indexed().collectMany(this::pretty).join(', ')
+}
+----
+
+When run, it gives the following output:
+
+----
+Cost: 12.08
+🥛 0.05, 🧀 0.45, 🥔 1.87, 🐟 0.50
+----
+
+This is the same solution as what we saw when using the spreadsheet.
+
+You can currently swap between Apache Commons Math and Hipparchus
+by switching the Maven coordinates of the jar being used on the classpath
+and changing a few import statements. This may change in future versions,
+but for now:
+
+* Using `org.apache.commons:commons-math3:3.6.1` gives an older stable version
+of Commons Math, starting to show its age at 8 years old.
+* Using `org.apache.commons:commons-math4-legacy:4.0-beta1`
+gives the latest version of these classes from Apache Commons Math.
+The naming possibly deserves some explanation. There has been ongoing
+effort to modularise Commons Math and there are numerous components
+delivered as a result. The optimisation classes haven't
+been worked on yet and are available in the aforementioned artifact.
+* Using `org.hipparchus:hipparchus-optim:3.0` gives classes from the forked
+project. For the classes we are using, there is essentially no difference
+in the fork, but other parts of the library have seen useful updates
+if you don't mind having a dependency that isn't backed by the ASF.
+
+If you don't like those options, there are many more, here are a few
+with Groovy solutions in the same repo:
+
+* For a solution using the SCIP simplex solver in Google 
https://developers.google.com/optimization/lp[OR-Tools], see 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietOrTools.groovy[DietOrTools.groovy]
+* For a solution showing Groovy support within 
https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/proc/p1x8agymll9gten1ocziihptcjzj.htm[SAS],
 see 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietGroovy.sas[DietGroovy.sas]
+* For a solution using the LP solver in https://www.ojalgo.org/[ojAlgo], see 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietOjalgo.groovy[DietOjalgo.groovy]
+* For a solution using the https://choco-solver.org/[Choco] constraint 
programming solver, see 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietChocoInt.groovy[DietChocoInt.groovy]
 for a solution using scaled integers, and 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietChocoReal.groovy[DietChocoReal.groovy]
 for a solution with real numbers using Ibex integration
+* For a solution using the https://github.com/radsz/jacop[JaCoP] constraint 
programming solver, see 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietJacopInt.groovy[DietJacopInt.groovy]
 for a solution using scaled integers, and 
https://github.com/paulk-asert/groovy-constraint-programming/blob/master/subprojects/Diet/src/main/groovy/DietJacopIntKnapsack.groovy[DietJacopIntKnapsack.groovy]
 for a solution utilizing a Knapsack constraint
+
+== Using OptaPlanner or Timefold
+
+[source,groovy]
+----
+@PlanningEntity
+@ToString(includePackage = false)
+class Food {
+    String name
+    @PlanningVariable(valueRangeProviderRefs = "amount")
+    Integer amount // times 100
+    double cost, protein, fat, carbs, calories
+}
+----
+
+[source,groovy]
+----
+@PlanningSolution
+class DietSolution {
+    @PlanningEntityCollectionProperty
+    List<Food> foods
+
+    @ValueRangeProvider(id = "amount")
+    CountableValueRange<Integer> getAmount() {
+        ValueRangeFactory.createIntValueRange(0, 200, 5)
+    }
+
+    @PlanningScore
+    HardSoftScore score
+
+    void display() {
+        foods.eachWithIndex { f, idx ->
+            var emoji = ['🍞', '🥛', '🧀', '🥔', '🐟', '🍶']
+            println "${emoji[idx]} $f.name: ${f.amount / 100}"
+        }
+        for (name in ['fat', 'carbs', 'protein', 'calories', 'cost']) {
+            var total = foods.sum{ f -> f."$name" * f.amount / 100 }
+            printf "Total %s: %.2f%n", name, total
+        }
+        println "Score: $score"
+    }
+}
+----
+
+[source,groovy]
+----
+class DietConstraintProvider implements ConstraintProvider {
+    @Override
+    Constraint[] defineConstraints(ConstraintFactory factory) {
+        new Constraint[]{
+                maxField(factory, 'protein', 10),
+                minField(factory, 'fat', 8),
+                minField(factory, 'carbs', 10),
+                minField(factory, 'calories', 300),
+                minFood(factory, 'Fish', 50),
+                maxFood(factory, 'Milk', 100),
+                minCost(factory),
+        }
+    }
+
+    private static int amountOf(Food f, String name) {
+        (f."$name" * f.amount).toInteger()
+    }
+
+    private static Constraint minField(ConstraintFactory factory, String 
fieldName, double minAmount) {
+        ToIntFunction<Food> amount = f -> amountOf(f, fieldName)
+        factory.forEach(Food)
+                .groupBy(sum(amount))
+                .filter(fs -> fs < minAmount * 100)
+                .penalize(ONE_HARD)
+                .asConstraint("Min $fieldName")
+    }
+
+    private static Constraint maxField(ConstraintFactory factory, String 
fieldName, double maxAmount) {
+        ToIntFunction<Food> amount = f -> amountOf(f, fieldName)
+        factory.forEach(Food)
+                .groupBy(sum(amount))
+                .filter(fs -> fs > maxAmount * 100)
+                .penalize(ONE_HARD)
+                .asConstraint("Max $fieldName")
+    }
+
+    private static Constraint minFood(ConstraintFactory factory, String 
foodName, double minAmount) {
+        factory.forEach(Food)
+                .filter(f -> f.name == foodName && f.amount < minAmount)
+                .penalize(ONE_HARD)
+                .asConstraint("Min $foodName")
+    }
+
+    private static Constraint maxFood(ConstraintFactory factory, String 
foodName, double maxAmount) {
+        factory.forEach(Food)
+                .filter(f -> f.name == foodName && f.amount > maxAmount)
+                .penalize(ONE_HARD)
+                .asConstraint("Max $foodName")
+    }
+
+    private static ToIntFunction<Food> totalCost = f -> (f.cost * 
f.amount).toInteger()
+
+    private static Constraint minCost(ConstraintFactory factory) {
+        factory.forEach(Food)
+                .filter(f -> f.amount > 0)
+                .groupBy(sum(totalCost))
+                .penalize(ONE_SOFT, fs -> fs >> 2)
+                .asConstraint('Min cost')
+    }
+}
+----
+
+[source,groovy]
+----
+def unsolved = new DietSolution(foods: [
+    new Food(name:  'Bread', cost:  2.0, protein: 4.0, fat: 1.0, carbs: 15.0, 
calories:  90),
+    new Food(name:   'Milk', cost:  3.5, protein: 8.0, fat: 5.0, carbs: 11.7, 
calories: 120),
+    new Food(name: 'Cheese', cost:  8.0, protein: 7.0, fat: 9.0, carbs:  0.4, 
calories: 106),
+    new Food(name: 'Potato', cost:  1.5, protein: 1.3, fat: 0.1, carbs: 22.6, 
calories:  97),
+    new Food(name:   'Fish', cost: 11.0, protein: 8.0, fat: 7.0, carbs:  0.0, 
calories: 130),
+    new Food(name: 'Yogurt', cost:  1.0, protein: 9.2, fat: 1.0, carbs: 17.0, 
calories: 180)
+])
+
+def construction = new 
ConstructionHeuristicPhaseConfig(constructionHeuristicType: FIRST_FIT)
+def moveSelector = new UnionMoveSelectorConfig([
+        new ChangeMoveSelectorConfig(),
+        new SwapMoveSelectorConfig()
+])
+def localSearch = new LocalSearchPhaseConfig(localSearchType: 
VARIABLE_NEIGHBORHOOD_DESCENT,
+        moveSelectorConfig: moveSelector)
+def config = new SolverConfig()
+        .withSolutionClass(DietSolution)
+        .withEntityClasses(Food)
+        .withConstraintProviderClass(DietConstraintProvider)
+        .withPhases(construction, localSearch)
+        .withTerminationSpentLimit(Duration.ofSeconds(10))
+
+def factory = SolverFactory.create(config)
+def solver = factory.buildSolver()
+def solved = solver.solve(unsolved)
+solved.display()
+----
+
+It has this output when run:
+
+----
+08:17:05.202 [main] INFO  a.t.s.core.impl.solver.DefaultSolver - Solving 
started: time spent (25), best score (-6init/0hard/0soft), environment mode 
(REPRODUCIBLE), move thread count (NONE), random (JDK with seed 0).
+08:17:05.385 [main] INFO  a.t.s.c.i.c.DefaultConstructionHeuristicPhase - 
Construction Heuristic phase (0) ended: time spent (210), best score 
(-1hard/-521soft), score calculation speed (1355/sec), step total (6).
+08:17:15.175 [main] INFO  a.t.s.c.i.l.DefaultLocalSearchPhase - Local Search 
phase (1) ended: time spent (10000), best score (-1hard/-261soft), score 
calculation speed (155967/sec), step total (1030).
+08:17:15.176 [main] INFO  a.t.s.core.impl.solver.DefaultSolver - Solving 
ended: time spent (10000), best score (-1hard/-261soft), score calculation 
speed (152685/sec), phase total (2), environment mode (REPRODUCIBLE), move 
thread count (NONE).
+🍞 Bread: 0.6
+🥛 Milk: 0.6
+🧀 Cheese: 0
+🥔 Potato: 0.4
+🐟 Fish: 0.5
+🍶 Yogurt: 1.05
+Total fat: 8.19
+Total carbs: 42.91
+Total protein: 21.38
+Total calories: 418.80
+Total cost: 10.45
+Score: -1hard/-261soft
+----
+
+It has this output when run:
+
+----
+🍞 Bread: 0
+🥛 Milk: 0
+🧀 Cheese: 0.5
+🥔 Potato: 1.9
+🐟 Fish: 0.5
+🍶 Yogurt: 0
+Total fat: 8.19
+Total carbs: 43.14
+Total protein: 9.97
+Total calories: 302.30
+Total cost: 12.35
+Score: 0hard/-308soft
+----
+
+== Further Information
+
+* https://developers.google.com/optimization/lp[OR-Tools] linear optimization
+* A related but more elaborate example based on the 
https://developers.google.com/optimization/lp/stigler_diet[Stigler Diet] 
problem using Google OR-Tools
+* A Python 
https://www.kaggle.com/code/nbuhagiar/diet-optimization-with-or-tools[Diet 
example] also using Google OR-Tools
+* GitHub repos containing sample code: 
https://github.com/paulk-asert/groovy-constraint-programming/tree/master/subprojects/Diet[Diet]
 
https://github.com/paulk-asert/groovy-constraint-programming/tree/master/subprojects/DietOptaPlanner[DietOptaPlanner]
 
https://github.com/paulk-asert/groovy-constraint-programming/tree/master/subprojects/DietTimeflow[DietTimeflow]
+
+== Conclusion
+
+We have looked at using Groovy and a few linear optimization
+libraries to solve a diet case study.

Reply via email to