"use strict"; var ingredientsm = {} var allIngredients = [] for (let recipe of allRecipes) { for (let ingredient of recipe.ingredients) { if (ingredient.special) { continue } if (!ingredientsm[ingredient.ingredient]) { allIngredients.push(ingredient.ingredient) ingredientsm[ingredient.ingredient] = 1 } } } allIngredients.sort() var ingredientsLookup = {} for (let [idx, ingredient] of allIngredients.entries()) { ingredientsLookup[ingredient] = idx } var globret = undefined; window.addEventListener('load', function(evt) { glpkPromise.then(function(glpk) { const worker = new Worker('/static/glpk-worker.js') worker.onerror = (err) => { console.log(err) } worker.onmessage = (evt) => { if (evt.data.initialized) {} if (evt.data.result) { hide(document.getElementById('waiting')) show(document.getElementById('solution')) renderSolution(glpk, evt.data, globret) } } document.getElementById('optimal').addEventListener('click', function(evt) { evt.preventDefault() window.globret = constructILP(glpk) solveILP(worker, globret) }) }) }) function renderSolution(glpk, sol, ret) { let ingredients = ret.ingredients let recipes = JSON.parse(JSON.stringify(ret.recipes)) let res = [] let batches = [] let origbatch = [] for (let [r, recipe] of recipes.entries()) { if (recipe.length == 0) { origbatch.push(r) } } for (let [varname, on] of Object.entries(sol.result.vars)) { if (!on) { continue } if (!varname.startsWith('y')) { continue } let y = Number.parseInt(varname.split('_')[1]) if (res.indexOf(y) != -1) { continue } res.push(y) let batch = [] for (let [r, recipe] of recipes.entries()) { if (recipe.length == 1 & recipe[0] == y) { batch.push(r) } } for (let [r, recipe] of recipes.entries()) { recipes[r] = recipe.filter(it => it != y) } batches.push(batch) } let ol = document.createElement('ol') let li = document.createElement('li') li.appendChild(document.createTextNode('On-hand')) if (origbatch.length) { let ul = document.createElement('ul') let drinks = origbatch.map(b => allRecipes[b].name).join(', ') let sli = document.createElement('li') sli.appendChild(document.createTextNode(drinks)) ul.appendChild(sli) li.appendChild(ul) } ol.appendChild(li) for (let [idx, y] of res.entries()) { let batch = batches[idx] let li = document.createElement('li') li.appendChild(document.createTextNode(ingredients[y])) if (batch.length) { let ul = document.createElement('ul') let drinks = batch.map(b => allRecipes[b].name).join(', ') let sli = document.createElement('li') sli.appendChild(document.createTextNode(drinks)) ul.appendChild(sli) li.appendChild(ul) } ol.appendChild(li) } document.getElementById('solution').innerHTML = '' let desc = document.createElement('p') if (sol.result.status == glpk.GLP_UNDEF) { desc.appendChild(document.createTextNode('Error: Failed to find feasible solution. Try increasing time limit.')) } else if (sol.result.status == glpk.GLP_FEAS) { desc.appendChild(document.createTextNode(`Found feasible solution of score ${-sol.result.z}.`)) } else if (sol.result.status == glpk.GLP_INFEAS) { desc.appendChild(document.createTextNode('Error: Did not find feasible solution.')) } else if (sol.result.status == glpk.GLP_NOFEAS) { desc.appendChild(document.createTextNode('Error: Problem has no feasible solution.')) } else if (sol.result.status == glpk.GLP_OPT) { desc.appendChild(document.createTextNode(`Found optimal solution of score ${-sol.result.z}.`)) } else if (sol.result.status == glpk.GLP_UNBND) { desc.appendChild(document.createTextNode('Error: Problem is unbounded.')) } document.getElementById('solution').appendChild(desc) if (sol.result.status == glpk.GLP_FEAS || sol.result.status == glpk.GLP_OPT) { document.getElementById('solution').appendChild(ol) } } let hide = e => e.style.display = 'none'; let show = e => e.style.display = 'inline-block'; function solveILP(worker, ret) { let seconds = Number.parseInt(document.getElementById('limit').value) document.getElementById('waiting').innerHTML = seconds + ' seconds...' hide(document.getElementById('solution')) show(document.getElementById('waiting')) let incrSeconds = () => { document.getElementById('waiting').innerHTML = seconds + ' seconds...' seconds-- } setInterval(incrSeconds, 1000) worker.postMessage(ret.lp) } function constructILP(glpk) { let skip = Object.values(document.querySelector('form')).reduce((obj, field) => { if (field.getAttribute('x-ingredient') && field.checked) { obj[field.getAttribute('x-ingredient')] = 1 } return obj }, {}) let ingredientsset = {} let recipes = [] for (let recipe of allRecipes) { for (let ingredient of recipe.ingredients) { if (ingredient.special) { continue } if (skip[ingredientsLookup[ingredient.ingredient]]) { continue } ingredientsset[ingredient.ingredient] = 1 } } let ingredients = Object.keys(ingredientsset) for (let recipe of allRecipes) { let x = [] for (let ingredient of recipe.ingredients) { if (ingredient.special) { continue } if (skip[ingredientsLookup[ingredient.ingredient]]) { continue } x.push(ingredients.indexOf(ingredient.ingredient)) } recipes.push(x) } let n_ingredients = ingredients.length let makeStepR = (step, r) => `r${step}_${r}` let makeStepY = (step, y) => `y${step}_${y}` let jsonvars = [] for (let step = 0; step < n_ingredients; step++) { for (let [r, recipe] of recipes.entries()) { jsonvars.push({name: makeStepR(step, r), coef: -1}) } } let jsonobjective = { direction: glpk.GLP_MIN, vars: jsonvars, } let jsonconstraints = [] for (let step = 0; step < n_ingredients; step++) { let vars = [] for (let y = 0; y < n_ingredients; y++) { vars.push({name: makeStepY(step, y), coef: 1}) } jsonconstraints.push({vars, bnds: {type: glpk.GLP_FX, lb: step+1}}) } for (let y = 0; y < n_ingredients; y++) { for (let step = 0; step < n_ingredients - 1; step++) { jsonconstraints.push({ vars: [{name: makeStepY(step+1, y), coef: 1}, {name: makeStepY(step, y), coef: -1}], bnds: {type: glpk.GLP_LO, lb: 0} }) } } for (let [r, recipe] of recipes.entries()) { for (let step = 0; step < n_ingredients; step++) { let total = [] for (let y of recipe) { total.push({name: makeStepY(step, y), coef: -1}) } total.push({name: makeStepR(step, r), coef: 1}) jsonconstraints.push({ vars: total, bnds: {type: glpk.GLP_LO, lb: (-recipe.length + 1)} }) for (let y of recipe) { jsonconstraints.push({ vars: [{name: makeStepR(step, r), coef: 1}, {name: makeStepY(step, y), coef: -1}], bnds: {type: glpk.GLP_UP, ub: 0} }) } } } let binaries = [] for (let y = 0; y < n_ingredients; y++) { for (let step = 0; step < n_ingredients; step++) { binaries.push(makeStepY(step, y)) } } for (let r = 0; r < recipes.length; r++) { for (let step = 0; step < n_ingredients; step++) { binaries.push(makeStepR(step, r)) } } let lp = { objective: jsonobjective, subjectTo: jsonconstraints, binaries, options: { msglev: 3, tmlim: Number.parseInt(document.getElementById('limit').value), } } return {ingredients, lp, recipes} }