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.