/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.tuwien.ifs.somtoolbox.visualization;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.commons.math.util.MathUtils;
import at.tuwien.ifs.somtoolbox.SOMToolboxException;
import at.tuwien.ifs.somtoolbox.data.InputData;
import at.tuwien.ifs.somtoolbox.data.SOMVisualisationData;
import at.tuwien.ifs.somtoolbox.data.distance.InputVectorDistanceMatrix;
import at.tuwien.ifs.somtoolbox.data.distance.LeightWeightMemoryInputVectorDistanceMatrix;
import at.tuwien.ifs.somtoolbox.layers.Unit;
import at.tuwien.ifs.somtoolbox.layers.UnitPair;
import at.tuwien.ifs.somtoolbox.layers.metrics.DistanceMetric;
import at.tuwien.ifs.somtoolbox.models.GrowingSOM;
import at.tuwien.ifs.somtoolbox.util.DateUtils;
import at.tuwien.ifs.somtoolbox.util.StdErrProgressWriter;
import at.tuwien.ifs.somtoolbox.util.VisualisationUtils;
/**
* This visualisation provides two visualization plugin-ins for neighbourhood graphs. The first one uses knn-based
* distances, the second one radius-based distances.<br>
* Described in:<br>
* <i><b>Georg Poelzlbauer, Andreas Rauber, and Michael Dittenbach</b>. <a
* href="http://www.ifs.tuwien.ac.at/~poelzlbauer/publications/Poe05ISNN.pdf"> Advanced visualization techniques for
* self-organizing maps with graph-based methods.</a> In Jun Wang, Xiaofeng Liao, Zhang Yi, editors, Proceedings of the
* Second International Symposium on Neural Networks (ISNN'05), pages 75-80, Chongqing, China, May 30 - June 1 2005.
* Springer-Verlag. </i>
*
* @author Stefan Ruemmele
* @author Christian Kapeller
* @author Frank Pourvoyeur
* @author Rudolf Mayer
* @version $Id: NeighbourhoodGraph.java 3883 2010-11-02 17:13:23Z frank $
*/
public class NeighbourhoodGraph extends AbstractBackgroundImageVisualizer {
/* range of possible k-values */
private static final int MIN_K = 1;
private static final int MAX_K = 30;
/* range of possible radius-values */
private static final double MIN_RADIUS = 0.1;
private static final double MAX_RADIUS = 10.0;
/** number of neighbours for knn-based distances */
private int k;
/** radius for radius-based distances */
private double radius;
/** control panel for this plug-in */
private NeighbourhoodControlPanel neighbourhoodPanel;
private int currentVisualization;
private InputData inputData;
private InputVectorDistanceMatrix distanceMatrix;
private DistanceMetric metric;
/** caches the results of knn-based connections */
@SuppressWarnings("unchecked")
private ArrayList<UnitPair>[] knnLinesCache = new ArrayList[MAX_K - MIN_K + 1];
/** caches the results of radius-based connections */
private Hashtable<Double, ArrayList<UnitPair>> radiusLinesCache = new Hashtable<Double, ArrayList<UnitPair>>();
int numVectors;
/**
* Constructor.
*/
public NeighbourhoodGraph() {
NUM_VISUALIZATIONS = 2;
VISUALIZATION_NAMES = new String[] { "Neighbourhood Graph: k-nn", "Neighbourhood Graph: Radius" };
VISUALIZATION_SHORT_NAMES = new String[] { "NeighbourhoodKnn", "NeighbourhoodRadius" };
VISUALIZATION_DESCRIPTIONS = new String[] {
"Implementation of Neighbourhood graph using knn-based distances, as described in \""
+ "G. Poelzlbauer, A. Rauber, M. Dittenbach. \n Advanced "
+ "visualization of Self-Organizing Maps with vector fields.\n"
+ "In Jun Wang, Xiaofeng Liao, Zhang Yi, editors, Proceedings of the Second International Symposium on Neural Networks (ISNN'05), pages 75-80, Chongqing, China, May 30 - June 1 2005. Springer-Verlag.\"",
"Neighbourhood graph using radius-based distances" };
neededInputObjects = new String[] { SOMVisualisationData.INPUT_VECTOR_DISTANCE_MATRIX,
SOMVisualisationData.INPUT_VECTOR };
k = MIN_K;
radius = MIN_RADIUS;
neighbourhoodPanel = new NeighbourhoodControlPanel();
controlPanel = neighbourhoodPanel;
}
@Override
public BufferedImage createVisualization(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
if (gsom.getSharedInputObjects().getData(neededInputObjects[0]) == null
&& gsom.getSharedInputObjects().getData(neededInputObjects[1]) == null) {
throw new SOMToolboxException("You need to specify at least one out of " + neededInputObjects[0] + " or "
+ neededInputObjects[1]);
}
if (inputData == null) {
inputData = gsom.getSharedInputObjects().getInputData();
metric = gsom.getLayer().getMetric();
numVectors = inputData.numVectors();
distanceMatrix = gsom.getSharedInputObjects().getInputVectorDistanceMatrix();
if (distanceMatrix == null) {
distanceMatrix = new LeightWeightMemoryInputVectorDistanceMatrix(inputData, metric);
}
}
BufferedImage res;
Graphics2D g;
ArrayList<UnitPair> lines;
if (index < 0 || index > 1) {
return null;
}
currentVisualization = index;
if (index == 0) {
neighbourhoodPanel.label.setText("KNN Control" + ": ");
neighbourhoodPanel.spinner.setModel(new SpinnerNumberModel(k, MIN_K, MAX_K, MIN_K));
lines = createKNNBased(gsom, width, height);
} else {
neighbourhoodPanel.label.setText("Radius Control" + ": ");
neighbourhoodPanel.spinner.setModel(new SpinnerNumberModel(radius, MIN_RADIUS, MAX_RADIUS, MIN_RADIUS));
lines = createRadiusBased(gsom, width, height);
}
neighbourhoodPanel.panel.revalidate();
// create empty image
res = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
g = (Graphics2D) res.getGraphics();
g.setColor(Color.WHITE);
g.setPaint(Color.WHITE);
g.fillRect(0, 0, width, height);
int unitWidth = width / gsom.getLayer().getXSize();
int unitHeight = height / gsom.getLayer().getYSize();
g.setColor(Color.RED);
g.setStroke(new BasicStroke(0.3f));
// connect the units of a pair with a line
for (UnitPair pair : lines) {
VisualisationUtils.drawThickLine(g, pair.getFirst(), pair.getSecond(), unitWidth, unitHeight);
}
return res;
}
/**
* Returns a list of unit-pairs, for which at least one of the two units is one of the k-nearest neighbours of the
* other one.
*/
private ArrayList<UnitPair> createKNNBased(GrowingSOM gsom, int width, int height) throws SOMToolboxException {
if (knnLinesCache[k - 1] == null) {
long start = System.currentTimeMillis();
Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Starting calculation of knn = " + k);
ArrayList<UnitPair> lines = new ArrayList<UnitPair>();
if (inputData == null) {
throw new SOMToolboxException("You need to specify the " + neededInputObjects[0]);
}
StdErrProgressWriter progress = new StdErrProgressWriter(numVectors,
"Calculating nearest neighbours for vector ", numVectors / 10);
// iterate through all vectors
for (int i = 0; i < numVectors; i++) {
// the unit of the current vector
Unit firstUnit = gsom.getLayer().getWinner(inputData.getInputDatum(i));
int[] nearest = distanceMatrix.getNNearest(i, k);
for (int element : nearest) {
if (element < 0) {
continue;
}
// create a pair out of the two units
Unit secondUnit = gsom.getLayer().getWinner(inputData.getInputDatum(element));
if (!firstUnit.equals(secondUnit)) {
UnitPair pair = new UnitPair(firstUnit, secondUnit);
// the list contains only one entry for each pair
if (!lines.contains(pair)) {
lines.add(pair);
}
}
}
progress.progress(i + 1);
}
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Finished calculation of knn = " + k + ", "
+ DateUtils.formatDurationOneUnit(System.currentTimeMillis() - start));
knnLinesCache[k - 1] = lines;
}
return knnLinesCache[k - 1];
}
/**
* Returns a list of unit-pairs, for which the distance of at least one of them to the other one is smaller than the
* fixed radius.
*/
private ArrayList<UnitPair> createRadiusBased(GrowingSOM gsom, int width, int height) throws SOMToolboxException {
if (radiusLinesCache.get(new Double(radius)) == null) {
if (inputData == null) {
throw new SOMToolboxException("You need to specify the " + neededInputObjects[0]);
}
long start = System.currentTimeMillis();
Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Starting calculation of radius = " + radius);
ArrayList<UnitPair> lines = new ArrayList<UnitPair>();
StdErrProgressWriter progress = new StdErrProgressWriter(numVectors,
"Calculating nearest neighbours for vector ", numVectors / 10);
// iterate through all vectors
for (int i = 0; i < numVectors; i++) {
progress.progress(i);
// the unit of the current vector
Unit firstUnit = gsom.getLayer().getWinner(inputData.getInputDatum(i));
// find the units with a distance smaller than radius
for (int j = 0; j < numVectors; j++) {
if (distanceMatrix.getDistance(i, j) < radius) {
// create a pair out of the two units
Unit secondUnit = gsom.getLayer().getWinner(inputData.getInputDatum(j));
if (!firstUnit.equals(secondUnit)) {
UnitPair pair = new UnitPair(firstUnit, secondUnit);
// the list contains only one entry for each pair
if (!lines.contains(pair)) {
lines.add(pair);
}
}
}
}
}
radiusLinesCache.put(new Double(radius), lines);
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Finished calculation of radius = " + radius + ", "
+ DateUtils.formatDurationOneUnit(System.currentTimeMillis() - start));
}
return radiusLinesCache.get(new Double(radius));
}
/** The control panel for the two plug-ins, containing a JSpinner. */
private class NeighbourhoodControlPanel extends VisualizationControlPanel {
private static final long serialVersionUID = 1L;
private JPanel panel;
private JLabel label;
private JSpinner spinner;
private NeighbourhoodControlPanel() {
super("Neighbourhood Control");
spinner = new JSpinner(new SpinnerNumberModel(0, 0, 1, 1));
spinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent ev) {
if (currentVisualization == 0) {
// Workaround for spinner.setMaxValue()
int newK = ((Number) neighbourhoodPanel.spinner.getValue()).intValue();
if (newK == k) {
return;
}
int maxK = distanceMatrix.columns();
if (newK > maxK) {
neighbourhoodPanel.spinner.getModel().setValue(maxK);
return;
}
k = ((Number) neighbourhoodPanel.spinner.getValue()).intValue();
} else {
radius = ((Number) neighbourhoodPanel.spinner.getValue()).doubleValue();
}
if (visualizationUpdateListener != null) {
visualizationUpdateListener.updateVisualization();
}
}
});
label = new JLabel("Control: ");
panel = new JPanel();
panel.add(label);
panel.add(spinner);
add(panel, c);
}
}
@Override
protected String getCacheKey(GrowingSOM gsom, int index, int width, int height) {
if (index == 0) {
return super.appendToCacheKey(gsom, index, width, height, "key:" + k);
} else {
return super.appendToCacheKey(gsom, index, width, height, "radius:" + radius);
}
}
public HashMap<String, BufferedImage> getVisualizationFlavours_K(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
// K
HashMap<String, BufferedImage> res = new HashMap<String, BufferedImage>();
int currentK = k;
for (int i = MIN_K; i <= MAX_K; i++) {
k = i;
res.put("_k" + i, getVisualization(index, gsom, width, height));
}
k = currentK;
return res;
}
public HashMap<String, BufferedImage> getVisualizationFlavours_R(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
// R
HashMap<String, BufferedImage> res = new HashMap<String, BufferedImage>();
double currentR = radius;
for (double i = MIN_RADIUS; MathUtils.round(i, 1) <= MAX_RADIUS;) {
radius = i;
res.put(String.format("_radius%.2f", i), getVisualization(index, gsom, width, height));
if (MathUtils.round(i, 1) < 5.0) {
i += 0.1;
} else {
i += 1;
}
}
radius = currentR;
return res;
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
if (index == 0) {
return getVisualizationFlavours_K(index, gsom, width, height);
} else {
return getVisualizationFlavours_R(index, gsom, width, height);
}
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height,
int maxFlavours) throws SOMToolboxException {
Logger.getLogger(this.getClass().getName()).warning("Not implemented, creating all flavours");
return getVisualizationFlavours(index, gsom, width, height);
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height,
Map<String, String> flavourParameters) throws SOMToolboxException {
Logger.getLogger(this.getClass().getName()).warning("Not implemented, creating all flavours");
return getVisualizationFlavours(index, gsom, width, height);
}
@Override
public String[] needsAdditionalFiles() {
String[] dataFiles = super.needsAdditionalFiles();
if (dataFiles.length < 2) { // we need only one of the files
return null;
} else {
return dataFiles;
}
}
}