Last active
August 8, 2021 21:29
-
-
Save zesterer/d3160490a1e094c5b511570bdd2438df to your computer and use it in GitHub Desktop.
A simulation of a materialist economy: centrally-planned workforce allocation and propagation of labour value and consumption value across the supply chain, leading to pareto efficiency
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::collections::BTreeMap as HashMap;//HashMap; | |
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | |
enum Good { | |
Log, // Units: Kg | |
Wood, // Units Kg | |
Fish, // Units: Kg | |
Food, | |
} | |
const GOODS: [Good; 4] = [ | |
Good::Log, | |
Good::Wood, | |
Good::Fish, | |
Good::Food, | |
]; | |
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | |
enum Labor { | |
Lumberjack, | |
Carpenter, | |
Fisher, | |
Cook, | |
} | |
const LABORS: [Labor; 4] = [ | |
Labor::Lumberjack, | |
Labor::Carpenter, | |
Labor::Fisher, | |
Labor::Cook, | |
]; | |
impl Labor { | |
fn industry(&self) -> Industry { | |
match self { | |
Labor::Lumberjack => Industry { | |
inputs: &[], | |
outputs: &[(Good::Log, 10.0)], | |
}, | |
Labor::Carpenter => Industry { | |
inputs: &[(Good::Log, 12.0)], | |
outputs: &[(Good::Wood, 8.0)], // 1/3rd is 'wasted' (sawdust, etc.) | |
}, | |
Labor::Fisher => Industry { | |
inputs: &[], | |
outputs: &[(Good::Fish, 2.0)], | |
}, | |
Labor::Cook => Industry { | |
inputs: &[(Good::Wood, 0.1), (Good::Fish, 1.0)], | |
outputs: &[(Good::Food, 9.0)], // Some fish is wasted (gutting) | |
}, | |
} | |
} | |
} | |
struct Industry { | |
inputs: &'static [(Good, f32)], | |
outputs: &'static [(Good, f32)], | |
} | |
struct Economy { | |
// Economy population | |
pop: f32, | |
// Number of laborers allocated to each industry | |
laborers: HashMap<Labor, f32>, | |
// The relative productivity of each labor in the last tick | |
// 0.0 = At least one of the required input goods was not available | |
// 1.0 = All of the required input goods were available, sufficiently to saturate demand | |
// This is the minimum of the proportion that each input was supplied | |
productivity: HashMap<Labor, (f32, Option<Good>)>, | |
// Given current workforce allocation, how much of each good will be produced on the next tick? | |
// This is expressed as a proportion of the total required for industry. i.e: | |
// >= 1.0 => supply completely saturates industry, oversupply | |
// <= 1.0 => supply is insufficient to satisfy industry, undersupply | |
available: HashMap<Good, f32>, | |
// Labor value and consumption value are in the same units: | |
// - Labor value are the average number of labor hours required to produce 1 unit | |
// - Consumption values are the number of labor hours that workers would be willing to exchange for 1 unit | |
// During each tick, labor values are propagated forwards through the supply chain and consumption values are | |
// propagated backwards through the supply change, accounting for scarcity. | |
labor_value: HashMap<Good, f32>, | |
consumption_value: HashMap<Good, f32>, | |
// The relative value of goods. Goods that are produced optimally are at 1.0 (i.e: labor value matches consumption value). | |
// > 1.0 => production of this good should increase | |
// < 1.0 => production of this good should reduce | |
value: HashMap<Good, f32>, | |
// Total output of this good that occured in the last tick | |
output: HashMap<Good, f32>, | |
} | |
impl Economy { | |
// Calculate to what extent supply will satisfy demand for each good on the upcoming tick. See Economy::available. | |
fn derive_available_goods(&mut self) { | |
let mut total_required = HashMap::new(); | |
let mut total_supply = HashMap::new(); | |
for labor in LABORS { | |
let industry = labor.industry(); | |
let laborers = self.laborers.get(&labor).unwrap_or(&0.0); | |
// Productivity may limit goods that can be produced if inputs are undersupplied | |
// If 1.0, all industry inputs are satisfied. If 0.0, no industry inputs are satisfied. | |
let (limiting_good, productivity) = industry.inputs | |
.iter() | |
// Productivity can never be lower than 0% or higher than 100%. You can throw capital at a tree as much | |
// as you like: labor is required for economic output! | |
.map(|(good, _)| (Some(*good), self.available.get(good).unwrap_or(&0.0).max(0.0).min(1.0))) | |
.min_by_key(|(_, available)| (*available * 100000.0) as i64) // PartialOrd hack | |
.unwrap_or((None, 1.0)); | |
for &(good, input) in industry.inputs { | |
*total_required.entry(good).or_insert(0.0) += input * laborers; | |
} | |
self.productivity.insert(labor, (productivity, limiting_good)); | |
for &(good, output) in industry.outputs { | |
*total_supply.entry(good).or_insert(0.0) += output * laborers * productivity; | |
} | |
} | |
for good in GOODS { | |
let total_supply = total_supply.get(&good).unwrap_or(&0.0); | |
let total_required = total_required.get(&good).unwrap_or(&0.0); | |
if *total_required > 0.001 { | |
self.available.insert(good, total_supply / total_required); | |
} | |
} | |
} | |
// Calculate labor values for each good by propagating its value forward through the supply chain (this is the easy | |
// part). The labor value of each good is simply the sum of the labor values of its inputs, in addition to the | |
// labor time required to create a unit of the input. | |
// | |
// Because more than one industry might produce the same good, we keep a running total of labour values vs outputs | |
// so that we can normalise this value across the industries afterwards. | |
fn derive_labor_values(&mut self) { | |
let mut total_labor_values = HashMap::<Good, f32>::new(); | |
let mut total_output = HashMap::<Good, f32>::new(); | |
for labor in LABORS { | |
let industry = labor.industry(); | |
let laborers = self.laborers.get(&labor).unwrap_or(&0.0); | |
let total_input_value = industry.inputs | |
.iter() | |
.map(|(good, input)| { | |
*self.labor_value.get(good).unwrap_or(&0.0) * input | |
}) | |
.sum::<f32>(); | |
let labor_time = 1.0; | |
let productivity = self.productivity.get(&labor).unwrap_or(&(0.0, None)).0; | |
for &(good, output) in industry.outputs { | |
*total_labor_values | |
.entry(good) | |
.or_insert(0.0) += (total_input_value * productivity + labor_time) * laborers; | |
*total_output.entry(good).or_insert(0.0) += output * laborers * productivity; | |
} | |
} | |
for good in GOODS { | |
let total_labor_value = total_labor_values.get(&good).unwrap_or(&0.0); | |
let total_output = total_output.get(&good).unwrap_or(&0.0); | |
if *total_output > 0.001 { | |
self.labor_value.insert(good, total_labor_value / total_output); | |
} | |
self.output.insert(good, *total_output); | |
} | |
} | |
// Calculate the consumption value for each good. This one is a bit more complicated. For each industry, we take a | |
// look at the outputs. We sum up the consumption values of the outputs derived from the last tick to get the total | |
// output's consumption value of that industry (based on the consumption value of the last tick). Then, we | |
// distribute that consumption value to industry inputs. Obviously, we weight this based on the total outputs of | |
// each industry. | |
fn derive_consumption_values(&mut self) { | |
let mut total_consumption_values = HashMap::<Good, f32>::new(); | |
let mut total_input = HashMap::<Good, f32>::new(); | |
for labor in LABORS { | |
let industry = labor.industry(); | |
let laborers = self.laborers.get(&labor).unwrap_or(&0.0); | |
let productivity = self.productivity.get(&labor).unwrap_or(&(0.0, None)).0; | |
let total_output_value = industry.outputs | |
.iter() | |
.map(|(good, output)| { | |
*self.consumption_value.get(good).unwrap_or(&0.0) * output | |
}) | |
.sum::<f32>(); | |
for &(good, input) in industry.inputs { | |
*total_consumption_values | |
.entry(good) | |
.or_insert(0.0) += total_output_value * input * laborers * productivity / self.available.get(&good).unwrap_or(&0.0).max(0.01); | |
*total_input.entry(good).or_insert(0.0) += input * laborers * productivity; | |
} | |
} | |
for good in GOODS { | |
let total_consumption_value = total_consumption_values.get(&good).unwrap_or(&0.0); | |
let total_input = total_input.get(&good).unwrap_or(&0.0); | |
if *total_input > 0.001 { | |
self.consumption_value.insert(good, total_consumption_value / total_input); | |
} | |
} | |
// Automatically derived (TODO: use a Maslow hierarchy to derive the value of consumables) | |
// This value is "How many hours of labor a citizen would be willing to trade for 1 unit of this good". | |
// The 'value' of food increases as population increases, and decreases as food production decreases | |
self.consumption_value.insert(Good::Food, 0.1 * self.pop / self.output.get(&Good::Food).unwrap_or(&0.0).max(0.1)); | |
} | |
// Derive relative values for goods from consumption value / labour value. See Economy::value. | |
fn derive_values(&mut self) { | |
for good in GOODS { | |
let consumption_value = self.consumption_value.get(&good).unwrap_or(&0.0); | |
let labor_value = self.labor_value.get(&good).unwrap_or(&0.0); | |
if *labor_value > 0.001 { | |
self.value.insert(good, consumption_value / labor_value); | |
} | |
} | |
} | |
fn redistribute_laborers(&mut self) { | |
// Redistribute labor according to relative values of industry outputs | |
for labor in LABORS { | |
let industry = labor.industry(); | |
// Total value of industry outputs | |
let total_value = industry.outputs | |
.iter() | |
.map(|(good, output)| { | |
*self.value.get(good).unwrap_or(&1.0) * output | |
}) | |
.sum::<f32>(); | |
// Total industry output (used to normalise later, meaningless except in relation to `total_value`) | |
let total_output = industry.outputs | |
.iter() | |
.map(|(_, output)| output) | |
.sum::<f32>(); | |
// Average normalised value of outputs (remember, all values the same = pareto efficiency) | |
let avg_value = total_value / total_output; | |
// If the productivity of an industry is 0, obviously there's no point allocating laborers to it, no matter | |
// how valuable the outputs are! | |
let (productivity, limiting_good) = *self.productivity.get(&labor).unwrap_or(&(0.0, None)); | |
// What proportion of the existing workforce should move industries each tick? (at maximum) | |
let rate = 0.25; | |
let change = avg_value * productivity * rate + (1.0 - rate); | |
println!("change {:?} = {}, avg_value = {}, productivity = {}, limiting_good = {:?}", labor, change, avg_value, productivity, limiting_good); | |
*self.laborers.entry(labor).or_insert(0.0) *= change; | |
} | |
// The allocation of the workforce might now be *higher* than the working population! If this is the case, we normalise | |
// the workforce over the population to ensure realism is maintained. | |
let working_pop = self.pop * 1.0; // For now, assume everybody in the economy can work (1.0) | |
let total_laborers = self.laborers.values().sum::<f32>(); | |
if total_laborers > working_pop { | |
self.laborers.values_mut().for_each(|l| { | |
// This prevents any industry becoming completely drained of workforce, thereby inhibiting any production. | |
// Keeping a small number of laborers in every industry keeps things 'ticking over' so the economy can | |
// quickly adapt to changing conditions | |
let min_workforce_alloc = 0.01; | |
*l = (*l / total_laborers).max(min_workforce_alloc) * working_pop | |
}); | |
} | |
} | |
fn tick(&mut self) { | |
self.derive_available_goods(); | |
self.derive_labor_values(); | |
self.derive_consumption_values(); | |
self.derive_values(); | |
self.redistribute_laborers(); | |
} | |
} | |
fn main() { | |
let mut economy = Economy { | |
pop: 100.0, | |
laborers: HashMap::new(), | |
productivity: HashMap::new(), | |
available: HashMap::new(), | |
labor_value: HashMap::new(), | |
consumption_value: HashMap::new(), | |
value: HashMap::new(), | |
output: HashMap::new(), | |
}; | |
economy.laborers.insert(Labor::Lumberjack, 30.0); | |
economy.laborers.insert(Labor::Carpenter, 20.0); | |
economy.laborers.insert(Labor::Fisher, 10.0); | |
economy.laborers.insert(Labor::Cook, 30.0); | |
for i in 0..100 { | |
println!("--- Tick {} ---", i); | |
economy.tick(); | |
println!("Laborers: {:?} ({}% lazy)", economy.laborers, 100.0 * (economy.pop - economy.laborers.values().sum::<f32>()) / economy.pop); | |
println!("Available: {:?}", economy.available); | |
println!("Consumption value: {:?}", economy.consumption_value); | |
println!("Labor value: {:?}", economy.labor_value); | |
println!("Value: {:?}", economy.value); | |
println!("Total output: {:?}", economy.output); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment