The Problem
As part of my doomed efforts to learn Rust, I decided to get to grips with how the language handles classes and objects. But, instead of taking on some well-known exercise, I thought it would be more fun to try to answer a question which has been nibbling at the back of my mind since I was eleven or twelve...
What is this question? The Wikipedia page for the song "Flowers in the Window" by Scottish rock band Travis describes the video like this:
The video features the band walking around a small town where all the residents are pregnant women. The band enter a clinic where women are staying and begin to perform the song. A chained-up man in distress appears at the end of the video.
The question which has fascinated me for two decades is: Would such a community actually be feasible, from a mathematical point of view? And, if so, what is the mathematical limit on its size? My answer to this involves simulating the scenario with
The Code
main.rs
/// This is the entry point to the program. // Declare local modules. mod female; mod gynaeceum; mod utils; /// This is where the magic happens. fn main() { let mut gynaeceum = gynaeceum::Gynaeceum::new(); gynaeceum.run_sim(); } female.rs
/// This code defines the Female class. // Local imports. use crate::utils; // Local constants. const PROB_FALLING_PREGNANT_PER_COPULATION: f64 = 0.05; const PROB_FEMALE_OFFSPRING: f64 = 0.5; const LENGTH_OF_PREGNANCY_IN_DAYS: i32 = 270; const LENGTH_OF_YEAR_IN_DAYS: i32 = 365; const MIN_FERTILE_AGE_IN_DAYS: i32 = 15*LENGTH_OF_YEAR_IN_DAYS; const MAX_FERTILE_AGE_IN_DAYS: i32 = 45*LENGTH_OF_YEAR_IN_DAYS; // Helper enum. #[derive(PartialEq)] pub enum FemaleStatusCode { Normal, GivenBirth, Infertile } /// Structure. pub struct Female { id: i32, age_in_days: i32, is_pregnant: bool, days_of_pregnancy: i32 } /// Implementation. impl Female { pub fn new(id: i32) -> Female { let result = Female { id: id, age_in_days: 0, is_pregnant: false, days_of_pregnancy: 0 }; return result; } pub fn set_age(&mut self, new_age_in_days: i32) { self.age_in_days = new_age_in_days; } pub fn can_copulate(&mut self) -> bool { if self.is_pregnant || self.age_in_days < MIN_FERTILE_AGE_IN_DAYS || self.age_in_days > MAX_FERTILE_AGE_IN_DAYS { return false; } return true; } pub fn copulate(&mut self) -> bool { if self.is_pregnant { return false; } if utils::happens(PROB_FALLING_PREGNANT_PER_COPULATION) { self.is_pregnant = true; return true; } return false; } pub fn give_birth(&mut self) -> bool { self.is_pregnant = false; self.days_of_pregnancy = 0; if utils::happens(PROB_FEMALE_OFFSPRING) { return true; // I.e. has given birth to a girl. } return false; // I.e. has given birth to a boy. } pub fn tick(&mut self) -> FemaleStatusCode { self.age_in_days += 1; if self.is_pregnant { if self.days_of_pregnancy > LENGTH_OF_PREGNANCY_IN_DAYS { return FemaleStatusCode::GivenBirth; } else { self.days_of_pregnancy += 1; } } else if self.age_in_days > MAX_FERTILE_AGE_IN_DAYS { return FemaleStatusCode::Infertile; } return FemaleStatusCode::Normal; } pub fn get_id(&mut self) -> i32 { return self.id; } } gynaeceum.rs
/// This code defines a class which models "the Gynaeceum". // Standard imports. use std::collections::HashMap; // Non-standard imports. use rand::seq::SliceRandom; // Local imports. use crate::female; // Local constants. const SIM_LENGTH_IN_DAYS: i32 = 365*1000; const SIM_INITIAL_FEMALE_COUNT: i32 = 4; const FOUNDING_FEMALE_AGE_IN_DAYS: i32 = 365*20; /// Structure. pub struct Gynaeceum { duration_in_days: i32, next_ticket: i32, females: HashMap<i32, female::Female> } /// Implementation. impl Gynaeceum { pub fn new() -> Gynaeceum { let result = Gynaeceum { duration_in_days: 0, next_ticket: 1, females: HashMap::new() }; return result; } fn get_ticket(&mut self) -> i32 { let result = self.next_ticket; self.next_ticket += 1; return result; } fn add_female(&mut self) { let mut new_female = female::Female::new(self.get_ticket()); self.females.insert(new_female.get_id(), new_female); } fn process_retirees(&mut self, retiree_ids: Vec<i32>) { for id in retiree_ids { self.females.remove(&id); } } fn service(&mut self, available: Vec<i32>) -> bool { if available.len() == 0 { return false; } let selected_id = available.choose(&mut rand::thread_rng()); let selected = self.females.get_mut(selected_id.unwrap()).unwrap(); selected.copulate(); return true; } fn tick(&mut self) { let mut available = Vec::new(); let mut retiree_ids = Vec::new(); let mut birth_count = 0; for (id, female) in self.females.iter_mut() { let status_code = female.tick(); if status_code == female::FemaleStatusCode::GivenBirth { if female.give_birth() { birth_count += 1; } } else if status_code == female::FemaleStatusCode::Infertile { retiree_ids.push(*id); } else if status_code == female::FemaleStatusCode::Normal { if female.can_copulate() { available.push(*id); } } } for _ in 0..birth_count { self.add_female(); } self.process_retirees(retiree_ids); self.service(available); self.duration_in_days += 1; } pub fn run_sim(&mut self) { for _ in 0..SIM_INITIAL_FEMALE_COUNT { self.add_female(); } for (_, female) in self.females.iter_mut() { female.set_age(FOUNDING_FEMALE_AGE_IN_DAYS); } while self.duration_in_days <= SIM_LENGTH_IN_DAYS { self.tick(); } println!("Female count: {}", self.females.len()); } } utils.rs
/// This code defines some utility functions. // Standard import. use rand::Rng; /// Decide randomly whether an event with a given probability happens. pub fn happens(event_probability: f64) -> bool { if rand::thread_rng().gen_range(0.0..1.0) < event_probability { return true; } return false; } The Results
- After sufficiently many days, the population seems to level off at around 400.
- If one tinkers with the constant
PROB_FALLING_PREGNANT_PER_COPULATION, any increase or decrease will result in a directly proportional change in the population limit. - Tinkering with the other constants did not result in any appreciable change in the population limit.