package swen40004.jordan.steele.rumourmill;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import au.com.bytecode.opencsv.CSVWriter;
/**
* The primary class from where the model is run.
* @author jordansteele
*
*/
public class RumourMill {
private World world; //the 'world' in which people and walls are contained
private Neighbourhood neighbourhood; //the neighbourhood method in use
private boolean wrapAround; //whether the world should wrap around or not
private boolean rediscussRumour; //whether two people who have discussed the
//rumour together can rediscuss it or not
private String outputFolder; //the folder where data should be output
//Defines the potential offsets in a vonNeumann neighbourhood
private final Coord[] vonNeumannNeighbourhood = {
new Coord(0,-1),
new Coord(-1,0), new Coord(1,0),
new Coord(0,1) };
//Defines the potential offsets in a moore neighbourhood
private final Coord[] mooreNeighbourhood = {
new Coord(-1,-1), new Coord(0,-1), new Coord(1,-1),
new Coord(-1,0), new Coord(1,0),
new Coord(-1,1), new Coord(0,1), new Coord(1,1) };
//An enum defining the two possible neighbourhoods
public static enum Neighbourhood {
VONNEUMANN,
MOORE
}
/**
* Constructor
* @param wrapAround Whether the world should wrap around or not
* @param xdimen The size of the world in the x dimension
* @param ydimen The size of the world in the y dimension
* @param rumourSources The coordinates of the people who will know the
* rumour at time 0
* @param walls The coordinates of all the 'walls'
* @param neighbourhood The method that should be used for the neighbourhood
* @param rumourAbout The coordinate of who the rumour is about. This can
* be null.
* @param rediscussRumour Whether two people who have discussed the rumour
* together can rediscuss it or not
* @param outputFolder The folder where data should be output
*/
public RumourMill(boolean wrapAround, int xdimen, int ydimen,
Coord[] rumourSources, Coord[] walls,
Neighbourhood neighbourhood, Coord rumourAbout,
boolean rediscussRumour, String outputFolder) {
this.world = new World(xdimen, ydimen, rumourSources, rumourAbout);
this.neighbourhood = neighbourhood;
this.wrapAround = wrapAround;
this.rediscussRumour = rediscussRumour;
this.outputFolder = outputFolder;
}
/**
* Constructor
* @param wrapAround Whether the world should wrap around or not
* @param xdimen The size of the world in the x dimension
* @param ydimen The size of the world in the y dimension
* @param intialClique Fraction of people who initially know the rumour
* @param walls The coordinates of all the 'walls'
* @param neighbourhood The method that should be used for the neighbourhood
* @param rumourAbout The coordinate of who the rumour is about. This can
* be null.
* @param rediscussRumour Whether two people who have discussed the rumour
* together can rediscuss it or not
* @param outputFolder The folder where data should be output
*/
public RumourMill(boolean wrapAround, int xdimen, int ydimen,
float initialClique, Coord[] walls,
Neighbourhood neighbourhood, Coord rumourAbout,
boolean rediscussRumour, String outputFolder) {
this.world = new World(xdimen, ydimen, initialClique, rumourAbout);
this.neighbourhood = neighbourhood;
this.wrapAround = wrapAround;
this.rediscussRumour = rediscussRumour;
this.outputFolder = outputFolder;
}
/**
* Runs the model for N iterations and the averages the results
* @param iterations The number of times the model should be run for
*/
public void runModel(int iterations) {
//LinkedHashMap provides in order iterating
//Stores stats for each tick in the simulation
LinkedHashMap<Integer, RunningData> runningDataAggregates =
new LinkedHashMap<Integer, RunningData>();
//Stores stats for each person at the end of the simulation
HashMap<Coord, Person> finalPersonDataAggregates =
new HashMap<Coord, Person>();
//Stores who has discussed the rumour together
Graph<Person> discussedRumourGraph;
int ticks;
int prevClique;
//run for n iterations
for (int n=0; n < iterations; n++) {
//Set the ticks to 0 and create a new graph
ticks = 0;
discussedRumourGraph = new Graph<Person>();
//run the simulation and stop when everyone has heard the rumour
while (this.world.getClique() < this.world.size()) {
ticks++;
prevClique = this.world.getClique();
//For each person in the world, if they have heard the rumour
//pass on the rumour with their chance to pass on the rumour
for (int i=0; i < this.world.sizeX(); i++) {
for (int j=0; j < this.world.sizeY(); j++) {
Cell currentCell = this.world.getCell(i, j);
if (currentCell instanceof Person) {
Person currentPerson = (Person) currentCell;
if (currentPerson.getTimesHeard() > 0 &&
currentPerson.getChanceToPassOnRumour() >=
new Random().nextFloat()) {
Person neighbour = determineNeighbour(i, j,
discussedRumourGraph);
if (neighbour != null) {
discussedRumourGraph.addRelationship(
currentPerson, neighbour);
neighbour.setJustHeardFrom(currentPerson);
}
}
}
}
}
//Update the world to reflect who just heard the rumour
this.world.update(ticks);
//Update our running stats with the stats for this tick
runningDataAggregates = updateRunningData(runningDataAggregates,
ticks, prevClique);
}
//After the simulation is run, record the final person stats
finalPersonDataAggregates = updateFinalPersonData(
finalPersonDataAggregates);
//Reset the world and inform user of the progress
this.world.resetWorld();
System.out.printf(Strings.COMPLETED_RUN, n + 1);
}
//Write the running and final person stats to a csv file
writeRunningDataAggregatesAsAverageToCSV(runningDataAggregates);
writeFinalPersonDataAggregatesAsAverageToCSV(finalPersonDataAggregates,
iterations);
}
/**
* Calculates the possible neighbours to a cell and chooses one to return
* @param currXDimen The xDimen position of the cell to find a neighbour of
* @param currYDimen The yDimen position of the cell to find a neighbour of
* @param discussedRumourGraph The graph of which users have discussed the
* rumour together
* @return A neighbour allowed with the current settings of the model
* or null if no possible neighbours
*/
private Person determineNeighbour(int currXDimen, int currYDimen,
Graph<Person> discussedRumourGraph) {
Person currentPerson = (Person)this.world.getCell(currXDimen,
currYDimen);
//Determine neighbourhood to use
Coord[] neighbourhood = this.vonNeumannNeighbourhood;
if (this.neighbourhood == Neighbourhood.MOORE) {
neighbourhood = this.mooreNeighbourhood;
}
//Intiate list possible neighbours to return
ArrayList<Person> personNeighbours = new ArrayList<Person>();
//For each neighbour in the neighbourhood
for (Coord neighbourOffset : neighbourhood) {
//Determine position of the neighbour
int neighbourXDimen = currXDimen + neighbourOffset.x;
int neighbourYDimen = currYDimen + neighbourOffset.y;
//if using wrap around adjust if outside world
if (this.wrapAround) {
if (neighbourXDimen >= this.world.sizeX()) {
neighbourXDimen = 0;
}
if (neighbourYDimen >= this.world.sizeY()) {
neighbourYDimen = 0;
}
if (neighbourXDimen < 0) {
neighbourXDimen = this.world.sizeX() - 1;
}
if (neighbourYDimen < 0) {
neighbourYDimen = this.world.sizeY() - 1;
}
}
//Try to add to possible neighbours if using wrap around or
//inside world and cell is a person
if (this.wrapAround || (neighbourXDimen < this.world.sizeX() &&
neighbourYDimen < this.world.sizeY() &&
neighbourXDimen >= 0 && neighbourYDimen >= 0)) {
Cell neighbour = this.world.getCell(neighbourXDimen,
neighbourYDimen);
if (neighbour instanceof Person) {
Person neighbourPerson = (Person) neighbour;
//add to possible neighbours if allowed to rediscuss rumours
//or have not discussed the rumour with this person
if (this.rediscussRumour ||
!discussedRumourGraph.getNeighbours(currentPerson).
contains(neighbourPerson)) {
personNeighbours.add(neighbourPerson);
}
}
}
}
//Choose a random neighbour from the possible neighbours and return it
if (personNeighbours.size() != 0){
return personNeighbours.get(
new Random().nextInt(personNeighbours.size()));
}
//if no possible neighbours, return null
return null;
}
/**
* Updates the aggregate stats being stored for each tick
* @param runningDataAggregates The current stats
* @param ticks The current ticks
* @param prevClique The size of the clique in the previous tick
* @param peopleRehearingRumour The amount of people who reheard the rumour
* @return The updated running data aggregate stats
*/
private LinkedHashMap<Integer, RunningData> updateRunningData(
LinkedHashMap<Integer, RunningData> runningDataAggregates,
int ticks, int prevClique) {
//Get the data for the current tick or init the data if no data for the
//current tick
RunningData currentTickData = runningDataAggregates.get(ticks);
if (currentTickData == null) {
currentTickData = runningDataAggregates.put(ticks,
new RunningData(0, 0, 0));
currentTickData = runningDataAggregates.get(ticks);
}
//Calculate the change in the clique
int deltaClique = (this.world.getClique() - prevClique);
//Update the stats for this tick
currentTickData.setClique(currentTickData.getClique() +
this.world.getClique());
currentTickData.setDeltaClique(currentTickData.getDeltaClique()
+ deltaClique);
currentTickData.setSuccessiveRatio(currentTickData.getSuccessiveRatio()
+ this.world.getClique() / (float)prevClique);
currentTickData.setValuesAddedTogether(
currentTickData.getValuesAddedTogether() + 1);
return runningDataAggregates;
}
/**
* Updates the aggregate stats being stored for each person
* @param finalPersonDataAggregates The current stats
* @return The updated aggregate stats for each person
*/
private HashMap<Coord, Person> updateFinalPersonData(HashMap<Coord,
Person> finalPersonDataAggregates) {
//For each Person
for (int i=0; i < this.world.sizeX(); i++) {
for (int j=0; j < this.world.sizeY(); j++) {
Cell currentCell = this.world.getCell(i, j);
if (currentCell instanceof Person) {
//Get current person for the simulation just run
Person currentPerson = (Person) currentCell;
//Get current aggregate person
Coord currentCoord = new Coord(i, j);
Person currentAggregatePerson =
finalPersonDataAggregates.get(currentCoord);
if (currentAggregatePerson == null) {
currentAggregatePerson =
finalPersonDataAggregates.put(currentCoord,
new Person());
currentAggregatePerson =
finalPersonDataAggregates.get(currentCoord);
}
//Update aggregate stats by adding the current aggregate
//and the result after the current simulation
currentAggregatePerson.setTimesHeard(
currentAggregatePerson.getTimesHeard() +
currentPerson.getTimesHeard());
currentAggregatePerson.setFirstTimeHeard(
currentAggregatePerson.getFirstTimeHeard() +
currentPerson.getFirstTimeHeard());
currentAggregatePerson.setKnowledgeOfRumour(
currentAggregatePerson.getKnowledgeOfRumour() +
currentPerson.getKnowledgeOfRumour());
}
}
}
return finalPersonDataAggregates;
}
/**
* Writes the running data averages for each tick and
* in order of ticks to csv
* @param runningDataAggregates The aggregate data
*/
private void writeRunningDataAggregatesAsAverageToCSV(LinkedHashMap<Integer,
RunningData> runningDataAggregates) {
//Open the csv file and write the header
CSVWriter runningDataWriter = CSVWrapper.openCSV("../" +
this.outputFolder + Strings.RUNNING_DATA_FILENAME);
runningDataWriter.writeNext(new String[] {"Ticks", "Clique",
"DeltaClique", "SuccessiveRatio"});
//Average the stats and write them out
for (Map.Entry<Integer, RunningData> entry :
runningDataAggregates.entrySet()) {
runningDataWriter.writeNext(new String[] {"" + entry.getKey(),
"" + entry.getValue().clique /
entry.getValue().getValuesAddedTogether(),
"" + entry.getValue().deltaClique /
entry.getValue().getValuesAddedTogether(),
"" + entry.getValue().successiveRatio /
entry.getValue().getValuesAddedTogether()});
}
//Close the csv
CSVWrapper.closeCSV(runningDataWriter);
}
/**
* Writes the average stats for each person to csv
* @param finalPersonDataAggregates The aggregate data
* @param iterations The number of iterations aggregated
*/
private void writeFinalPersonDataAggregatesAsAverageToCSV(HashMap<Coord,
Person> finalPersonDataAggregates, int iterations) {
//Open the csv file and write the header
CSVWriter personDetailsWriter = CSVWrapper.openCSV("../" +
this.outputFolder + Strings.FINAL_PERSON_DATA_FILENAME);
personDetailsWriter.writeNext(new String[] {"TimesHeard",
"FirstTimeHeard", "KnowledgeOfRumour"});
//Average the stats and write them out
for (Map.Entry<Coord, Person> entry :
finalPersonDataAggregates.entrySet()) {
personDetailsWriter.writeNext(new String[] {
"" +
entry.getValue().getTimesHeard() / (float)iterations,
""
+ entry.getValue().getFirstTimeHeard() / (float)iterations,
"" +
entry.getValue().getKnowledgeOfRumour()
/ (float)iterations});
}
//Close the csv
CSVWrapper.closeCSV(personDetailsWriter);
}
/**
* Inner class to store the running data
* @author jordansteele
*
*/
private class RunningData {
private float clique; //the amount of people in the clique
private float deltaClique; //the change in the clique
private float successiveRatio; //the ratio of people first hearing
//to rehearing
private int valuesAddedTogether; //how many values have been aggregated
/**
* Constructor
* @param clique Initial size of clique
* @param deltaClique Initial change in clique
* @param successiveRatio //Initial ratio
*/
public RunningData (float clique, float deltaClique,
float successiveRatio) {
this.clique = clique;
this.deltaClique = deltaClique;
this.successiveRatio = successiveRatio;
this.valuesAddedTogether = 0;
}
/**
* Getter for clique
* @return The value for clique
*/
public float getClique() {
return clique;
}
/**
* Getter for deltaClique
* @return The value for deltaClique
*/
public float getDeltaClique() {
return deltaClique;
}
/**
* Getter for successiveRatio
* @return The value for successiveRatio
*/
public float getSuccessiveRatio() {
return successiveRatio;
}
/**
* Getter for valuesAddedTogether
* @return The value for valuesAddedTogether
*/
public int getValuesAddedTogether() {
return valuesAddedTogether;
}
/**
* Setter for clique
* @param clique The value to set clique to
*/
public void setClique(float clique) {
this.clique = clique;
}
/**
* Setter for deltaClique
* @param deltaClique The value to set deltaClique to
*/
public void setDeltaClique(float deltaClique) {
this.deltaClique = deltaClique;
}
/**
* Setter for successiveRatio
* @param successiveRatio The value to set successiveRatio to
*/
public void setSuccessiveRatio(float successiveRatio) {
this.successiveRatio = successiveRatio;
}
/**
* Setter for valuesAddedTogether
* @param valuesAddedTogether The value to set valuesAddedTogether to
*/
public void setValuesAddedTogether(int valuesAddedTogether) {
this.valuesAddedTogether = valuesAddedTogether;
}
}
}