| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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 org.apache.commons.math4.neuralnet.twod; |
| |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.Collection; |
| |
| import org.apache.commons.math4.neuralnet.DistanceMeasure; |
| import org.apache.commons.math4.neuralnet.EuclideanDistance; |
| import org.apache.commons.math4.neuralnet.FeatureInitializer; |
| import org.apache.commons.math4.neuralnet.Network; |
| import org.apache.commons.math4.neuralnet.Neuron; |
| import org.apache.commons.math4.neuralnet.SquareNeighbourhood; |
| import org.apache.commons.math4.neuralnet.MapRanking; |
| import org.apache.commons.math4.neuralnet.internal.NeuralNetException; |
| import org.apache.commons.math4.neuralnet.twod.util.LocationFinder; |
| |
| /** |
| * Neural network with the topology of a two-dimensional surface. |
| * Each neuron defines one surface element. |
| * <br> |
| * This network is primarily intended to represent a |
| * <a href="http://en.wikipedia.org/wiki/Kohonen"> |
| * Self Organizing Feature Map</a>. |
| * |
| * @see org.apache.commons.math4.neuralnet.sofm |
| * @since 3.3 |
| */ |
| public class NeuronSquareMesh2D |
| implements Iterable<Neuron> { |
| /** Minimal number of rows or columns. */ |
| private static final int MIN_ROWS = 2; |
| /** Underlying network. */ |
| private final Network network; |
| /** Number of rows. */ |
| private final int numberOfRows; |
| /** Number of columns. */ |
| private final int numberOfColumns; |
| /** Wrap. */ |
| private final boolean wrapRows; |
| /** Wrap. */ |
| private final boolean wrapColumns; |
| /** Neighbourhood type. */ |
| private final SquareNeighbourhood neighbourhood; |
| /** |
| * Mapping of the 2D coordinates (in the rectangular mesh) to |
| * the neuron identifiers (attributed by the {@link #network} |
| * instance). |
| */ |
| private final long[][] identifiers; |
| |
| /** |
| * Horizontal (along row) direction. |
| * @since 3.6 |
| */ |
| public enum HorizontalDirection { |
| /** Column at the right of the current column. */ |
| RIGHT, |
| /** Current column. */ |
| CENTER, |
| /** Column at the left of the current column. */ |
| LEFT, |
| } |
| /** |
| * Vertical (along column) direction. |
| * @since 3.6 |
| */ |
| public enum VerticalDirection { |
| /** Row above the current row. */ |
| UP, |
| /** Current row. */ |
| CENTER, |
| /** Row below the current row. */ |
| DOWN, |
| } |
| |
| /** |
| * @param wrapRowDim Whether to wrap the first dimension (i.e the first |
| * and last neurons will be linked together). |
| * @param wrapColDim Whether to wrap the second dimension (i.e the first |
| * and last neurons will be linked together). |
| * @param neighbourhoodType Neighbourhood type. |
| * @param featuresList Arrays that will initialize the features sets of |
| * the network's neurons. |
| * @throws IllegalArgumentException if {@code numRows < 2} or |
| * {@code numCols < 2}. |
| */ |
| public NeuronSquareMesh2D(boolean wrapRowDim, |
| boolean wrapColDim, |
| SquareNeighbourhood neighbourhoodType, |
| double[][][] featuresList) { |
| numberOfRows = featuresList.length; |
| numberOfColumns = featuresList[0].length; |
| |
| if (numberOfRows < MIN_ROWS) { |
| throw new NeuralNetException(NeuralNetException.TOO_SMALL, numberOfRows, MIN_ROWS); |
| } |
| if (numberOfColumns < MIN_ROWS) { |
| throw new NeuralNetException(NeuralNetException.TOO_SMALL, numberOfColumns, MIN_ROWS); |
| } |
| |
| wrapRows = wrapRowDim; |
| wrapColumns = wrapColDim; |
| neighbourhood = neighbourhoodType; |
| |
| final int fLen = featuresList[0][0].length; |
| network = new Network(0, fLen); |
| identifiers = new long[numberOfRows][numberOfColumns]; |
| |
| // Add neurons. |
| for (int i = 0; i < numberOfRows; i++) { |
| for (int j = 0; j < numberOfColumns; j++) { |
| identifiers[i][j] = network.createNeuron(featuresList[i][j]); |
| } |
| } |
| |
| // Add links. |
| createLinks(); |
| } |
| |
| /** |
| * Creates a two-dimensional network composed of square cells: |
| * Each neuron not located on the border of the mesh has four |
| * neurons linked to it. |
| * <br> |
| * The links are bi-directional. |
| * <br> |
| * The topology of the network can also be a cylinder (if one |
| * of the dimensions is wrapped) or a torus (if both dimensions |
| * are wrapped). |
| * |
| * @param numRows Number of neurons in the first dimension. |
| * @param wrapRowDim Whether to wrap the first dimension (i.e the first |
| * and last neurons will be linked together). |
| * @param numCols Number of neurons in the second dimension. |
| * @param wrapColDim Whether to wrap the second dimension (i.e the first |
| * and last neurons will be linked together). |
| * @param neighbourhoodType Neighbourhood type. |
| * @param featureInit Array of functions that will initialize the |
| * corresponding element of the features set of each newly created |
| * neuron. In particular, the size of this array defines the size of |
| * feature set. |
| * @throws IllegalArgumentException if {@code numRows < 2} or |
| * {@code numCols < 2}. |
| */ |
| public NeuronSquareMesh2D(int numRows, |
| boolean wrapRowDim, |
| int numCols, |
| boolean wrapColDim, |
| SquareNeighbourhood neighbourhoodType, |
| FeatureInitializer[] featureInit) { |
| if (numRows < MIN_ROWS) { |
| throw new NeuralNetException(NeuralNetException.TOO_SMALL, numRows, MIN_ROWS); |
| } |
| if (numCols < MIN_ROWS) { |
| throw new NeuralNetException(NeuralNetException.TOO_SMALL, numCols, MIN_ROWS); |
| } |
| |
| numberOfRows = numRows; |
| wrapRows = wrapRowDim; |
| numberOfColumns = numCols; |
| wrapColumns = wrapColDim; |
| neighbourhood = neighbourhoodType; |
| identifiers = new long[numberOfRows][numberOfColumns]; |
| |
| final int fLen = featureInit.length; |
| network = new Network(0, fLen); |
| |
| // Add neurons. |
| for (int i = 0; i < numRows; i++) { |
| for (int j = 0; j < numCols; j++) { |
| final double[] features = new double[fLen]; |
| for (int fIndex = 0; fIndex < fLen; fIndex++) { |
| features[fIndex] = featureInit[fIndex].value(); |
| } |
| identifiers[i][j] = network.createNeuron(features); |
| } |
| } |
| |
| // Add links. |
| createLinks(); |
| } |
| |
| /** |
| * Constructor with restricted access, solely used for making a |
| * {@link #copy() deep copy}. |
| * |
| * @param wrapRowDim Whether to wrap the first dimension (i.e the first |
| * and last neurons will be linked together). |
| * @param wrapColDim Whether to wrap the second dimension (i.e the first |
| * and last neurons will be linked together). |
| * @param neighbourhoodType Neighbourhood type. |
| * @param net Underlying network. |
| * @param idGrid Neuron identifiers. |
| */ |
| private NeuronSquareMesh2D(boolean wrapRowDim, |
| boolean wrapColDim, |
| SquareNeighbourhood neighbourhoodType, |
| Network net, |
| long[][] idGrid) { |
| numberOfRows = idGrid.length; |
| numberOfColumns = idGrid[0].length; |
| wrapRows = wrapRowDim; |
| wrapColumns = wrapColDim; |
| neighbourhood = neighbourhoodType; |
| network = net; |
| identifiers = idGrid; |
| } |
| |
| /** |
| * Performs a deep copy of this instance. |
| * Upon return, the copied and original instances will be independent: |
| * Updating one will not affect the other. |
| * |
| * @return a new instance with the same state as this instance. |
| * @since 3.6 |
| */ |
| public synchronized NeuronSquareMesh2D copy() { |
| final long[][] idGrid = new long[numberOfRows][numberOfColumns]; |
| for (int r = 0; r < numberOfRows; r++) { |
| for (int c = 0; c < numberOfColumns; c++) { |
| idGrid[r][c] = identifiers[r][c]; |
| } |
| } |
| |
| return new NeuronSquareMesh2D(wrapRows, |
| wrapColumns, |
| neighbourhood, |
| network.copy(), |
| idGrid); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Iterator<Neuron> iterator() { |
| return network.iterator(); |
| } |
| |
| /** |
| * Retrieves the underlying network. |
| * A reference is returned (enabling, for example, the network to be |
| * trained). |
| * This also implies that calling methods that modify the {@link Network} |
| * topology may cause this class to become inconsistent. |
| * |
| * @return the network. |
| */ |
| public Network getNetwork() { |
| return network; |
| } |
| |
| /** |
| * Gets the number of neurons in each row of this map. |
| * |
| * @return the number of rows. |
| */ |
| public int getNumberOfRows() { |
| return numberOfRows; |
| } |
| |
| /** |
| * Gets the number of neurons in each column of this map. |
| * |
| * @return the number of column. |
| */ |
| public int getNumberOfColumns() { |
| return numberOfColumns; |
| } |
| |
| /** |
| * Retrieves the neuron at location {@code (i, j)} in the map. |
| * The neuron at position {@code (0, 0)} is located at the upper-left |
| * corner of the map. |
| * |
| * @param i Row index. |
| * @param j Column index. |
| * @return the neuron at {@code (i, j)}. |
| * @throws IllegalArgumentException if {@code i} or {@code j} is |
| * out of range. |
| * |
| * @see #getNeuron(int,int,HorizontalDirection,VerticalDirection) |
| */ |
| public Neuron getNeuron(int i, |
| int j) { |
| if (i < 0 || |
| i >= numberOfRows) { |
| throw new NeuralNetException(NeuralNetException.OUT_OF_RANGE, |
| i, 0, numberOfRows - 1); |
| } |
| if (j < 0 || |
| j >= numberOfColumns) { |
| throw new NeuralNetException(NeuralNetException.OUT_OF_RANGE, |
| i, 0, numberOfColumns - 1); |
| } |
| |
| return network.getNeuron(identifiers[i][j]); |
| } |
| |
| /** |
| * Retrieves the requested neuron relative to the given {@code (row, col)} |
| * position. |
| * The neuron at position {@code (0, 0)} is located at the upper-left |
| * corner of the map. |
| * |
| * @param row Row index. |
| * @param col Column index. |
| * @param alongRowDir Direction along the given {@code row} (i.e. an |
| * offset will be added to the given <em>column</em> index. |
| * @param alongColDir Direction along the given {@code col} (i.e. an |
| * offset will be added to the given <em>row</em> index. |
| * @return the neuron at the requested location, or {@code null} if |
| * the location is not on the map. |
| * |
| * @see #getNeuron(int,int) |
| */ |
| public Neuron getNeuron(int row, |
| int col, |
| HorizontalDirection alongRowDir, |
| VerticalDirection alongColDir) { |
| final int[] location = getLocation(row, col, alongRowDir, alongColDir); |
| |
| return location == null ? null : getNeuron(location[0], location[1]); |
| } |
| |
| /** |
| * Computes various {@link DataVisualization indicators} of the quality |
| * of the representation of the given {@code data} by this map. |
| * |
| * @param data Features. |
| * @return a new instance holding quality indicators. |
| */ |
| public DataVisualization computeQualityIndicators(Iterable<double[]> data) { |
| return DataVisualization.from(copy(), data); |
| } |
| |
| /** |
| * Computes the location of a neighbouring neuron. |
| * Returns {@code null} if the resulting location is not part |
| * of the map. |
| * Position {@code (0, 0)} is at the upper-left corner of the map. |
| * |
| * @param row Row index. |
| * @param col Column index. |
| * @param alongRowDir Direction along the given {@code row} (i.e. an |
| * offset will be added to the given <em>column</em> index. |
| * @param alongColDir Direction along the given {@code col} (i.e. an |
| * offset will be added to the given <em>row</em> index. |
| * @return an array of length 2 containing the indices of the requested |
| * location, or {@code null} if that location is not part of the map. |
| * |
| * @see #getNeuron(int,int) |
| */ |
| private int[] getLocation(int row, |
| int col, |
| HorizontalDirection alongRowDir, |
| VerticalDirection alongColDir) { |
| final int colOffset; |
| switch (alongRowDir) { |
| case LEFT: |
| colOffset = -1; |
| break; |
| case RIGHT: |
| colOffset = 1; |
| break; |
| case CENTER: |
| colOffset = 0; |
| break; |
| default: |
| // Should never happen. |
| throw new IllegalStateException(); |
| } |
| int colIndex = col + colOffset; |
| if (wrapColumns) { |
| if (colIndex < 0) { |
| colIndex += numberOfColumns; |
| } else { |
| colIndex %= numberOfColumns; |
| } |
| } |
| |
| final int rowOffset; |
| switch (alongColDir) { |
| case UP: |
| rowOffset = -1; |
| break; |
| case DOWN: |
| rowOffset = 1; |
| break; |
| case CENTER: |
| rowOffset = 0; |
| break; |
| default: |
| // Should never happen. |
| throw new IllegalStateException(); |
| } |
| int rowIndex = row + rowOffset; |
| if (wrapRows) { |
| if (rowIndex < 0) { |
| rowIndex += numberOfRows; |
| } else { |
| rowIndex %= numberOfRows; |
| } |
| } |
| |
| if (rowIndex < 0 || |
| rowIndex >= numberOfRows || |
| colIndex < 0 || |
| colIndex >= numberOfColumns) { |
| return null; |
| } else { |
| return new int[] {rowIndex, colIndex}; |
| } |
| } |
| |
| /** |
| * Creates the neighbour relationships between neurons. |
| */ |
| private void createLinks() { |
| // "linkEnd" will store the identifiers of the "neighbours". |
| final List<Long> linkEnd = new ArrayList<>(); |
| final int iLast = numberOfRows - 1; |
| final int jLast = numberOfColumns - 1; |
| for (int i = 0; i < numberOfRows; i++) { |
| for (int j = 0; j < numberOfColumns; j++) { |
| linkEnd.clear(); |
| |
| switch (neighbourhood) { |
| |
| case MOORE: |
| // Add links to "diagonal" neighbours. |
| if (i > 0) { |
| if (j > 0) { |
| linkEnd.add(identifiers[i - 1][j - 1]); |
| } |
| if (j < jLast) { |
| linkEnd.add(identifiers[i - 1][j + 1]); |
| } |
| } |
| if (i < iLast) { |
| if (j > 0) { |
| linkEnd.add(identifiers[i + 1][j - 1]); |
| } |
| if (j < jLast) { |
| linkEnd.add(identifiers[i + 1][j + 1]); |
| } |
| } |
| if (wrapRows) { |
| if (i == 0) { |
| if (j > 0) { |
| linkEnd.add(identifiers[iLast][j - 1]); |
| } |
| if (j < jLast) { |
| linkEnd.add(identifiers[iLast][j + 1]); |
| } |
| } else if (i == iLast) { |
| if (j > 0) { |
| linkEnd.add(identifiers[0][j - 1]); |
| } |
| if (j < jLast) { |
| linkEnd.add(identifiers[0][j + 1]); |
| } |
| } |
| } |
| if (wrapColumns) { |
| if (j == 0) { |
| if (i > 0) { |
| linkEnd.add(identifiers[i - 1][jLast]); |
| } |
| if (i < iLast) { |
| linkEnd.add(identifiers[i + 1][jLast]); |
| } |
| } else if (j == jLast) { |
| if (i > 0) { |
| linkEnd.add(identifiers[i - 1][0]); |
| } |
| if (i < iLast) { |
| linkEnd.add(identifiers[i + 1][0]); |
| } |
| } |
| } |
| if (wrapRows && |
| wrapColumns) { |
| if (i == 0 && |
| j == 0) { |
| linkEnd.add(identifiers[iLast][jLast]); |
| } else if (i == 0 && |
| j == jLast) { |
| linkEnd.add(identifiers[iLast][0]); |
| } else if (i == iLast && |
| j == 0) { |
| linkEnd.add(identifiers[0][jLast]); |
| } else if (i == iLast && |
| j == jLast) { |
| linkEnd.add(identifiers[0][0]); |
| } |
| } |
| |
| // Case falls through since the "Moore" neighbourhood |
| // also contains the neurons that belong to the "Von |
| // Neumann" neighbourhood. |
| |
| // fallthru (CheckStyle) |
| case VON_NEUMANN: |
| // Links to preceding and following "row". |
| if (i > 0) { |
| linkEnd.add(identifiers[i - 1][j]); |
| } |
| if (i < iLast) { |
| linkEnd.add(identifiers[i + 1][j]); |
| } |
| if (wrapRows) { |
| if (i == 0) { |
| linkEnd.add(identifiers[iLast][j]); |
| } else if (i == iLast) { |
| linkEnd.add(identifiers[0][j]); |
| } |
| } |
| |
| // Links to preceding and following "column". |
| if (j > 0) { |
| linkEnd.add(identifiers[i][j - 1]); |
| } |
| if (j < jLast) { |
| linkEnd.add(identifiers[i][j + 1]); |
| } |
| if (wrapColumns) { |
| if (j == 0) { |
| linkEnd.add(identifiers[i][jLast]); |
| } else if (j == jLast) { |
| linkEnd.add(identifiers[i][0]); |
| } |
| } |
| break; |
| |
| default: |
| throw new IllegalStateException(); // Cannot happen. |
| } |
| |
| final Neuron aNeuron = network.getNeuron(identifiers[i][j]); |
| for (final long b : linkEnd) { |
| final Neuron bNeuron = network.getNeuron(b); |
| // Link to all neighbours. |
| // The reverse links will be added as the loop proceeds. |
| network.addLink(aNeuron, bNeuron); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Miscellaneous indicators of the map quality. |
| * <ul> |
| * <li>Hit histogram</li> |
| * <li>Quantization error</li> |
| * <li>Topographic error</li> |
| * <li>Unified distance matrix</li> |
| * </ul> |
| */ |
| public static final class DataVisualization { |
| /** Distance function. */ |
| private static final DistanceMeasure DISTANCE = new EuclideanDistance(); |
| /** Total number of samples. */ |
| private final int numberOfSamples; |
| /** Hit histogram. */ |
| private final double[][] hitHistogram; |
| /** Quantization error. */ |
| private final double[][] quantizationError; |
| /** Mean quantization error. */ |
| private final double meanQuantizationError; |
| /** Topographic error. */ |
| private final double[][] topographicError; |
| /** Mean topographic error. */ |
| private final double meanTopographicError; |
| /** U-matrix. */ |
| private final double[][] uMatrix; |
| |
| /** |
| * @param numberOfSamples Number of samples. |
| * @param hitHistogram Hit histogram. |
| * @param quantizationError Quantization error. |
| * @param topographicError Topographic error. |
| * @param uMatrix U-matrix. |
| */ |
| private DataVisualization(int numberOfSamples, |
| double[][] hitHistogram, |
| double[][] quantizationError, |
| double[][] topographicError, |
| double[][] uMatrix) { |
| this.numberOfSamples = numberOfSamples; |
| this.hitHistogram = hitHistogram; |
| this.quantizationError = quantizationError; |
| meanQuantizationError = hitWeightedMean(quantizationError, hitHistogram); |
| this.topographicError = topographicError; |
| meanTopographicError = hitWeightedMean(topographicError, hitHistogram); |
| this.uMatrix = uMatrix; |
| } |
| |
| /** |
| * @param map Map |
| * @param data Data. |
| * @return the metrics. |
| */ |
| static DataVisualization from(NeuronSquareMesh2D map, |
| Iterable<double[]> data) { |
| final LocationFinder finder = new LocationFinder(map); |
| final MapRanking rank = new MapRanking(map, DISTANCE); |
| final Network net = map.getNetwork(); |
| final int nR = map.getNumberOfRows(); |
| final int nC = map.getNumberOfColumns(); |
| |
| // Hit bins. |
| final int[][] hitCounter = new int[nR][nC]; |
| // Hit bins. |
| final double[][] hitHistogram = new double[nR][nC]; |
| // Quantization error bins. |
| final double[][] quantizationError = new double[nR][nC]; |
| // Topographic error bins. |
| final double[][] topographicError = new double[nR][nC]; |
| // U-matrix. |
| final double[][] uMatrix = new double[nR][nC]; |
| |
| int numSamples = 0; |
| for (final double[] sample : data) { |
| ++numSamples; |
| |
| final List<Neuron> winners = rank.rank(sample, 2); |
| final Neuron best = winners.get(0); |
| final Neuron secondBest = winners.get(1); |
| |
| final LocationFinder.Location locBest = finder.getLocation(best); |
| final int rowBest = locBest.getRow(); |
| final int colBest = locBest.getColumn(); |
| // Increment hit counter. |
| hitCounter[rowBest][colBest] += 1; |
| |
| // Aggregate quantization error. |
| quantizationError[rowBest][colBest] += DISTANCE.applyAsDouble(sample, best.getFeatures()); |
| |
| // Aggregate topographic error. |
| if (!net.getNeighbours(best).contains(secondBest)) { |
| // Increment count if first and second best matching units |
| // are not neighbours. |
| topographicError[rowBest][colBest] += 1; |
| } |
| } |
| |
| for (int r = 0; r < nR; r++) { |
| for (int c = 0; c < nC; c++) { |
| final Neuron neuron = map.getNeuron(r, c); |
| final Collection<Neuron> neighbours = net.getNeighbours(neuron); |
| final double[] features = neuron.getFeatures(); |
| double uDistance = 0; |
| int neighbourCount = 0; |
| for (final Neuron n : neighbours) { |
| ++neighbourCount; |
| uDistance += DISTANCE.applyAsDouble(features, n.getFeatures()); |
| } |
| |
| final int hitCount = hitCounter[r][c]; |
| if (hitCount != 0) { |
| hitHistogram[r][c] = hitCount / (double) numSamples; |
| quantizationError[r][c] /= hitCount; |
| topographicError[r][c] /= hitCount; |
| } |
| |
| uMatrix[r][c] = uDistance / neighbourCount; |
| } |
| } |
| |
| return new DataVisualization(numSamples, |
| hitHistogram, |
| quantizationError, |
| topographicError, |
| uMatrix); |
| } |
| |
| /** |
| * @return the total number of samples. |
| */ |
| public int getNumberOfSamples() { |
| return numberOfSamples; |
| } |
| |
| /** |
| * @return the quantization error. |
| * Each bin will contain the average of the distances between samples |
| * mapped to the corresponding unit and the weight vector of that unit. |
| * @see #getMeanQuantizationError() |
| */ |
| public double[][] getQuantizationError() { |
| return copy(quantizationError); |
| } |
| |
| /** |
| * @return the topographic error. |
| * Each bin will contain the number of data for which the first and |
| * second best matching units are not adjacent in the map. |
| * @see #getMeanTopographicError() |
| */ |
| public double[][] getTopographicError() { |
| return copy(topographicError); |
| } |
| |
| /** |
| * @return the hits histogram (normalized). |
| * Each bin will contain the number of data for which the corresponding |
| * neuron is the best matching unit. |
| */ |
| public double[][] getNormalizedHits() { |
| return copy(hitHistogram); |
| } |
| |
| /** |
| * @return the U-matrix. |
| * Each bin will contain the average distance between a unit and all its |
| * neighbours will be computed (and stored in the pixel corresponding to |
| * that unit of the 2D-map). The number of neighbours taken into account |
| * depends on the network {@link org.apache.commons.math4.neuralnet.SquareNeighbourhood |
| * neighbourhood type}. |
| */ |
| public double[][] getUMatrix() { |
| return copy(uMatrix); |
| } |
| |
| /** |
| * @return the mean (hit-weighted) quantization error. |
| * @see #getQuantizationError() |
| */ |
| public double getMeanQuantizationError() { |
| return meanQuantizationError; |
| } |
| |
| /** |
| * @return the mean (hit-weighted) topographic error. |
| * @see #getTopographicError() |
| */ |
| public double getMeanTopographicError() { |
| return meanTopographicError; |
| } |
| |
| /** |
| * @param orig Source. |
| * @return a deep copy of the original array. |
| */ |
| private static double[][] copy(double[][] orig) { |
| final double[][] copy = new double[orig.length][]; |
| for (int i = 0; i < orig.length; i++) { |
| copy[i] = orig[i].clone(); |
| } |
| |
| return copy; |
| } |
| |
| /** |
| * @param metrics Metrics. |
| * @param normalizedHits Hits histogram (normalized). |
| * @return the hit-weighted mean of the given {@code metrics}. |
| */ |
| private static double hitWeightedMean(double[][] metrics, |
| double[][] normalizedHits) { |
| double mean = 0; |
| final int rows = metrics.length; |
| final int cols = metrics[0].length; |
| for (int i = 0; i < rows; i++) { |
| for (int j = 0; j < cols; j++) { |
| mean += normalizedHits[i][j] * metrics[i][j]; |
| } |
| } |
| |
| return mean; |
| } |
| } |
| } |