Skip to content

Instantly share code, notes, and snippets.

@zesterer
Last active August 8, 2021 21:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zesterer/d3160490a1e094c5b511570bdd2438df to your computer and use it in GitHub Desktop.
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
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