UNPKG

@tannerntannern/budgeteer

Version:

A specialized constraint solver for budget flows

236 lines (235 loc) 9.66 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var kiwi_js_1 = require("kiwi.js"); var two_key_map_1 = require("./two-key-map"); /** * All nodes that are part of the network. */ var allNodes = []; /** * Maps each Node to the Set of its suppliers. */ var suppliers = new Map(); /** * Maps each Node to the Set of its consumers. */ var consumers = new Map(); /** * Maps each Node to its balance. */ var balances = new Map(); /** * Maps a pair of Nodes to the amount transferred between them. */ var transfers = new two_key_map_1.TwoKeyMap(); /** * Array of functions that setup constraints on the solver. Due to the nature of some of these constraints, * they can't be applied until all the nodes exist, which is why they have to be batched up in functions. */ var constraints = []; /** * Constraint solver that does all the heavy lifting. */ var solver = new kiwi_js_1.Solver(); /** * Clears all nodes, relationships, and constraints, and resets the kiwi.js solver. */ var reset = function () { allNodes.length = 0; constraints.length = 0; [suppliers, consumers, balances, transfers] .forEach(function (collection) { return collection.clear(); }); var solver = new kiwi_js_1.Solver(); }; exports.reset = reset; /** * Returns an Expression that represents the total value consumed by the given node's consumers. */ function sumOfConsumption(node) { var result = new kiwi_js_1.Expression(0); consumers.get(node).forEach(function (consumer) { result = result.plus(transfers.get(node, consumer)); }); return result; } /** * Returns an Expression that represents the total value supplied by the given node's suppliers. */ function sumOfSupply(node) { var result = new kiwi_js_1.Expression(0); suppliers.get(node).forEach(function (supplier) { result = result.plus(transfers.get(supplier, node)); }); return result; } /** * Registers sets for the suppliers and consumers of the given node. */ var registerSuppliersAndConsumers = function (node) { if (node.type === 'consumer' || node.type === 'pipe') suppliers.set(node, new Set()); if (node.type === 'supply' || node.type === 'pipe') consumers.set(node, new Set()); }; /** * Registering a transfer between a consumable and a supplyable requires also registering the inverse transfer. * This is tedious, so this function takes care of it. */ var registerTransfers = function (consumable, supplyable) { suppliers.get(supplyable).add(consumable); consumers.get(consumable).add(supplyable); var consumableToSupplyable = new kiwi_js_1.Variable(consumable.name + "->" + supplyable.name); var supplyableToConsumable = new kiwi_js_1.Variable(supplyable.name + "->" + consumable.name); transfers.set(consumable, supplyable, consumableToSupplyable); transfers.set(supplyable, consumable, supplyableToConsumable); return { consumableToSupplyable: consumableToSupplyable, supplyableToConsumable: supplyableToConsumable }; }; /** * Registers and returns a balance for the given node. */ var registerBalance = function (node) { var balance = new kiwi_js_1.Variable("Bal-" + node.name); balances.set(node, balance); return balance; }; /** * Turns a node into a consumable. The given node is modified in place and returned. */ var consumableMixin = function (node) { // The given node will become a Node<Consumable> by the end of the function, so we preemptively assign // the type to make the compiler happy. var result = node; result.supplies = function (amount, multiplier) { if (multiplier === void 0) { multiplier = 1; } return ({ to: function (supplyable) { amount *= multiplier; var _a = registerTransfers(result, supplyable), consumableToSupplyable = _a.consumableToSupplyable, supplyableToConsumable = _a.supplyableToConsumable; constraints.push(function () { solver.createConstraint(consumableToSupplyable, kiwi_js_1.Operator.Eq, amount, kiwi_js_1.Strength.required); solver.createConstraint(supplyableToConsumable, kiwi_js_1.Operator.Eq, consumableToSupplyable.multiply(-1), kiwi_js_1.Strength.required); }); return result; } }); }; result.suppliesAsMuchAsNecessary = function () { return ({ to: function (supplyable) { var _a = registerTransfers(result, supplyable), consumableToSupplyable = _a.consumableToSupplyable, supplyableToConsumable = _a.supplyableToConsumable; constraints.push(function () { solver.createConstraint(consumableToSupplyable, kiwi_js_1.Operator.Ge, 0, kiwi_js_1.Strength.required); solver.createConstraint(consumableToSupplyable, kiwi_js_1.Operator.Eq, 0, kiwi_js_1.Strength.weak); solver.createConstraint(supplyableToConsumable, kiwi_js_1.Operator.Eq, consumableToSupplyable.multiply(-1), kiwi_js_1.Strength.required); }); return result; } }); }; result.suppliesAsMuchAsPossible = function () { return ({ to: function (supplyable) { var _a = registerTransfers(result, supplyable), consumableToSupplyable = _a.consumableToSupplyable, supplyableToConsumable = _a.supplyableToConsumable; constraints.push(function () { solver.createConstraint(consumableToSupplyable, kiwi_js_1.Operator.Ge, 0, kiwi_js_1.Strength.required); solver.createConstraint(consumableToSupplyable, kiwi_js_1.Operator.Eq, Number.MAX_SAFE_INTEGER, kiwi_js_1.Strength.weak); solver.createConstraint(supplyableToConsumable, kiwi_js_1.Operator.Eq, consumableToSupplyable.multiply(-1), kiwi_js_1.Strength.required); }); return result; } }); }; return result; }; /** * Turns a node into a supplyable. The given node is modified in place and returned. */ var supplyableMixin = function (node) { var result = node; result.consumes = function (amount, multiplier) { if (multiplier === void 0) { multiplier = 1; } return ({ from: function (consumable) { consumable.supplies(amount, multiplier).to(result); return result; } }); }; result.consumesAsMuchAsNecessary = function () { return ({ from: function (consumable) { consumable.suppliesAsMuchAsNecessary().to(result); return result; } }); }; result.consumesAsMuchAsPossible = function () { return ({ from: function (consumable) { consumable.suppliesAsMuchAsPossible().to(result); return result; } }); }; return result; }; /** * Creates a supply node. */ function supply(name, capacity, multiplier) { if (multiplier === void 0) { multiplier = 1; } capacity *= multiplier; var supply = consumableMixin({ name: name, type: 'supply' }); var balance = registerBalance(supply); registerSuppliersAndConsumers(supply); allNodes.push(supply); constraints.push(function () { solver.createConstraint(balance, kiwi_js_1.Operator.Ge, 0, kiwi_js_1.Strength.required); solver.createConstraint(balance, kiwi_js_1.Operator.Eq, new kiwi_js_1.Expression(capacity).minus(sumOfConsumption(supply)), kiwi_js_1.Strength.required); }); return supply; } exports.supply = supply; /** * Creates a consumer node. */ function consumer(name) { var consumer = supplyableMixin({ name: name, type: 'consumer' }); var balance = registerBalance(consumer); registerSuppliersAndConsumers(consumer); allNodes.push(consumer); constraints.push(function () { solver.createConstraint(balance, kiwi_js_1.Operator.Eq, sumOfSupply(consumer), kiwi_js_1.Strength.required); }); return consumer; } exports.consumer = consumer; /** * Creates a pipe node. */ function pipe(name) { var pipe = supplyableMixin(consumableMixin({ name: name, type: 'pipe' })); var balance = registerBalance(pipe); registerSuppliersAndConsumers(pipe); allNodes.push(pipe); constraints.push(function () { solver.createConstraint(balance, kiwi_js_1.Operator.Eq, 0, kiwi_js_1.Strength.required); solver.createConstraint(balance, kiwi_js_1.Operator.Eq, sumOfSupply(pipe).minus(sumOfConsumption(pipe)), kiwi_js_1.Strength.required); }); return pipe; } exports.pipe = pipe; /** * Resolves the balances and tranfers of the network. */ function solve() { for (var _i = 0, constraints_1 = constraints; _i < constraints_1.length; _i++) { var constraint = constraints_1[_i]; constraint(); } solver.updateVariables(); // Make a transfer map with just numbers rather than kiwi variables var resultTransfers = new two_key_map_1.TwoKeyMap(); transfers.forEach(function (node1, node2, value) { var amount = value.value(); if (amount > 0) resultTransfers.set(node1, node2, amount); }); // Make a balance map with just numbers rather than kiwi variables var resultBalances = new Map(); balances.forEach(function (value, node) { return resultBalances.set(node, value.value()); }); return { allNodes: allNodes, transfers: resultTransfers, balances: resultBalances }; } exports.solve = solve;