xxxxxxxxxx
544
//
// Dividend
//
// Originally by Philip, February 21, 2021
//
// Modified in December of 2022 for FairShare (https://fairshare.social)
//
// Simulate how money moves around between people in an economy,
// with the goal of seeing how different economic policies affect
// wealth distribution, in particular a daily income.
//
// The size (area) of a person is proportional to their wealth.
// Collisions are an opportunity for trade. Trades are an exchange
// where the amount exchange is a fraction of the less wealthy
// person's balance and the buyer/seller is random.
//
// The Gini Index is constantly computed and displayed.
//
let groupSize
const defaultGroupSize = 500 // How many people in the simulation?
const balance_initial = 3400.0 // How much money does everyone start with?
// 3400 is the median cash balance for US
const transactions_per_day = 2 // How many transactions correspond to a person's day?
const fraction_to_risk = 0.05 // The fraction of wealth risked in a transaction.
// This value strongly influences the rate of wealth
// accumulation. 0.05 approximates average USA
// credit card transaction compared to cash balance.
let enableFairShare = true
let enableLaborBase = true
let dailyIncome
let transactionFee
const defaultDailyIncome = 50
const defaultTransactionFee = 0.08
const minimumLabor = 10.0
const chanceOfSuicide = 0.000135 //https://www.nimh.nih.gov/health/statistics/suicide
const povertyThreshold = defaultDailyIncome
let numDead = 0
let adjustRadius = 0.05 // How big are the people relative to their wealth?
let minRadius = 2
let days = 0 // How many days have gone by?
let frame = 0
let days_last_frame = 0
let transactions = 0
let switchOn = true
let v_min = 10
let v_max = 50
let r_min = 5
let people = []
let repeat = false
let collisions = true
let moneySupply = balance_initial * groupSize
let averageBalance = moneySupply / groupSize
let numRich = 0
let numPoor = 0
let giniIndex = 0
let blue = ""
let red = ""
let green = ""
let black = ""
// A function to map the speed to an angle in radians between -PI/2 and PI/2
function giniToAngle(gini) {
return map(gini, 0, 100, -PI/2, PI/2)
}
function maybeTrade(i, j) {
//
// Two people meet, and maybe they make a transaction
//
let richer = j
let poorer = i
if (people[i].balance >= people[j].balance) {
richer = i
poorer = j
}
// Randomly choose whether richer person buys or sells:
let buy = (random() > 0.5)
let buyer = (buy) ? richer : poorer
let seller = (buy) ? poorer : richer
let amount = people[poorer].balance * fraction_to_risk
if (enableLaborBase && (seller == poorer) && (minimumLabor > amount)) {
amount = minimumLabor;
}
let tax = (enableFairShare) ? amount * transactionFee : 0
if (people[buyer].balance >= amount) {
people[buyer].balance -= amount
people[seller].balance += amount - tax
transactions++
}
}
function sendDailyIncome(days) {
for (let i = 0; i < groupSize; i++) {
if (!people[i].dead)
people[i].balance += dailyIncome * days
}
}
function setup() {
createCanvas(800, 600)
noStroke()
blue = color(0, 0, 255)
red = color(255, 0, 0)
green = color(0, 255, 0)
black = color(0, 0, 0)
createGroup(defaultGroupSize)
dailyIncome = defaultDailyIncome
transactionFee = defaultTransactionFee
createSliders(transactionFee, dailyIncome, defaultGroupSize, true)
updateLabels(true)
angleMode(RADIANS) // Set the angle mode to radians
}
function createGroup(size) {
groupSize = size
people = []
let multiplier = 30 / groupSize**.4
v_max = 50 * multiplier
for (let i = 0; i < size; i++) {
let r = r_min
let c = blue
let v = createVector((v_min + random(-1.0, 1.0) * (v_max - v_min))*multiplier,
(v_min + random(-1.0, 1.0) * (v_max - v_min))*multiplier)
let p = createVector(random(width), random(height))
people[i] = new personClass(p, v, r, c, people, i)
}
}
function resetSim(newSize) {
clear()
days = 0
numDead = 0
createGroup(newSize)
}
function createSliders(fee, income, size, start) {
if (!start) {
fee_slider.remove()
daily_slider.remove()
groupSlider.remove()
}
// Create slider bars to adjust the parameters
fee_slider = createSlider(0, 0.2, fee, 0.01)
fee_slider.position(200, height + 10)
fee_slider.input(() => {
transactionFee = fee_slider.value()
})
daily_slider = createSlider(0, 200, income, 5)
daily_slider.position(200, height + 40)
daily_slider.input(() => {
dailyIncome = daily_slider.value()
})
groupSlider = createSlider(2, 1000, size, 1)
groupSlider.position(200, height + 70)
groupSlider.input(() => {
resetSim(groupSlider.value())
})
// Create labels to display the values of the parameters
let fee_label = createElement('p', 'Transaction fee: ')
fee_label.position(50, height - 10)
fee_label.style('font-size', '14pt')
let daily_label = createElement('p', 'Daily income: ')
daily_label.position(50, height + 20)
daily_label.style('font-size', '14pt')
let groupSizeLabel = createElement('p', 'Group size: ')
groupSizeLabel.position(50, height + 50)
groupSizeLabel.style('font-size', '14pt')
let daysLabel = createElement('p', 'Days: ')
daysLabel.position(500, height - 10)
daysLabel.style('font-size', '14pt')
let deathsLabel = createElement('p', 'Deaths: ')
deathsLabel.position(500, height + 50)
deathsLabel.style('font-size', '14pt')
let moneySupplyLabel = createElement('p', 'Money Supply: ')
moneySupplyLabel.position(500, height + 20)
moneySupplyLabel.style('font-size', '14pt')
}
function updateLabels(start) {
if (!start) {
feeValue.remove()
dailyValue.remove()
groupValue.remove()
daysValue.remove()
deathsValue.remove()
moneySupplyValue.remove()
}
// Create labels to display the values of the parameters
feeValue = createElement('p', (round(transactionFee * 100)).toString() + '%')
feeValue.position(350, height - 10)
feeValue.style('font-size', '14pt')
dailyValue = createElement('p', '$' + dailyIncome)
dailyValue.position(350, height + 20)
dailyValue.style('font-size', '14pt')
groupValue = createElement('p', groupSize.toString())
groupValue.position(350, height + 50)
groupValue.style('font-size', '14pt')
daysValue = createElement('p', round(days).toString())
daysValue.position(650, height - 10)
daysValue.style('font-size', '14pt')
deathsValue = createElement('p', numDead.toString())
deathsValue.position(650, height + 50)
deathsValue.style('font-size', '14pt')
let prettyMoneySupply = moneySupply.toLocaleString('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0})
moneySupplyValue = createElement('p', prettyMoneySupply)
moneySupplyValue.position(650, height + 20)
moneySupplyValue.style('font-size', '14pt')
}
function draw() {
frame++
background(128)
let last_transactions = transactions
for (let i = 0; i < groupSize; i++) {
people[i].update()
people[i].display()
}
days_last_frame = (transactions - last_transactions) / groupSize / transactions_per_day
days += days_last_frame
if (enableFairShare) {
sendDailyIncome(days_last_frame)
}
updateStats(frame)
drawSwitch()
drawGiniMeter()
}
function drawSwitch() {
checkSliders()
// set the stroke weight and color
strokeWeight(4)
stroke(0)
// draw the switch body as a rectangle
fill(200)
rect(10, 10, 100, 50, 20)
// draw the switch knob as a circle
// check the switch state and set the fill color accordingly
if (switchOn) {
fill(0, 255, 0)
} else {
fill(255, 0, 0)
}
// check the switch state and set the circle position accordingly
if (switchOn) {
circle(80, 35, 30)
} else {
circle(40, 35, 30)
}
}
function computeGiniIndex() {
//
// Uses discrete formula found at:
// https://en.wikipedia.org/wiki/Gini_coefficient#Calculation
//
averageBalance = 0
for (let i = 0; i < groupSize; i++) {
averageBalance += people[i].balance
if (people[i].balance < 0.0) {
fill(0)
noStroke()
text("Error: negative balance!", width/2, height/2)
}
}
averageBalance /= groupSize
let sumOfDifferences = 0
for (let i = 0; i < groupSize; i++) {
for (let j = 0; j < groupSize; j++) {
sumOfDifferences += abs(people[i].balance - people[j].balance)
}
}
return sumOfDifferences/(2 * groupSize * groupSize * averageBalance)
}
function updateStats(frame) {
textAlign(LEFT, BASELINE)
numRich = 0
numPoor = 0
moneySupply = 0
for (let i = 0; i < groupSize; i++) {
if (people[i].balance > averageBalance * 2) numRich++
else if (people[i].balance < averageBalance / 2) numPoor++
moneySupply += people[i].balance
}
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/groupSize * 100)+"%", 10, height - 5)
fill(red)
text(round(numPoor/groupSize * 100)+"%", 40, height - 5)
fill(blue)
text(round((groupSize - numPoor - numRich - numDead)/groupSize * 100)+"%", 70, height - 5)
fill(black)
text(round(numDead/groupSize * 100)+"%", 100, height - 5)
updateLabels(false)
}
function drawGiniMeter() {
translate(200, 380) // Shift the origin to the bottom edge
fill(128) // fill for the shapes
stroke(0) // Black stroke for the shapes
strokeWeight(2) // Thin stroke for the shapes
// Draw the dial arc
arc(200, 200, 300, 300, PI, 2*PI)
// Draw the red section between 0 and 100
fill(255, 0, 0) // Red fill for the section
// Use the speedToAngle function to calculate the start and end angles of the section
arc(200, 200, 200, 200, PI + giniToAngle(50), PI + giniToAngle(150))
// Draw the green section between 10 and 40
fill(0, 255, 0) // Green fill for the section
// Use the speedToAngle function to calculate the start and end angles of the section
arc(200, 200, 200, 200, PI + giniToAngle(60), PI + giniToAngle(90))
// Draw the yellow section between 40 and 60
fill(255, 255, 0) // Green fill for the section
// Use the speedToAngle function to calculate the start and end angles of the section
arc(200, 200, 200, 200, PI + giniToAngle(90), PI + giniToAngle(110))
// Draw the tick marks and the labels
for (let i = 0; i <= 10; i++) {
// Calculate the angle and the position of each tick mark
// Change the formula to add the mapped angle to PI/2
let angle = 3*PI/2 + giniToAngle(i * 10)
let x1 = 200 + 140 * cos(angle)
let y1 = 200 + 140 * sin(angle)
let x2 = 200 + 150 * cos(angle)
let y2 = 200 + 150 * sin(angle)
// Draw the tick mark
line(x1, y1, x2, y2)
// Draw the label
textSize(12)
textAlign(CENTER, CENTER)
fill(0)
let x3 = 200 + 125 * cos(angle)
let y3 = 200 + 125 * sin(angle)
text(i * 10, x3, y3)
}
// Draw the needle
stroke(0, 0, 0) // Black stroke for the needle
strokeWeight(4) // Thick stroke for the needle
// Change the formula to add the mapped angle to PI/2
let angle = 3*PI/2 + giniToAngle(round(giniIndex * 100,0))
let x4 = 200 + 105 * cos(angle) // Calculate the x-coordinate of the tip of the needle
let y4 = 200 + 105 * sin(angle) // Calculate the y-coordinate of the tip of the needle
line(200, 200, x4, y4) // Draw the needle
// Draw the center circle
fill(0) // Black fill for the center circle
strokeWeight(2) // Thin stroke for the center circle
circle(200, 200, 10) // Draw the center circle
strokeWeight(1)
text('Gini (inequality) Index', 200, 212)
}
// person class
function personClass(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.dead = false
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 (!this.dead && !this.others[i].dead) {
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())
// Your radius corresponds to your fraction of the money supply
this.r = max(sqrt(this.balance / moneySupply * width * height * adjustRadius), minRadius)
let average_balance = moneySupply / groupSize
if ((this.balance < povertyThreshold) && (random() <= chanceOfSuicide)) {
this.c = black
if (!this.dead) {
this.balance = 0
numDead++
this.dead = true
}
} else {
if (!this.dead) {
this.p.add(p5.Vector.mult(this.v, this.dt))
if (this.balance < average_balance / 2) this.c = red
else if (this.balance > average_balance * 2) this.c = green
else this.c = blue
} else {
this.c = black
}
}
// 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 checkSliders() {
if (transactionFee == 0 && dailyIncome == 0) {
switchOn = false
enableFairShare = false
} else {
switchOn = true
enableFairShare = true
}
}
function mousePressed() {
// check if the mouse is inside the switch body
if (mouseX > 10 && mouseX < 110 && mouseY > 10 && mouseY < 60) {
if (transactionFee == 0 && dailyIncome == 0) {
transactionFee = 0.08
dailyIncome = 50
} else {
transactionFee = 0
dailyIncome = 0
}
// toggle the switch state
switchOn = !switchOn
enableFairShare = !enableFairShare
if (!switchOn) {
createSliders(0, 0, groupSize, false)
} else {
createSliders(defaultTransactionFee, defaultDailyIncome, groupSize, false)
}
}
}