
package edu.uci.ics.jung.algorithms.generators.random;

/*
* Copyright (c) 2009, the JUNG Project and the Regents of the University 
* of California
* All rights reserved.
*
* This software is open-source under the BSD license; see either
* "license.txt" or
* http://jung.sourceforge.net/license.txt for a description.
*/

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import org.apache.commons.collections15.Factory;

import edu.uci.ics.jung.algorithms.generators.Lattice2DGenerator;
import edu.uci.ics.jung.algorithms.util.WeightedChoice;
import edu.uci.ics.jung.graph.Graph;

/**
 * Graph generator that produces a random graph with small world properties. The
 * underlying model is an mxn (optionally toroidal) lattice. Each node u has
 * four local connections, one to each of its neighbors, and in addition 1+ long
 * range connections to some node v where v is chosen randomly according to
 * probability proportional to d^-alpha where d is the lattice distance between
 * u and v and alpha is the clustering exponent.
 * 
 * @see "Navigation in a small world J. Kleinberg, Nature 406(2000), 845."
 * @author Joshua O'Madadhain
 */
public class KleinbergSmallWorldGenerator<V, E>
		extends Lattice2DGenerator<V, E> {
	private double clustering_exponent;
	private Random random;
	private int num_connections = 1;

	/**
	 * Creates
	 * 
	 * @param graph_factory
	 * @param vertex_factory
	 * @param edge_factory
	 * @param latticeSize
	 * @param clusteringExponent
	 */
	public KleinbergSmallWorldGenerator(
			Factory<? extends Graph<V, E>> graph_factory,
			Factory<V> vertex_factory, Factory<E> edge_factory, int latticeSize,
			double clusteringExponent) {
		this(graph_factory, vertex_factory, edge_factory, latticeSize,
				latticeSize, clusteringExponent);
	}

	/**
	 * @param graph_factory
	 * @param vertex_factory
	 * @param edge_factory
	 * @param row_count
	 * @param col_count
	 * @param clusteringExponent
	 */
	public KleinbergSmallWorldGenerator(
			Factory<? extends Graph<V, E>> graph_factory,
			Factory<V> vertex_factory, Factory<E> edge_factory, int row_count,
			int col_count, double clusteringExponent) {
		super(graph_factory, vertex_factory, edge_factory, row_count, col_count,
				true);
		clustering_exponent = clusteringExponent;
		initialize();
	}

	/**
	 * @param graph_factory
	 * @param vertex_factory
	 * @param edge_factory
	 * @param row_count
	 * @param col_count
	 * @param clusteringExponent
	 * @param isToroidal
	 */
	public KleinbergSmallWorldGenerator(
			Factory<? extends Graph<V, E>> graph_factory,
			Factory<V> vertex_factory, Factory<E> edge_factory, int row_count,
			int col_count, double clusteringExponent, boolean isToroidal) {
		super(graph_factory, vertex_factory, edge_factory, row_count, col_count,
				isToroidal);
		clustering_exponent = clusteringExponent;
		initialize();
	}

	private void initialize() {
		this.random = new Random();
	}

	/**
	 * Sets the {@code Random} instance used by this instance. Useful for unit
	 * testing.
	 */
	public void setRandom(Random random) {
		this.random = random;
	}

	/**
	 * Sets the seed of the internal random number generator. May be used to
	 * provide repeatable experiments.
	 */
	public void setRandomSeed(long seed) {
		random.setSeed(seed);
	}

	/**
	 * Sets the number of new 'small-world' connections (outgoing edges) to be
	 * added to each vertex.
	 */
	public void setConnectionCount(int num_connections) {
		if (num_connections <= 0) {
			throw new IllegalArgumentException(
					"Number of new connections per vertex must be >= 1");
		}
		this.num_connections = num_connections;
	}

	/**
	 * Returns the number of new 'small-world' connections to be made to each
	 * vertex.
	 */
	public int getConnectionCount() {
		return this.num_connections;
	}

	/**
	 * Generates a random small world network according to the parameters given
	 * 
	 * @return a random small world graph
	 */
	@Override
	public Graph<V, E> create() {
		Graph<V, E> graph = super.create();

		// TODO: For toroidal graphs, we can make this more clever by
		// pre-creating the WeightedChoice object
		// and using the output as an offset to the current vertex location.
		WeightedChoice<V> weighted_choice;

		// Add long range connections
		for (int i = 0; i < graph.getVertexCount(); i++) {
			V source = getVertex(i);
			int row = getRow(i);
			int col = getCol(i);
			int row_offset = row < row_count / 2 ? -row_count : row_count;
			int col_offset = col < col_count / 2 ? -col_count : col_count;

			Map<V, Float> vertex_weights = new HashMap<V, Float>();
			for (int j = 0; j < row_count; j++) {
				for (int k = 0; k < col_count; k++) {
					if (j == row && k == col) {
						continue;
					}
					int v_dist = Math.abs(j - row);
					int h_dist = Math.abs(k - col);
					if (is_toroidal) {
						v_dist = Math.min(v_dist,
								Math.abs(j - row + row_offset));
						h_dist = Math.min(h_dist,
								Math.abs(k - col + col_offset));
					}
					int distance = v_dist + h_dist;
					if (distance < 2) {
						continue;
					}
					vertex_weights.put(getVertex(j, k),
							(float) Math.pow(distance, -clustering_exponent));
				}
			}

			for (int j = 0; j < this.num_connections; j++) {
				weighted_choice = new WeightedChoice<V>(vertex_weights, random);
				V target = weighted_choice.nextItem();
				graph.addEdge(edge_factory.create(), source, target);
			}
		}

		return graph;
	}
}
