xxxxxxxxxx
291
//
// Money (with Brad's Rule)
//
// by Philip, January 29, 2021
//
// Simulate how money moves around between people in an economy,
// with the goal of seeing how different economic policies affect
// wealth distribution.
//
// The size (area) of a person is proportional to their wealth.
// Collisions are an opportunity for trade.
// The Gini Index is constantly computed and displayed.
//
// To Turn ON/OFF basic income and see how it effects the Gini
// index, click in the preview screen and press the "i" key.
//
let num = 400;
let enable_income = false; // Press 'i' in the window to toggle basic
// income and decay (wealth tax) on/off.
//
// Set a tax per transaction, based on wealth difference
//
let transaction_wealth_tax = 0.0; // Fractional 'tip' paid by wealthier party
let transaction_wealth_tax_max = 0.0;
//
// try a tax on transactions that funds dividend
//
let tax_dividend_rate = 0.00;
let dividend = 0;
//
// Inflate money supply with a direct dividend
//
let inflation_amount = 0.0;
//
// Wealth tax
//
let decay_rate = 0.001; //0.003; Wealth tax (loss per time step)
let balance_initial = 10.0; // How much money does everyone start with?
let fraction_to_risk = 0.5; // At most, what % of balance to pay?
let basic_income = decay_rate * balance_initial;
let adjust_radius = 0.05; // How big are the people?
let min_radius = 2;
let max_radius = 10;
let v_min = 10;
let v_max = 50;
let r_min = 5;
let atoms = [];
let repeat = false;
let collisions = true;
let moneySupply = balance_initial * num;
let averageBalance = moneySupply / num;
let numRich = 0;
let numPoor = 0;
let giniIndex = 0;
let frame = 0;
let transactions = 0;
let blue = "";
let red = "";
let green = "";
function setup() {
createCanvas(800, 600);
noStroke();
blue = color(0, 0, 255);
red = color(255, 0, 0);
green = color(0, 255, 0);
for (let i = 0; i < num; i++) {
let r = r_min;
let c = blue;
let v = createVector(v_min + random(-1.0, 1.0) * (v_max - v_min),
v_min + random(-1.0, 1.0) * (v_max - v_min));
let p = createVector(random(width), random(height));
atoms[i] = new atom(p, v, r, c, atoms, i);
}
}
function draw() {
frame++;
background(128);
for (let i = 0; i < num; i++) {
atoms[i].update();
atoms[i].display();
}
noStroke();
strokeWeight(1);
updateStats(frame);
}
function computeGiniIndex() {
//
// Gini index given as summation |xi - xj|
// https://en.wikipedia.org/wiki/Gini_coefficient#Calculation
//
averageBalance = 0;
for (let i = 0; i < num; i++) {
averageBalance += atoms[i].balance;
}
averageBalance /= num;
let sumOfDifferences = 0;
for (let i = 0; i < num; i++) {
for (let j = 0; j < num; j++) {
sumOfDifferences += abs(atoms[i].balance - atoms[j].balance);
}
}
return sumOfDifferences/(2 * num * num * averageBalance);
}
function updateStats(frame) {
numRich = 0;
numPoor = 0;
moneySupply = 0;
for (let i = 0; i < num; i++) {
if (atoms[i].balance > averageBalance * 2) numRich++;
else if (atoms[i].balance < averageBalance / 2) numPoor++;
atoms[i].balance += dividend / num;
atoms[i].balance += inflation_amount;
moneySupply += atoms[i].balance;
}
dividend = 0;
if (frame % 100 == 0) {
// every once in a while, re-compute the giniIndex
giniIndex = computeGiniIndex();
}
noStroke();
fill(128,128,128,220);
rect(0, height - 20, width, 20);
fill(0);
noStroke();
fill(green);
text(round(numRich/num * 100)+"%", 10, height - 5);
fill(red);
text(round(numPoor/num * 100)+"%", 40, height - 5);
fill(blue);
text(round((num - numPoor - numRich)/num * 100)+"%", 70, height - 5);
fill (0);
text("Gini Index: " + round(giniIndex * 100,0), 150, height - 5);
text("kXs: " + round(transactions / 1000,0), width - 60, height - 5);
text("MS: " + round(moneySupply), width - 150, height - 5);
if (enable_income) {
fill(red);
text("Income ON", width / 2, height - 5);
} else {
fill(0);
text("Income OFF", width / 2, height - 5);
}
}
function maybeTrade(i, j) {
// Two people meet, maybe they exchange some money.
let richer = j;
let poorer = i;
if (atoms[i].balance >= atoms[j].balance) {
richer = i;
poorer = j;
}
let buy = (random() > 0.5);
let amount = atoms[poorer].balance * random(fraction_to_risk);
// Tip is a fraction of transaction, proportional to the wealth difference between
// the trading partners.
let tip = (atoms[richer].balance / atoms[poorer].balance - 1.0)
* amount * transaction_wealth_tax;
tip = min(tip, amount * transaction_wealth_tax_max);
let tax = tax_dividend_rate * amount;
if ((buy ? atoms[richer].balance + tip : atoms[poorer].balance) >= amount) {
atoms[richer].balance += (buy ? -1.0 : (1.0 - tax_dividend_rate)) * amount;
atoms[poorer].balance += (buy ? (1.0 - tax_dividend_rate) : -1.0) * amount;
atoms[richer].balance -= tip;
atoms[poorer].balance += tip;
transactions++;
dividend += tax;
}
}
// atom class
function atom(p, v, r, c, _others, _id) {
this.p = p.copy();
this.v = v.copy();
this.r = r;
this.c = c;
this.others = _others;
this.id = _id;
this.dt = 1.0/60.0;
this.balance = balance_initial;
this.update = function() {
if (collisions) {
// Check for collisions and add restoring forces
let collisionForces = createVector(0,0,0);
for (let i = 0; i < this.others.length; i++) {
if (i != this.id) {
let d = p5.Vector.sub(this.p, this.others[i].p);
let penetration = ((this.r + this.others[i].r)) - d.mag();
if (penetration > 0) {
d.normalize();
// Elastic component - restitution (note: does not conserve energy)
collisionForces.add(d.mult(10 * penetration));
// When in collision, after first few secs, there may be trade
maybeTrade(this.id, i);
}
}
}
this.v.add(collisionForces.mult(1.0));
}
if (this.v.mag() > v_max) this.v.mult(v_max / this.v.mag());
this.p.add(p5.Vector.mult(this.v, this.dt));
// Your radius corresponds to your fraction of the money supply
this.r = max(sqrt(this.balance / moneySupply * width * height * adjust_radius), min_radius);
if (enable_income) {
this.balance += basic_income;
this.balance *= (1.0 - decay_rate);
}
let average_balance = moneySupply / num;
if (this.balance < average_balance / 2) this.c = red;
else if (this.balance > average_balance * 2) this.c = green;
else this.c = blue;
// Edge conditions
if (repeat) {
if (this.p.x > width) this.p.x -= width;
if (this.p.x < 0) this.p.x += width;
if (this.p.y > height) this.p.y -= height;
if (this.p.y < 0) this.p.y += height;
} else {
if (this.p.x >= width) {
this.v.x *= -1.0;
this.p.x = width - 1;
}
if (this.p.x < 0) {
this.v.x *= -1.0;
this.p.x = 0.0;
}
if (this.p.y >= height) {
this.v.y *= -1.0;
this.p.y = height - 1;
}
if (this.p.y < 0) {
this.v.y *= -1.0;
this.p.y = 0.0;
}
}
}
this.display = function() {
stroke(0);
//strokeWeight(1);
noStroke();
fill(this.c);
ellipse(this.p.x, this.p.y, this.r * 2, this.r * 2);
}
};
function keyTyped() {
if (key === 'i') {
enable_income = !enable_income;
}
}