/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package automenta.netention.graph; /****************************************************************************** * * * Copyright: (c) Syncleus, Inc. * * * * You may redistribute and modify this source code under the terms and * * conditions of the Open Source Community License - Type C version 1.0 * * or any later version as published by Syncleus, Inc. at www.syncleus.com. * * There should be a copy of the license included with this file. If a copy * * of the license is not included you are granted no right to distribute or * * otherwise use this file except through a legal and valid license. You * * should also contact Syncleus, Inc. at the information below if you cannot * * find a license: * * * * Syncleus, Inc. * * 2604 South 12th Street * * Philadelphia, PA 19148 * * * ******************************************************************************/ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; import com.syncleus.dann.*; import com.syncleus.dann.graph.*; import com.syncleus.dann.graph.drawing.GraphDrawer; import com.syncleus.dann.graph.drawing.hyperassociativemap.HyperassociativeMap; import com.syncleus.dann.graph.topological.Topography; import com.syncleus.dann.math.Vector; import org.apache.log4j.Logger; /** * TODO verify that anchor functionality works * @param <G> * @param <N> */ public class SeHHyperassociativeMap<G extends Graph<N, ?>, N> implements GraphDrawer<G, N> { private static final double REPULSIVE_WEAKNESS = 2.0; private static final double ATTRACTION_STRENGTH = 4.0; private static final double DEFAULT_LEARNING_RATE = 0.4; private static final double DEFAULT_MAX_MOVEMENT = 0.0; private static final double DEFAULT_TOTAL_MOVEMENT = 0.0; private static final double DEFAULT_ACCEPTABLE_DISTANCE_FACTOR = 0.75; //private static final double EQUILIBRIUM_DISTANCE = 1.0; private static final double EQUILIBRIUM_ALIGNMENT_FACTOR = 0.005; private static final double LEARNING_RATE_INCREASE_FACTOR = 0.9; private static final double LEARNING_RATE_PROCESSING_ADJUSTMENT = 1.01; private final G graph; private final int dimensions; private final ExecutorService threadExecutor; private static final Logger LOGGER = Logger.getLogger(HyperassociativeMap.class); private Map<N, Vector> coordinates = Collections.synchronizedMap(new HashMap<N, Vector>()); private static final Random RANDOM = new Random(); private final boolean useWeights; private double equilibriumDistance; private double learningRate = DEFAULT_LEARNING_RATE; private double maxMovement = DEFAULT_MAX_MOVEMENT; private double totalMovement = DEFAULT_TOTAL_MOVEMENT; private double acceptableDistanceFactor = DEFAULT_ACCEPTABLE_DISTANCE_FACTOR; private Map<N, Vector> anchors = new WeakHashMap(); // public void anchor(N n, Vec3f vf) { // Vector v = new Vector(vf.x(), vf.y(), vf.z()); // anchors.put(n, v); // } public void unAnchor(N n) { anchors.remove(n); } /** * TODO use a parameterized randomization function * @return */ private Vector newRandomPosition() { Vector v = new Vector(getDimensions()); for (int i = 1; i <= getDimensions(); i++) { v.setCoordinate(Math.random(), i); } return v; } private class Align implements Callable<Vector> { private final N node; private final List<N> nodesToProcess; public Align(final N backingNode, List<N> nodesToProcess) { this.node = backingNode; this.nodesToProcess = nodesToProcess; } @Override public Vector call() { return align(this.node, nodesToProcess); } } public SeHHyperassociativeMap(final G ourGraph, final int ourDimensions, final double ourEquilibriumDistance, final boolean shouldUseWeights, final ExecutorService ourThreadExecutor) { if (ourGraph == null) { throw new IllegalArgumentException("Graph can not be null"); } if (ourDimensions <= 0) { throw new IllegalArgumentException("ourDimensions must be 1 or more"); } this.graph = ourGraph; this.dimensions = ourDimensions; this.threadExecutor = ourThreadExecutor; this.equilibriumDistance = ourEquilibriumDistance; this.useWeights = shouldUseWeights; //refresh all nodes for (final N node : this.graph.getNodes()) { this.coordinates.put(node, randomCoordinates(this.dimensions)); } } public SeHHyperassociativeMap(final G ourGraph, final int ourDimensions, final double ourEquilibriumDistance, final boolean shouldUseWeights) { this(ourGraph, ourDimensions, ourEquilibriumDistance, shouldUseWeights, null); } @Override public G getGraph() { return this.graph; } public double getEquilibriumDistance() { return this.equilibriumDistance; } public void setEquilibriumDistance(final double newEquilbirumDistance) { this.equilibriumDistance = newEquilbirumDistance; } public void resetLearning() { this.learningRate = DEFAULT_LEARNING_RATE; this.maxMovement = DEFAULT_TOTAL_MOVEMENT; this.totalMovement = DEFAULT_TOTAL_MOVEMENT; this.acceptableDistanceFactor = DEFAULT_ACCEPTABLE_DISTANCE_FACTOR; } @Override public void reset() { this.resetLearning(); //randomize all nodes for (final N node : this.coordinates.keySet()) { this.coordinates.put(node, randomCoordinates(this.dimensions)); } } @Override public boolean isAlignable() { return true; } @Override public boolean isAligned() { if (this.isAlignable()) { return ((this.maxMovement < EQUILIBRIUM_ALIGNMENT_FACTOR * this.equilibriumDistance) && (this.maxMovement > DEFAULT_MAX_MOVEMENT)); } else { return false; } } private double getAverageMovement() { return this.totalMovement / ((double) Topography.getOrder(this.graph)); } @Override public void align() { if (graph.getNodes() == null) { return; } List<N> nodesToProcess = new LinkedList(graph.getNodes()); if (nodesToProcess.isEmpty()) { return; } //refresh all nodes if (!this.coordinates.keySet().equals(this.graph.getNodes())) { final Map<N, Vector> newCoordinates = new HashMap<N, Vector>(); for (final N node : nodesToProcess) { if (this.coordinates.containsKey(node)) { newCoordinates.put(node, this.coordinates.get(node)); } else { newCoordinates.put(node, randomCoordinates(this.dimensions)); } } this.coordinates = Collections.synchronizedMap(newCoordinates); } this.totalMovement = DEFAULT_TOTAL_MOVEMENT; this.maxMovement = DEFAULT_MAX_MOVEMENT; Vector center; if (this.threadExecutor != null) { //align all nodes in parallel final List<Future<Vector>> futures = this.submitFutureAligns(nodesToProcess); //wait for all nodes to finish aligning and calculate new sum of all the points try { center = this.waitAndProcessFutures(futures); } catch (InterruptedException caught) { LOGGER.warn("waitAndProcessFutures was unexpectidy interupted", caught); throw new UnexpectedInterruptedException("Unexpected interuption. Get should block indefinately", caught); } } else { center = this.processLocally(nodesToProcess); } LOGGER.debug("maxMove: " + this.maxMovement + ", Average Move: " + this.getAverageMovement()); //divide each coordinate of the sum of all the points by the number of //nodes in order to calculate the average point, or center of all the //points for (int dimensionIndex = 1; dimensionIndex <= this.dimensions; dimensionIndex++) { center = center.setCoordinate(center.getCoordinate(dimensionIndex) / ((double) this.graph.getNodes().size()), dimensionIndex); } this.recenterNodes(center); for (N node : anchors.keySet()) { //TODO make a Vector.set(Vector v) method getCoordinates().get(node).setCoordinate(anchors.get(node).getCoordinate(1), 1); if (getDimensions() > 1) { getCoordinates().get(node).setCoordinate(anchors.get(node).getCoordinate(2), 2); } if (getDimensions() > 2) { getCoordinates().get(node).setCoordinate(anchors.get(node).getCoordinate(3), 3); } } } @Override public int getDimensions() { return this.dimensions; } @Override public Map<N, Vector> getCoordinates() { return Collections.unmodifiableMap(this.coordinates); } private void recenterNodes(final Vector center) { if ((graph.getNodes() == null) || (this.coordinates == null)) { return; } for (final N node : new LinkedList<N>(this.graph.getNodes())) { //TODO this is hackish try { this.coordinates.put(node, this.coordinates.get(node).calculateRelativeTo(center)); } catch (NullPointerException e) { } } } public boolean isUsingWeights() { return this.useWeights; } Map<N, Double> getNeighbors(final N nodeToQuery) { final Map<N, Double> neighbors = new HashMap<N, Double>(); for (final Edge<N> neighborEdge : this.graph.getAdjacentEdges(nodeToQuery)) { final Double currentWeight = ((neighborEdge instanceof Weighted) && this.useWeights ? ((Weighted) neighborEdge).getWeight() : this.equilibriumDistance); for (final N neighbor : neighborEdge.getNodes()) { if (!neighbor.equals(nodeToQuery)) { neighbors.put(neighbor, currentWeight); } } } return neighbors; } private Vector align(final N nodeToAlign, List<N> nodesToProcess) { //calculate equilibrium with neighbors final Vector location = this.coordinates.get(nodeToAlign); final Map<N, Double> neighbors = this.getNeighbors(nodeToAlign); { Vector compositeVector = new Vector(location.getDimensions()); for (final Entry<N, Double> neighborEntry : neighbors.entrySet()) { final N neighbor = neighborEntry.getKey(); final double associationEquilibriumDistance = neighborEntry.getValue(); Vector neighborVector; //TODO this is hackish.. try { neighborVector = this.coordinates.get(neighbor).calculateRelativeTo(location); } catch (NullPointerException e) { continue; } if (Math.abs(neighborVector.getDistance()) > associationEquilibriumDistance) { double newDistance = Math.pow(Math.abs(neighborVector.getDistance()) - associationEquilibriumDistance, ATTRACTION_STRENGTH); if (Math.abs(newDistance) > Math.abs(Math.abs(neighborVector.getDistance()) - associationEquilibriumDistance)) { newDistance = Math.copySign(Math.abs(Math.abs(neighborVector.getDistance()) - associationEquilibriumDistance), newDistance); } newDistance *= this.learningRate; neighborVector = neighborVector.setDistance(Math.signum(neighborVector.getDistance()) * newDistance); } else { double newDistance = -equilibriumDistance * atanh((associationEquilibriumDistance - Math.abs(neighborVector.getDistance())) / associationEquilibriumDistance); if (Math.abs(newDistance) > Math.abs(associationEquilibriumDistance - Math.abs(neighborVector.getDistance()))) { newDistance = -equilibriumDistance * (associationEquilibriumDistance - Math.abs(neighborVector.getDistance())); } newDistance *= this.learningRate; neighborVector = neighborVector.setDistance(Math.signum(neighborVector.getDistance()) * newDistance); } compositeVector = compositeVector.add(neighborVector); } //calculate repulsion with all non-neighbors for (final N node : nodesToProcess) { //TODO this is hackish try { if ((!neighbors.containsKey(node)) && (node != nodeToAlign) && (!this.graph.getAdjacentNodes(node).contains(nodeToAlign))) { Vector nodeVector; if (coordinates.get(node) == null) { nodeVector = this.newRandomPosition(); } else { nodeVector = this.coordinates.get(node).calculateRelativeTo(location); } double newDistance = -equilibriumDistance / Math.pow(nodeVector.getDistance(), REPULSIVE_WEAKNESS); if (Math.abs(newDistance) > Math.abs(this.equilibriumDistance)) { newDistance = Math.copySign(this.equilibriumDistance, newDistance); } newDistance *= this.learningRate; nodeVector = nodeVector.setDistance(newDistance); compositeVector = compositeVector.add(nodeVector); } } catch (NullPointerException e) { } } Vector newLocation = location.add(compositeVector); final Vector oldLocation = this.coordinates.get(nodeToAlign); double moveDistance = Math.abs(newLocation.calculateRelativeTo(oldLocation).getDistance()); if (moveDistance > this.equilibriumDistance * this.acceptableDistanceFactor) { final double newLearningRate = ((this.equilibriumDistance * this.acceptableDistanceFactor) / moveDistance); if (newLearningRate < this.learningRate) { this.learningRate = newLearningRate; LOGGER.debug("learning rate: " + this.learningRate); } else { this.learningRate *= LEARNING_RATE_INCREASE_FACTOR; LOGGER.debug("learning rate: " + this.learningRate); } newLocation = oldLocation; moveDistance = DEFAULT_TOTAL_MOVEMENT; } if (moveDistance > this.maxMovement) { this.maxMovement = moveDistance; } this.totalMovement += moveDistance; if (!anchors.containsKey(nodeToAlign)) { this.coordinates.put(nodeToAlign, newLocation); } return newLocation; } } /** * Obtains a Vector with RANDOM coordinates for the specified number of * dimensions. * * @param dimensions Number of dimensions for the RANDOM Vector * @return New RANDOM Vector * @since 1.0 */ public static Vector randomCoordinates(final int dimensions) { final double[] randomCoords = new double[dimensions]; for (int randomCoordsIndex = 0; randomCoordsIndex < dimensions; randomCoordsIndex++) { randomCoords[randomCoordsIndex] = (RANDOM.nextDouble() * 2.0) - 1.0; } return new Vector(randomCoords); } private static double atanh(final double value) { final double oneHalf = 0.5; //for checkstyle. return oneHalf * Math.log(Math.abs((value + 1.0) / (1.0 - value))); } private List<Future<Vector>> submitFutureAligns(final List<N> nodesToProcess) { final ArrayList<Future<Vector>> futures = new ArrayList<Future<Vector>>(); for (final N node : this.graph.getNodes()) { futures.add(this.threadExecutor.submit(new Align(node, nodesToProcess))); } return futures; } private Vector processLocally(List<N> nodesToProcess) { Vector pointSum = new Vector(this.dimensions); //List<N> nodesToProcess = new LinkedList(graph.getNodes()) for (final N node : nodesToProcess) { Vector newPoint = this.align(node, nodesToProcess); for (int dimensionIndex = 1; dimensionIndex <= this.dimensions; dimensionIndex++) { pointSum = pointSum.setCoordinate(pointSum.getCoordinate(dimensionIndex) + newPoint.getCoordinate(dimensionIndex), dimensionIndex); } } if (this.learningRate * LEARNING_RATE_PROCESSING_ADJUSTMENT < DEFAULT_LEARNING_RATE) { final double acceptableDistanceAdjustment = 0.1; if (this.getAverageMovement() < (this.equilibriumDistance * this.acceptableDistanceFactor * acceptableDistanceAdjustment)) { this.acceptableDistanceFactor *= LEARNING_RATE_INCREASE_FACTOR; } this.learningRate *= LEARNING_RATE_PROCESSING_ADJUSTMENT; LOGGER.debug("learning rate: " + this.learningRate + ", acceptableDistanceFactor: " + this.acceptableDistanceFactor); } return pointSum; } private Vector waitAndProcessFutures(final List<Future<Vector>> futures) throws InterruptedException { //wait for all nodes to finish aligning and calculate new center point Vector pointSum = new Vector(this.dimensions); try { for (final Future<Vector> future : futures) { final Vector newPoint = future.get(); for (int dimensionIndex = 1; dimensionIndex <= this.dimensions; dimensionIndex++) { pointSum = pointSum.setCoordinate(pointSum.getCoordinate(dimensionIndex) + newPoint.getCoordinate(dimensionIndex), dimensionIndex); } } } catch (ExecutionException caught) { LOGGER.error("Align had an unexpected problem executing.", caught); throw new UnexpectedDannError("Unexpected execution exception. Get should block indefinitely", caught); } if (this.learningRate * LEARNING_RATE_PROCESSING_ADJUSTMENT < DEFAULT_LEARNING_RATE) { final double acceptableDistanceAdjustment = 0.1; if (this.getAverageMovement() < (this.equilibriumDistance * this.acceptableDistanceFactor * acceptableDistanceAdjustment)) { this.acceptableDistanceFactor = this.maxMovement * 2.0; } this.learningRate *= LEARNING_RATE_PROCESSING_ADJUSTMENT; LOGGER.debug("learning rate: " + this.learningRate + ", acceptableDistanceFactor: " + this.acceptableDistanceFactor); } return pointSum; } public void randomize(double uniformDimensionLength) { List<N> l = new LinkedList<N>(getCoordinates().keySet()); for (N n : l) { Vector v = (Vector) getCoordinates().get(n); for (int i = 1; i <= v.getDimensions(); i++) { double c = (Math.random() * 2.0 - 1.0) * uniformDimensionLength; v.setCoordinate(c, i); } } } }