@tannerntannern/budgeteer
Version:
A specialized constraint solver for budget flows
236 lines (235 loc) • 9.66 kB
JavaScript
;
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;