From aa7a8a6e288342b5ce39ad0da412281fda1db7f8 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Wed, 5 Feb 2025 16:31:49 +0100 Subject: [PATCH 01/18] Refactored classes and test class --- .../clustering/art2a/Art2aClusteringTask.java | 252 ---- .../cheminf/clustering/art2a/Art2aData.java | 301 +++++ .../cheminf/clustering/art2a/Art2aKernel.java | 1128 +++++++++++++++++ .../cheminf/clustering/art2a/Art2aResult.java | 505 ++++++++ .../cheminf/clustering/art2a/Art2aTask.java | 304 +++++ .../cheminf/clustering/art2a/Art2aUtils.java | 948 ++++++++++++++ .../abstractResult/Art2aAbstractResult.java | 216 ---- .../clustering/Art2aDoubleClustering.java | 609 --------- .../clustering/Art2aFloatClustering.java | 600 --------- .../ConvergenceFailedException.java | 55 - .../art2a/interfaces/IArt2aClustering.java | 70 - .../interfaces/IArt2aClusteringResult.java | 117 -- .../results/Art2aDoubleClusteringResult.java | 225 ---- .../results/Art2aFloatClusteringResult.java | 222 ---- .../clustering/art2a/util/FileUtil.java | 249 ---- .../art2a/Art2aDoubleClusteringTest.java | 767 ----------- .../art2a/Art2aFloatClusteringTest.java | 769 ----------- .../cheminf/clustering/art2a/Art2aTest.java | 948 ++++++++++++++ .../clustering/art2a/Bit_Fingerprints.txt | 10 - .../clustering/art2a/Count_Fingerprints.txt | 6 - 20 files changed, 4134 insertions(+), 4167 deletions(-) delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aClusteringTask.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/abstractResult/Art2aAbstractResult.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aDoubleClustering.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aFloatClustering.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/exceptions/ConvergenceFailedException.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClustering.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClusteringResult.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aDoubleClusteringResult.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aFloatClusteringResult.java delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/util/FileUtil.java delete mode 100644 src/test/java/de/unijena/cheminf/clustering/art2a/Art2aDoubleClusteringTest.java delete mode 100644 src/test/java/de/unijena/cheminf/clustering/art2a/Art2aFloatClusteringTest.java create mode 100644 src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java delete mode 100644 src/test/resources/de/unijena/cheminf/clustering/art2a/Bit_Fingerprints.txt delete mode 100644 src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aClusteringTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aClusteringTask.java deleted file mode 100644 index 2e3440a..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aClusteringTask.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a; - -import de.unijena.cheminf.clustering.art2a.clustering.Art2aDoubleClustering; -import de.unijena.cheminf.clustering.art2a.clustering.Art2aFloatClustering; -import de.unijena.cheminf.clustering.art2a.exceptions.ConvergenceFailedException; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClustering; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClusteringResult; - -import java.util.concurrent.Callable; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Callable class for clustering input vectors (fingerprints). - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aClusteringTask implements Callable { - // - /** - * Float clustering task constructor. - * Creates a new Art2aClusteringTask instance with the specified parameters. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aDataMatrix matrix contains all inputs for clustering. Each row of the matrix contains one input. - * In addition, all inputs must have the same length. Each column of the matrix contains one component of the input. - * @param aMaximumEpochsNumber maximum number of epochs that the system may use for convergence. - * @param anIsClusteringResultExported if the parameter is set to true, the cluster results - * are exported to text files. - * @param aRequiredSimilarity parameter indicating the minimum similarity between the current - * cluster vectors and the previous cluster vectors. The parameter is crucial - * for the convergence of the system. If the parameter is set too high, a much - * more accurate similarity is expected and the convergence may take longer, - * while a small parameter expects a lower similarity between the cluster - * vectors and thus the system may converge faster. - * @param aLearningParameter parameter to define the intensity of keeping the old cluster vector in mind - * before the system adapts it to the new sample vector. - * @throws IllegalArgumentException is thrown, if the given arguments are invalid. The checking of the arguments - * is done in the constructor of Art2aFloatClustering. - * @throws NullPointerException is thrown, if the given aDataMatrix is null. The checking of the data matrix is - * done in the constructor of the ArtaFloatClustering. - * - */ - public Art2aClusteringTask(float aVigilanceParameter, float[][] aDataMatrix, int aMaximumEpochsNumber, - boolean anIsClusteringResultExported, float aRequiredSimilarity, float aLearningParameter) - throws IllegalArgumentException, NullPointerException { - this.isClusteringResultExported = anIsClusteringResultExported; - this.isSeedSet = false; - this.art2aClustering = new Art2aFloatClustering(aDataMatrix, aMaximumEpochsNumber, aVigilanceParameter, - aRequiredSimilarity, aLearningParameter); - } - // - /** - * Float clustering task constructor. - * Creates a new Art2aClusteringTask instance with the specified parameters. - * For the required similarity and learning parameter default values are used. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aDataMatrix matrix contains all inputs for clustering. Each row of the matrix contains one input. - * In addition, all inputs must have the same length. - * Each column of the matrix contains one component of the input. - * @param aMaximumEpochsNumber maximum number of epochs that the system may use for convergence. - * @param anIsClusteringResultExported if the parameter is set to true, the cluster results - * are exported to text files. - * @throws IllegalArgumentException is thrown, if the given arguments are invalid. The checking of the arguments - * is done in the constructor of Art2aFloatClustering. - * @throws NullPointerException is thrown, if the given aDataMatrix is null. The checking of the data matrix is - * done in the constructor of the ArtaFloatClustering. - * - * @see de.unijena.cheminf.clustering.art2a.Art2aClusteringTask#Art2aClusteringTask(float, float[][], int, - * boolean, float, float) - */ - public Art2aClusteringTask(float aVigilanceParameter, float[][] aDataMatrix, int aMaximumEpochsNumber, - boolean anIsClusteringResultExported) - throws IllegalArgumentException, NullPointerException { - this(aVigilanceParameter, aDataMatrix, aMaximumEpochsNumber, anIsClusteringResultExported, - Art2aClusteringTask.REQUIRED_SIMILARITY_FLOAT, Art2aClusteringTask.DEFAULT_LEARNING_PARAMETER_FLOAT); - } - // - /** - * Double clustering task constructor. - * Creates a new Art2aDoubleClustering instance with the specified parameters. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aDataMatrix matrix contains all inputs for clustering. Each row of the matrix contains one input. - * In addition, all inputs must have the same length. - * Each column of the matrix contains one component of the input. - * @param aMaximumEpochsNumber maximum number of epochs that the system may use for convergence. - * @param anIsClusteringResultExported if the parameter is set to true, the cluster results are - * exported to text files. - * @param aRequiredSimilarity parameter indicating the minimum similarity between the current - * cluster vectors and the previous cluster vectors. - * @param aLearningParameter parameter to define the intensity of keeping the old cluster vector in mind - * before the system adapts it to the new sample vector. - * @throws IllegalArgumentException is thrown, if the given arguments are invalid. The checking of the arguments - * is done in the constructor of Art2aFloatClustering. - * @throws NullPointerException is thrown, if the given aDataMatrix is null. The checking of the data matrix is - * done in the constructor of the ArtaFloatClustering. - */ - public Art2aClusteringTask(double aVigilanceParameter, double[][] aDataMatrix, int aMaximumEpochsNumber, - boolean anIsClusteringResultExported, double aRequiredSimilarity, double aLearningParameter) - throws IllegalArgumentException, NullPointerException { - this.isClusteringResultExported = anIsClusteringResultExported; - this.art2aClustering = new Art2aDoubleClustering(aDataMatrix, aMaximumEpochsNumber, aVigilanceParameter, - aRequiredSimilarity, aLearningParameter); - } - // - /** - * Double clustering task constructor. - * Creates a new Art2aDoubleClustering instance with the specified parameters. - * For the required similarity and learning parameter default values are used. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aDataMatrix matrix contains all inputs for clustering. Each row of the matrix contains one input. - * In addition, all inputs must have the same length. Each column of the matrix contains one component of the input. - * @param aMaximumEpochsNumber maximum number of epochs that the system may use for convergence. - * @param anIsClusteringResultExported if the parameter is set to true, the cluster results are - * exported to text files. - * @throws IllegalArgumentException is thrown, if the given arguments are invalid. The checking of the arguments - * is done in the constructor of Art2aFloatClustering. - * @throws NullPointerException is thrown, if the given aDataMatrix is null. The checking of the data matrix is - * done in the constructor of the ArtaFloatClustering. - * - * @see de.unijena.cheminf.clustering.art2a.Art2aClusteringTask#Art2aClusteringTask(double, double[][], int, - * boolean, double, double) - * - */ - public Art2aClusteringTask(double aVigilanceParameter, double[][] aDataMatrix, int aMaximumEpochsNumber, - boolean anIsClusteringResultExported) throws IllegalArgumentException, NullPointerException { - this(aVigilanceParameter, aDataMatrix, aMaximumEpochsNumber, anIsClusteringResultExported, - Art2aClusteringTask.REQUIRED_SIMILARITY_DOUBLE, Art2aClusteringTask.DEFAULT_LEARNING_PARAMETER_DOUBLE); - } - // - // - // - /** - * Executes the clustering. - * - * @return clustering result. - */ - @Override - public IArt2aClusteringResult call() { - try { - if(this.isSeedSet) { - return this.art2aClustering.getClusterResult(this.isClusteringResultExported, this.seed); - } else { - return this.art2aClustering.getClusterResult(this.isClusteringResultExported, - this.DEFAULT_SEED_VALUE_TO_RANDOMIZE_INPUT_VECTORS); - } - } catch (ConvergenceFailedException anException) { - Art2aClusteringTask.LOGGER.log(Level.SEVERE, anException.toString(), anException); - return null; - } - } - // - // - // - /** - * - * User-defined seed value to randomize input vectors. - * Different seed values can lead to different clustering results. - * - * @param aSeed seed value - * @return user-defined seed value. - */ - public int setSeed(int aSeed) { - this.seed = aSeed; - this.isSeedSet = true; - return this.seed; - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java new file mode 100644 index 0000000..69fbb61 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java @@ -0,0 +1,301 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Data class for ART-2a clustering. + *

+ * Note: Art2aData objects are to be generated with Art2aKernel.getArt2aData() + * methods to obtain preprocessed data for faster ART-2a clustering. + *

+ * Art2aData is also used for internal data preprocessing in class Art2aKernel. + * A private constructor ensures that original dataMatrix and preprocessed + * contrastEnhancedUnitMatrix/dataVectorZeroLengthFlags are mutually exclusive. + * Use method hasPreprocessedData() to check wether preprocessed + * contrastEnhancedUnitMatrix/dataVectorZeroLengthFlags are available. + *

+ * Note: Art2aData is a read-only class, i.e. thread-safe. The same Art2aData + * object may be distributed to several concurrently working Art2aTasks without + * any mutual interference problems. + * + * @author Achim Zielesny + */ +public class Art2aData { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aData.class.getName()); + // + // + /** + * Original data matrix with data row vectors + */ + private final float[][] dataMatrix; + /** + * Matrix of contrast enhanced unit vectors + */ + private final float[][] contrastEnhancedUnitMatrix; + /** + * Flags array that indicates if scaled data row vectors have a length + * of zero (i.e. where all components are equal to zero, the corresponding + * contrast enhanced unit vector is set to null in this case). True: + * Scaled data row vector has a length of zero, false: Otherwise. + */ + private final boolean[] dataVectorZeroLengthFlags; + /** + * Min-max components of original data matrix (see method + * Art2aUtils.getMinMaxComponents() for data structure) + */ + private final Art2aUtils.MinMaxValue[] minMaxComponentsOfDataMatrix; + /** + * Offset for contrast enhancement + */ + private final float offsetForContrastEnhancement; + /** + * Returns if Art2aData object has preprocessed data, i.e. + * contrastEnhancedUnitMatrix and dataVectorZeroLengthFlags are defined: + * True: Art2aData object has preprocessed data, false: Otherwise + */ + private final boolean hasPreprocessedData; + // + + + // + /** + * Private constructor + * Note: No checks are necessary + * + * @param aDataMatrix Original data matrix with data row vectors (MAY BE NULL) + * @param aContrastEnhancedUnitMatrix Matrix of contrast enhanced unit + * vectors (MAY BE NULL) + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @param aHasPreprocessedData True: Art2aData object has preprocessed data, + * false: Otherwise + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + private Art2aData ( + float[][] aDataMatrix, + float[][] aContrastEnhancedUnitMatrix, + boolean[] aDataVectorZeroLengthFlags, + Art2aUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement, + boolean aHasPreprocessedData + ) { + this.dataMatrix = aDataMatrix; + this.contrastEnhancedUnitMatrix = aContrastEnhancedUnitMatrix; + this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; + this.minMaxComponentsOfDataMatrix = aMinMaxComponentsOfDataMatrix; + this.offsetForContrastEnhancement = anOffsetForContrastEnhancement; + this.hasPreprocessedData = aHasPreprocessedData; + } + // + // + /** + * Constructor + * + * @param aDataMatrix Original data matrix with data row vectors (NOT + * allowed to be null) + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + protected Art2aData ( + float[][] aDataMatrix, + Art2aUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement + ) { + this ( + aDataMatrix, + null, + null, + aMinMaxComponentsOfDataMatrix, + anOffsetForContrastEnhancement, + false + ); + if (!Art2aUtils.isMatrixValid(aDataMatrix)) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: aDataMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aData.Constructor: aDataMatrix is invalid"); + } + if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aDataMatrix[0].length) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); + } + if (anOffsetForContrastEnhancement <= 0.0f) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero." + ); + throw new IllegalArgumentException("Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero."); + } + } + + /** + * Constructor + * + * @param aContrastEnhancedUnitMatrix Matrix of contrast enhanced unit + * vectors (NOT allowed to be null) + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + protected Art2aData ( + float[][] aContrastEnhancedUnitMatrix, + boolean[] aDataVectorZeroLengthFlags, + Art2aUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement + ) { + this ( + null, + aContrastEnhancedUnitMatrix, + aDataVectorZeroLengthFlags, + aMinMaxComponentsOfDataMatrix, + anOffsetForContrastEnhancement, + true + ); + if (!Art2aUtils.isMatrixValid(aContrastEnhancedUnitMatrix)) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: aContrastEnhancedUnitMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aData.Constructor: aContrastEnhancedUnitMatrix is invalid."); + } + if (aDataVectorZeroLengthFlags == null || aDataVectorZeroLengthFlags.length == 0 || aDataVectorZeroLengthFlags.length != aContrastEnhancedUnitMatrix.length) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: aDataVectorZeroLengthFlags is illegal." + ); + throw new IllegalArgumentException("Art2aData.Constructor: aDataVectorZeroLengthFlags is illegal."); + } + if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aContrastEnhancedUnitMatrix[0].length) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); + } + if (anOffsetForContrastEnhancement <= 0.0f) { + Art2aData.LOGGER.log( + Level.SEVERE, + "Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero." + ); + throw new IllegalArgumentException("Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero."); + } + } + // + + // + /** + * Original data matrix with data row vectors + * + * @return Original data matrix with data row vectors or null if + * hasPreprocessedData() returns true + */ + protected float[][] getDataMatrix() { + return this.dataMatrix; + } + + /** + * Matrix of contrast enhanced unit vectors + * + * @return Matrix of contrast enhanced unit vectors or null if + * hasPreprocessedData() returns false + */ + protected float[][] getContrastEnhancedUnitMatrix() { + return this.contrastEnhancedUnitMatrix; + } + + /** + * Flags array that indicates if scaled data row vectors have a length + * of zero (i.e. where all components are equal to zero, the corresponding + * contrast enhanced unit vector is set to null in this case). True: + * Scaled data row vector has a length of zero, false: Otherwise. + * + * @return Array with flags or null if hasPreprocessedData() returns false + */ + protected boolean[] getDataVectorZeroLengthFlags() { + return this.dataVectorZeroLengthFlags; + } + + /** + * Min-max components of original data matrix (see method + Art2aUtils.getMinMaxComponents() for data structure) + * + * @return Min-max components of original data matrix + */ + protected Art2aUtils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { + return this.minMaxComponentsOfDataMatrix; + } + + /** + * Returns if Art2aData object has preprocessed data, i.e. + * contrastEnhancedUnitMatrix and dataVectorZeroLengthFlags are defined. + * + * @return True: Art2aData object has preprocessed data, false: Otherwise + */ + protected boolean hasPreprocessedData() { + return this.hasPreprocessedData; + } + + /** + * Returns offset for contrast enhancement + * + * @return Offset for contrast enhancement + */ + protected float getOffsetForContrastEnhancement() { + return this.offsetForContrastEnhancement; + } + // + +} \ No newline at end of file diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java new file mode 100644 index 0000000..3cf4f12 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -0,0 +1,1128 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * ART-2a algorithm implementation for unsupervised, open categorical + * clustering. + *

+ * Literature: G.A. Carpenter, S. Grossberg and D.B. Rosen, Neural Networks 4 + * (1991) 493-504; D. Wienke, Y. Xie, P. K. Hopke, Chemometrics and Intelligent + * Laboratory Systems 24 (1994) 367-387 + *

+ * Use Art2aKernel for sequential clustering instances and Art2aTask for + * clustering instances to be executed concurrently (parallelized). See hints + * for ART-2a clustering with minimal additional memory allocation or maximum + * speed below. + *

+ * Note: For clustering of the SAME data with DIFFERENT vigilance parameters use + * method getClusterResults() where the mode of calculation may be specified to + * be sequential or concurrent (parallelized). + *

+ * All numerical calculations are performed in single (float) precision. + *

+ * Note, that aDataMatrix may contain data vectors with all components being + * equal to zero (or some constant minimal value). These data vectors are + * removed from the clustering process and their indices are returned by method + * getZeroLengthDataVectorIndices() of an Art2aResult object. + *

+ * ART-2a clustering with minimal memory allocation: + * If a data matrix with N data row vectors is used to construct a clustering + * instance without preprocessing (parameter isDataPreprocessing is set to + * false), minimal additional memory is allocated. The data matrix itself is not + * changed. The additional allocated memory can be controlled by the + * maximumNumberOfClusters parameter and estimated to be about + * (additional memory of ART-2a instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 2 MByte of additionally + * allocated memory. Note, that memory for cluster vectors is only allocated if + * needed, e.g. if specified parameter maximumNumberOfClusters allows 150 + * clusters but only 27 are needed, then only memory for these 27 cluster + * vectors is allocated. The minimal memory allocation comes at the expense of + * clustering speed since preprocessing steps have to be executed repeatedly. + * This also decreases the performance of some methods of the Art2aResult object + * generated by the clustering process, e.g. getClusterRepresentatives(). + *

+ * ART-2a clustering with maximum speed: + * If parameter isDataPreprocessing is set to true, preprocessing steps are + * calculated in advance for maximum clustering speed (as well as maximum speed + * of the Art2aResult methods). This requires an additional memory allocation + * for the preprocessed data for an ART-2a clustering instance: + * (additional memory of ART-2a instance) = + * (1 + 2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 12 MByte of additionally + * allocated memory. + *

+ * CAUTION: Construction of several ART-2a clustering instances with the SAME + * data matrix PLUS preprocessing is NOT advised due to the significant memory + * consumption of each instance. In this case, the data matrix should be + * checked with static method Art2aKernel.isDataMatrixValid() and then a priori + * converted into a preprocessed Art2aData object with static method + * Art2aKernel.getArt2aData(). The generated Art2aData object does NOT change + * or refer to the data matrix so that the data matrix memory could be released + * after conversion (by setting the data matrix object to null). The generated + * Art2aData object has additionally allocated about the same memory as the + * original data matrix, e.g., a 10 MByte data matrix is converted into a + * roughly 10 MByte Art2aData object. But this single Art2aData object can now + * be used to construct several ART-2a clustering instances (Art2aKernel + * instance or Art2aTask instances for concurrent (parallelized) execution) + * where each of these ART-2a clustering instances (and their generated + * Art2aResult object methods) performs with maximum speed and allocates only + * the minimal additional memory of + * (additional memory of ART-2a instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., for 9 constructed ART-2a clustering instances for concurrent execution + * only 18 MBytes of additional memory are allocated in total. Compare this + * total additional allocated memory of only 10 + 18 = 28 MByte for an + * Art2aData object plus 9 ART-2a clustering instances with the alternative + * 9 x 12 = 108 MByte of memory for 9 ART-2a clustering instances constructed + * with the same data matrix plus independent preprocessing in each instance! + * (Just for completeness: For a minimal memory realization of these 9 ART-2a + * clustering instances, each instance can be constructed with the same data + * matrix WITHOUT preprocessing, which would require only 18 MBytes of + * additional allocated memory in total.) + * + * @author Betuel Sevindik, Achim Zielesny + */ +public class Art2aKernel { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aKernel.class.getName()); + // + // + /** + * Default fraction of the (maximum) number of clusters relative to + * number of data vectors + */ + private static final float DEFAULT_FRACTION_OF_CLUSTERS = 0.2f; + /** + * Default seed value for random number generator + */ + private static final long DEFAULT_RANDOM_SEED = 1L; + /** + * Default maximum number of epochs + */ + private static final int DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS = 100; + /** + * Default value for the learning parameter + */ + private static final float DEFAULT_LEARNING_PARAMETER = 0.01f; + /** + * Default offset for contrast enhancement + */ + private static final float DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT = 1.0f; + /** + * Default value of the convergence threshold for cluster centroid + * similarity + */ + private static final float DEFAULT_CONVERGENCE_THRESHOLD = 0.99f; + // + // + /** + * Maximum number of clusters in interval [2, number of data row vectors of getDataMatrix] + */ + private final int maximumNumberOfClusters; + /** + * Maximum number of epochs for training + */ + private final int maximumNumberOfEpochs; + /** + * Convergence threshold for cluster centroid similarity in interval (0,1) + */ + private final float convergenceThreshold; + /** + * Learning parameter in interval (0,1) + */ + private final float learningParameter; + /** + * Random seed value + */ + private final long randomSeed; + /** + * Art2aData data object + */ + private final Art2aData art2aData; + // + // + /** + * Helper callable for a single getClusterResult() calculation task of + * Art2aKernel with a distinct vigilance parameter. + *

+ * Note: No checks are performed. + */ + private static class HelperTask implements Callable { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(HelperTask.class.getName()); + // + // + /** + * Art2aKernel + */ + private final Art2aKernel art2aKernel; + /** + * Vigilance parameter + */ + private final float vigilance; + // + + // + /** + * Constructor + */ + protected HelperTask( + Art2aKernel anArt2aKernel, + float aVigilance + ) { + this.art2aKernel = anArt2aKernel; + this.vigilance = aVigilance; + } + // + + // + /** + * Performs single getClusterResult() calculation task. + * + * @return Art2aResult or null if getClusterResult() calculation task + * could not be performed. + */ + @Override + public Art2aResult call() { + try { + return this.art2aKernel.getClusterResult(this.vigilance); + } catch (Exception anException) { + HelperTask.LOGGER.log( + Level.SEVERE, + "SingleTask.call: Can not calculate a cluster result." + ); + HelperTask.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + return null; + } + } + // + + } + //
+ + // + /** + * Constructor. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * similarity (must be in interval (0,1)) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * Otherwise. + * @throws IllegalArgumentException Thrown if an argument is illegal + * + */ + public Art2aKernel( + float[][] aDataMatrix, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + float anOffsetForContrastEnhancement, + long aRandomSeed, + boolean anIsDataPreprocessing + ) throws IllegalArgumentException { + // + if(!Art2aKernel.isDataMatrixValid(aDataMatrix)) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aDataMatrix is not valid." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aDataMatrix is not valid."); + } + if(aMaximumNumberOfEpochs <= 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); + } + if(aConvergenceThreshold <= 0.0f || aConvergenceThreshold > 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]."); + } + if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)."); + } + if(anOffsetForContrastEnhancement <= 0.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: anOffsetForContrastEnhancement must be greater zero." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: anOffsetForContrastEnhancement must be greater zero."); + } + if(aRandomSeed <= 0L) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aRandomSeed must be greater 0." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aRandomSeed must be greater/equal 0."); + } + // + + if(anIsDataPreprocessing) { + this.art2aData = + Art2aKernel.getArt2aData( + aDataMatrix, + anOffsetForContrastEnhancement + ); + } else { + this.art2aData = + new Art2aData( + aDataMatrix, + Art2aUtils.getMinMaxComponents(aDataMatrix), + anOffsetForContrastEnhancement + ); + } + + this.maximumNumberOfClusters = aMaximumNumberOfClusters; + this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; + this.convergenceThreshold = aConvergenceThreshold; + this.learningParameter = aLearningParameter; + this.randomSeed = aRandomSeed; + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * (= 1.0) and RANDOM_SEED (= 1). + * Note: There is NO data preprocessing. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aKernel( + float[][] aDataMatrix + ) throws IllegalArgumentException { + this( + aDataMatrix, + (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? + (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) : + 2, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_LEARNING_PARAMETER, + DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT, + DEFAULT_RANDOM_SEED, + false + ); + } + + /** + * Constructor. + * + * @param anArt2aData ART-2a data object created by method + * Art2aKernel.getArt2aData() + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * similarity (must be in interval (0,1)) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + public Art2aKernel( + Art2aData anArt2aData, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + long aRandomSeed + ) throws IllegalArgumentException { + // + if(anArt2aData == null) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: anArt2aData is null." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: anArt2aData is null."); + } + if(aMaximumNumberOfEpochs <= 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); + } + if(aConvergenceThreshold <= 0.0f || aConvergenceThreshold > 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]."); + } + if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)."); + } + if(aRandomSeed <= 0L) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aRandomSeed must be greater 0." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aRandomSeed must be greater/equal 0."); + } + // + + this.art2aData = anArt2aData; + this.maximumNumberOfClusters = aMaximumNumberOfClusters; + this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; + this.convergenceThreshold = aConvergenceThreshold; + this.learningParameter = aLearningParameter; + this.randomSeed = aRandomSeed; + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). + * + * @param anArt2aData ART-2a data object created by method + * Art2aKernel.getArt2aData() + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aKernel( + Art2aData anArt2aData + ) throws IllegalArgumentException { + this( + anArt2aData, + (int) (anArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? + (int) (anArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) : + 2, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_LEARNING_PARAMETER, + DEFAULT_RANDOM_SEED + ); + } + // + + // + /** + * Performs ART-2a clustering and returns corresponding + * Art2aResult. + * + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @return Art2aResult instance + * @throws IllegalArgumentException Thrown if argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public Art2aResult getClusterResult( + float aVigilance + ) throws Exception { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResult: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aKernel.getClusterResult: aVigilance must be in interval (0,1)."); + } + // + + try { + Random tmpRandomNumberGenerator = new Random(this.randomSeed); + boolean tmpIsClusterOverflow = false; + + float[][] tmpDataMatrix = null; + float[][] tmpContrastEnhancedUnitMatrix = null; + // Flags array that indicates if data row vectors have a length + // of zero (i.e. where all components are equal to zero). True: + // Data row vector has a length of zero, false: Otherwise. + boolean[] tmpDataVectorZeroLengthFlags = null; + int tmpNumberOfComponents = -1; + int tmpNumberOfDataVectors = -1; + if (this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitMatrix = (float[][]) this.art2aData.getContrastEnhancedUnitMatrix(); + tmpDataVectorZeroLengthFlags = (boolean[]) this.art2aData.getDataVectorZeroLengthFlags(); + tmpNumberOfDataVectors = tmpContrastEnhancedUnitMatrix.length; + tmpNumberOfComponents = tmpContrastEnhancedUnitMatrix[0].length; + } else { + tmpDataMatrix = this.art2aData.getDataMatrix(); + tmpDataVectorZeroLengthFlags = new boolean[tmpDataMatrix.length]; + Art2aUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + tmpNumberOfDataVectors = tmpDataMatrix.length; + tmpNumberOfComponents = tmpDataMatrix[0].length; + } + Art2aUtils.MinMaxValue[] tmpMinMaxComponents = this.art2aData.getMinMaxComponentsOfDataMatrix(); + + // Definitions + float tmpThresholdForContrastEnhancement = + Art2aUtils.getThresholdForContrastEnhancement( + tmpNumberOfComponents, + this.art2aData.getOffsetForContrastEnhancement() + ); + // Scaling factor alpha + float tmpScalingFactor = tmpThresholdForContrastEnhancement; + + // Initialize cluster matrix and that for previous epoch (old) with + // all row vectors being null + float[][] tmpClusterMatrix = new float[this.maximumNumberOfClusters][]; + float[][] tmpClusterMatrixOld = new float[this.maximumNumberOfClusters][]; + // Cluster usage flags. True: Cluster is used, false: Cluster is + // empty and can be removed. + boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; + + // Initialize cluster indices for data row vectors with -1 to + // indicate missing cluster assignment + int[] tmpClusterIndexOfDataVector = new int[tmpNumberOfDataVectors]; + Art2aUtils.fillVector(tmpClusterIndexOfDataVector, -1); + + // Initialize random indices + int[] tmpRandomIndices = new int[tmpNumberOfDataVectors]; + for(int i = 0; i < tmpRandomIndices.length; i++) { + tmpRandomIndices[i] = i; + } + + // Initialize buffer vector for vector operations + float[] tmpBufferVector = new float[tmpNumberOfComponents]; + + // Main clustering loop + int tmpCurrentNumberOfEpochs = 0; + int tmpNumberOfDetectedClusters = 0; + Art2aUtils.RhoWinner tmpRhoWinner = new Art2aUtils.RhoWinner(); + Art2aUtils.ClusterRemovalInfo tmpClusterRemovalInfo = new Art2aUtils.ClusterRemovalInfo(); + boolean tmpIsConverged = false; + while(!tmpIsConverged && tmpCurrentNumberOfEpochs < this.maximumNumberOfEpochs) { + tmpCurrentNumberOfEpochs++; + + // Get random sequence of indices for data row vectors + Art2aUtils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); + + Arrays.fill(tmpClusterUsageFlags, false); + for(int i = 0; i < tmpNumberOfDataVectors; i++) { + int tmpRandomIndex = tmpRandomIndices[i]; + + if (tmpDataVectorZeroLengthFlags[tmpRandomIndex]) { + // Shifted data row vector has length of zero: Ignore! + continue; + } + + if (this.art2aData.hasPreprocessedData()) { + Art2aUtils.copyVector(tmpContrastEnhancedUnitMatrix[tmpRandomIndex], tmpBufferVector); + } else { + tmpDataVectorZeroLengthFlags[tmpRandomIndex] = + Art2aUtils.setContrastEnhancedUnitVector( + tmpDataMatrix[tmpRandomIndex], + tmpBufferVector, + tmpMinMaxComponents, + tmpThresholdForContrastEnhancement + ); + if (tmpDataVectorZeroLengthFlags[tmpRandomIndex]) { + continue; + } + } + + if(tmpNumberOfDetectedClusters == 0) { + // Create first cluster + Art2aUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; + tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; + tmpNumberOfDetectedClusters++; + } else { + // Cluster number is greater than or equal to 1 + Art2aUtils.setRhoWinner( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoWinner + ); + // Assign to existing cluster or increment clusters + if(tmpRhoWinner.getIndexOfCluster() < 0 || tmpRhoWinner.getRhoValue() < aVigilance) { + // Increment clusters (if possible) + if (tmpNumberOfDetectedClusters == this.maximumNumberOfClusters) { + tmpIsClusterOverflow = true; + } else { + // Increment clusters + Art2aUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; + tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; + tmpNumberOfDetectedClusters++; + } + } else { + // Assign to existing winner cluster with modification + // Note: tmpBufferVector (= contrast enhanced unit vector) + // is used for modification + Art2aUtils.modifyWinnerCluster( + tmpBufferVector, + tmpClusterMatrix[tmpRhoWinner.getIndexOfCluster()], + tmpThresholdForContrastEnhancement, + this.learningParameter + ); + tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpRhoWinner.getIndexOfCluster(); + tmpClusterUsageFlags[tmpRhoWinner.getIndexOfCluster()] = true; + } + } + } + Art2aUtils.removeEmptyClusters( + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpClusterRemovalInfo + ); + if (tmpClusterRemovalInfo.isClusterRemoved()) { + tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); + tmpIsConverged = false; + } else { + tmpIsConverged = + Art2aUtils.isConverged( + tmpNumberOfDetectedClusters, + tmpCurrentNumberOfEpochs, + tmpClusterMatrix, + tmpClusterMatrixOld, + this.maximumNumberOfEpochs, + this.convergenceThreshold + ); + } + } + // Check if cluster overflow occurred + if (tmpIsClusterOverflow) { + // Cluster overflow occurred: Finally assign ALL data vectors + Art2aUtils.assignDataVectorsToClusters( + tmpNumberOfDetectedClusters, + tmpDataVectorZeroLengthFlags, + this.art2aData, + tmpBufferVector, + tmpThresholdForContrastEnhancement, + tmpClusterMatrix, + tmpClusterIndexOfDataVector, + tmpClusterUsageFlags + ); + // Remove possible empty clusters + Art2aUtils.removeEmptyClusters( + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpClusterRemovalInfo + ); + tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); + } + // Check if clusters were removed in last epoch and assure non-empty + // clusters in the cluster matrix + while (tmpClusterRemovalInfo.isClusterRemoved()) { + // Empty clusters are removed: Assign data vectors again + Art2aUtils.assignDataVectorsToClusters( + tmpNumberOfDetectedClusters, + tmpDataVectorZeroLengthFlags, + this.art2aData, + tmpBufferVector, + tmpThresholdForContrastEnhancement, + tmpClusterMatrix, + tmpClusterIndexOfDataVector, + tmpClusterUsageFlags + ); + Art2aUtils.removeEmptyClusters( + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpClusterRemovalInfo + ); + tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); + } + return new Art2aResult( + aVigilance, + tmpThresholdForContrastEnhancement, + tmpCurrentNumberOfEpochs, + tmpNumberOfDetectedClusters, + tmpClusterIndexOfDataVector, + tmpClusterMatrix, + tmpDataVectorZeroLengthFlags, + tmpIsClusterOverflow, + this.art2aData + ); + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResult: An exception occurred: This should never happen!" + ); + Art2aKernel.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + throw anException; + } + } + + /** + * Performs ART-2a clustering for specified vigilance parameters and + * returns corresponding Art2aResult objects. + * + * @param aVigilances Vigilance parameters (must each be in interval (0,1)) + * @return Art2aResult objects or null if clustering result could + * not be calculated. + * @param aNumberOfConcurrentCalculationThreads Number of concurrent + * calculation threads for the different vigilance parameters to be + * calculated concurrently (in parallel). If zero, then the different + * vigilance parameters are calculated one after another (sequentially) + * @throws IllegalArgumentException Thrown if argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public Art2aResult[] getClusterResults( + float[] aVigilances, + int aNumberOfConcurrentCalculationThreads + ) throws Exception { + // + if (aVigilances == null || aVigilances.length == 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: aVigilances is null or has length 0." + ); + throw new IllegalArgumentException("Art2aKernel.getClusterResults: aVigilances is null or has length 0."); + } + for (float tmpVigilance : aVigilances) { + if(tmpVigilance <= 0.0f || tmpVigilance >= 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: Vigilance parameter must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aKernel.getClusterResults: Vigilance parameter must be in interval (0,1)."); + } + } + if (aNumberOfConcurrentCalculationThreads < 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0." + ); + throw new IllegalArgumentException("Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0."); + } + // + + if (aNumberOfConcurrentCalculationThreads > 0) { + LinkedList tmpSingleTaskList = new LinkedList<>(); + for (float tmpVigilance : aVigilances) { + tmpSingleTaskList.add(new HelperTask(this, tmpVigilance)); + } + ExecutorService tmpExecutorService = Executors.newFixedThreadPool(aNumberOfConcurrentCalculationThreads); + List> tmpFutureList = null; + try { + tmpFutureList = tmpExecutorService.invokeAll(tmpSingleTaskList); + } catch (InterruptedException anInterruptedException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: Interrupted exception during concurrent calculation: This should never happen." + ); + throw anInterruptedException; + } + tmpExecutorService.shutdown(); + Art2aResult[] tmpParallelResults = new Art2aResult[aVigilances.length]; + int tmpIndex = 0; + for (Future tmpFuture : tmpFutureList) { + try { + tmpParallelResults[tmpIndex++] = tmpFuture.get(); + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: Exception in tmpFuture.get()." + ); + throw anException; + } + } + return tmpParallelResults; + } else { + Art2aResult[] tmpSequentialResults = new Art2aResult[aVigilances.length]; + for (int i = 0; i < aVigilances.length; i++) { + tmpSequentialResults[i] = this.getClusterResult(aVigilances[i]); + } + return tmpSequentialResults; + } + } + + /** + * Nearest (smaller) indices of approximants to the desired number of + * representatives. + * + * @param aNumberOfRepresentatives Number of representatives (MUST be + * greater or equal to 2) + * @param aVigilanceMin Minimal vigilance parameter (must be in interval + * (0,1)) + * @param aVigilanceMax Maximal vigilance parameter (must be in interval + * (0,1)) + * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or + * equal to 1) + * @return Nearest (smaller) indices of approximants to the desired number + * of representatives. + * @throws IllegalArgumentException Thrown if an argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public int[] getRepresentatives( + int aNumberOfRepresentatives, + float aVigilanceMin, + float aVigilanceMax, + int aNumberOfTrialSteps + ) throws IllegalArgumentException, Exception { + // + if(aNumberOfRepresentatives < 2) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2." + ); + throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2."); + } + if(aVigilanceMin <= 0.0f || aVigilanceMin >= 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)."); + } + if(aVigilanceMax <= 0.0f || aVigilanceMax >= 1.0f) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)."); + } + if(aVigilanceMin >= aVigilanceMax) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax." + ); + throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax."); + } + if(aNumberOfTrialSteps < 1) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1." + ); + throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1."); + } + // + + try { + Art2aResult tmpArt2aResult = this.getClusterResult(aVigilanceMin); + int[] tmpRepresentativeIndicesOfClusters = tmpArt2aResult.getRepresentativeIndicesOfClusters(); + if (tmpArt2aResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { + return tmpRepresentativeIndicesOfClusters; + } + tmpArt2aResult = this.getClusterResult(aVigilanceMax); + if (tmpArt2aResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { + return tmpArt2aResult.getRepresentativeIndicesOfClusters(); + } + + float tmpVigilanceMin = aVigilanceMin; + float tmpVigilanceMax = aVigilanceMax; + for (int i = 0; i < aNumberOfTrialSteps; i++) { + float tmpVigilanceMean = (tmpVigilanceMin + tmpVigilanceMax) / 2.0f; + tmpArt2aResult = this.getClusterResult(tmpVigilanceMean); + if (tmpArt2aResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { + tmpVigilanceMax = tmpVigilanceMean; + } else if (tmpArt2aResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { + tmpVigilanceMin = tmpVigilanceMean; + tmpRepresentativeIndicesOfClusters = tmpArt2aResult.getRepresentativeIndicesOfClusters(); + } else { + return tmpArt2aResult.getRepresentativeIndicesOfClusters(); + } + } + return tmpRepresentativeIndicesOfClusters; + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getRepresentatives: An exception occurred: This should never happen!" + ); + Art2aKernel.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + throw anException; + } + } + + /** + * Returns representatives whose mean distance is nearest to the mean + * distance of all data vectors of specified original data matrix. + * Note: This is a O(N^2) operation, N: Number of data vectors. + * + * @param aDataMatrix Original data matrix (IS NOT CHANGED and NOT properly + * CHECKED) + * @param aMinimumNumberOfRepresentatives Minimum number of representatives + * @param aMaximumNumberOfRepresentatives Maximum number of representatives + * @return Representatives whose mean distance is nearest to the mean + * distance of all data vectors of specified original data matrix. + * @throws IllegalArgumentException Thrown if an argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public int[] getBestRepresentatives( + float[][] aDataMatrix, + int aMinimumNumberOfRepresentatives, + int aMaximumNumberOfRepresentatives + ) throws IllegalArgumentException, Exception { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getBestRepresentatives: aDataMatrix is null/has length zero." + ); + throw new IllegalArgumentException("Art2aKernel.getBestRepresentatives: aDataMatrix is null/has length zero."); + } + if(aMinimumNumberOfRepresentatives < 2 || aMinimumNumberOfRepresentatives > aDataMatrix.length - 1) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid." + ); + throw new IllegalArgumentException("Art2aKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid."); + } + if(aMaximumNumberOfRepresentatives <= aMinimumNumberOfRepresentatives || aMaximumNumberOfRepresentatives > aDataMatrix.length) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid." + ); + throw new IllegalArgumentException("Art2aKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid."); + } + // + + try { + int[] tmpAllIndices = new int[aDataMatrix.length]; + for (int i = 0; i < tmpAllIndices.length; i++) { + tmpAllIndices[i] = i; + } + float tmpBaseMeanDistance = Art2aUtils.getMeanDistance(aDataMatrix, tmpAllIndices); + + float tmpVigilanceMin = 0.0001f; + float tmpVigilanceMax = 0.9999f; + int tmpNumberOfTrialSteps = 32; + + float tmpMinimalDifference = Float.MAX_VALUE; + int[] tmpBestRepresentatives = null; + for (int i = aMinimumNumberOfRepresentatives; i < aMaximumNumberOfRepresentatives; i++) { + int[] tmpRepresentatives = + this.getRepresentatives( + i, + tmpVigilanceMin, + tmpVigilanceMax, + tmpNumberOfTrialSteps + ); + float tmpMeanDistance = Art2aUtils.getMeanDistance(aDataMatrix, tmpRepresentatives); + float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); + if (tmpDifference < tmpMinimalDifference) { + tmpMinimalDifference = tmpDifference; + tmpBestRepresentatives = tmpRepresentatives; + } + } + return tmpBestRepresentatives; + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getBestRepresentatives: An exception occurred: This should never happen!" + ); + Art2aKernel.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + throw anException; + } + } + // + // + /** + * Checks if aDataMatrix is valid. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @return True if aDataMatrix is valid, false otherwise. + */ + public static boolean isDataMatrixValid( + float[][] aDataMatrix + ) { + if(aDataMatrix == null || aDataMatrix.length == 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.isDataMatrixValid: aDataMatrixis is null or empty." + ); + return false; + } + + int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; + if(tmpNumberOfDataVectorComponents < 2) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.isDataMatrixValid: Data row vectors must have at least 2 components." + ); + return false; + } + + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.isDataMatrixValid: A data row vector of aDataMatrix is not allowed to be null or empty." + ); + return false; + } + + if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.isDataMatrixValid: Data row vectors in aDataMatrix must have the same length." + ); + return false; + } + + for (float tmpValue : tmpDataVector) { + if (tmpValue == Float.NaN + || tmpValue == Float.MIN_VALUE + || tmpValue == Float.MAX_VALUE + ) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.isDataMatrixValid: NaN/Float.MIN/Float.MAX are not allowed." + ); + return false; + } + } + } + return true; + } + + /** + * Creates ART-2a data object with preprocessed data for maximum speed + * of the clustering process. The ART-2a data object allocates about the + * same memory as aDataMatrix. + *
+ * Note: aDataMatrix could be set to null after this operation to release + * its memory. + + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * with Art2aKernel.isDataMatrixValid() in advance) + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @return ART-2a data object for maximum clustering speed but with + * additionally allocated memory (about the same memory as aDataMatrix) + */ + public static Art2aData getArt2aData( + float[][] aDataMatrix, + float anOffsetForContrastEnhancement + ) { + int tmpNumberOfComponents = aDataMatrix[0].length; + float tmpThresholdForContrastEnhancement = + Art2aUtils.getThresholdForContrastEnhancement( + tmpNumberOfComponents, + anOffsetForContrastEnhancement + ); + + // Initialize flags array for scaled data row vectors which have a + // length of zero (i.e. where all components are equal to zero) + boolean[] tmpDataVectorZeroLengthFlags = new boolean[aDataMatrix.length]; + Art2aUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + + float[][] tmpContrastEnhancedUnitMatrix = new float[aDataMatrix.length][]; + + Art2aUtils.MinMaxValue[] tmpMinMaxComponents = Art2aUtils.getMinMaxComponents(aDataMatrix); + + for(int i = 0; i < aDataMatrix.length; i++) { + float[] tmpContrastEnhancedUnitVector = new float[tmpNumberOfComponents]; + tmpDataVectorZeroLengthFlags[i] = + Art2aUtils.setContrastEnhancedUnitVector( + aDataMatrix[i], + tmpContrastEnhancedUnitVector, + tmpMinMaxComponents, + tmpThresholdForContrastEnhancement + ); + tmpContrastEnhancedUnitMatrix[i] = tmpContrastEnhancedUnitVector; + } + return new Art2aData( + tmpContrastEnhancedUnitMatrix, + tmpDataVectorZeroLengthFlags, + tmpMinMaxComponents, + anOffsetForContrastEnhancement + ); + } + + /** + * Creates ART-2a data object with preprocessed data for maximum speed + * of the clustering process. The ART-2a data object allocates about twice + * the memory of aDataMatrix. A default value of 0.5 is used for the offset + * for contrast enhancement. + *
+ * Note: aDataMatrix could be set to null after this operation to release + * its memory. + + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * with Art2aKernel.isDataMatrixValid() in advance) + * @return ART-2a data object for maximum clustering speed but with + * additionally allocated memory (about the same memory as aDataMatrix) + */ + public static Art2aData getArt2aData( + float[][] aDataMatrix + ) { + return Art2aKernel.getArt2aData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); + } + //
+ +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java new file mode 100644 index 0000000..d240fbc --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -0,0 +1,505 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Result of an ART-2a clustering process. + *

+ * Note: Art2aResult is a read-only class, i.e. thread-safe. In addition, there + * are NO internal calculated values cached, i.e. each method call performs + * a full calculation procedure. An Art2aResult object may be distributed to + * several concurrent (parallelized) evaluation tasks without any mutual + * interference problems. + * + * @author Betuel Sevindik, Achim Zielesny + */ +public class Art2aResult { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aResult.class.getName()); + // + // + /** + * Conversion constant from radiant to degree + */ + private static final float CONVERSION_TO_DEGREE = 180.0f / (float) Math.PI; + // + // + /** + * Cluster index of data vector + */ + private final int[] clusterIndexOfDataVector; + /** + * Vigilance parameter + */ + private final float vigilance; + /** + * Threshold for contrast enhancement + */ + private final float thresholdForContrastEnhancement; + /** + * Number of epochs + */ + private final int numberOfEpochs; + /** + * Number of detected clusters + */ + private final int numberOfDetectedClusters; + /** + * Cluster matrix + */ + private final float[][] clusterMatrix; + /** + * Array with flags. True: Scaled data vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise + */ + private final boolean[] dataVectorZeroLengthFlags; + /** + * True: Cluster overflow occurred, false: Otherwise + */ + private final boolean isClusterOverflow; + /** + * Art2aData object + */ + private final Art2aData art2aData; + // + // + /** + * Indexed value + */ + private record IndexedValue ( + int index, + float value + ) implements Comparable { + + /** + * Constructor + * + * @param index Index + * @param value Value + */ + public IndexedValue {} + + @Override + public int compareTo(IndexedValue anotherIndexedValue) { + return Float.compare(value, anotherIndexedValue.value()); + } + } + // + + // + /** + * Constructor. + * Note: No checks are performed. + * + * @param aVigilance Vigilance parameter in interval (0,1) + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @param aNumberOfEpochs Number of epochs used for clustering + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterIndexOfDataVector Cluster index of data vector + * @param aClusterMatrix Cluster matrix + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param anIsClusterOverflow True: Cluster overflow occurred, false: + * Otherwise + * @param anArt2aData Art2aData instance + */ + public Art2aResult( + float aVigilance, + float aThresholdForContrastEnhancement, + int aNumberOfEpochs, + int aNumberOfDetectedClusters, + int[] aClusterIndexOfDataVector, + float[][] aClusterMatrix, + boolean[] aDataVectorZeroLengthFlags, + boolean anIsClusterOverflow, + Art2aData anArt2aData + ) { + this.vigilance = aVigilance; + this.thresholdForContrastEnhancement = aThresholdForContrastEnhancement; + this.numberOfEpochs = aNumberOfEpochs; + this.numberOfDetectedClusters = aNumberOfDetectedClusters; + this.clusterIndexOfDataVector = aClusterIndexOfDataVector; + this.clusterMatrix = aClusterMatrix; + this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; + this.isClusterOverflow = anIsClusterOverflow; + this.art2aData = anArt2aData; + } + // + + // + /** + * Returns specified cluster vector with index aClusterIndex in + * clusterMatrix. + * + * @param aClusterIndex Index of cluster vector in clusterMatrix + * @return Specified cluster vector + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public float[] getClusterVector( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getClusterVector: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aResult.getClusterVector: aClusterIndex is illegal."); + } + // + return this.clusterMatrix[aClusterIndex]; + } + + /** + * Returns specified cluster vector with index aClusterIndex in + * cluster matrix with components being scaled to interval [0,1]. + * Note: Cluster matrix is NOT changed. + * + * @param aClusterIndex Index of cluster vector in cluster matrix + * @return Specified scaled cluster vector + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public float[] getScaledClusterVector( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getClusterVector: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aResult.getClusterVector: aClusterIndex is illegal."); + } + // + return Art2aUtils.getScaledVector(this.clusterMatrix[aClusterIndex]); + } + + /** + * Returns indices of data vectors in original data matrix that belong to + * the specified cluster with index aClusterIndex. + * Note: The returned indices are cached for successive fast usage. + * + * @param aClusterIndex Index of cluster in cluster matrix + * @return Indices of data vectors in original data matrix that belong to + * the specified cluster with index aClusterIndex. + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public int[] getDataVectorIndicesOfCluster( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("rt2aClusteringResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal."); + } + // + + LinkedList tmpIndexListOfCluster = new LinkedList<>(); + for (int i = 0; i < this.clusterIndexOfDataVector.length; i++) { + if (this.clusterIndexOfDataVector[i] == aClusterIndex) { + tmpIndexListOfCluster.add(i); + } + } + return tmpIndexListOfCluster.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Returns all indices of (scaled) data vectors that have a length of + * zero. The indices refer to the original data matrix. + * Note: The returned indices are cached for successive fast usage. + * + * @return All indices of (scaled) data vectors that have a length of + * zero. The indices refer to the original data matrix. + */ + public int[] getZeroLengthDataVectorIndices() { + LinkedList tmpIndexList = new LinkedList<>(); + for (int i = 0; i < this.dataVectorZeroLengthFlags.length; i++) { + if (this.dataVectorZeroLengthFlags[i]) { + tmpIndexList.add(i); + } + } + return tmpIndexList.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Return angle in degree between specified clusters with aClusterIndex1 and + * aClusterIndex2. + * + * @param aClusterIndex1 Index of cluster 1 in cluster matrix + * @param aClusterIndex2 Index of cluster 2 in cluster matrix + * @return Angle in degree between specified clusters with aClusterIndex1 + * and aClusterIndex2. + * @throws IllegalArgumentException Thrown if an argument is illegal. + */ + public float getAngleBetweenClusters( + int aClusterIndex1, + int aClusterIndex2 + ) throws IllegalArgumentException { + // + if(aClusterIndex1 < 0 || aClusterIndex1 >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getAngleBetweenClusters: aClusterIndex1 is illegal." + ); + throw new IllegalArgumentException("Art2aResult.getAngleBetweenClusters: aClusterIndex1 is illegal."); + } + if(aClusterIndex2 < 0 || aClusterIndex2 >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getAngleBetweenClusters: aClusterIndex2 is illegal." + ); + throw new IllegalArgumentException("Art2aResult.getAngleBetweenClusters: aClusterIndex2 is illegal."); + } + // + if (aClusterIndex1 == aClusterIndex2) { + return 0.0f; + } else { + return + (float) Math.acos( + Art2aUtils.getScalarProduct( + this.clusterMatrix[aClusterIndex1], + this.clusterMatrix[aClusterIndex2] + ) + ) * CONVERSION_TO_DEGREE; + } + } + + /** + * Returns size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the + * cluster. + * Note: The internally evaluated indices of data vectors that belong to the + * specified cluster are cached for successive fast usage. + * + * @param aClusterIndex Index of cluster in cluster matrix + * @return Size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the + * cluster. + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public int getClusterSize( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getClusterSize: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("rt2aClusteringResult.getClusterSize: aClusterIndex is illegal."); + } + // + + int tmpCounter = 0; + for (int i = 0; i < this.clusterIndexOfDataVector.length; i++) { + if (this.clusterIndexOfDataVector[i] == aClusterIndex) { + tmpCounter++; + } + } + return tmpCounter; + } + + /** + * Returns if cluster overflow occurred. + * + * @return True: Cluster overflow occurred, false: Otherwise + */ + public boolean isClusterOverflow() { + return this.isClusterOverflow; + } + + /** + * Calculates index of representative data vector which is closest to the + * specified cluster vector with index aClusterIndex. + * + * @param aClusterIndex Index of cluster vector in cluster matrix + * @return Index of representative data vector which is closest to the + * specified cluster vector with index aClusterIndex + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public int getClusterRepresentativeIndex( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getClusterRepresentativeIndex: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aResult.getClusterRepresentativeIndex: aClusterIndex is illegal."); + } + // + int[] tmpDataVectorIndicesOfCluster = this.getDataVectorIndicesOfCluster(aClusterIndex); + if (tmpDataVectorIndicesOfCluster.length == 1) { + return tmpDataVectorIndicesOfCluster[0]; + } + float[] tmpClusterVector = this.clusterMatrix[aClusterIndex]; + int tmpBestIndex = 0; + float tmpMaximumScalarProduct = Float.MIN_VALUE; + float[] tmpContrastEnhancedUnitVector = null; + if (!this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitVector = new float[tmpClusterVector.length]; + } + for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { + int tmpIndex = tmpDataVectorIndicesOfCluster[i]; + if (this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitVector = this.art2aData.getContrastEnhancedUnitMatrix()[tmpIndex]; + } else { + // Check of length is NOT necessary + Art2aUtils.setContrastEnhancedUnitVector( + this.art2aData.getDataMatrix()[tmpIndex], + tmpContrastEnhancedUnitVector, + this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.thresholdForContrastEnhancement + ); + } + float tmpScalarProduct = Art2aUtils.getScalarProduct(tmpContrastEnhancedUnitVector, tmpClusterVector); + if (tmpScalarProduct > tmpMaximumScalarProduct) { + tmpBestIndex = tmpIndex; + tmpMaximumScalarProduct = tmpScalarProduct; + } + } + return tmpBestIndex; + } + + /** + * Calculates array of indices of sorted representative data vectors of the + * specified cluster with index aClusterIndex. The data vector with index 0 + * is closest to the cluster vector, the one with index 1 is the second + * closest etc. + * + * @param aClusterIndex Index of cluster vector in cluster matrix + * @return Array of indices of sorted representative data vectors of the + * specified cluster + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public int[] getClusterRepresentativeIndices( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aResult.LOGGER.log( + Level.SEVERE, + "Art2aResult.getClusterRepresentativeIndices: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aResult.getClusterRepresentativeIndices: aClusterIndex is illegal."); + } + // + int[] tmpDataVectorIndicesOfCluster = this.getDataVectorIndicesOfCluster(aClusterIndex); + if (tmpDataVectorIndicesOfCluster.length == 1) { + return tmpDataVectorIndicesOfCluster; + } + float[] tmpClusterVector = this.clusterMatrix[aClusterIndex]; + IndexedValue[] tmpIndexedValues = new IndexedValue[tmpDataVectorIndicesOfCluster.length]; + float[] tmpContrastEnhancedUnitVector = null; + if (!this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitVector = new float[tmpClusterVector.length]; + } + for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { + int tmpIndex = tmpDataVectorIndicesOfCluster[i]; + if (this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitVector = this.art2aData.getContrastEnhancedUnitMatrix()[tmpIndex]; + } else { + // Check of length is NOT necessary + Art2aUtils.setContrastEnhancedUnitVector( + this.art2aData.getDataMatrix()[tmpIndex], + tmpContrastEnhancedUnitVector, + this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.thresholdForContrastEnhancement + ); + } + tmpIndexedValues[i] = new IndexedValue(tmpIndex, Art2aUtils.getScalarProduct(tmpContrastEnhancedUnitVector, tmpClusterVector)); + } + // NOTE: LARGEST scalar product FIRST! + Arrays.sort(tmpIndexedValues, Collections.reverseOrder()); + int[] tmpClusterRepresentativeIndices = new int[tmpIndexedValues.length]; + for (int i = 0; i < tmpIndexedValues.length; i++) { + tmpClusterRepresentativeIndices[i] = tmpIndexedValues[i].index(); + } + return tmpClusterRepresentativeIndices; + } + + /** + * Returns data vector indices which are closest to their cluster vectors. + * + * @return Data vector indices which are closest to their cluster vectors + */ + public int[] getRepresentativeIndicesOfClusters() { + int[] tmpRepresentativeIndicesOfClusters = new int[this.numberOfDetectedClusters]; + for (int i = 0; i < this.numberOfDetectedClusters; i++) { + tmpRepresentativeIndicesOfClusters[i] = this.getClusterRepresentativeIndex(i); + } + return tmpRepresentativeIndicesOfClusters; + } + + /** + * Vigilance parameter + * + * @return Vigilance parameter + */ + public float getVigilance() { + return this.vigilance; + }; + + /** + * Number of epochs + * + * @return Number of epochs + */ + public int getNumberOfEpochs() { + return this.numberOfEpochs; + }; + + /** + * Number of detected clusters + * + * @return Number of detected clusters + */ + public int getNumberOfDetectedClusters() { + return this.numberOfDetectedClusters; + }; + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java new file mode 100644 index 0000000..1077f97 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -0,0 +1,304 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Callable that wraps an Art2aKernel instance where the call() method returns + * an Art2aResult object. See Art2aKernel for further details. + * + * @author Betuel Sevindik, Achim Zielesny + */ +public class Art2aTask implements Callable { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aTask.class.getName()); + // + // + /** + * ART-2a clustering kernel instance + */ + private final Art2aKernel art2aClusteringKernel; + /** + * Vigilance parameter (must be in interval (0,1)) + */ + private final float vigilance; + // + + // + /** + * Constructor. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * similarity (must be in interval (0,1)) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * Otherwise. + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + public Art2aTask( + float[][] aDataMatrix, + float aVigilance, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + float anOffsetForContrastEnhancement, + long aRandomSeed, + boolean anIsDataPreprocessing + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aKernel( + aDataMatrix, + aMaximumNumberOfClusters, + aMaximumNumberOfEpochs, + aConvergenceThreshold, + aLearningParameter, + anOffsetForContrastEnhancement, + aRandomSeed, + anIsDataPreprocessing + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: Can not instantiate Art2aKernel object." + ); + Art2aTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * (= 1.0) and RANDOM_SEED (= 1). + * Note: There is NO data preprocessing. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aTask( + float[][] aDataMatrix, + float aVigilance + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aKernel( + aDataMatrix + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: Can not instantiate Art2aKernel object." + ); + Art2aTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + + /** + * Constructor. + * + * @param anArt2aData ART-2a data object created by method + * Art2aKernel.getArt2aData() + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * similarity (must be in interval (0,1)) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + public Art2aTask( + Art2aData anArt2aData, + float aVigilance, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + long aRandomSeed + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aKernel( + anArt2aData, + aMaximumNumberOfClusters, + aMaximumNumberOfEpochs, + aConvergenceThreshold, + aLearningParameter, + aRandomSeed + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: Can not instantiate Art2aKernel object." + ); + Art2aTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). + * + * @param anArt2aData ART-2a data object created by method + * Art2aKernel.getArt2aData() + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aTask( + Art2aData anArt2aData, + float aVigilance + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aKernel( + anArt2aData + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.Constructor: Can not instantiate Art2aKernel object." + ); + Art2aTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + // + + // + /** + * Performs the clustering process. + * + * @return Clustering result or null if clustering process could not be + * performed. + */ + @Override + public Art2aResult call() { + try { + return this.art2aClusteringKernel.getClusterResult(this.vigilance); + } catch (Exception anException) { + Art2aTask.LOGGER.log( + Level.SEVERE, + "Art2aTask.call: Can not calculate a cluster result." + ); + Art2aTask.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + return null; + } + } + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java new file mode 100644 index 0000000..64569ba --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -0,0 +1,948 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Random; + +/** + * Library of helper records, static helper classes and static, thread-safe + * (stateless) utility methods for ART-2a clustering. + *

+ * Note: No checks are performed. + * + * @author Achim Zielesny + */ +public class Art2aUtils { + + // + /** + * Value 1.0 + */ + private static final float ONE = 1.0f; + // + // + /** + * Helper record: Minimum and maximum value. + *

+ * Note: No checks are performed. + * + * @param minValue Minimum value + * @param maxValue Maximum value + */ + protected record MinMaxValue(float minValue, float maxValue) { + + /** + * Constructor + * + * @param minValue Minimum value + * @param maxValue Maximum value + */ + public MinMaxValue {} + + } + //
+ // + /** + * Helper class: Rho winner. + *

+ * Note: No checks are performed. + */ + protected static class RhoWinner { + + // + /** + * Rho value + */ + private float rhoValue; + /** + * Index of cluster + */ + private int indexOfCluster; + // + + // + /** + * Constructor + */ + protected RhoWinner() {} + // + + // + /** + * Set rho winner + * + * @param aRhoValue Rho value + * @param anIndexOfCluster Index of cluster + */ + protected void setRhoWinner( + float aRhoValue, + int anIndexOfCluster + ) { + this.rhoValue = aRhoValue; + this.indexOfCluster = anIndexOfCluster; + } + + /** + * Rho value + * + * @return Rho value + */ + protected float getRhoValue() { + return this.rhoValue; + } + + /** + * Index of cluster + * + * @return Index of cluster + */ + protected int getIndexOfCluster() { + return this.indexOfCluster; + } + // + + } + + /** + * Helper class: Cluster removal info. + *

+ * Note: No checks are performed. + */ + protected static class ClusterRemovalInfo { + + // + /** + * True: Cluster is removed, false: Otherwise + */ + private boolean isClusterRemoved; + /** + * Number of detected clusters + */ + private int numberOfDetectedClusters; + // + + // + /** + * Constructor + */ + protected ClusterRemovalInfo() {} + // + + // + /** + * Set cluster removal info + * + * @param anIsClusterRemoved True: Cluster is removed, false: Otherwise + * @param aNumberOfDetectedClusters Number of detected clusters + */ + protected void setClusterRemovalInfo( + boolean anIsClusterRemoved, + int aNumberOfDetectedClusters + ) { + this.isClusterRemoved = anIsClusterRemoved; + this.numberOfDetectedClusters = aNumberOfDetectedClusters; + } + + /** + * True: Cluster is removed, false: Otherwise + * + * @return True: Cluster is removed, false: Otherwise + */ + protected boolean isClusterRemoved() { + return this.isClusterRemoved; + } + + /** + * Number of detected clusters + * + * @return Number of detected clusters + */ + protected int getNumberOfDetectedClusters() { + return this.numberOfDetectedClusters; + } + // + + } + //
+ + // + /** + * Constructor + */ + protected Art2aUtils() {} + // + + // + /** + * Assigns data vectors to clusters + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param anArt2aData Art2aData instance (IS NOT CHANGED) + * @param aBufferVector Buffer vector (MUST BE ALREADY INSTANTIATED) + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aClusterIndexOfDataVector Cluster index of data vector (MAY BE + * CHANGED and MUST ALREADY BE INSTANTIATED) + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (MAY BE CHANGED and MUST + * ALREADY BE INSTANTIATED) + */ + protected static void assignDataVectorsToClusters( + int aNumberOfDetectedClusters, + boolean[] aDataVectorZeroLengthFlags, + Art2aData anArt2aData, + float[] aBufferVector, + float aThresholdForContrastEnhancement, + float[][]aClusterMatrix, + int[] aClusterIndexOfDataVector, + boolean[] aClusterUsageFlags + ) { + Arrays.fill(aClusterUsageFlags, false); + for (int i = 0; i < aDataVectorZeroLengthFlags.length; i++) { + if (!aDataVectorZeroLengthFlags[i]) { + if (anArt2aData.hasPreprocessedData()) { + aBufferVector = anArt2aData.getContrastEnhancedUnitMatrix()[i]; + } else { + // Check of length is NOT necessary + Art2aUtils.setContrastEnhancedUnitVector( + anArt2aData.getDataMatrix()[i], + aBufferVector, + anArt2aData.getMinMaxComponentsOfDataMatrix(), + aThresholdForContrastEnhancement + ); + } + int tmpWinnerClusterIndex = + Art2aUtils.getClusterIndex( + aBufferVector, + aNumberOfDetectedClusters, + aClusterMatrix + ); + aClusterIndexOfDataVector[i] = tmpWinnerClusterIndex; + aClusterUsageFlags[tmpWinnerClusterIndex] = true; + } + } + } + + /** + * (Deep) Copies source matrix to destination matrix. Row vectors of + * destination matrix may not have been instantiated. + * + * @param aSourceMatrix Source matrix (IS NOT CHANGED) + * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + */ + protected static void copyMatrix( + float[][] aSourceMatrix, + float[][] aDestinationMatrix + ) { + for (int i = 0; i < aSourceMatrix.length; i++) { + if (aDestinationMatrix[i] == null) { + aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; + } + System.arraycopy( + aSourceMatrix[i], + 0, + aDestinationMatrix[i], + 0, + aSourceMatrix[i].length + ); + } + } + + /** + * (Deep) Copies specified number of rows of source matrix to destination + * matrix. Row vectors of destination matrix may not have been instantiated. + * + * @param aSourceMatrix Source matrix (IS NOT CHANGED) + * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + * @param aNumberOfRows Number of rows to be copied from source matrix to + * destination matrix + */ + protected static void copyRows( + float[][] aSourceMatrix, + float[][] aDestinationMatrix, + int aNumberOfRows + ) { + for (int i = 0; i < aNumberOfRows; i++) { + if (aDestinationMatrix[i] == null) { + aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; + } + System.arraycopy( + aSourceMatrix[i], + 0, + aDestinationMatrix[i], + 0, + aSourceMatrix[i].length + ); + } + } + + /** + * (Deep) Copies source vector to destination vector. + * + * @param aSourceVector Source vector (IS NOT CHANGED) + * @param aDestinationVector Destination vector (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + */ + protected static void copyVector( + float[] aSourceVector, + float[] aDestinationVector + ) { + System.arraycopy( + aSourceVector, + 0, + aDestinationVector, + 0, + aSourceVector.length + ); + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + */ + protected static void enhanceContrast( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + } + } + } + + /** + * Fills matrix with value. + * + * @param aMatrix Matrix (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillMatrix( + float[][] aMatrix, + float aValue + ) { + for (float [] tmpRowVector : aMatrix) { + Arrays.fill(tmpRowVector , aValue); + } + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + float[] aVector, + float aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + boolean[] aVector, + boolean aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + int[] aVector, + int aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Returns mean distance of all specified row vectors. + * + * @param aMatrix Matrix with row vectors (IS NOT CHANGED) + * @param anIndicesOfRowVectors Indices of row vectors of aMatrix + * @return Mean squared distance of all specified row vectors. + */ + protected static float getMeanDistance( + float[][] aMatrix, + int[] anIndicesOfRowVectors + ) { + float tmpSum = 0.0f; + for (int i = 0; i < anIndicesOfRowVectors.length; i++) { + for (int j = i + 1; j < anIndicesOfRowVectors.length; j++) { + tmpSum += (float) Math.sqrt(Art2aUtils.getSquaredDistance(aMatrix[anIndicesOfRowVectors[i]], aMatrix[anIndicesOfRowVectors[j]])); + } + } + return tmpSum / (float) (anIndicesOfRowVectors.length * (anIndicesOfRowVectors.length - 1) / 2); + } + + /** + * Returns index of cluster for contrast enhanced unit vector + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterMatrix Cluster matrix + * @return Index of cluster for contrast enhanced unit vector + */ + protected static int getClusterIndex( + float[] aContrastEnhancedUnitVector, + int aNumberOfDetectedClusters, + float[][] aClusterMatrix + ) { + float tmpMaxScalarProduct = Float.MIN_VALUE; + int tmpWinnerClusterIndex = -1; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpScalarProduct = Art2aUtils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); + if (tmpScalarProduct > tmpMaxScalarProduct) { + tmpMaxScalarProduct = tmpScalarProduct; + tmpWinnerClusterIndex = i; + } + } + return tmpWinnerClusterIndex; + } + + /** + * Returns min-max components for matrix where MinMaxValue[j] + * corresponds to column j of the row vectors of the matrix. The min-max + * components may be used to scale row vectors to interval [0,1], see + * method scaleVector(). + * + * @param aMatrix Matrix (IS NOT CHANGED) + * @return Min-max components + */ + protected static MinMaxValue[] getMinMaxComponents( + float[][] aMatrix + ) { + MinMaxValue[] tmpMinMaxComponents = new MinMaxValue[aMatrix[0].length]; + for (int j = 0; j < aMatrix[0].length; j++) { + float tmpMinValue = aMatrix[0][j]; + float tmpMaxValue = aMatrix[0][j]; + for (int i = 1; i < aMatrix.length; i++) { + if (aMatrix[i][j] < tmpMinValue) { + tmpMinValue = aMatrix[i][j]; + } else if (aMatrix[i][j] > tmpMaxValue) { + tmpMaxValue = aMatrix[i][j]; + } + } + tmpMinMaxComponents[j] = new MinMaxValue(tmpMinValue, tmpMaxValue); + } + return tmpMinMaxComponents; + } + + /** + * Calculates the scalar product (dot product) of aVector1 and aVector2. + * + * @param aVector1 Vector 1 (IS NOT CHANGED) + * @param aVector2 Vector 2 (IS NOT CHANGED) + * @return Scalar product (dot product) + */ + protected static float getScalarProduct( + float[] aVector1, + float[] aVector2 + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector1.length; i++) { + // tmpSum += aVector1[i] * aVector2[i]; + tmpSum = Math.fma(aVector1[i], aVector2[i], tmpSum); + } + return tmpSum; + } + + /** + * Scales components of aVectorToBeScaled to interval [0,1]. + * + * @param aVectorToBeScaled Vector (IS NOT CHANGED) + * @return New scaled vector with components in interval [0,1] or new + * vector of length zero if all components of aVectorToBeScaled are the + * same. + */ + protected static float[] getScaledVector( + float[] aVectorToBeScaled + ) { + float tmpMinValue = aVectorToBeScaled[0]; + float tmpMaxValue = aVectorToBeScaled[0]; + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aVectorToBeScaled[i] < tmpMinValue) { + tmpMinValue = aVectorToBeScaled[i]; + } else if (aVectorToBeScaled[i] > tmpMaxValue) { + tmpMaxValue = aVectorToBeScaled[i]; + } + } + float[] tmpScaledVector = new float[aVectorToBeScaled.length]; + if (tmpMinValue == tmpMaxValue) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + tmpScaledVector[i] = aVectorToBeScaled[i] - tmpMinValue; + } + } else { + float tmpDenominator = tmpMaxValue - tmpMinValue; + for(int i = 0; i < aVectorToBeScaled.length; i++) { + tmpScaledVector[i] = (aVectorToBeScaled[i] - tmpMinValue) / tmpDenominator; + } + } + return tmpScaledVector; + } + + /** + * Calculates the squared distance between aVector1 and aVector2. + * + * @param aVector1 Vector 1 (IS NOT CHANGED) + * @param aVector2 Vector 2 (IS NOT CHANGED) + * @return Squared distance + */ + protected static float getSquaredDistance( + float[] aVector1, + float[] aVector2 + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector1.length; i++) { + float tmpDelta = aVector1[i] - aVector2[i]; + // tmpSum += (aVector1[i] - aVector2[i])^2; + tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); + } + return tmpSum; + } + + /** + * Calculates the sum of components of aVector. + * + * @param aVector Vector (IS NOT CHANGED) + * @return Sum of components + */ + protected static float getSumOfComponents( + float[] aVector + ) { + float tmpSum = 0.0f; + for (float tmpComponent : aVector) { + tmpSum += tmpComponent; + } + return tmpSum; + } + + /** + * Threshold for contrast enhancement + * + * @param aNumberOfComponents Number of components + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @return Threshold for contrast enhancement + */ + protected static float getThresholdForContrastEnhancement( + int aNumberOfComponents, + float anOffsetForContrastEnhancement + ) { + // Original code: + // return (float) (1.0 / Math.sqrt(aNumberOfComponents + 1.0)); + return (float) (1.0 / Math.sqrt(aNumberOfComponents + anOffsetForContrastEnhancement)); + } + + /** + * Calculates the length of aVector. + * + * @param aVector Vector (IS NOT CHANGED) + * @return Length of vector + */ + protected static float getVectorLength( + float[] aVector + ) { + float tmpSum = 0.0f; + for (float tmpComponent : aVector) { + // tmpSum += tmpComponent * tmpComponent; + tmpSum = Math.fma(tmpComponent, tmpComponent, tmpSum); + } + return (float) Math.sqrt(tmpSum); + } + + /** + * Checks if vector has a length of zero (i.e. if all components are equal + * to zero). + * + * @param aVector Vector (IS NOT CHANGED) + * @return True: Vector has a length of zero, false: Otherwise + */ + protected static boolean hasLengthOfZero( + float[] aVector + ) { + for(float tmpComponent : aVector) { + if (tmpComponent != 0.0f) { + return false; + } + } + return true; + } + + /** + * Removes empty clusters from cluster matrix + * + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (IS NOT CHANGED) + * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterRemovalInfo Cluster removal info (is set according to the + * operations performed, IS CHANGED) + */ + protected static void removeEmptyClusters( + boolean[] aClusterUsageFlags, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + ClusterRemovalInfo aClusterRemovalInfo + ) { + boolean tmpIsEmptyClusterRemoval = false; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (!aClusterUsageFlags[i]) { + tmpIsEmptyClusterRemoval = true; + break; + } + } + if (tmpIsEmptyClusterRemoval) { + // Remove empty clusters from cluster matrix + LinkedList tmpClusterVectorList = new LinkedList<>(); + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (aClusterUsageFlags[i]) { + tmpClusterVectorList.add(aClusterMatrix[i]); + aClusterMatrix[i] = null; + } + } + int tmpIndex = 0; + for (float[] tmpClusterVector : tmpClusterVectorList) { + aClusterMatrix[tmpIndex++] = tmpClusterVector; + } + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); + } else { + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); + } + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + * @return True if aVector is changed by contrast enhancement, false otherwise. + */ + protected static boolean isContrastEnhanced( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + boolean tmpIsVectorChanged = false; + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + tmpIsVectorChanged = true; + } + } + return tmpIsVectorChanged; + } + + /** + * Determines convergence of clustering process. + * Note: No checks are performed. + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param anEpoch Current epochs + * @param aClusterCentroidMatrix Cluster centroid matrix with centroid row + * vectors + * @param aClusterCentroidMatrixOld Cluster centroid matrix with + * centroid row vectors of the previous epoch + * @param aMaximumNumberOfEpochs Maximum number of epochs + * @param aConvergenceThreshold Convergence threshold + * @return True if clustering process has converged, false otherwise. + */ + protected static boolean isConverged( + int aNumberOfDetectedClusters, + int anEpoch, + float[][] aClusterCentroidMatrix, + float[][] aClusterCentroidMatrixOld, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold + ) { + if (anEpoch == 1) { + // Convergence check needs at least 2 epochs + Art2aUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + return false; + } else { + boolean tmpIsConverged = false; + if(anEpoch < aMaximumNumberOfEpochs) { + // Check convergence by evaluating the similarity (scalar product) + // of the cluster vectors of this and the previous epoch + tmpIsConverged = true; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if ( + aClusterCentroidMatrixOld[i] == null || + Art2aUtils.getScalarProduct(aClusterCentroidMatrix[i], aClusterCentroidMatrixOld[i]) < aConvergenceThreshold + ) { + tmpIsConverged = false; + break; + } + } + if(!tmpIsConverged) { + Art2aUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + } + } + return tmpIsConverged; + } + } + + /** + * Checks if matrix is valid. + * + * @param aMatrix Matrix + * @return True: Matrix is valid, false: Otherwise + */ + protected static boolean isMatrixValid( + float[][] aMatrix + ) { + if (aMatrix == null || aMatrix.length == 0) { + return false; + } + for (float[] tmpRowVector : aMatrix) { + if (tmpRowVector == null || tmpRowVector.length == 0) { + return false; + } + } + int tmpRowVectorLength = aMatrix[0].length; + for (int i = 1; i < aMatrix.length; i++) { + if (aMatrix[i].length != tmpRowVectorLength) { + return false; + } + } + return true; + } + + /** + * Modifies winner cluster (see code). + * Note: aContrastEnhancedUnitVector is used for modification and may be + * changed. + * Note: No checks are performed. + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector for + * modification (MAY BE CHANGED) + * @param aWinnerClusterVector Winner cluster centroid vector (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + * @param aLearningParameter Learning parameter + */ + protected static void modifyWinnerCluster( + float[] aContrastEnhancedUnitVector, + float[] aWinnerClusterVector, + float aThresholdForContrastEnhancement, + float aLearningParameter + ) { + // Note: aContrastEnhancedUnitVector is used for modification + boolean tmpIsChanged = false; + for(int j = 0; j < aWinnerClusterVector.length; j++) { + if(aWinnerClusterVector[j] <= aThresholdForContrastEnhancement) { + aContrastEnhancedUnitVector[j] = 0.0f; + tmpIsChanged = true; + } + } + float tmpFactor1; + if (tmpIsChanged) { + tmpFactor1 = aLearningParameter / Art2aUtils.getVectorLength(aContrastEnhancedUnitVector); + } else { + tmpFactor1 = aLearningParameter; + } + float tmpFactor2 = ONE - aLearningParameter; + for(int j = 0; j < aWinnerClusterVector.length; j++) { + aContrastEnhancedUnitVector[j] = tmpFactor1 * aContrastEnhancedUnitVector[j] + tmpFactor2 * aWinnerClusterVector[j]; + } + Art2aUtils.normalizeVector(aContrastEnhancedUnitVector); + Art2aUtils.copyVector(aContrastEnhancedUnitVector, aWinnerClusterVector); + } + + /** + * Calculates normalized (unit) vector of length 1. + * + * @param aVector Vector to be normalized (MAY BE CHANGED) + */ + protected static void normalizeVector( + float[] aVector + ) { + float tmpInverseVectorLength = ONE / Art2aUtils.getVectorLength(aVector); + for(int i = 0; i < aVector.length; i++) { + aVector[i] *= tmpInverseVectorLength; + } + } + + /** + * Sets rho winner with the rho value and the cluster index of the winner + * (see code). If the cluster index is negative the first scaled rho value + * is the winner. + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector (IS NOT + * CHANGED) + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aScalingFactor Scaling factor + * @param aRhoWinner Rho winner: Is set with the rho value and the cluster + * index of the winner. If the cluster index is negative the first scaled + * rho value is the winner. + */ + protected static void setRhoWinner( + float[] aContrastEnhancedUnitVector, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + float aScalingFactor, + Art2aUtils.RhoWinner aRhoWinner + ) { + // Calculate first rho value + float tmpRhoValue = aScalingFactor * Art2aUtils.getSumOfComponents(aContrastEnhancedUnitVector); + // Set winner index to negative value + int tmpIndex = -1; + // Calculate other rho values + for(int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpRhoForCluster = Art2aUtils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); + if(tmpRhoForCluster > tmpRhoValue) { + tmpRhoValue = tmpRhoForCluster; + tmpIndex = i; + } + } + aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); + } + + /** + * Sets copied (!) row vector at index in matrix. + * + * @param aMatrix Matrix (MAY BE CHANGED) + * @param aRowVector Row vector (IS NOT CHANGED) + * @param anIndex Index of row vector in matrix + */ + protected static void setRowVector( + float[][] aMatrix, + float[] aRowVector, + int anIndex + ) { + float[] tmpNewMatrixRowVector = new float[aRowVector.length]; + Art2aUtils.copyVector(aRowVector, tmpNewMatrixRowVector); + aMatrix[anIndex] = tmpNewMatrixRowVector; + } + + /** + * Transforms original data vector into corresponding contrast enhanced + * unit vector (see code). + * Note: No checks are performed. + * + * @param aDataVector Data vector (IS NOT CHANGED) + * @param aBufferVector Buffer vector for contrast enhanced unit vector + * derived from data vector (MUST ALREADY BE INSTANTIATED and is set within + * the method) + * @param aMinMaxComponents Min-max components of original data matrix + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @return True: Scaled data vector has a length of zero, false: Otherwise + */ + protected static boolean setContrastEnhancedUnitVector( + float[] aDataVector, + float[] aBufferVector, + Art2aUtils.MinMaxValue[] aMinMaxComponents, + float aThresholdForContrastEnhancement + ) { + // Already allocated memory of aBufferVector is reused + Art2aUtils.copyVector(aDataVector, aBufferVector); + // Scale components of vector to interval [0,1] + Art2aUtils.scaleVector(aBufferVector, aMinMaxComponents); + // Check length + if (Art2aUtils.hasLengthOfZero(aBufferVector)) { + // True: Scaled source vector has a length of zero + return true; + } else { + Art2aUtils.normalizeVector(aBufferVector); + // Enhance contrast + if (Art2aUtils.isContrastEnhanced(aBufferVector, aThresholdForContrastEnhancement)) { + Art2aUtils.normalizeVector(aBufferVector); + } + // False: Scaled data vector has a length different from zero + return false; + } + } + + /** + * Scales components of aVectorToBeScaled according to min-max components + * to interval [0,1] (see code and method getMinMaxComponents()). + * + * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) + * @param aMinMaxComponents Min-max components + */ + protected static void scaleVector( + float[] aVectorToBeScaled, + MinMaxValue[] aMinMaxComponents + ) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { + // Scale component to interval [0,1] + aVectorToBeScaled[i] = + (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); + } else { + // Shift component to zero + aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); + } + } + } + + /** + * Randomly shuffles indices from 0 to (anIndices.Length - 1) in + * anIndexArray using Fisher-Yates shuffling (i.e. the modern version + * introduced by Richard Durstenfeld). + * Note: No checks are performed. + * + * @param anIndexArray Array with indices from 0 to (anIndices.Length - 1) + * @param aRandomNumberGenerator Random number generator + */ + protected static void shuffleIndices( + int[] anIndexArray, + Random aRandomNumberGenerator + ) { + for (int i = anIndexArray.length - 1; i > 0; i--) { + // Generate a random index between 0 and i (inclusive) + int j = aRandomNumberGenerator.nextInt(i + 1); + // Swap the elements at indices i and j + int tmpIntBuffer = anIndexArray[i]; + anIndexArray[i] = anIndexArray[j]; + anIndexArray[j] = tmpIntBuffer; + } + } + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/abstractResult/Art2aAbstractResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/abstractResult/Art2aAbstractResult.java deleted file mode 100644 index 6ea3bbd..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/abstractResult/Art2aAbstractResult.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.abstractResult; - -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClusteringResult; - -import java.io.IOException; -import java.io.Writer; -import java.util.HashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Abstract class. - * This abstract class implements the IArt2aClusteringResult interface. - * The interface provides methods to access clustering results. - * The concrete implementation of the clustering result properties in the IArt2aClusteringResult interface - * is taken over by this abstract class. - * - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public abstract class Art2aAbstractResult implements IArt2aClusteringResult { - // - /** - * Queue of typ String for clustering result (process) - */ - private ConcurrentLinkedQueue clusteringProcess; - /** - * Queue of typ String for clustering result - */ - private ConcurrentLinkedQueue clusteringResult; - /** - * The map maps the cluster number to the number of inputs in the cluster. - */ - private HashMap clusterNumberToClusterMemberMap; - // - // - // - /** - * Represents the cluster assignment of each input vector. For example clusterView[4] = 0 means that - * input vector with index 4 cluster 0 has been assigned. - */ - private final int[] clusterView; - /** - * Final number of epochs the system needed to converge. - */ - private final int numberOfEpochs; - /** - * Final number of clusters detected after successful completion of clustering. - */ - private final int numberOfDetectedClusters; - /** - * Initial capacity value for maps - */ - private final double INITIAL_CAPACITY_VALUE = 1.5; - // - // - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(Art2aAbstractResult.class.getName()); - // - // - // - /** - * Constructor. - * - * @param aNumberOfEpochs final epoch number. - * @param aNumberOfDetectedClusters final number of detected clusters. - * @param aClusteringProcessQueue clustering result (process) queue of typ string. Queues are used or - * thread security but these here should not be used by more than one thread. - * @param aClusteringResultQueue clustering result queue of typ string. Queues are used or thread security - * but these here should not be used by more than one thread. - * @param aClusterView array for cluster assignment of each input vector. - * @throws IllegalArgumentException is thrown, if the given arguments are invalid. - */ - public Art2aAbstractResult(int aNumberOfEpochs, int aNumberOfDetectedClusters, - int[] aClusterView, ConcurrentLinkedQueue aClusteringProcessQueue, - ConcurrentLinkedQueue aClusteringResultQueue) throws IllegalArgumentException { - if(aNumberOfEpochs <= 0) { - throw new IllegalArgumentException("aNumberOfEpochs is invalid."); - } - if(aNumberOfDetectedClusters < 1) { - throw new IllegalArgumentException("aNumberOfDetectedClusters is invalid."); - } - this.clusterView = aClusterView; - this.numberOfEpochs = aNumberOfEpochs; - this.numberOfDetectedClusters = aNumberOfDetectedClusters; - this.clusteringProcess = aClusteringProcessQueue; - this.clusteringResult = aClusteringResultQueue; - this.clusterNumberToClusterMemberMap = this.getClusterSize(this.clusterView); - } - // - // - // - /** - * {@inheritDoc} - */ - @Override - public int getNumberOfEpochs() { - return this.numberOfEpochs; - } - // - /** - * {@inheritDoc} - */ - @Override - public int getNumberOfDetectedClusters() { - return this.numberOfDetectedClusters; - } - // - /** - * {@inheritDoc} - */ - @Override - public int[] getClusterIndices(int aClusterNumber) throws IllegalArgumentException { - if (aClusterNumber >= this.numberOfDetectedClusters) { - throw new IllegalArgumentException("The specified cluster number does not exist and exceeds " + - "the maximum number of clusters."); - } else { - int[] tmpIndicesInCluster = new int[this.clusterNumberToClusterMemberMap.get(aClusterNumber)]; - int tmpInputIndices = 0; - int tmpIterator = 0; - for (int tmpClusterMember : this.clusterView) { - if (tmpClusterMember == aClusterNumber) { - tmpIndicesInCluster[tmpIterator] = tmpInputIndices; - tmpIterator++; - } - tmpInputIndices++; - } - return tmpIndicesInCluster; - } - } - // - // - // - // - /** - * {@inheritDoc} - */ - @Override - public void exportClusteringResultsToTextFiles(Writer aClusteringResultWriter, Writer aClusteringProcessWriter) - throws NullPointerException { - if(aClusteringResultWriter == null || aClusteringProcessWriter == null) { - throw new NullPointerException("At least one of the writers is null."); - } - if (this.clusteringProcess == null || this.clusteringResult == null) { - throw new NullPointerException("The associated argument that enables the export of clustering results is " + - "is set to false.\n" + - "Please set the argument for export to true."); - } - try { - for (String tmpClusteringResult : this.clusteringResult) { - aClusteringResultWriter.write(tmpClusteringResult + "\n"); - } - for (String tmpClusteringProcess : this.clusteringProcess) { - aClusteringProcessWriter.write(tmpClusteringProcess + "\n"); - } - } - catch(IOException anException) { - Art2aAbstractResult.LOGGER.log(Level.SEVERE, "Export to text files failed."); - } - } - // - // - // - /** - * Method for determining the size of the detected clusters. - * - * @param aClusterView represents the cluster assignment of each input vector. - * @return HashMap maps the cluster number to the number of inputs in the cluster. - */ - private HashMap getClusterSize(int[] aClusterView) { - HashMap tmpClusterToMembersMap = - new HashMap<>((int) (this.getNumberOfDetectedClusters() * this.INITIAL_CAPACITY_VALUE)); - for(int tmpClusterMembers : aClusterView) { - if (tmpClusterMembers == -1) { - continue; - } - if(!tmpClusterToMembersMap.containsKey(tmpClusterMembers)) { - tmpClusterToMembersMap.put(tmpClusterMembers, 1); - } else { - tmpClusterToMembersMap.put(tmpClusterMembers, tmpClusterToMembersMap.get(tmpClusterMembers) + 1); - } - } - return tmpClusterToMembersMap; - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aDoubleClustering.java b/src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aDoubleClustering.java deleted file mode 100644 index 01da214..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aDoubleClustering.java +++ /dev/null @@ -1,609 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.clustering; - -import de.unijena.cheminf.clustering.art2a.exceptions.ConvergenceFailedException; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClustering; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClusteringResult; -import de.unijena.cheminf.clustering.art2a.results.Art2aDoubleClusteringResult; - -import java.util.Random; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.logging.Logger; - -/** - * The class implements an Art-2A algorithm in double machine precision for fast, - * stable unsupervised clustering for open categorical problems. The class is primarily intended for the - * clustering of fingerprints.
- * LITERATURE SOURCE:
- * - * @see - * "Primary : G.A. Carpenter,S. Grossberg and D.B. Rosen, Neural Networks 4 (1991) 493-504"

- * - * "Secondary : D. Wienke et al., Chemometrics and Intelligent Laboratory Systems 24 (1994) 367-387" - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aDoubleClustering implements IArt2aClustering { - // - /** - * Matrix with all fingerprints to be clustered. - * Each row of the matrix represents a fingerprint. - */ - private double[][] dataMatrix; - /** - * Matrix contains all cluster vectors. - */ - private double[][] clusterMatrix; - /** - * Matrix contains all cluster vectors of previous epoch. Is needed to check the convergence of - * the system. - */ - private double[][] clusterMatrixPreviousEpoch; - /** - * Queue of typ String for clustering process. Queues are used or thread security - * but these here should not be used by more than one thread. - */ - private ConcurrentLinkedQueue clusteringProcess; - /** - * Queue of typ String for clustering result. Queues are used or thread security - * but these here should not be used by more than one thread. - */ - private ConcurrentLinkedQueue clusteringResult; - /** - * The seed value for permutation of the vector field. - */ - private int seed; - // - // - // - /** - * The vigilance parameter is between 0 and 1. The parameter influences the type of clustering. - * A vigilance parameter close to 0 leads to a coarse clustering (few clusters) and a vigilance - * parameter close to 1, on the other hand, leads to a fine clustering (many clusters). - */ - private final double vigilanceParameter; - /** - * Maximum number of epochs the system may need to converge. - */ - private final int maximumNumberOfEpochs; - /** - * Threshold for contrast enhancement. If a vector/fingerprint component is below the threshold, it is set to zero. - */ - private final double thresholdForContrastEnhancement; - /** - * Number of fingerprints to be clustered. - */ - private final int numberOfInputVectors; - /** - * Dimensionality of the fingerprint. - */ - private final int numberOfComponents; - /** - * The scaling factor controls the sensitivity of the algorithm to new inputs. A low scaling factor makes - * the algorithm more sensitive to new inputs, while a high scaling factor decreases the sensitivity. - * Thus, too low a scaling factor will cause a new input to be added to a new cluster, while - * a high scaling factor will cause the new inputs to be added to existing clusters.
- * Default value: 1 / Math.sqrt(numberOfComponents - 1) - */ - private final double scalingFactor; - /** - * The required similarity parameter represents the minimum value that must exist between the current - * cluster vector and the previous cluster vector for the system to be considered convergent. - * The clustering process continues until there are no more significant changes between - * the cluster vectors of the current epoch and the previous epoch. - */ - private final double requiredSimilarity; - /** - * Parameter to define the intensity of keeping the old cluster vector in mind - * before the system adapts it to the new sample vector. - */ - private final double learningParameter; - //
- // - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(Art2aDoubleClustering.class.getName()); - // - // - // - /** - * Constructor. - * The data matrix with the input vectors/fingerprints is checked for correctness. Each row of the matrix - * corresponds to an input vector/fingerprint. The vectors must not have components smaller than 0. - * All input vectors must have the same length. - * If there are components greater than 1, these input vectors are scaled so that all vector components - * are between 0 and 1. - *
- * WARNING: If the data matrix consists only of null vectors, no clustering is possible, - * because they do not contain any information that can be used for similarity evaluation. - * - * @param aDataMatrix matrix contains all inputs for clustering. - * @param aMaximumNumberOfEpochs maximum number of epochs that the system may use for convergence. - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aRequiredSimilarity parameter indicating the minimum similarity between the current - * cluster vectors and the previous cluster vectors. - * @param aLearningParameter parameter to define the intensity of keeping the old cluster vector in mind - * before the system adapts it to the new sample vector. - * @throws IllegalArgumentException is thrown if the given arguments are invalid. - * @throws NullPointerException is thrown if aDataMatrix is null. - * - */ - public Art2aDoubleClustering(double[][] aDataMatrix, int aMaximumNumberOfEpochs, double aVigilanceParameter, - double aRequiredSimilarity, double aLearningParameter) - throws IllegalArgumentException, NullPointerException { - if(aDataMatrix == null) { - throw new NullPointerException("aDataMatrix is null."); - } - if(aMaximumNumberOfEpochs <= 0) { - throw new IllegalArgumentException("Number of epochs must be at least greater than zero."); - } - if(aVigilanceParameter <= 0.0 || aVigilanceParameter >= 1.0) { - throw new IllegalArgumentException("The vigilance parameter must be greater than 0 and smaller than 1."); - } - if(aRequiredSimilarity < 0.0 || aRequiredSimilarity > 1.0) { - throw new IllegalArgumentException("The required similarity parameter must be between 0 and 1."); - } - if(aLearningParameter < 0.0 || aLearningParameter > 1.0) { - throw new IllegalArgumentException("The learning parameter must be greater than 0 and smaller than 1."); - } - this.vigilanceParameter = aVigilanceParameter; - this.requiredSimilarity = aRequiredSimilarity; - this.learningParameter = aLearningParameter; - this.dataMatrix = this.getCheckedAndScaledDataMatrix(aDataMatrix); - this.numberOfInputVectors = this.dataMatrix.length; - this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; - this.numberOfComponents = this.dataMatrix[0].length; - this.scalingFactor = 1.0 / Math.sqrt(this.numberOfComponents + 1.0); - this.thresholdForContrastEnhancement = 1.0 / Math.sqrt(this.numberOfComponents + 1.0); - } - //
- // - // - /** - * The input data matrix with the input vectors/fingerprints is checked for correctness. - * Accordingly, the input matrix must not contain any vectors that consist of components smaller than 0. - * All input vectors must have the same length. Components larger than 1 are allowed, but are - * so that all components of an input vector range between 0 and 1. - * - * @param aDataMatrix the matrix contains all input vectors/fingerprints to be clustered. - * @return valid data matrix - * @throws NullPointerException is thrown if the given data matrix is null. - * @throws IllegalArgumentException is thrown if the input vectors are invalid - */ - private double[][] getCheckedAndScaledDataMatrix(double[][] aDataMatrix) throws NullPointerException, - IllegalArgumentException { - if(aDataMatrix == null) { - throw new IllegalArgumentException("aDataMatrix is null."); - } - if(aDataMatrix.length <= 0) { - throw new IllegalArgumentException("The number of vectors must greater than 0 to cluster inputs."); - } - int tmpNumberOfNullComponentsInDataMatrix = 0; - int tmpNumberOfElementsInDataMatrix = aDataMatrix.length * aDataMatrix[0].length; - int tmpNumberOfVectorComponents = aDataMatrix[0].length; - double tmpMaxValueInDataMatrix = aDataMatrix[0][0]; - double tmpMinValueInDatamatrix = aDataMatrix[0][0]; - double tmpCurrentVectorComponent; - double[] tmpSingleFingerprint; - boolean tmpIsDataMatrixInCorrectRangeOfValues = false; - for(int i = 0; i < aDataMatrix.length; i++) { - tmpSingleFingerprint = aDataMatrix[i]; - if(tmpNumberOfVectorComponents != tmpSingleFingerprint.length) { - throw new IllegalArgumentException("The input vectors must be have the same length!"); - } - for(int j = 0; j < tmpSingleFingerprint.length; j++) { - tmpCurrentVectorComponent = tmpSingleFingerprint[j]; - if(tmpCurrentVectorComponent > tmpMaxValueInDataMatrix) { - tmpMaxValueInDataMatrix = tmpCurrentVectorComponent; - } - if(tmpCurrentVectorComponent < tmpMinValueInDatamatrix) { - tmpMinValueInDatamatrix = tmpCurrentVectorComponent; - } - if(tmpCurrentVectorComponent > 1.0) { - tmpIsDataMatrixInCorrectRangeOfValues = true; - } - if(tmpCurrentVectorComponent < 0.0) { - throw new IllegalArgumentException("Only positive values allowed."); - } - if(tmpCurrentVectorComponent == 0.0) { - tmpNumberOfNullComponentsInDataMatrix++; - } - } - if(tmpNumberOfNullComponentsInDataMatrix == tmpNumberOfElementsInDataMatrix) { - throw new IllegalArgumentException("All vectors are null vectors. Clustering not possible"); - } - } - if(tmpIsDataMatrixInCorrectRangeOfValues) { - this.getScaledDataMatrix(aDataMatrix, tmpMinValueInDatamatrix, tmpMaxValueInDataMatrix); - } - return aDataMatrix; - } - // - /** - * Calculates the length of a vector. The length is needed for the normalisation of the vector. - * - * @param anInputVector vector whose length is calculated. - * @return double vector length. - * @throws ArithmeticException is thrown if the addition of the vector components results in zero. - */ - private double getVectorLength (double[] anInputVector) throws ArithmeticException { - double tmpVectorComponentsSqrtSum = 0.0; - double tmpVectorLength; - for (int i = 0; i < anInputVector.length; i++) { - tmpVectorComponentsSqrtSum += anInputVector[i] * anInputVector[i]; - } - if (tmpVectorComponentsSqrtSum == 0.0) { - throw new ArithmeticException("Addition of the vector components results in zero!"); - } else { - tmpVectorLength = Math.sqrt(tmpVectorComponentsSqrtSum); - } - return tmpVectorLength; - } - // - /** - * Method for scaling the input vectors/fingerprints if they are not between 0 and 1. - * Thus serves for the scaling of count fingerprints. - * - * @param aDataMatrixToScale the matrix contains input vectors and at least one component - * of an input vector is not in the specified range, i.e. between 0 and 1. - * @param aMaxValue is the highest value in the matrix. - * @param aMinValue is the lowest value in the matrix. - * - */ - private void getScaledDataMatrix(double[][] aDataMatrixToScale, double aMinValue, double aMaxValue) { - double[] tmpSingleFingerprint; - double tmpScaledVectorComponent; - double tmpCurrentVectorComponent; - for(int i = 0; i < aDataMatrixToScale.length; i++) { - tmpSingleFingerprint = aDataMatrixToScale[i]; - for (int j = 0; j < tmpSingleFingerprint.length; j++) { - tmpCurrentVectorComponent = tmpSingleFingerprint[j]; - tmpScaledVectorComponent = (tmpCurrentVectorComponent-aMinValue)/(aMaxValue-aMinValue); // normalization - tmpSingleFingerprint[j] = tmpScaledVectorComponent; - aDataMatrixToScale[i] = tmpSingleFingerprint; - } - } - } - // - /** - * At the end of each epoch, it is checked whether the system has converged or not. If the system has not - * converged, a new epoch is performed, otherwise the clustering is completed successfully. - * The system is considered converged if the cluster vectors of the current epoch and the previous epoch - * have a minimum similarity. The default value of the similarity parameter is 0.99, but it can also be set - * by the user when initialising the clustering. - * - * @param aNumberOfDetectedClasses number of detected clusters per epoch. - * @param aConvergenceEpoch current epochs number. - * @return boolean true is returned if the system has converged. - * False is returned if the system has not converged to the epoch. - * @throws ConvergenceFailedException is thrown, when convergence fails. - */ - private boolean isConverged(int aNumberOfDetectedClasses, int aConvergenceEpoch) - throws ConvergenceFailedException { - boolean tmpIsConverged; - double[] tmpRow; - if(aConvergenceEpoch < this.maximumNumberOfEpochs) { - // Check convergence by evaluating the similarity of the cluster vectors of this and the previous epoch. - tmpIsConverged = true; - double tmpScalarProductOfClassVector; - double[] tmpCurrentRowInClusterMatrix; - double[] tmpPreviousEpochRow; - for (int i = 0; i < aNumberOfDetectedClasses; i++) { - tmpScalarProductOfClassVector = 0.0; - tmpCurrentRowInClusterMatrix = this.clusterMatrix[i]; - tmpPreviousEpochRow = this.clusterMatrixPreviousEpoch[i]; - for (int j = 0; j < this.numberOfComponents; j++) { - tmpScalarProductOfClassVector += tmpCurrentRowInClusterMatrix[j] * tmpPreviousEpochRow[j]; - } - if (tmpScalarProductOfClassVector < this.requiredSimilarity) { - tmpIsConverged = false; - break; - } - } - if(!tmpIsConverged) { - for(int tmpCurrentClusterMatrixVector = 0; tmpCurrentClusterMatrixVector < this.clusterMatrix.length; - tmpCurrentClusterMatrixVector++) { - tmpRow = this.clusterMatrix[tmpCurrentClusterMatrixVector]; - this.clusterMatrixPreviousEpoch[tmpCurrentClusterMatrixVector] = tmpRow; - } - } - } else { - throw new ConvergenceFailedException(String.format("Convergence failed for vigilance parameter: %2f" - ,this.vigilanceParameter)); - } - return tmpIsConverged; - } - // - // - // - /** - * {@inheritDoc} - */ - @Override - public void initializeMatrices() { - this.clusterMatrix = new double[this.numberOfInputVectors][this.numberOfComponents]; - this.clusterMatrixPreviousEpoch = new double[this.numberOfInputVectors][this.numberOfComponents]; - } - // - /** - * {@inheritDoc} - * - * @author Thomas Kuhn - */ - @Override - public int[] getRandomizeVectorIndices() { - int[] tmpSampleVectorIndicesInRandomOrder = new int[this.numberOfInputVectors]; - for(int i = 0; i < this.numberOfInputVectors; i++) { - tmpSampleVectorIndicesInRandomOrder[i] = i; - } - Random tmpRnd = new Random(this.seed); - this.seed++; - int tmpNumberOfIterations = (this.numberOfInputVectors >> 1) + 1; - int tmpRandomIndex1; - int tmpRandomIndex2; - int tmpBuffer; - for(int j = 0; j < tmpNumberOfIterations; j++) { - tmpRandomIndex1 = (int) (this.numberOfInputVectors * tmpRnd.nextDouble()); - tmpRandomIndex2 = (int) (this.numberOfInputVectors * tmpRnd.nextDouble()); - - tmpBuffer = tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex1]; - tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex1] = - tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex2]; - tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex2] = tmpBuffer; - } - return tmpSampleVectorIndicesInRandomOrder; - } - // - /** - * {@inheritDoc} - * Starts the clustering in double machine precision. - */ - @Override - public IArt2aClusteringResult getClusterResult(boolean anIsClusteringResultExported, int aSeedValue) throws ConvergenceFailedException { - // - this.clusteringProcess = null; - this.clusteringResult = null; - if(anIsClusteringResultExported) { - this.clusteringProcess = new ConcurrentLinkedQueue<>(); - this.clusteringResult = new ConcurrentLinkedQueue<>(); - } - // - // - this.initializeMatrices(); - this.seed = aSeedValue; - double[] tmpClusterMatrixRow; - double[] tmpClusterMatrixRowOld; - double tmpInitialClusterVectorWeightValue = 1.0 / Math.sqrt(this.numberOfComponents); - int tmpNumberOfDetectedClusters = 0; - int[] tmpClusterOccupation = new int[this.numberOfInputVectors]; - double tmpVectorLengthForFirstNormalizationStep; - double tmpVectorLengthAfterContrastEnhancement; - double tmpRho; - double tmpVectorLengthForModificationWinnerCluster; - int tmpWinnerClusterIndex; - boolean tmpIsSystemConverged = false; - // - // - for(int tmpCurrentClusterMatrixVectorIndex = 0; tmpCurrentClusterMatrixVectorIndex < this.clusterMatrix.length; - tmpCurrentClusterMatrixVectorIndex++) { - tmpClusterMatrixRow = this.clusterMatrix[tmpCurrentClusterMatrixVectorIndex]; - tmpClusterMatrixRowOld = this.clusterMatrixPreviousEpoch[tmpCurrentClusterMatrixVectorIndex]; - for (int tmpCurrentVectorComponentsInClusterMatrixIndex = 0; - tmpCurrentVectorComponentsInClusterMatrixIndex < tmpClusterMatrixRow.length; - tmpCurrentVectorComponentsInClusterMatrixIndex++) { - tmpClusterMatrixRow[tmpCurrentVectorComponentsInClusterMatrixIndex] = tmpInitialClusterVectorWeightValue; - tmpClusterMatrixRowOld[tmpCurrentVectorComponentsInClusterMatrixIndex] = tmpInitialClusterVectorWeightValue; - } - } - // - // - int tmpCurrentNumberOfEpochs = 0; - if(anIsClusteringResultExported) { - this.clusteringResult.add(String.format("Vigilance parameter: %2f",this.vigilanceParameter)); - } - // - // - while(!tmpIsSystemConverged && tmpCurrentNumberOfEpochs <= this.maximumNumberOfEpochs) { - // - if(anIsClusteringResultExported) { - this.clusteringProcess.add(String.format("Art-2a clustering result for vigilance parameter: %2f",this.vigilanceParameter)); - this.clusteringProcess.add(String.format("Number of epochs: %d",tmpCurrentNumberOfEpochs)); - this.clusteringProcess.add(""); - } - int[] tmpSampleVectorIndicesInRandomOrder = this.getRandomizeVectorIndices(); - // - // - for(int tmpCurrentInput = 0; tmpCurrentInput < this.numberOfInputVectors; tmpCurrentInput++) { - double[] tmpInputVector = new double[this.numberOfComponents]; - boolean tmpIsNullVector = true; - for(int tmpCurrentInputVectorComponentsIndex = 0; tmpCurrentInputVectorComponentsIndex < this.numberOfComponents; - tmpCurrentInputVectorComponentsIndex++) { - tmpInputVector[tmpCurrentInputVectorComponentsIndex] = - this.dataMatrix[tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput]][tmpCurrentInputVectorComponentsIndex]; - if(tmpInputVector[tmpCurrentInputVectorComponentsIndex] !=0.0) { - tmpIsNullVector = false; - } - } - if(anIsClusteringResultExported) { - this.clusteringProcess.add(String.format("Input: %d / Vector %d", tmpCurrentInput, - tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput])); - } - // - if(tmpIsNullVector) { - tmpClusterOccupation[tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput]] = -1; - if(anIsClusteringResultExported) { - this.clusteringProcess.add("This input is a null vector"); - } - } - // - else { - // - tmpVectorLengthForFirstNormalizationStep = this.getVectorLength(tmpInputVector); - for(int tmpManipulateComponentsIndex = 0; tmpManipulateComponentsIndex < tmpInputVector.length; - tmpManipulateComponentsIndex++) { - tmpInputVector[tmpManipulateComponentsIndex] *= (1.0 / tmpVectorLengthForFirstNormalizationStep); - if(tmpInputVector[tmpManipulateComponentsIndex] <= this.thresholdForContrastEnhancement) { - tmpInputVector[tmpManipulateComponentsIndex] = 0.0; - } - } - // - // - tmpVectorLengthAfterContrastEnhancement = this.getVectorLength(tmpInputVector); - for(int tmpNumberOfNormalizedInputComponents = 0; tmpNumberOfNormalizedInputComponents < tmpInputVector.length; - tmpNumberOfNormalizedInputComponents++) { - tmpInputVector[tmpNumberOfNormalizedInputComponents] *= (1.0 / tmpVectorLengthAfterContrastEnhancement); - } - // - // - if(tmpNumberOfDetectedClusters == 0) { - this.clusterMatrix[0] = tmpInputVector; - tmpClusterOccupation[tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput]] = - tmpNumberOfDetectedClusters; - tmpNumberOfDetectedClusters++; - if(anIsClusteringResultExported) { - this.clusteringProcess.add("Cluster number: 0"); - this.clusteringProcess.add(String.format("Number of detected clusters: %d",tmpNumberOfDetectedClusters)); - } - } - // - else { - // - double tmpSumOfComponents = 0.0; - for(double tmpVectorComponentsOfNormalizeVector : tmpInputVector) { - tmpSumOfComponents += tmpVectorComponentsOfNormalizeVector; - } - tmpWinnerClusterIndex = tmpNumberOfDetectedClusters; - boolean tmpIsMatchingClusterAvailable = true; - // - // - // tmpRho) { - tmpRho = tmpRhoForExistingClusters; - tmpWinnerClusterIndex = tmpCurrentClusterMatrixRowIndex; - tmpIsMatchingClusterAvailable = false; - } - } - // - // - // - // - } - // - } - } - // - } - // - // - // - } - // - // - // - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aFloatClustering.java b/src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aFloatClustering.java deleted file mode 100644 index f088ef4..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/clustering/Art2aFloatClustering.java +++ /dev/null @@ -1,600 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.clustering; - -import de.unijena.cheminf.clustering.art2a.exceptions.ConvergenceFailedException; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClustering; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClusteringResult; -import de.unijena.cheminf.clustering.art2a.results.Art2aFloatClusteringResult; - -import java.util.Random; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.logging.Logger; - -/** - * The class implements an Art-2A algorithm in single machine precision for fast, - * stable unsupervised clustering for open categorical problems. The class is primarily intended for the - * clustering of fingerprints.
- * LITERATURE SOURCE:
- * - * @see - * "Primary : G.A. Carpenter,S. Grossberg and D.B. Rosen, Neural Networks 4 (1991) 493-504"

- * - * "Secondary : D. Wienke et al., Chemometrics and Intelligent Laboratory Systems 24 (1994) 367-387" - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aFloatClustering implements IArt2aClustering { - // - /** - * Matrix with all fingerprints to be clustered. - * Each row of the matrix represents a fingerprint. - */ - private float[][] dataMatrix; - /** - * Matrix contains all cluster vectors. - */ - private float[][] clusterMatrix; - /** - * Matrix contains all cluster vectors of previous epoch. Is needed to check the convergence of - * the system. - */ - private float[][] clusterMatrixPreviousEpoch; - /** - * Queue of typ String for clustering process. - */ - private ConcurrentLinkedQueue clusteringProcess; - /** - * Queue of typ String for clustering result. - */ - private ConcurrentLinkedQueue clusteringResult; - /** - * The seed value for permutation of the vector field. - */ - private int seed; - // - // - // - /** - * Maximum number of epochs the system may need to converge. - */ - private int maximumNumberOfEpochs; - /** - * The vigilance parameter is between 0 and 1. The parameter influences the type of clustering. - * A vigilance parameter close to 0 leads to a coarse clustering (few clusters) and a vigilance - * parameter close to 1, on the other hand, leads to a fine clustering (many clusters). - */ - private float vigilanceParameter; - /** - * Threshold for contrast enhancement. If a vector/fingerprint component is below the threshold, it is set to zero. - */ - private final float thresholdForContrastEnhancement; - /** - * Number of fingerprints to be clustered. - */ - private final int numberOfFingerprints; - /** - * Dimensionality of the fingerprint. - */ - private final int numberOfComponents; - /** - * The scaling factor controls the sensitivity of the algorithm to new inputs. A low scaling factor makes - * the algorithm more sensitive to new inputs, while a high scaling factor decreases the sensitivity. - * Thus, too low a scaling factor will cause a new input to be added to a new cluster, while - * a high scaling factor will cause the new inputs to be added to existing clusters.
- * Default value: 1 / Math.sqrt(numberOfComponents - 1) - */ - private final float scalingFactor; - /** - * The required similarity parameter represents the minimum value that must exist between the current - * cluster vector and the previous cluster vector for the system to be considered convergent. - * The clustering process continues until there are no more significant changes between - * the cluster vectors of the current epoch and the previous epoch. - */ - private final float requiredSimilarity; - /** - * Parameter to define the intensity of keeping the old cluster vector in mind - * before the system adapts it to the new sample vector. - */ - private final float learningParameter; - //
- // - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(Art2aFloatClustering.class.getName()); - // - // - // - /** - * Constructor. - * The data matrix with the input vectors/fingerprints is checked for correctness. Each row of the matrix - * corresponds to an input vector/fingerprint. The vectors must not have components smaller than 0. - * All input vectors must have the same length. - * If there are components greater than 1, these input vectors are scaled so that all vector components - * are between 0 and 1. - *
- * WARNING: If the data matrix consists only of null vectors, no clustering is possible, - * because they do not contain any information that can be used for similarity evaluation. - * - * @param aDataMatrix matrix contains all inputs for clustering. - * @param aMaximumNumberOfEpochs maximum number of epochs that the system may use for convergence. - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aRequiredSimilarity parameter indicating the minimum similarity between the current - * cluster vectors and the previous cluster vectors. - * @param aLearningParameter parameter to define the intensity of keeping the old class vector in mind - * before the system adapts it to the new sample vector. - * @throws IllegalArgumentException is thrown if the given arguments are invalid. - * @throws NullPointerException is thrown if aDataMatrix is null. - * - */ - public Art2aFloatClustering(float[][] aDataMatrix, int aMaximumNumberOfEpochs, float aVigilanceParameter, - float aRequiredSimilarity, float aLearningParameter) - throws IllegalArgumentException, NullPointerException { - if(aDataMatrix == null) { - throw new NullPointerException("aDataMatrix is null."); - } - if(aMaximumNumberOfEpochs <= 0) { - throw new IllegalArgumentException("Number of epochs must be at least greater than zero."); - } - if(aVigilanceParameter <= 0.0f || aVigilanceParameter >= 1.0f) { - throw new IllegalArgumentException("The vigilance parameter must be greater than 0 and smaller than 1."); - } - if(aRequiredSimilarity < 0.0f || aRequiredSimilarity > 1.0f) { - throw new IllegalArgumentException("The required similarity parameter must be between 0 and 1"); - } - if(aLearningParameter < 0.0f || aLearningParameter > 1.0f) { - throw new IllegalArgumentException("The learning parameter must be greater than 0 and smaller than 1."); - } - this.vigilanceParameter = aVigilanceParameter; - this.requiredSimilarity = aRequiredSimilarity; - this.learningParameter = aLearningParameter; - this.dataMatrix = this.getCheckedAndScaledDataMatrix(aDataMatrix); - this.numberOfFingerprints = this.dataMatrix.length; - this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; - this.numberOfComponents = this.dataMatrix[0].length; - this.scalingFactor = (float) (1.0 / Math.sqrt(this.numberOfComponents + 1.0)); - this.thresholdForContrastEnhancement = (float) (1.0 / Math.sqrt(this.numberOfComponents + 1.0)); - } - //
- // - // - /** - * The input data matrix with the input vectors/fingerprints is checked for correctness. - * Accordingly, the input matrix must not contain any vectors that consist of components smaller than 0. - * All input vectors must have the same length. Components larger than 1 are allowed, but are scaled in the - * following steps so that all components of an input vector range between 0 and 1. - * - * @param aDataMatrix the matrix contains all input vectors/fingerprints to be clustered. - * @return a valid data matrix. - * @throws NullPointerException is thrown if the given data matrix is null. - * @throws IllegalArgumentException is thrown if the input vectors are invalid. - */ - private float[][] getCheckedAndScaledDataMatrix(float[][] aDataMatrix) throws NullPointerException, IllegalArgumentException { - if(aDataMatrix == null) { - throw new IllegalArgumentException("aDataMatrix is null."); - } - if(aDataMatrix.length <= 0) { - throw new IllegalArgumentException("The number of vectors must greater than 0 to cluster inputs."); - } - int tmpNumberOfNullComponentsInDataMatrix = 0; - int tmpNumberOfElementsInDataMatrix = aDataMatrix.length * aDataMatrix[0].length; - int tmpNumberOfVectorComponents = aDataMatrix[0].length; - float tmpMaxValueInDataMatrix = aDataMatrix[0][0]; - float tmpMinValueInDatamatrix = aDataMatrix[0][0]; - float tmpCurrentVectorComponent; - float[] tmpSingleFingerprint; - boolean tmpIsDataMatrixInCorrectRangeOfValues = false; - for(int i = 0; i < aDataMatrix.length; i++) { - tmpSingleFingerprint = aDataMatrix[i]; - if(tmpNumberOfVectorComponents != tmpSingleFingerprint.length) { - throw new IllegalArgumentException("The input vectors must be have the same length!"); - } - for(int j = 0; j < tmpSingleFingerprint.length; j++) { - tmpCurrentVectorComponent = tmpSingleFingerprint[j]; - if(tmpCurrentVectorComponent > tmpMaxValueInDataMatrix) { - tmpMaxValueInDataMatrix = tmpCurrentVectorComponent; - } - if(tmpCurrentVectorComponent < tmpMinValueInDatamatrix) { - tmpMinValueInDatamatrix = tmpCurrentVectorComponent; - } - if(tmpCurrentVectorComponent > 1.0f) { - tmpIsDataMatrixInCorrectRangeOfValues = true; - } - if(tmpCurrentVectorComponent < 0.0f) { - throw new IllegalArgumentException("Only positive values allowed."); - } - if(tmpCurrentVectorComponent == 0.0f) { - tmpNumberOfNullComponentsInDataMatrix++; - } - } - if(tmpNumberOfNullComponentsInDataMatrix == tmpNumberOfElementsInDataMatrix) { - throw new IllegalArgumentException("All vectors are null vectors. Clustering not possible."); - } - } - if(tmpIsDataMatrixInCorrectRangeOfValues) { - this.getScaledDataMatrix(aDataMatrix, tmpMinValueInDatamatrix, tmpMaxValueInDataMatrix); - } - return aDataMatrix; - } - // - /** - * Method for scaling the input vectors/fingerprints if they are not between 0 and 1. - * Thus serves for the scaling of count fingerprints. - * - * @param aDataMatrixToScale the matrix contains input vectors and at least one component - * of an input vector is not in the specified range, i.e. between 0 and 1. - * @param aMaxValue is the highest value in the matrix. - * @param aMinValue is the lowest value in the matrix. - * - */ - private void getScaledDataMatrix(float[][] aDataMatrixToScale, float aMinValue, float aMaxValue) { - float[] tmpSingleFingerprint; - float tmpScaledVectorComponent; - float tmpCurrentVectorComponent; - for(int i = 0; i < aDataMatrixToScale.length; i++) { - tmpSingleFingerprint = aDataMatrixToScale[i]; - for (int j = 0; j < tmpSingleFingerprint.length; j++) { - tmpCurrentVectorComponent = tmpSingleFingerprint[j]; - tmpScaledVectorComponent = (tmpCurrentVectorComponent-aMinValue)/(aMaxValue-aMinValue); // normalization - tmpSingleFingerprint[j] = tmpScaledVectorComponent; - aDataMatrixToScale[i] = tmpSingleFingerprint; - } - } - } - // - /** - * Calculates the length of a vector. The length is needed for the normalisation of the vector. - * - * @param anInputVector vector whose length is calculated. - * @return float vector length. - * @throws ArithmeticException is thrown if the addition of the vector components results in zero. - */ - private float getVectorLength (float[] anInputVector) throws ArithmeticException { - float tmpVectorComponentsSqrtSum = 0.0f; - float tmpVectorLength; - for (int i = 0; i < anInputVector.length; i++) { - tmpVectorComponentsSqrtSum += anInputVector[i] * anInputVector[i]; - } - if (tmpVectorComponentsSqrtSum == 0.0f) { - throw new ArithmeticException("Addition of the vector components results in zero!"); - } else { - tmpVectorLength = (float) Math.sqrt(tmpVectorComponentsSqrtSum); - } - return tmpVectorLength; - } - // - /** - * At the end of each epoch, it is checked whether the system has converged or not. If the system has not - * converged, a new epoch is performed, otherwise the clustering is completed successfully. - * The system is considered converged if the cluster vectors of the current epoch and the previous epoch - * have a minimum similarity. The default value of the similarity parameter is 0.99, but it can also be set - * by the user when initialising the clustering. - * - * @param aNumberOfDetectedClasses number of detected clusters per epoch. - * @param aConvergenceEpoch current epochs number. - * @return boolean true is returned if the system has converged. - * False is returned if the system has not converged to the epoch. - * @throws ConvergenceFailedException is thrown, when convergence fails. - */ - private boolean isConverged(int aNumberOfDetectedClasses, int aConvergenceEpoch) throws ConvergenceFailedException { - boolean tmpIsConverged; - float[] tmpRow; - if(aConvergenceEpoch < this.maximumNumberOfEpochs) { - // Check convergence by evaluating the similarity of the cluster vectors of this and the previous epoch. - tmpIsConverged = true; - float tmpScalarProductOfClusterVector; - float[] tmpCurrentRowInClusterMatrix; - float[] tmpPreviousEpochRow; - for (int i = 0; i < aNumberOfDetectedClasses; i++) { - tmpScalarProductOfClusterVector = 0.0f; - tmpCurrentRowInClusterMatrix = this.clusterMatrix[i]; - tmpPreviousEpochRow = this.clusterMatrixPreviousEpoch[i]; - for (int j = 0; j < this.numberOfComponents; j++) { - tmpScalarProductOfClusterVector += tmpCurrentRowInClusterMatrix[j] * tmpPreviousEpochRow[j]; - } - if (tmpScalarProductOfClusterVector < this.requiredSimilarity) { - tmpIsConverged = false; - break; - } - } - if(!tmpIsConverged) { - for(int tmpCurrentClusterMatrixVector = 0; tmpCurrentClusterMatrixVector < this.clusterMatrix.length; - tmpCurrentClusterMatrixVector++) { - tmpRow = this.clusterMatrix[tmpCurrentClusterMatrixVector]; - this.clusterMatrixPreviousEpoch[tmpCurrentClusterMatrixVector] = tmpRow; - } - } - } else { - throw new ConvergenceFailedException(String.format("Convergence failed for vigilance parameter: %2f",this.vigilanceParameter)); - } - return tmpIsConverged; - } - // - // - // - /** - * {@inheritDoc} - */ - @Override - public void initializeMatrices() { - this.clusterMatrix = new float[this.numberOfFingerprints][this.numberOfComponents]; - this.clusterMatrixPreviousEpoch = new float[this.numberOfFingerprints][this.numberOfComponents]; - } - // - /** - * {@inheritDoc} - * - * @author Thomas Kuhn - */ - @Override - public int[] getRandomizeVectorIndices() { - int[] tmpSampleVectorIndicesInRandomOrder = new int[this.numberOfFingerprints]; - for(int i = 0; i < this.numberOfFingerprints; i++) { - tmpSampleVectorIndicesInRandomOrder[i] = i; - } - Random tmpRnd = new Random(this.seed); - this.seed++; - int tmpNumberOfIterations = (this.numberOfFingerprints >> 1) + 1; - int tmpRandomIndex1; - int tmpRandomIndex2; - int tmpBuffer; - for(int j = 0; j < tmpNumberOfIterations; j++) { - tmpRandomIndex1 = (int) (this.numberOfFingerprints * tmpRnd.nextDouble()); - tmpRandomIndex2 = (int) (this.numberOfFingerprints * tmpRnd.nextDouble()); - tmpBuffer = tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex1]; - tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex1] = tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex2]; - tmpSampleVectorIndicesInRandomOrder[tmpRandomIndex2] = tmpBuffer; - } - return tmpSampleVectorIndicesInRandomOrder; - } - // - /** - * {@inheritDoc} - * - */ - @Override - public IArt2aClusteringResult getClusterResult(boolean anIsClusteringResultExported, int aSeedValue) throws ConvergenceFailedException { - // - this.clusteringProcess = null; - this.clusteringResult = null; - if(anIsClusteringResultExported) { - this.clusteringProcess = new ConcurrentLinkedQueue<>(); - this.clusteringResult = new ConcurrentLinkedQueue<>(); - } - // - // - this.initializeMatrices(); - this.seed = aSeedValue; - float[] tmpClusterMatrixRow; - float[] tmpClusterMatrixRowOld; - float tmpInitialClusterVectorWeightValue = (float) (1.0 / Math.sqrt(this.numberOfComponents)); - int tmpNumberOfDetectedClusters = 0; - int[] tmpClusterOccupation = new int[this.numberOfFingerprints]; - float tmpVectorLengthForFirstNormalizationStep; - float tmpVectorLengthAfterContrastEnhancement; - float tmpRho; - float tmpVectorLengthForModificationWinnerCluster; - int tmpWinnerClusterIndex; - boolean tmpIsSystemConverged = false; - // - // - for(int tmpCurrentClusterMatrixVectorIndex = 0; tmpCurrentClusterMatrixVectorIndex < this.clusterMatrix.length; - tmpCurrentClusterMatrixVectorIndex++) { - tmpClusterMatrixRow = this.clusterMatrix[tmpCurrentClusterMatrixVectorIndex]; - tmpClusterMatrixRowOld = this.clusterMatrixPreviousEpoch[tmpCurrentClusterMatrixVectorIndex]; - for (int tmpCurrentVectorComponentsInClusterMatrix = 0; tmpCurrentVectorComponentsInClusterMatrix < tmpClusterMatrixRow.length; - tmpCurrentVectorComponentsInClusterMatrix++) { - tmpClusterMatrixRow[tmpCurrentVectorComponentsInClusterMatrix] = tmpInitialClusterVectorWeightValue; - tmpClusterMatrixRowOld[tmpCurrentVectorComponentsInClusterMatrix] = tmpInitialClusterVectorWeightValue; - } - } - // - // - int tmpCurrentNumberOfEpochs = 0; - if(anIsClusteringResultExported) { - this.clusteringResult.add(String.format("Vigilance parameter: %2f", this.vigilanceParameter)); - } - // - // - while(!tmpIsSystemConverged && tmpCurrentNumberOfEpochs <= this.maximumNumberOfEpochs) { - // - if(anIsClusteringResultExported) { - this.clusteringProcess.add(String.format("Art-2a clustering result for vigilance parameter: %2f",this.vigilanceParameter)); - this.clusteringProcess.add(String.format("Number of epochs: %d",tmpCurrentNumberOfEpochs)); - this.clusteringProcess.add(""); - } - int[] tmpSampleVectorIndicesInRandomOrder = this.getRandomizeVectorIndices(); - // - // - for(int tmpCurrentInput = 0; tmpCurrentInput < this.numberOfFingerprints; tmpCurrentInput++) { - float[] tmpInputVector = new float[this.numberOfComponents]; - boolean tmpIsNullVector = true; - for(int tmpCurrentInputVectorComponentsIndex = 0; tmpCurrentInputVectorComponentsIndex < this.numberOfComponents; - tmpCurrentInputVectorComponentsIndex++) { - tmpInputVector[tmpCurrentInputVectorComponentsIndex] = - this.dataMatrix[tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput]][tmpCurrentInputVectorComponentsIndex]; - if(tmpInputVector[tmpCurrentInputVectorComponentsIndex] !=0.0f) { - tmpIsNullVector = false; - } - } - if(anIsClusteringResultExported) { - this.clusteringProcess.add(String.format("Input: %d / Vector %d", tmpCurrentInput, - tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput])); - } - // - if(tmpIsNullVector) { - tmpClusterOccupation[tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput]] = -1; - if(anIsClusteringResultExported) { - this.clusteringProcess.add("This input is a null vector"); - } - } - // - else { - // - tmpVectorLengthForFirstNormalizationStep = this.getVectorLength(tmpInputVector); - for(int tmpManipulateComponentsIndex = 0; tmpManipulateComponentsIndex < tmpInputVector.length; - tmpManipulateComponentsIndex++) { - tmpInputVector[tmpManipulateComponentsIndex] *= (1.0f / tmpVectorLengthForFirstNormalizationStep); - if(tmpInputVector[tmpManipulateComponentsIndex] <= this.thresholdForContrastEnhancement) { - tmpInputVector[tmpManipulateComponentsIndex] = 0.0f; - } - } - // - // - tmpVectorLengthAfterContrastEnhancement = this.getVectorLength(tmpInputVector); - for(int tmpNUmberOfNormalizedInputComponents = 0; tmpNUmberOfNormalizedInputComponents < tmpInputVector.length; - tmpNUmberOfNormalizedInputComponents++) { - tmpInputVector[tmpNUmberOfNormalizedInputComponents] *= (1.0f / tmpVectorLengthAfterContrastEnhancement); - } - // - // - if(tmpNumberOfDetectedClusters == 0) { - this.clusterMatrix[0] = tmpInputVector; - tmpClusterOccupation[tmpSampleVectorIndicesInRandomOrder[tmpCurrentInput]] = - tmpNumberOfDetectedClusters; - tmpNumberOfDetectedClusters++; - if(anIsClusteringResultExported) { - this.clusteringProcess.add("Cluster number: 0"); - this.clusteringProcess.add(String.format("Number of detected clusters: %d",tmpNumberOfDetectedClusters)); - } - } - // - else { - // - float tmpSumOfComponents = 0.0f; - for(float tmpVectorComponentsOfNormalizeVector : tmpInputVector) { - tmpSumOfComponents += tmpVectorComponentsOfNormalizeVector; - } - tmpWinnerClusterIndex = tmpNumberOfDetectedClusters; - boolean tmpIsMatchingClusterAvailable = true; - // - // - // tmpRho) { - tmpRho = tmpRhoForExistingClusters; - tmpWinnerClusterIndex = tmpCurrentClusterMatrixRowIndex; - tmpIsMatchingClusterAvailable = false; - } - } - // - // - // - // - } - // - } - } - // - } - // - // - // - } - // - // - // - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/exceptions/ConvergenceFailedException.java b/src/main/java/de/unijena/cheminf/clustering/art2a/exceptions/ConvergenceFailedException.java deleted file mode 100644 index ab3efac..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/exceptions/ConvergenceFailedException.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.exceptions; - -/** - * An exception thrown when convergence fails. This exception occurs when the system is unable to, achieve a - * convergent state or meet the desired convergence criteria.The ConvergenceFailedException is a special type of - * Exception and inherits from this class. - * It can be used to handle convergence failures in order to take appropriate action. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class ConvergenceFailedException extends Exception { - // - /** - * Constructor. - */ - public ConvergenceFailedException() { - super("Convergence failed!"); - } - // - /** - * Constructor. - * - * @param anErrorMessage error message is displayed, when the exception is thrown. - */ - public ConvergenceFailedException(String anErrorMessage) { - super(anErrorMessage); - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClustering.java b/src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClustering.java deleted file mode 100644 index 75ee0ad..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClustering.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.interfaces; - -import de.unijena.cheminf.clustering.art2a.exceptions.ConvergenceFailedException; - -/** - * Interface for implementing float and double Art-2a clustering. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public interface IArt2aClustering { - // - /** - * Initialise the cluster matrices. - */ - void initializeMatrices(); - // - /** - * Since the Art-2a algorithm randomly selects any input vector, the input vectors must first be randomized. - * The input vectors/fingerprints are randomized so that all input vectors can be clustered by random selection. - * - * Here, the Fisher-Yates method is used to randomize the inputs. - * - * @return an array with vector indices in a random order - */ - int[] getRandomizeVectorIndices(); - // - /** - * Starts an Art-2A clustering algorithm. - * The clustering process begins by randomly selecting an input vector/fingerprint from the data matrix. - * After normalizing the first input vector, it is assigned to the first cluster. For all other subsequent - * input vectors, they also undergo certain normalization steps. If there is sufficient similarity to an - * existing cluster, they are assigned to that cluster. Otherwise, a new cluster is formed, and the - * input is added to it. Null vectors are not clustered. - * - * @param anIsClusteringResultExported If the parameter == true, all information about the - * clustering is exported to 2 text files.The first exported text file is a detailed log of the clustering process - * and the intermediate results and the second file is a rough overview of the final result. - * @param aSeedValue user-defined seed value to randomize input vectors. - * @return IArt2aClusteringResult - * @throws ConvergenceFailedException is thrown, when convergence fails. - */ - IArt2aClusteringResult getClusterResult(boolean anIsClusteringResultExported, int aSeedValue) throws ConvergenceFailedException; - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClusteringResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClusteringResult.java deleted file mode 100644 index b8ac4c1..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/interfaces/IArt2aClusteringResult.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.interfaces; - -import java.io.Writer; - -/** - * Interface for implementing clustering result classes. - * - * @param generic parameter. This parameter is either a Double or a Float. - * The type of teh method @code {@link #getAngleBetweenClusters(int, int)} - * is calculated either as a float or as a double, depending on the clustering precision option. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public interface IArt2aClusteringResult { - // - /** - * Returns the vigilance parameter of the clustering algorithm. - * - * @return float vigilance parameter - */ - T getVigilanceParameter(); - // - /** - * Returns the number of detected clusters. - * - * @return int detected cluster number - */ - int getNumberOfDetectedClusters(); - // - /** - * Returns the number of epochs. - * - * @return int epoch number - */ - int getNumberOfEpochs(); - // - /** - * Returns the input indices assigned to the given cluster. - * - * @param aClusterNumber given cluster number - * @return array with the input indices for a given cluster - * @throws IllegalArgumentException is thrown if the given cluster does not exist. - */ - int[] getClusterIndices(int aClusterNumber) throws IllegalArgumentException; - // - /** - * Calculates the cluster representative. This means that the input that is most - * similar to the cluster vector is determined. - * - * @param aClusterNumber Cluster number for which the representative is to be calculated. - * @return int input indices of the representative input in the cluster. - * @throws IllegalArgumentException is thrown if the given cluster number is invalid. - */ - int getClusterRepresentatives(int aClusterNumber) throws IllegalArgumentException; - // - // - // - /** - * The result of the clustering is additionally exported in two text files. One of these files is a - * very detailed representation of the results (clustering process file), while in the other only the - * most important results are summarized (clustering result file). - * IMPORTANT: In order to additionally export the clustering results into text files, - * the folder must be created first. - * This requires the method call setUpClusteringResultTextFilePrinter(String aPathName, Class) - * or user own Writer and text files. This method call is optional, the folder can also be created by the user. - * - * @see de.unijena.cheminf.clustering.art2a.util.FileUtil#setUpClusteringResultTextFilePrinters(String, Class) - * - * @param aClusteringProcessWriter clustering result (process) writer - * @param aClusteringResultWriter clustering result writer - * @throws NullPointerException is thrown, if the Writers are null. - * - */ - void exportClusteringResultsToTextFiles(Writer aClusteringResultWriter, Writer aClusteringProcessWriter) - throws NullPointerException; - // - /** - * Calculates the angle between two clusters. - * The angle between the clusters defines the distance between them. - * Since all vectors are normalized to unit vectors in the first step of clustering - * and only positive components are allowed, they all lie in the positive quadrant - * of the unit sphere, so the maximum distance between two clusters can be 90 degrees. - * - * @param aFirstCluster first cluster - * @param aSecondCluster second cluster - * @return generic angle double or float. - * @throws IllegalArgumentException if the given parameters are invalid. - */ - T getAngleBetweenClusters(int aFirstCluster, int aSecondCluster) throws IllegalArgumentException; - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aDoubleClusteringResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aDoubleClusteringResult.java deleted file mode 100644 index 436a130..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aDoubleClusteringResult.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.results; - -import de.unijena.cheminf.clustering.art2a.abstractResult.Art2aAbstractResult; - -import java.util.Arrays; -import java.util.Objects; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.logging.Logger; - -/** - * Result class for the double clustering. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aDoubleClusteringResult extends Art2aAbstractResult { - // - /** - * Cache for cluster representatives. - */ - private int[] cacheClusterRepresentativesIndices; - /** - * Cache for cluster angles. - */ - private double[][] cacheAngleBetweenClusters; - // - // - // - /** - * Matrix contains all cluster vectors. - */ - private final double[][] doubleClusterMatrix; - /** - * Matrix contains all input vector/fingerprints to be clustered. - * Each row in the matrix corresponds to an input vector. - */ - private final double[][] dataMatrix; - /** - * The vigilance parameter is between 0 and 1. The parameter influences the type of clustering. - * A vigilance parameter close to 0 leads to a coarse clustering (few clusters) and a vigilance - * parameter close to 1, on the other hand, leads to a fine clustering (many clusters). - */ - private final double vigilanceParameter; - // - // - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(Art2aDoubleClusteringResult.class.getName()); - // - // - // - /** - * Constructor. - * - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aNumberOfEpochs final epoch number. - * @param aNumberOfDetectedClusters final number of detected clusters. - * @param aClusteringProcessQueue clustering result (process) queue of ty String. - * The queue is required to be able to export the cluster results. If it is not specified, they are set to null and - * export is not possible. - * @param aClusteringResultQueue clustering result queue of typ String. See {@code #aClusteringProcessQueue} - * @param aClusterView array for cluster assignment of each input vector. - * @param aClusterMatrix cluster vector matrix. All cluster vectors created after double ART-2a clustering are - * stored in this matrix. - * @param aDataMatrix matrix with all input vectors/fingerprints. - * Each row in the matrix corresponds to an input vector. - * @throws NullPointerException is thrown, if the specified matrices are null. - * @throws IllegalArgumentException is thrown, if the specified vigilance parameter is invalid. - * - */ - public Art2aDoubleClusteringResult(double aVigilanceParameter, int aNumberOfEpochs, - int aNumberOfDetectedClusters,int[] aClusterView, - double[][] aClusterMatrix, double[][] aDataMatrix, - ConcurrentLinkedQueue aClusteringProcessQueue, - ConcurrentLinkedQueue aClusteringResultQueue) - throws NullPointerException, IllegalArgumentException { - super(aNumberOfEpochs, aNumberOfDetectedClusters, aClusterView, aClusteringProcessQueue, aClusteringResultQueue); - Objects.requireNonNull(aClusterMatrix, "aClusterMatrix is null."); - Objects.requireNonNull(aDataMatrix, "aDataMatrix is null."); - if (aVigilanceParameter <= 0.0 || aVigilanceParameter >= 1.0) { - throw new IllegalArgumentException("The vigilance parameter must be greater than 0 and smaller than 1."); - } - this.vigilanceParameter = aVigilanceParameter; - this.doubleClusterMatrix = aClusterMatrix; - this.dataMatrix = aDataMatrix; - this.cacheClusterRepresentativesIndices = new int[aNumberOfDetectedClusters]; - Arrays.fill(this.cacheClusterRepresentativesIndices, -2); - this.cacheAngleBetweenClusters = new double[aNumberOfDetectedClusters][aNumberOfDetectedClusters]; - } - // - /** - * Constructor. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aNumberOfEpochs final epoch number. - * @param aNumberOfDetectedClusters final number of detected clusters. - * @param aClusterView array for cluster assignment of each input vector. - * @param aClusterMatrix double cluster vector matrix. All cluster vectors created after double ART-2a clustering are - * stored in this matrix. - * @param aDataMatrix double matrix with all input vectors/fingerprints. - * Each row in the matrix corresponds to an input vector. - * @throws NullPointerException is thrown, if the specified matrices are null. - * @throws IllegalArgumentException is thrown, if the specified vigilance parameter is invalid. - *

- * - * @see de.unijena.cheminf.clustering.art2a.results.Art2aDoubleClusteringResult#Art2aDoubleClusteringResult(double, - * int, int, int[], double[][], double[][], ConcurrentLinkedQueue, ConcurrentLinkedQueue) - * - */ - public Art2aDoubleClusteringResult(double aVigilanceParameter, int aNumberOfEpochs, int aNumberOfDetectedClusters, int[] aClusterView, - double[][] aClusterMatrix, double[][] aDataMatrix) throws NullPointerException { - this(aVigilanceParameter, aNumberOfEpochs, aNumberOfDetectedClusters, aClusterView, aClusterMatrix, aDataMatrix,null, null); - } - //
- // - // - /** - * {@inheritDoc} - */ - @Override - public Double getVigilanceParameter() { - return this.vigilanceParameter; - } - /** - * {@inheritDoc} - */ - @Override - public int getClusterRepresentatives(int aClusterNumber) throws IllegalArgumentException { - if(aClusterNumber >= this.getNumberOfDetectedClusters() || aClusterNumber < 0) { - throw new IllegalArgumentException("The given cluster number does not exist or is invalid."); - } - if(this.cacheClusterRepresentativesIndices[aClusterNumber] == -2) { - int[] tmpClusterIndices = this.getClusterIndices(aClusterNumber); - double[] tmpCurrentClusterVector = this.doubleClusterMatrix[aClusterNumber]; - double tmpFactor; - double[] tmpMatrixRow; - double[] tmpScalarProductArray = new double[tmpClusterIndices.length + 1]; - int tmpIterator = 0; - for (int tmpCurrentInput : tmpClusterIndices) { - tmpMatrixRow = this.dataMatrix[tmpCurrentInput]; - tmpFactor = 0.0; - for (int i = 0; i < tmpMatrixRow.length; i++) { - tmpFactor += tmpMatrixRow[i] * tmpCurrentClusterVector[i]; - } - tmpScalarProductArray[tmpIterator] = tmpFactor; - tmpIterator++; - } - int tmpIndexOfGreatestScalarProduct = 0; - for (int i = 0; i < tmpScalarProductArray.length; i++) { - if (tmpScalarProductArray[i] > tmpScalarProductArray[tmpIndexOfGreatestScalarProduct]) { - tmpIndexOfGreatestScalarProduct = i; - } - } - this.cacheClusterRepresentativesIndices[aClusterNumber] = tmpClusterIndices[tmpIndexOfGreatestScalarProduct]; - return tmpClusterIndices[tmpIndexOfGreatestScalarProduct]; - } else { - return this.cacheClusterRepresentativesIndices[aClusterNumber]; - } - } - // - /** - * {@inheritDoc} - */ - @Override - public Double getAngleBetweenClusters(int aFirstCluster, int aSecondCluster) throws IllegalArgumentException { - if(aFirstCluster < 0 || aSecondCluster < 0) { - throw new IllegalArgumentException("The given cluster number is negative/invalid."); - } - int tmpNumberOfDetectedCluster = this.getNumberOfDetectedClusters(); - double tmpAngle; - if(aFirstCluster == aSecondCluster && (aFirstCluster >= tmpNumberOfDetectedCluster)) { - throw new IllegalArgumentException("The given cluster number(s) do(es) not exist"); - } else if (aFirstCluster == aSecondCluster) { - return 0.0; - } else { - if (aFirstCluster >= tmpNumberOfDetectedCluster || aSecondCluster >= tmpNumberOfDetectedCluster) { - throw new IllegalArgumentException("The given cluster number(s) do(es) not exist."); - } - if(this.cacheAngleBetweenClusters[aFirstCluster] [aSecondCluster] == 0) { - double[] tmpFirstCluster = this.doubleClusterMatrix[aFirstCluster]; - double[] tmpSecondCluster = this.doubleClusterMatrix[aSecondCluster]; - double tmpFactor = 180.0 / Math.PI; - double tmpProduct = 0.0; - for (int i = 0; i < tmpFirstCluster.length; i++) { - tmpProduct += tmpFirstCluster[i] * tmpSecondCluster[i]; - } - tmpAngle = tmpFactor * Math.acos(tmpProduct); - this.cacheAngleBetweenClusters[aFirstCluster][aSecondCluster] = tmpAngle; - this.cacheAngleBetweenClusters[aSecondCluster][aFirstCluster] = tmpAngle; - return tmpAngle; - } else { - return this.cacheAngleBetweenClusters[aFirstCluster][aSecondCluster]; - } - } - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aFloatClusteringResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aFloatClusteringResult.java deleted file mode 100644 index 43bbe41..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/results/Art2aFloatClusteringResult.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.results; - -import de.unijena.cheminf.clustering.art2a.abstractResult.Art2aAbstractResult; - -import java.util.Arrays; -import java.util.Objects; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.logging.Logger; - -/** - * Result class for float clustering. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aFloatClusteringResult extends Art2aAbstractResult { - // - /** - * Cache for cluster representatives. - */ - private int[] cacheClusterRepresentativesIndices; - /** - * Cache for cluster angles. - */ - private float[][] cacheAngleBetweenClusters; - // - // - // - /** - * Matrix contains all cluster vectors. - */ - private final float[][] floatClusterMatrix; - /** - * Matrix contains all input vector/fingerprints to be clustered. - * Each row in the matrix corresponds to an input vector. - */ - private final float[][] dataMatrix; - /** - * The vigilance parameter is between 0 and 1. The parameter influences the type of clustering. - * A vigilance parameter close to 0 leads to a coarse clustering (few clusters) and a vigilance - * parameter close to 1, on the other hand, leads to a fine clustering (many clusters). - */ - private final float vigilanceParameter; - // - // - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(Art2aFloatClusteringResult.class.getName()); - // - // - // - /** - * Constructor. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aNumberOfEpochs final epoch number. - * @param aNumberOfDetectedClusters final number of detected clusters. - * @param aClusteringProcessQueue clustering result (process) queue of typ String. - * The queue is required to be able to export the cluster results. If it is not specified, they are set to null and - * export is not possible. - * @param aClusteringResultQueue clustering result queue of typ String. See {@code #aClusteringProcessQueue} - * @param aClusterView array for cluster assignment of each input vector. - * @param aClusterMatrix float cluster vector matrix. All cluster vectors created after float ART-2a clustering are - * stored in this matrix. - * @param aDataMatrix float matrix with all input vectors/fingerprints. - * Each row in the matrix corresponds to an input vector. - * @throws NullPointerException is thrown, if the specified matrices are null. - * @throws IllegalArgumentException is thrown, if the specified vigilance parameter is invalid. - * - */ - public Art2aFloatClusteringResult(float aVigilanceParameter, int aNumberOfEpochs, int aNumberOfDetectedClusters, - int[] aClusterView, float[][] aClusterMatrix, float[][] aDataMatrix, - ConcurrentLinkedQueue aClusteringProcessQueue, - ConcurrentLinkedQueue aClusteringResultQueue) - throws NullPointerException, IllegalArgumentException { - super(aNumberOfEpochs, aNumberOfDetectedClusters, aClusterView, aClusteringProcessQueue, aClusteringResultQueue); - Objects.requireNonNull(aClusterMatrix, "aClusterMatrix is null."); - Objects.requireNonNull(aDataMatrix, "aDataMatrix is null."); - if (aVigilanceParameter <= 0.0f || aVigilanceParameter >= 1.0f) { - throw new IllegalArgumentException("The vigilance parameter must be greater than 0 and smaller than 1."); - } - this.vigilanceParameter = aVigilanceParameter; - this.floatClusterMatrix = aClusterMatrix; - this.dataMatrix = aDataMatrix; - this.cacheClusterRepresentativesIndices = new int[aNumberOfDetectedClusters]; - Arrays.fill(this.cacheClusterRepresentativesIndices, -2); - this.cacheAngleBetweenClusters = new float[aNumberOfDetectedClusters][aNumberOfDetectedClusters]; - } - // - /** - * Constructor. - * - * @param aVigilanceParameter parameter to influence the number of clusters. - * @param aNumberOfEpochs final epoch number. - * @param aNumberOfDetectedClusters final number of detected clusters. - * @param aClusterView array for cluster assignment of each input vector. - * @param aClusterMatrix float cluster vector matrix. All cluster vectors created after float Art-2a clustering are - * stored in this matrix. - * @param aDataMatrix float matrix with all input vectors/fingerprints. - * Each row in the matrix corresponds to an input vector. - * @throws NullPointerException is thrown, if the specified matrices are null. - * @throws IllegalArgumentException is thrown, if the specified vigilance parameter is invalid. - *

- * - * @see de.unijena.cheminf.clustering.art2a.results.Art2aFloatClusteringResult#Art2aFloatClusteringResult(float, - * int, int, int[], float[][], float[][],ConcurrentLinkedQueue, ConcurrentLinkedQueue) - */ - public Art2aFloatClusteringResult(float aVigilanceParameter, int aNumberOfEpochs, int aNumberOfDetectedClusters, - int[] aClusterView, float[][] aClusterMatrix, float[][] aDataMatrix) throws NullPointerException { - this(aVigilanceParameter, aNumberOfEpochs, aNumberOfDetectedClusters, aClusterView, aClusterMatrix, aDataMatrix,null, null); - } - //
- // - // - /** - * {@inheritDoc} - */ - @Override - public Float getVigilanceParameter() { - return this.vigilanceParameter; - } - /** - * {@inheritDoc} - */ - @Override - public int getClusterRepresentatives(int aClusterNumber) throws IllegalArgumentException { - if(aClusterNumber >= this.getNumberOfDetectedClusters() || aClusterNumber < 0) { - throw new IllegalArgumentException("The given cluster number does not exist or is invalid."); - } - if(this.cacheClusterRepresentativesIndices[aClusterNumber] == -2) { - int[] tmpClusterIndices = this.getClusterIndices(aClusterNumber); - float[] tmpCurrentClusterVector = this.floatClusterMatrix[aClusterNumber]; - float tmpFactor; - float[] tmpMatrixRow; - float[] tmpScalarProductArray = new float[tmpClusterIndices.length + 1]; - int tmpIterator = 0; - for (int tmpCurrentInput : tmpClusterIndices) { - tmpMatrixRow = this.dataMatrix[tmpCurrentInput]; - tmpFactor = 0.0f; - for (int i = 0; i < tmpMatrixRow.length; i++) { - tmpFactor += tmpMatrixRow[i] * tmpCurrentClusterVector[i]; - } - tmpScalarProductArray[tmpIterator] = tmpFactor; - tmpIterator++; - } - int tmpIndexOfGreatestScalarProduct = 0; - for (int i = 0; i < tmpScalarProductArray.length; i++) { - if (tmpScalarProductArray[i] > tmpScalarProductArray[tmpIndexOfGreatestScalarProduct]) { - tmpIndexOfGreatestScalarProduct = i; - } - } - this.cacheClusterRepresentativesIndices[aClusterNumber] = tmpClusterIndices[tmpIndexOfGreatestScalarProduct]; - return tmpClusterIndices[tmpIndexOfGreatestScalarProduct]; - } else { - return this.cacheClusterRepresentativesIndices[aClusterNumber]; - } - } - // - /** - * {@inheritDoc} - */ - @Override - public Float getAngleBetweenClusters(int aFirstCluster, int aSecondCluster) throws IllegalArgumentException { - if(aFirstCluster < 0 || aSecondCluster < 0) { - throw new IllegalArgumentException("The given cluster number is negative/invalid."); - } - int tmpNumberOfDetectedCluster = this.getNumberOfDetectedClusters(); - float tmpAngle; - if(aFirstCluster == aSecondCluster && (aFirstCluster >= tmpNumberOfDetectedCluster)) { - throw new IllegalArgumentException("The given cluster number(s) do(es) not exist"); - } else if (aFirstCluster == aSecondCluster) { - return 0.0f; - } else { - if (aFirstCluster >= tmpNumberOfDetectedCluster || aSecondCluster >= tmpNumberOfDetectedCluster) { - throw new IllegalArgumentException("The given cluster number does not exist."); - } - if(this.cacheAngleBetweenClusters[aFirstCluster] [aSecondCluster] == 0) { - float[] tmpFirstCluster = this.floatClusterMatrix[aFirstCluster]; - float[] tmpSecondCluster = this.floatClusterMatrix[aSecondCluster]; - float tmpFactor = (float) (180.0 / Math.PI); - float tmpProduct = 0.0f; - for (int i = 0; i < tmpFirstCluster.length; i++) { - tmpProduct += tmpFirstCluster[i] * tmpSecondCluster[i]; - } - tmpAngle = (float) (tmpFactor * Math.acos(tmpProduct)); - this.cacheAngleBetweenClusters[aFirstCluster][aSecondCluster] = tmpAngle; - this.cacheAngleBetweenClusters[aSecondCluster][aFirstCluster] = tmpAngle; - return tmpAngle; - } else { - return this.cacheAngleBetweenClusters[aFirstCluster][aSecondCluster]; - } - } - } - // -} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/util/FileUtil.java b/src/main/java/de/unijena/cheminf/clustering/art2a/util/FileUtil.java deleted file mode 100644 index df76c04..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/util/FileUtil.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a.util; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; -import java.lang.reflect.Array; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * File utility. - * The class provides convenience methods. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public final class FileUtil { - // - /** - * Name of file for exporting clustering process. - */ - private static final String CLUSTERING_PROCESS_FILE_NAME = "Clustering_Process"; - /** - * Name of the file for exporting clustering result. - */ - private static final String CLUSTERING_RESULT_FILE_NAME = "Clustering_Result"; - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(FileUtil.class.getName()); - // - // - // - /** - * Set up the clustering result text file writers. This method creates a set of Writer objects based on the - * specified and desired Writer typ for writing the clustering result and clustering process to - * separate text files. This methode allows the user to create 3 different Writer types, e.g. FileWriter, - * PrintWriter and BufferedWriter. The user can specify the Writer typ via the aWriterClass parameter. - * The file names will include a timestamp to make them unique. - * The method creates the necessary files in the specified path. - * - * If necessary, existing result files will also be deleted. - * - * @param aPathName path to the export folder where the text files are to be saved. - * @param aWriterClass Writer ytp to be created. The user can choose one of the supported classes. - * @param generic typ of the Writer. This will be determined, based on the specified aWriterClass parameter. - * @return Writer[] an array of a Writer objects, - * where index 0 corresponds to the clustering result Writer, - * and index 1 corresponds to the clustering process Writer. - */ - public static T[] setUpClusteringResultTextFilePrinters(String aPathName, Class aWriterClass) { - T tmpClusteringResultWriter = null; - T tmpClusteringProcessWriter = null; - String tmpWorkingPath; - try { - LocalDateTime tmpDateTime = LocalDateTime.now(); - String tmpProcessingTime = tmpDateTime.format(DateTimeFormatter.ofPattern("uuuu_MM_dd_HH_mm_ss")); - tmpWorkingPath = (new File(aPathName) + File.separator); - new File(tmpWorkingPath + CLUSTERING_RESULT_FILE_NAME).mkdirs(); - File tmpClusteringResultFile = new File(tmpWorkingPath + File.separator + CLUSTERING_RESULT_FILE_NAME + - File.separator + CLUSTERING_RESULT_FILE_NAME + "_" + tmpProcessingTime + ".txt"); - if (aWriterClass.equals(PrintWriter.class)) { - tmpClusteringResultWriter = (T) new PrintWriter(tmpClusteringResultFile); - } else if (aWriterClass.equals(BufferedWriter.class)) { - tmpClusteringResultWriter = (T) new BufferedWriter(new FileWriter(tmpClusteringResultFile)); - } else if (aWriterClass.equals(FileWriter.class)) { - tmpClusteringResultWriter = (T) new FileWriter(tmpClusteringResultFile); - } - FileUtil.deleteOldestFileIfNecessary(tmpWorkingPath + File.separator + CLUSTERING_RESULT_FILE_NAME); - new File(tmpWorkingPath + CLUSTERING_PROCESS_FILE_NAME).mkdirs(); - File tmpClusteringProcessFile = new File(tmpWorkingPath + File.separator + CLUSTERING_PROCESS_FILE_NAME + - File.separator + CLUSTERING_PROCESS_FILE_NAME + "_" + tmpProcessingTime + ".txt"); - if (aWriterClass.equals(PrintWriter.class)) { - tmpClusteringProcessWriter = (T) new PrintWriter(tmpClusteringProcessFile); - } else if (aWriterClass.equals(BufferedWriter.class)) { - tmpClusteringProcessWriter = (T) new BufferedWriter(new FileWriter(tmpClusteringProcessFile)); - } else if (aWriterClass.equals(FileWriter.class)) { - tmpClusteringProcessWriter = (T) new FileWriter(tmpClusteringProcessFile); - } - FileUtil.deleteOldestFileIfNecessary(tmpWorkingPath + File.separator + CLUSTERING_PROCESS_FILE_NAME); - } catch (IOException anException) { - FileUtil.LOGGER.log(Level.SEVERE, "The files could not be created."); - } - T[] tmpWriterArray = (T[]) Array.newInstance(aWriterClass, 2); - tmpWriterArray[0] = tmpClusteringResultWriter; - tmpWriterArray[1] = tmpClusteringProcessWriter; - return tmpWriterArray; - } - // - /** - * The text file contains input vectors/fingerprints that are read in to prepare them for float clustering. - * Each line of the text file represents one input vector/fingerprint. Each component of the vector is - * separated by a separator. The file has no header line. - * - * @param aFilePath path of the text file - * @param aSeparator separator of the text file to separate the input vector/fingerprint components from each other. - * @return float matrix is returned that contains the input vectors/fingerprints that were read in. - * Each row of the matrix represents one fingerprint. - * @throws IllegalArgumentException is thrown if the given file path is invalid. - */ - public static float[][] importFloatDataMatrixFromTextFile(String aFilePath, char aSeparator) throws IllegalArgumentException { - if (aFilePath == null || aFilePath.isEmpty() || aFilePath.isBlank()) { - throw new IllegalArgumentException("aFileName is null or empty/blank."); - } - BufferedReader tmpFingerprintFileReader = null; - ArrayList tmpFingerprintList = new ArrayList<>(); - String tmpFingerprintLine; - int tmpDataMatrixRow = 0; - try { - tmpFingerprintFileReader = new BufferedReader(new FileReader(aFilePath)); - while ((tmpFingerprintLine = tmpFingerprintFileReader.readLine()) != null) { - String[] tmpFingerprint = tmpFingerprintLine.split(String.valueOf(aSeparator)); - float[] tmpFingerprintFloatArray = new float[tmpFingerprint.length]; - for (int i = 0; i < tmpFingerprint.length; i++) { - try { - tmpFingerprintFloatArray[i] = Float.parseFloat(tmpFingerprint[i]); - } catch (NumberFormatException anException) { - FileUtil.LOGGER.log(Level.SEVERE, anException.toString(), anException); - } - } - tmpDataMatrixRow++; - tmpFingerprintList.add(tmpFingerprintFloatArray); - } - } catch (IOException anException) { - FileUtil.LOGGER.log(Level.SEVERE, anException.toString(), anException + " invalid fingerprint file." + - " At least one line is not readable."); - } finally { - if (tmpFingerprintFileReader != null) { - try { - tmpFingerprintFileReader.close(); - } catch (IOException anException) { - FileUtil.LOGGER.log(Level.SEVERE, anException.toString(), anException + "The reader could not " + - "be closed." ); - } - } - } - float[][] aDataMatrix = new float[tmpDataMatrixRow][tmpFingerprintList.get(0).length]; - for (int tmpCurrentMatrixRow = 0; tmpCurrentMatrixRow < tmpDataMatrixRow; tmpCurrentMatrixRow++) { - aDataMatrix[tmpCurrentMatrixRow] = tmpFingerprintList.get(tmpCurrentMatrixRow); - } - return aDataMatrix; - } - /** - * The text file contains input vectors/fingerprints that are read in to prepare them for double clustering. - * Each line of the text file represents one input vector/fingerprint. Each component of the vector is - * separated by a separator. The file has no header line. - * - * @param aFilePath path of the text file - * @param aSeparator separator of the text file to separate the input vector/fingerprint components from each other. - * @return double matrix is returned that contains the input vectors/fingerprints that were read in. - * Each row of the matrix represents one fingerprint. - * @throws IllegalArgumentException is thrown if the given file path is invalid. - */ - public static double[][] importDoubleDataMatrixFromTextFile(String aFilePath, char aSeparator) throws IllegalArgumentException { - if (aFilePath == null || aFilePath.isEmpty() || aFilePath.isBlank()) { - throw new IllegalArgumentException("aFileName is null or empty/blank."); - } - BufferedReader tmpFingerprintFileReader = null; - ArrayList tmpFingerprintList = new ArrayList<>(); - String tmpFingerprintLine; - int tmpDataMatrixRow = 0; - try { - tmpFingerprintFileReader = new BufferedReader(new FileReader(aFilePath)); - while ((tmpFingerprintLine = tmpFingerprintFileReader.readLine()) != null) { - String[] tmpFingerprint = tmpFingerprintLine.split(String.valueOf(aSeparator)); - double[] tmpFingerprintDoubleArray = new double[tmpFingerprint.length]; - for (int i = 0; i < tmpFingerprint.length; i++) { - try { - tmpFingerprintDoubleArray[i] = Double.parseDouble(tmpFingerprint[i]); - } catch (NumberFormatException anException) { - FileUtil.LOGGER.log(Level.SEVERE, anException.toString(), anException); - } - } - tmpDataMatrixRow++; - tmpFingerprintList.add(tmpFingerprintDoubleArray); - } - } catch (IOException anException) { - FileUtil.LOGGER.log(Level.SEVERE, anException.toString(), anException + " invalid fingerprint file." + - " At least one line is not readable."); - } finally { - if (tmpFingerprintFileReader != null) { - try { - tmpFingerprintFileReader.close(); - } catch (IOException anException) { - FileUtil.LOGGER.log(Level.SEVERE, anException.toString(), anException + "The reader " + - "could not be closed." ); - } - } - } - double[][] tmpDataMatrix = new double[tmpDataMatrixRow][tmpFingerprintList.get(0).length]; - for (int tmpCurrentMatrixRow = 0; tmpCurrentMatrixRow < tmpDataMatrixRow; tmpCurrentMatrixRow++) { - tmpDataMatrix[tmpCurrentMatrixRow] = tmpFingerprintList.get(tmpCurrentMatrixRow); - } - return tmpDataMatrix; - } - // - // - // - /** - * Clustering files are deleted when their number is greater than 10. - * - * @param aPathName of the clustering files. - */ - private static void deleteOldestFileIfNecessary(String aPathName) { - File tmpDirectory = new File(aPathName); - File[] tmpClusteringFiles = tmpDirectory.listFiles(); - if (tmpClusteringFiles != null && tmpClusteringFiles.length > 10) { // magic number - Arrays.sort(tmpClusteringFiles, Comparator.comparingLong(File::lastModified)); - if (tmpClusteringFiles[0].delete()) { - FileUtil.LOGGER.log(Level.INFO, "Deleted file: " + tmpClusteringFiles[0].getName()); - } - } - } - // -} diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aDoubleClusteringTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aDoubleClusteringTest.java deleted file mode 100644 index b0a38bd..0000000 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aDoubleClusteringTest.java +++ /dev/null @@ -1,767 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a; -import de.unijena.cheminf.clustering.art2a.clustering.Art2aDoubleClustering; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClusteringResult; -import de.unijena.cheminf.clustering.art2a.util.FileUtil; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.BufferedWriter; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -/** - * Test class for double clustering. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aDoubleClusteringTest { - // - /** - * Clustering result instance - */ - private static IArt2aClusteringResult clusteringResult; - /** - * Array for storing number of epochs for all vigilance parameters - */ - private static int[] numberOfEpochsForAllVigilanceParameter; - /** - * Array for storing number of detected clusters for all vigilance parameters - */ - private static int[] numberOfDetectedClustersForAllVigilanceParameter; - /** - * Matrix for storing the indices in different clusters for certain vigilance parameters - */ - private static int[][] clusterIndicesForAllVigilanceParameter; - /** - * Array for storing the cluster representatives in different clusters for certain vigilance parameters - */ - private static int[] clusterRepresentativesForAllVigilanceParameter; - /** - * Array for storing the angle between different clusters for certain vigilance parameters - */ - private static double[] clusterAnglesForAllVigilanceParameter; - // - // - // - /** - * Starts double clustering and stores the results in arrays to check for correctness. - * Clustering is performed for vigilance parameters from 0.1 to 0.9 in 0.1 steps. - * The clustering process for the different vigilance parameters is performed in parallel. - * - */ - @BeforeAll - public static void startArt2aClusteringTest() throws Exception { - double[][] tmpTestDataMatrix = FileUtil.importDoubleDataMatrixFromTextFile( - "src/test/resources/de/unijena/cheminf/clustering/art2a/Bit_Fingerprints.txt", ','); - ExecutorService tmpExecutorService = Executors.newFixedThreadPool(9); // number of tasks - List tmpClusteringTask = new LinkedList<>(); - for (double tmpVigilanceParameter = 0.1; tmpVigilanceParameter < 0.9; tmpVigilanceParameter += 0.1) { //to 0.9 in order to leave the number of vigilance parameters at 9. - Art2aClusteringTask tmpART2aDoubleClusteringTask = new Art2aClusteringTask(tmpVigilanceParameter, - tmpTestDataMatrix, 100, true); - tmpClusteringTask.add(tmpART2aDoubleClusteringTask); - } - BufferedWriter[] tmpWriter = FileUtil.setUpClusteringResultTextFilePrinters("Clustering_Result_Folder", - BufferedWriter.class); - List> tmpFuturesList; - Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter = new int[9]; - Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter = new int[9]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter = new int[9][]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[0] = new int[4]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[1] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[2] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[3] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[4] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[5] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[6] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[7] = new int[1]; - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[8] = new int[1]; - Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter = new int[9]; - Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter = new double[9]; - tmpFuturesList = tmpExecutorService.invokeAll(tmpClusteringTask); - int tmpIterator = 0; - for (Future tmpFuture : tmpFuturesList) { - try { - Art2aDoubleClusteringTest.clusteringResult = tmpFuture.get(); - Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[tmpIterator] = Art2aDoubleClusteringTest.clusteringResult.getNumberOfEpochs(); - Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[tmpIterator] = Art2aDoubleClusteringTest.clusteringResult.getNumberOfDetectedClusters(); - Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[tmpIterator] = Art2aDoubleClusteringTest.clusteringResult.getClusterIndices(tmpIterator); - Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[tmpIterator] = (double) Art2aDoubleClusteringTest.clusteringResult.getAngleBetweenClusters(tmpIterator, tmpIterator + 1); - Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[tmpIterator] = Art2aDoubleClusteringTest.clusteringResult.getClusterRepresentatives(tmpIterator); - Art2aDoubleClusteringTest.clusteringResult.exportClusteringResultsToTextFiles(tmpWriter[0], tmpWriter[1]); - tmpIterator++; - } catch (RuntimeException anException) { - throw anException; - } - } - tmpWriter[0].flush(); - tmpWriter[0].close(); - tmpWriter[1].flush(); - tmpWriter[1].close(); - tmpExecutorService.shutdown(); - } - // - // - // - /** - * Tests number of epoch for vigilance parameter 0.1 - */ - @Test - public void testNumberOfEpochsFor01f() { - int tmpTestNumberOfEpochsFor01f = 2; - int tmpNumberOfEpochs01f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestNumberOfEpochsFor01f, tmpNumberOfEpochs01f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.2 - */ - @Test - public void testNumberOfEpochsFor02f() { - int tmpTestNumberOfEpochs02f = 2; - int tmpNumberOfEpochs02f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestNumberOfEpochs02f, tmpNumberOfEpochs02f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.3 - */ - @Test - public void testNumberOfEpochsFor03() { - int tmpTestNumberOfEpochs03f = 2; - int tmpNumberOfEpochs03f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestNumberOfEpochs03f, tmpNumberOfEpochs03f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.4 - */ - @Test - public void testNumberOfEpochsFor04() { - int tmpTestNumberOfEpochs04f = 2; - int tmpNumberOfEpochs04f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestNumberOfEpochs04f, tmpNumberOfEpochs04f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.5 - */ - @Test - public void testNumberOfEpochsFor05() { - int tmpTestNumberOfEpochs05f = 2; - int tmpNumberOfEpochs05f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestNumberOfEpochs05f, tmpNumberOfEpochs05f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.6 - */ - @Test - public void testNumberOfEpochsFor06() { - int tmpTestNumberOfEpochs06f = 2; - int tmpNumberOfEpochs06f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestNumberOfEpochs06f, tmpNumberOfEpochs06f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.7 - */ - @Test - public void testNumberOfEpochsFor07() { - int tmpTestNumberOfEpochs07f = 2; - int tmpNumberOfEpochs07f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestNumberOfEpochs07f, tmpNumberOfEpochs07f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.8 - */ - @Test - public void testNumberOfEpochsFor08() { - int tmpTestNumberOfEpochs08f = 2; - int tmpNumberOfEpochs08f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestNumberOfEpochs08f, tmpNumberOfEpochs08f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.9 - */ - @Test - public void testNumberOfEpochsFor09() { - int tmpTestNumberOfEpochs09f = 2; - int tmpNumberOfEpochs09f = Art2aDoubleClusteringTest.numberOfEpochsForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestNumberOfEpochs09f, tmpNumberOfEpochs09f); - } - // - // - // - /** - * Tests number of detected clusters for vigilance parameter 0.1 - */ - @Test - public void testNumberOfDetectedClustersFor01() { - int tmpTestNumberOfDetectedClusters01 = 6; - int tmpNumberOfDetectedClusters01 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters01, tmpNumberOfDetectedClusters01); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.2 - */ - @Test - public void testNumberOfDetectedClustersFor02() { - int tmpTestNumberOfDetectedClusters02 = 6; - int tmpNumberOfDetectedClusters02 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters02, tmpNumberOfDetectedClusters02); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.3 - */ - @Test - public void testNumberOfDetectedClustersFor03() { - int tmpTestNumberOfDetectedClusters03 = 6; - int tmpNumberOfDetectedClusters03 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters03, tmpNumberOfDetectedClusters03); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.4 - */ - @Test - public void testNumberOfDetectedClustersFor04() { - int tmpTestNumberOfDetectedClusters04 = 8; - int tmpNumberOfDetectedClusters04 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters04, tmpNumberOfDetectedClusters04); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.5 - */ - @Test - public void testNumberOfDetectedClustersFor05() { - int tmpTestNumberOfDetectedClusters05 = 9; - int tmpNumberOfDetectedClusters05 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters05, tmpNumberOfDetectedClusters05); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.6 - */ - @Test - public void testNumberOfDetectedClustersFor06() { - int tmpTestNumberOfDetectedClusters06 = 10; - int tmpNumberOfDetectedClusters06 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters06, tmpNumberOfDetectedClusters06); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.7 - */ - @Test - public void testNumberOfDetectedClustersFor07() { - int tmpTestNumberOfDetectedClusters07 = 10; - int tmpNumberOfDetectedClusters07 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters07, tmpNumberOfDetectedClusters07); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.8 - */ - @Test - public void testNumberOfDetectedClustersFor08() { - int tmpTestNumberOfDetectedClusters08 = 10; - int tmpNumberOfDetectedClusters08 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters08, tmpNumberOfDetectedClusters08); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.9 - */ - @Test - public void testNumberOfDetectedClustersFor09() { - int tmpTestNumberOfDetectedClusters09 = 10; - int tmpNumberOfDetectedClusters09 = Art2aDoubleClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters09, tmpNumberOfDetectedClusters09); - } - // - // - // - /** - * Tests the cluster indices in cluster 0 for vigilance parameter 0.1 - */ - @Test - public void testClusterIndicesInCluster0ForVigilanceParameter01() { - int[] tmpTestClusterIndicesInCluster0For01 = {4,6,7,9}; - int[] tmpClusterIndicesInClusterFor0For01 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[0]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster0For01, tmpClusterIndicesInClusterFor0For01); - } - // - /** - * Tests the cluster indices in cluster 1 for vigilance parameter 0.2 - */ - @Test - public void testClusterIndicesInCLuster1ForVigilanceParameter02() { - int[] tmpTestClusterIndicesInCluster1For02 = {1,2}; - int[] tmpClusterIndicesInClusterFor1For02 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[1]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster1For02, tmpClusterIndicesInClusterFor1For02); - } - // - /** - * Tests the cluster indices in cluster 2 for vigilance parameter 0.3 - */ - @Test - public void testClusterIndicesInCLuster2ForVigilanceParameter03() { - int[] tmpTestClusterIndicesInCluster2For03 = {3}; - int[] tmpClusterIndicesInClusterFor2For03 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[2]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster2For03, tmpClusterIndicesInClusterFor2For03); - } - // - /** - * Tests the cluster indices in cluster 3 for vigilance parameter 0.4 - */ - @Test - public void testClusterIndicesInCLuster3ForVigilanceParameter04() { - int[] tmpTestClusterIndicesInCluster3For04 = {2}; - int[] tmpClusterIndicesInClusterFor3For04 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[3]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster3For04, tmpClusterIndicesInClusterFor3For04); - } - // - /** - * Tests the cluster indices in cluster 4 for vigilance parameter 0.5 - */ - @Test - public void testClusterIndicesInCLuster4ForVigilanceParameter05() { - int[] tmpTestClusterIndicesInCluster4For05 = {5}; - int[] tmpClusterIndicesInCluster4For05 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[4]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster4For05, tmpClusterIndicesInCluster4For05); - } - // - /** - * Tests the cluster indices in cluster 5 for vigilance parameter 0.6 - */ - @Test - public void testClusterIndicesInCLuster5ForVigilanceParameter06() { - int[] tmpTestClusterIndicesInCluster5For06 = {5}; - int[] tmpClusterIndicesInCluster5For06 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[5]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster5For06, tmpClusterIndicesInCluster5For06); - } - // - /** - * Tests the cluster indices in cluster 6 for vigilance parameter 0.7 - */ - @Test - public void testClusterIndicesInCLuster6ForVigilanceParameter07() { - int[] tmpTestClusterIndicesInCluster6For07 = {6}; - int[] tmpClusterIndicesInClusterFor6For07 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[6]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster6For07, tmpClusterIndicesInClusterFor6For07); - } - // - /** - * Tests the cluster indices in cluster 7 for vigilance parameter 0.8 - */ - @Test - public void testClusterIndicesInCLuster7ForVigilanceParameter08() { - int[] tmpTestClusterIndicesInCluster7For08 = {4}; - int[] tmpClusterIndicesInCluster7For08 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[7]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster7For08, tmpClusterIndicesInCluster7For08); - } - // - /** - * Tests the cluster indices in cluster 8 for vigilance parameter 0.9 - */ - @Test - public void testClusterIndicesInCLuster8ForVigilanceParameter09() { - int[] tmpTestClusterIndicesInCluster8For09 = {8}; - int[] tmpClusterIndicesInCluster8For09 = Art2aDoubleClusteringTest.clusterIndicesForAllVigilanceParameter[8]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster8For09, tmpClusterIndicesInCluster8For09); - } - // - // - // - /** - * Tests the cluster representatives in cluster 0 for vigilance parameter 0.1 - */ - @Test - public void testClusterRepresentativesInCluster0ForVigilanceParameter01() { - int tmpTestClusterRepresentativesIndexInCluster0For01 = 9; - int tmpClusterRepresentativesIndexInCluster0For01 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster0For01, tmpClusterRepresentativesIndexInCluster0For01); - } - // - /** - * Tests the cluster representatives in cluster 1 for vigilance parameter 0.2 - */ - @Test - public void testClusterRepresentativesInCluster1ForVigilanceParameter02() { - int tmpTestClusterRepresentativesIndexInCluster1For02 = 1; - int tmpClusterRepresentativesIndexInCluster1For02 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster1For02, tmpClusterRepresentativesIndexInCluster1For02); - } - // - /** - * Tests the cluster representatives in cluster 2 for vigilance parameter 0.3 - */ - @Test - public void testClusterRepresentativesInCluster2ForVigilanceParameter03() { - int tmpTestClusterRepresentativesIndexInCluster2For03 = 3; - int tmpClusterRepresentativesIndexInCluster2For03 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster2For03, tmpClusterRepresentativesIndexInCluster2For03); - } - // - /** - * Tests the cluster representatives in cluster 3 for vigilance parameter 0.4 - */ - @Test - public void testClusterRepresentativesInCluster3ForVigilanceParameter04() { - int tmpTestClusterRepresentativesIndexInCluster3For04 = 2; - int tmpClusterRepresentativesIndexInCluster3For04 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster3For04, tmpClusterRepresentativesIndexInCluster3For04); - } - // - /** - * Tests the cluster representatives in cluster 4 for vigilance parameter 0.5 - */ - @Test - public void testClusterRepresentativesInCluster4ForVigilanceParameter05() { - int tmpTestClusterRepresentativesIndexInCluster4For05 = 5; - int tmpClusterRepresentativesIndexInCluster4For05 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster4For05, tmpClusterRepresentativesIndexInCluster4For05); - } - // - /** - * Tests the cluster representatives in cluster 5 for vigilance parameter 0.6 - */ - @Test - public void testClusterRepresentativesInCluster5ForVigilanceParameter06() { - int tmpTestClusterRepresentativesIndexInCluster5For06 = 5; - int tmpClusterRepresentativesIndexInCluster5For06 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster5For06, tmpClusterRepresentativesIndexInCluster5For06); - } - // - /** - * Tests the cluster representatives in cluster 6 for vigilance parameter 0.7 - */ - @Test - public void testClusterRepresentativesInCluster6ForVigilanceParameter07() { - int tmpTestClusterRepresentativesIndexInCluster6For07 = 6; - int tmpClusterRepresentativesIndexInCluster6For07 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster6For07, tmpClusterRepresentativesIndexInCluster6For07); - } - // - /** - * Tests the cluster representatives in cluster 7 for vigilance parameter 0.8 - */ - @Test - public void testClusterRepresentativesInCluster7ForVigilanceParameter08() { - int tmpTestClusterRepresentativesIndexInCluster7For08 = 4; - int tmpClusterRepresentativesIndexInCluster7For08 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster7For08, tmpClusterRepresentativesIndexInCluster7For08); - } - // - /** - * Tests the cluster representatives in cluster 8 for vigilance parameter 0.9 - */ - @Test - public void testClusterRepresentativesInCluster8ForVigilanceParameter09() { - int tmpTestClusterRepresentativesIndexInCluster8For09 = 8; - int tmpClusterRepresentativesIndexInCluster8For09 = Art2aDoubleClusteringTest.clusterRepresentativesForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster8For09, tmpClusterRepresentativesIndexInCluster8For09); - } - // - // - // - /** - * Tests the angle between cluster 0 and 1 for vigilance parameter 0.1 - */ - @Test - public void testAngleBetweenCluster0And1For01() { - double tmpTestAngleBetweenCluster0And1For01 = 64.71956849036344; - double tmpAngleBetweenCluster0And1For01 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestAngleBetweenCluster0And1For01, tmpAngleBetweenCluster0And1For01, 1e-8f); - } - // - /** - * Tests the angle between cluster 1 and 2 for vigilance parameter 0.2 - */ - @Test - public void testAngleBetweenCluster1And2For02() { - double tmpTestAngleBetweenCluster1And2For02 = 80.98592593273575; - double tmpAngleBetweenCluster1And2For02 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestAngleBetweenCluster1And2For02, tmpAngleBetweenCluster1And2For02, 1e-8); - } - // - /** - * Tests the angle between cluster 2 and 3 for vigilance parameter 0.3 - */ - @Test - public void testAngleBetweenCluster2And3For03() { - double tmpTestAngleBetweenCluster2And3For03 = 90.0000; - double tmpAngleBetweenCluster2And3For03 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestAngleBetweenCluster2And3For03, tmpAngleBetweenCluster2And3For03, 1e-8); - } - // - /** - * Tests the angle between cluster 3 and 4 for vigilance parameter 0.4 - */ - @Test - public void testAngleBetweenCluster3And4For04() { - double tmpTestAngleBetweenCluster3And4For04 = 90.0000; - double tmpAngleBetweenCluster3And4For04 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestAngleBetweenCluster3And4For04, tmpAngleBetweenCluster3And4For04, 1e-8); - } - // - /** - * Tests the angle between cluster 4 and 5 for vigilance parameter 0.5 - */ - @Test - public void testAngleBetweenCluster4And5For05() { - double tmpTestAngleBetweenCluster4And5For05 = 71.56505117707799; - double tmpAngleBetweenCluster4And5For05 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestAngleBetweenCluster4And5For05, tmpAngleBetweenCluster4And5For05, 1e-8); - } - // - /** - * Tests the angle between cluster 5 and 6 for vigilance parameter 0.6 - */ - @Test - public void testAngleBetweenCluster5And6For06() { - double tmpTestAngleBetweenCluster5And6For06 = 71.56505117707799; - double tmpAngleBetweenCluster5And6For06 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestAngleBetweenCluster5And6For06, tmpAngleBetweenCluster5And6For06, 1e-8); - } - // - /** - * Tests the angle between cluster 6 and 7 for vigilance parameter 0.7 - */ - @Test - public void testAngleBetweenCluster6And7For07() { - double tmpTestAngleBetweenCluster6And7For07 = 75.03678256669288; - double tmpAngleBetweenCluster6And7For07 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestAngleBetweenCluster6And7For07, tmpAngleBetweenCluster6And7For07, 1e-8); - } - // - /** - * Tests the angle between cluster 7 and 8 for vigilance parameter 0.8 - */ - @Test - public void testAngleBetweenCluster7And8For08() { - double tmpTestAngleBetweenCluster7And8For08 = 90.000; - double tmpAngleBetweenCluster7And8For08 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestAngleBetweenCluster7And8For08, tmpAngleBetweenCluster7And8For08, 1e-8); - } - // - /** - * Tests the angle between cluster 8 and 9 for vigilance parameter 0.9 - */ - @Test - public void testAngleBetweenCluster8And9For09() { - double tmpTestAngleBetweenCluster8And9For09 = 90.000; - double tmpAngleBetweenCluster8And9For09 = Art2aDoubleClusteringTest.clusterAnglesForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestAngleBetweenCluster8And9For09, tmpAngleBetweenCluster8And9For09, 1e-8); - } - // - // - // - /** - * Method tests whether the import of a data matrix from a text file works correctly. - * - * @throws NoSuchMethodException is thrown, if the private method is not found - */ - @Test - public void testImportFloatDataMatrix() throws NoSuchMethodException { - double[][] tmpImportDoubleDataMatrix = FileUtil.importDoubleDataMatrixFromTextFile("src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt", ','); - Method tmpImportMethod = FileUtil.class.getDeclaredMethod("importDoubleDataMatrixFromTextFile", String.class, char.class); - tmpImportMethod.setAccessible(true); - double[][] tmpTestDataMatrix = new double[6][10]; - - tmpTestDataMatrix[0][0] = 1.0; - tmpTestDataMatrix[0][1] = 0.0; - tmpTestDataMatrix[0][2] = 0.0; - tmpTestDataMatrix[0][3] = 0.0; - tmpTestDataMatrix[0][4] = 0.0; - tmpTestDataMatrix[0][5] = 0.0; - tmpTestDataMatrix[0][6] = 0.0; - tmpTestDataMatrix[0][7] = 0.0; - tmpTestDataMatrix[0][8] = 1.0; - tmpTestDataMatrix[0][9] = 0.0; - - tmpTestDataMatrix[1][0] = 0.0; - tmpTestDataMatrix[1][1] = 0.0; - tmpTestDataMatrix[1][2] = 0.0; - tmpTestDataMatrix[1][3] = 0.0; - tmpTestDataMatrix[1][4] = 0.0; - tmpTestDataMatrix[1][5] = 3.0; - tmpTestDataMatrix[1][6] = 1.0; - tmpTestDataMatrix[1][7] = 1.0; - tmpTestDataMatrix[1][8] = 0.0; - tmpTestDataMatrix[1][9] = 0.0; - - tmpTestDataMatrix[2][0] = 0.0; - tmpTestDataMatrix[2][1] = 0.0; - tmpTestDataMatrix[2][2] = 0.0; - tmpTestDataMatrix[2][3] = 0.0; - tmpTestDataMatrix[2][4] = 0.0; - tmpTestDataMatrix[2][5] = 0.0; - tmpTestDataMatrix[2][6] = 0.0; - tmpTestDataMatrix[2][7] = 0.0; - tmpTestDataMatrix[2][8] = 0.0; - tmpTestDataMatrix[2][9] = 0.0; - - tmpTestDataMatrix[3][0] = 0.0; - tmpTestDataMatrix[3][1] = 0.0; - tmpTestDataMatrix[3][2] = 0.0; - tmpTestDataMatrix[3][3] = 0.0; - tmpTestDataMatrix[3][4] = 0.0; - tmpTestDataMatrix[3][5] = 0.0; - tmpTestDataMatrix[3][6] = 0.0; - tmpTestDataMatrix[3][7] = 0.0; - tmpTestDataMatrix[3][8] = 0.0; - tmpTestDataMatrix[3][9] = 0.0; - - tmpTestDataMatrix[4][0] = 0.0; - tmpTestDataMatrix[4][1] = 0.0; - tmpTestDataMatrix[4][2] = 0.0; - tmpTestDataMatrix[4][3] = 0.0; - tmpTestDataMatrix[4][4] = 1.0; - tmpTestDataMatrix[4][5] = 0.0; - tmpTestDataMatrix[4][6] = 0.0; - tmpTestDataMatrix[4][7] = 0.0; - tmpTestDataMatrix[4][8] = 0.0; - tmpTestDataMatrix[4][9] = 0.0; - - tmpTestDataMatrix[5][0] = 0.0; - tmpTestDataMatrix[5][1] = 1.0; - tmpTestDataMatrix[5][2] = 0.0; - tmpTestDataMatrix[5][3] = 0.0; - tmpTestDataMatrix[5][4] = 0.0; - tmpTestDataMatrix[5][5] = 0.0; - tmpTestDataMatrix[5][6] = 0.0; - tmpTestDataMatrix[5][7] = 0.0; - tmpTestDataMatrix[5][8] = 0.0; - tmpTestDataMatrix[5][9] = 8.0; - Assertions.assertArrayEquals(tmpTestDataMatrix,tmpImportDoubleDataMatrix); - } - // - // - // - /** - * Method tests whether the checks and possible scaling in the data matrix work successfully. - * - * @throws NoSuchMethodException is thrown, if the private method is not found. - * @throws InvocationTargetException is thrown if an exception occurs during the call of the private method. - * @throws IllegalAccessException is thrown if the private method is inaccessible and cannot be called. - */ - @Test - public void testCheckAndScaleDataMatrix() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - double[][] tmpImportDoubleDataMatrix = FileUtil.importDoubleDataMatrixFromTextFile( - "src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt", ','); - Art2aDoubleClustering tmpDoubleClustering = new Art2aDoubleClustering(tmpImportDoubleDataMatrix, - 10, 0.1, 0.99, 0.1); - Method tmpCheckAndScaleDataMatrix = Art2aDoubleClustering.class.getDeclaredMethod( - "getCheckedAndScaledDataMatrix", double[][].class); - tmpCheckAndScaleDataMatrix.setAccessible(true); - tmpCheckAndScaleDataMatrix.invoke(tmpDoubleClustering, (Object) tmpImportDoubleDataMatrix); - double[][] tmpTestDataMatrix = new double[6][10]; - - tmpTestDataMatrix[0][0] = 0.125; - tmpTestDataMatrix[0][1] = 0.0; - tmpTestDataMatrix[0][2] = 0.0; - tmpTestDataMatrix[0][3] = 0.0; - tmpTestDataMatrix[0][4] = 0.0; - tmpTestDataMatrix[0][5] = 0.0; - tmpTestDataMatrix[0][6] = 0.0; - tmpTestDataMatrix[0][7] = 0.0; - tmpTestDataMatrix[0][8] = 0.125; - tmpTestDataMatrix[0][9] = 0.0; - - tmpTestDataMatrix[1][0] = 0.0; - tmpTestDataMatrix[1][1] = 0.0; - tmpTestDataMatrix[1][2] = 0.0; - tmpTestDataMatrix[1][3] = 0.0; - tmpTestDataMatrix[1][4] = 0.0f; - tmpTestDataMatrix[1][5] = 0.375; - tmpTestDataMatrix[1][6] = 0.125; - tmpTestDataMatrix[1][7] = 0.125; - tmpTestDataMatrix[1][8] = 0.0; - tmpTestDataMatrix[1][9] = 0.0; - - tmpTestDataMatrix[2][0] = 0.0; - tmpTestDataMatrix[2][1] = 0.0; - tmpTestDataMatrix[2][2] = 0.0; - tmpTestDataMatrix[2][3] = 0.0; - tmpTestDataMatrix[2][4] = 0.0; - tmpTestDataMatrix[2][5] = 0.0; - tmpTestDataMatrix[2][6] = 0.0; - tmpTestDataMatrix[2][7] = 0.0; - tmpTestDataMatrix[2][8] = 0.0; - tmpTestDataMatrix[2][9] = 0.0; - - tmpTestDataMatrix[3][0] = 0.0; - tmpTestDataMatrix[3][1] = 0.0; - tmpTestDataMatrix[3][2] = 0.0; - tmpTestDataMatrix[3][3] = 0.0; - tmpTestDataMatrix[3][4] = 0.0; - tmpTestDataMatrix[3][5] = 0.0; - tmpTestDataMatrix[3][6] = 0.0; - tmpTestDataMatrix[3][7] = 0.0; - tmpTestDataMatrix[3][8] = 0.0; - tmpTestDataMatrix[3][9] = 0.0; - - tmpTestDataMatrix[4][0] = 0.0; - tmpTestDataMatrix[4][1] = 0.0; - tmpTestDataMatrix[4][2] = 0.0; - tmpTestDataMatrix[4][3] = 0.0; - tmpTestDataMatrix[4][4] = 0.125; - tmpTestDataMatrix[4][5] = 0.0; - tmpTestDataMatrix[4][6] = 0.0; - tmpTestDataMatrix[4][7] = 0.0; - tmpTestDataMatrix[4][8] = 0.0; - tmpTestDataMatrix[4][9] = 0.0; - - tmpTestDataMatrix[5][0] = 0.0; - tmpTestDataMatrix[5][1] = 0.125; - tmpTestDataMatrix[5][2] = 0.0; - tmpTestDataMatrix[5][3] = 0.0; - tmpTestDataMatrix[5][4] = 0.0; - tmpTestDataMatrix[5][5] = 0.0; - tmpTestDataMatrix[5][6] = 0.0; - tmpTestDataMatrix[5][7] = 0.0; - tmpTestDataMatrix[5][8] = 0.0; - tmpTestDataMatrix[5][9] = 1.0; - Assertions.assertArrayEquals(tmpTestDataMatrix,tmpImportDoubleDataMatrix); - } - // -} diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aFloatClusteringTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aFloatClusteringTest.java deleted file mode 100644 index 2fef4e5..0000000 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aFloatClusteringTest.java +++ /dev/null @@ -1,769 +0,0 @@ -/* - * ART2a Clustering for Java - * Copyright (C) 2023 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny - * - * Source code is available at - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a; - -import de.unijena.cheminf.clustering.art2a.clustering.Art2aFloatClustering; -import de.unijena.cheminf.clustering.art2a.interfaces.IArt2aClusteringResult; -import de.unijena.cheminf.clustering.art2a.util.FileUtil; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.PrintWriter; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -/** - * Test class for float clustering. - * - * @author Betuel Sevindik - * @version 1.0.0.0 - */ -public class Art2aFloatClusteringTest { - // - /** - * Clustering result instance - */ - private static IArt2aClusteringResult clusteringResult; - /** - * Array for storing number of epochs for all vigilance parameters - */ - private static int[] numberOfEpochsForAllVigilanceParameter; - /** - * Array for storing number of detected clusters for all vigilance parameters - */ - private static int[] numberOfDetectedClustersForAllVigilanceParameter; - /** - * Matrix for storing the indices in different clusters for certain vigilance parameters - */ - private static int[][] clusterIndicesForAllVigilanceParameter; - /** - * Array for storing the cluster representatives in different clusters for certain vigilance parameters - */ - private static int[] clusterRepresentativesForAllVigilanceParameter; - /** - * Array for storing the angle between different clusters for certain vigilance parameters - */ - private static float[] clusterAnglesForAllVigilanceParameter; - private static float[][] tmpTestDatamatrix; - // - // - // - /** - * Starts float clustering and stores the results in arrays to check for correctness. - * Clustering is performed for vigilance parameters from 0.1 to 0.9 in 0.1 steps. - * The clustering process for the different vigilance parameters is performed in parallel. - * - */ - @BeforeAll - public static void startArt2aClusteringTest() throws Exception { - float[][] tmpTestDataMatrix = FileUtil.importFloatDataMatrixFromTextFile( - "src/test/resources/de/unijena/cheminf/clustering/art2a/Bit_Fingerprints.txt", ','); - ExecutorService tmpExecutorService = Executors.newFixedThreadPool(9); // number of tasks - List tmpClusteringTask = new LinkedList<>(); - for (float tmpVigilanceParameter = 0.1f; tmpVigilanceParameter < 1.0f; tmpVigilanceParameter += 0.1f) { - Art2aClusteringTask tmpART2aFloatClusteringTask = new Art2aClusteringTask(tmpVigilanceParameter, - tmpTestDataMatrix, 100,true); - tmpClusteringTask.add(tmpART2aFloatClusteringTask); - } - PrintWriter[] tmpWriter = FileUtil.setUpClusteringResultTextFilePrinters("Clustering_Result_Folder", PrintWriter.class); - List> tmpFuturesList; - Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter = new int[9]; - Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter = new int[9]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter = new int[9][]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[0] = new int[4]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[1] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[2] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[3] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[4] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[5] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[6] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[7] = new int[1]; - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[8] = new int[1]; - Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter = new int[9]; - Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter = new float[9]; - tmpFuturesList = tmpExecutorService.invokeAll(tmpClusteringTask); - int tmpIterator = 0; - for (Future tmpFuture : tmpFuturesList) { - try { - Art2aFloatClusteringTest.clusteringResult = tmpFuture.get(); - Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[tmpIterator] = Art2aFloatClusteringTest.clusteringResult.getNumberOfEpochs(); - Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[tmpIterator] = Art2aFloatClusteringTest.clusteringResult.getNumberOfDetectedClusters(); - Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[tmpIterator] = Art2aFloatClusteringTest.clusteringResult.getClusterIndices(tmpIterator); - Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[tmpIterator] = (float) Art2aFloatClusteringTest.clusteringResult.getAngleBetweenClusters(tmpIterator, tmpIterator + 1); - Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[tmpIterator] = Art2aFloatClusteringTest.clusteringResult.getClusterRepresentatives(tmpIterator); - Art2aFloatClusteringTest.clusteringResult.exportClusteringResultsToTextFiles(tmpWriter[0], tmpWriter[1]); - tmpIterator++; - } catch (RuntimeException anException) { - System.out.println(anException); - } - } - tmpWriter[0].flush(); - tmpWriter[0].close(); - tmpWriter[1].flush(); - tmpWriter[1].close(); - tmpExecutorService.shutdown(); - } - // - // - // - /** - * Tests number of epoch for vigilance parameter 0.1f - */ - @Test - public void testNumberOfEpochsFor01f() { - int tmpTestNumberOfEpochsFor01f = 2; - int tmpNumberOfEpochs01f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestNumberOfEpochsFor01f, tmpNumberOfEpochs01f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.2f - */ - @Test - public void testNumberOfEpochsFor02f() { - int tmpTestNumberOfEpochs02f = 2; - int tmpNumberOfEpochs02f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestNumberOfEpochs02f, tmpNumberOfEpochs02f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.3f - */ - @Test - public void testNumberOfEpochsFor03() { - int tmpTestNumberOfEpochs03f = 2; - int tmpNumberOfEpochs03f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestNumberOfEpochs03f, tmpNumberOfEpochs03f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.4f - */ - @Test - public void testNumberOfEpochsFor04() { - int tmpTestNumberOfEpochs04f = 2; - int tmpNumberOfEpochs04f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestNumberOfEpochs04f, tmpNumberOfEpochs04f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.5f - */ - @Test - public void testNumberOfEpochsFor05() { - int tmpTestNumberOfEpochs05f = 2; - int tmpNumberOfEpochs05f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestNumberOfEpochs05f, tmpNumberOfEpochs05f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.6f - */ - @Test - public void testNumberOfEpochsFor06() { - int tmpTestNumberOfEpochs06f = 2; - int tmpNumberOfEpochs06f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestNumberOfEpochs06f, tmpNumberOfEpochs06f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.7f - */ - @Test - public void testNumberOfEpochsFor07() { - int tmpTestNumberOfEpochs07f = 2; - int tmpNumberOfEpochs07f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestNumberOfEpochs07f, tmpNumberOfEpochs07f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.8f - */ - @Test - public void testNumberOfEpochsFor08() { - int tmpTestNumberOfEpochs08f = 2; - int tmpNumberOfEpochs08f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestNumberOfEpochs08f, tmpNumberOfEpochs08f); - } - // - /** - * Tests number of epoch for vigilance parameter 0.9f - */ - @Test - public void testNumberOfEpochsFor09() { - int tmpTestNumberOfEpochs09f = 2; - int tmpNumberOfEpochs09f = Art2aFloatClusteringTest.numberOfEpochsForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestNumberOfEpochs09f, tmpNumberOfEpochs09f); - } - // - // - // - /** - * Tests number of detected clusters for vigilance parameter 0.1f - */ - @Test - public void testNumberOfDetectedClustersFor01() { - int tmpTestNumberOfDetectedClusters01 = 6; - int tmpNumberOfDetectedClusters01 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters01, tmpNumberOfDetectedClusters01); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.2f - */ - @Test - public void testNumberOfDetectedClustersFor02() { - int tmpTestNumberOfDetectedClusters02 = 6; - int tmpNumberOfDetectedClusters02 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters02, tmpNumberOfDetectedClusters02); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.3f - */ - @Test - public void testNumberOfDetectedClustersFor03() { - int tmpTestNumberOfDetectedClusters03 = 6; - int tmpNumberOfDetectedClusters03 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters03, tmpNumberOfDetectedClusters03); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.4f - */ - @Test - public void testNumberOfDetectedClustersFor04() { - int tmpTestNumberOfDetectedClusters04 = 8; - int tmpNumberOfDetectedClusters04 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters04, tmpNumberOfDetectedClusters04); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.5f - */ - @Test - public void testNumberOfDetectedClustersFor05() { - int tmpTestNumberOfDetectedClusters05 = 9; - int tmpNumberOfDetectedClusters05 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters05, tmpNumberOfDetectedClusters05); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.6f - */ - @Test - public void testNumberOfDetectedClustersFor06() { - int tmpTestNumberOfDetectedClusters06 = 10; - int tmpNumberOfDetectedClusters06 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters06, tmpNumberOfDetectedClusters06); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.7f - */ - @Test - public void testNumberOfDetectedClustersFor07() { - int tmpTestNumberOfDetectedClusters07 = 10; - int tmpNumberOfDetectedClusters07 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters07, tmpNumberOfDetectedClusters07); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.8f - */ - @Test - public void testNumberOfDetectedClustersFor08() { - int tmpTestNumberOfDetectedClusters08 = 10; - int tmpNumberOfDetectedClusters08 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters08, tmpNumberOfDetectedClusters08); - } - // - /** - * Tests number of detected clusters for vigilance parameter 0.9f - */ - @Test - public void testNumberOfDetectedClustersFor09() { - int tmpTestNumberOfDetectedClusters09 = 10; - int tmpNumberOfDetectedClusters09 = Art2aFloatClusteringTest.numberOfDetectedClustersForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestNumberOfDetectedClusters09, tmpNumberOfDetectedClusters09); - } - // - // - // - /** - * Tests the cluster indices in cluster 0 for vigilance parameter 0.1f - */ - @Test - public void testClusterIndicesInCluster0ForVigilanceParameter01() { - int[] tmpTestClusterIndicesInCluster0For01 = {4,6,7,9}; - int[] tmpClusterIndicesInClusterFor0For01 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[0]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster0For01, tmpClusterIndicesInClusterFor0For01); - } - // - /** - * Tests the cluster indices in cluster 1 for vigilance parameter 0.2f - */ - @Test - public void testClusterIndicesInCLuster1ForVigilanceParameter02() { - int[] tmpTestClusterIndicesInCluster1For02 = {1,2}; - int[] tmpClusterIndicesInClusterFor1For02 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[1]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster1For02, tmpClusterIndicesInClusterFor1For02); - } - // - /** - * Tests the cluster indices in cluster 2 for vigilance parameter 0.3f - */ - @Test - public void testClusterIndicesInCLuster2ForVigilanceParameter03() { - int[] tmpTestClusterIndicesInCluster2For03 = {3}; - int[] tmpClusterIndicesInClusterFor2For03 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[2]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster2For03, tmpClusterIndicesInClusterFor2For03); - } - // - /** - * Tests the cluster indices in cluster 3 for vigilance parameter 0.4f - */ - @Test - public void testClusterIndicesInCLuster3ForVigilanceParameter04() { - int[] tmpTestClusterIndicesInCluster3For04 = {2}; - int[] tmpClusterIndicesInClusterFor3For04 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[3]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster3For04, tmpClusterIndicesInClusterFor3For04); - } - // - /** - * Tests the cluster indices in cluster 4 for vigilance parameter 0.5f - */ - @Test - public void testClusterIndicesInCLuster4ForVigilanceParameter05() { - int[] tmpTestClusterIndicesInCluster4For05 = {5}; - int[] tmpClusterIndicesInCluster4For05 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[4]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster4For05, tmpClusterIndicesInCluster4For05); - } - // - /** - * Tests the cluster indices in cluster 5 for vigilance parameter 0.6f - */ - @Test - public void testClusterIndicesInCLuster5ForVigilanceParameter06() { - int[] tmpTestClusterIndicesInCluster5For06 = {5}; - int[] tmpClusterIndicesInCluster5For06 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[5]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster5For06, tmpClusterIndicesInCluster5For06); - } - // - /** - * Tests the cluster indices in cluster 6 for vigilance parameter 0.7f - */ - @Test - public void testClusterIndicesInCLuster6ForVigilanceParameter07() { - int[] tmpTestClusterIndicesInCluster6For07 = {6}; - int[] tmpClusterIndicesInClusterFor6For07 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[6]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster6For07, tmpClusterIndicesInClusterFor6For07); - } - // - /** - * Tests the cluster indices in cluster 7 for vigilance parameter 0.8f - */ - @Test - public void testClusterIndicesInCLuster7ForVigilanceParameter08() { - int[] tmpTestClusterIndicesInCluster7For08 = {4}; - int[] tmpClusterIndicesInCluster7For08 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[7]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster7For08, tmpClusterIndicesInCluster7For08); - } - // - /** - * Tests the cluster indices in cluster 8 for vigilance parameter 0.9f - */ - @Test - public void testClusterIndicesInCLuster8ForVigilanceParameter09() { - int[] tmpTestClusterIndicesInCluster8For09 = {8}; - int[] tmpClusterIndicesInCluster8For09 = Art2aFloatClusteringTest.clusterIndicesForAllVigilanceParameter[8]; - Assertions.assertArrayEquals(tmpTestClusterIndicesInCluster8For09, tmpClusterIndicesInCluster8For09); - } - // - // - // - /** - * Tests the cluster representatives in cluster 0 for vigilance parameter 0.1f - */ - @Test - public void testClusterRepresentativesInCluster0ForVigilanceParameter01() { - int tmpTestClusterRepresentativesIndexInCluster0For01 = 9; - int tmpClusterRepresentativesIndexInCluster0For01 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster0For01, tmpClusterRepresentativesIndexInCluster0For01); - } - // - /** - * Tests the cluster representatives in cluster 1 for vigilance parameter 0.2f - */ - @Test - public void testClusterRepresentativesInCluster1ForVigilanceParameter02() { - int tmpTestClusterRepresentativesIndexInCluster1For02 = 1; - int tmpClusterRepresentativesIndexInCluster1For02 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster1For02, tmpClusterRepresentativesIndexInCluster1For02); - } - // - /** - * Tests the cluster representatives in cluster 2 for vigilance parameter 0.3f - */ - @Test - public void testClusterRepresentativesInCluster2ForVigilanceParameter03() { - int tmpTestClusterRepresentativesIndexInCluster2For03 = 3; - int tmpClusterRepresentativesIndexInCluster2For03 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster2For03, tmpClusterRepresentativesIndexInCluster2For03); - } - // - /** - * Tests the cluster representatives in cluster 3 for vigilance parameter 0.4f - */ - @Test - public void testClusterRepresentativesInCluster3ForVigilanceParameter04() { - int tmpTestClusterRepresentativesIndexInCluster3For04 = 2; - int tmpClusterRepresentativesIndexInCluster3For04 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster3For04, tmpClusterRepresentativesIndexInCluster3For04); - } - // - /** - * Tests the cluster representatives in cluster 4 for vigilance parameter 0.5f - */ - @Test - public void testClusterRepresentativesInCluster4ForVigilanceParameter05() { - int tmpTestClusterRepresentativesIndexInCluster4For05 = 5; - int tmpClusterRepresentativesIndexInCluster4For05 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster4For05, tmpClusterRepresentativesIndexInCluster4For05); - } - // - /** - * Tests the cluster representatives in cluster 5 for vigilance parameter 0.6f - */ - @Test - public void testClusterRepresentativesInCluster5ForVigilanceParameter06() { - int tmpTestClusterRepresentativesIndexInCluster5For06 = 5; - int tmpClusterRepresentativesIndexInCluster5For06 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster5For06, tmpClusterRepresentativesIndexInCluster5For06); - } - // - /** - * Tests the cluster representatives in cluster 6 for vigilance parameter 0.7f - */ - @Test - public void testClusterRepresentativesInCluster6ForVigilanceParameter07() { - int tmpTestClusterRepresentativesIndexInCluster6For07 = 6; - int tmpClusterRepresentativesIndexInCluster6For07 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster6For07, tmpClusterRepresentativesIndexInCluster6For07); - } - // - /** - * Tests the cluster representatives in cluster 7 for vigilance parameter 0.8f - */ - @Test - public void testClusterRepresentativesInCluster7ForVigilanceParameter08() { - int tmpTestClusterRepresentativesIndexInCluster7For08 = 4; - int tmpClusterRepresentativesIndexInCluster7For08 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster7For08, tmpClusterRepresentativesIndexInCluster7For08); - } - // - /** - * Tests the cluster representatives in cluster 8 for vigilance parameter 0.9f - */ - @Test - public void testClusterRepresentativesInCluster8ForVigilanceParameter09() { - int tmpTestClusterRepresentativesIndexInCluster8For09 = 8; - int tmpClusterRepresentativesIndexInCluster8For09 = Art2aFloatClusteringTest.clusterRepresentativesForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestClusterRepresentativesIndexInCluster8For09, tmpClusterRepresentativesIndexInCluster8For09); - } - // - // - // - /** - * Tests the angle between cluster 0 and 1 for vigilance parameter 0.1f - */ - @Test - public void testAngleBetweenCluster0And1For01() { - float tmpTestAngleBetweenCluster0And1For01 = 64.71957f; - float tmpAngleBetweenCluster0And1For01 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[0]; - Assertions.assertEquals(tmpTestAngleBetweenCluster0And1For01, tmpAngleBetweenCluster0And1For01, 1e-4f); - } - // - /** - * Tests the angle between cluster 1 and 2 for vigilance parameter 0.2f - */ - @Test - public void testAngleBetweenCluster1And2For02() { - float tmpTestAngleBetweenCluster1And2For02 = 80.98592f; - float tmpAngleBetweenCluster1And2For02 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[1]; - Assertions.assertEquals(tmpTestAngleBetweenCluster1And2For02, tmpAngleBetweenCluster1And2For02, 1e-4f); - } - // - /** - * Tests the angle between cluster 2 and 3 for vigilance parameter 0.3f - */ - @Test - public void testAngleBetweenCluster2And3For03() { - float tmpTestAngleBetweenCluster2And3For03 = 90.000f; - float tmpAngleBetweenCluster2And3For03 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[2]; - Assertions.assertEquals(tmpTestAngleBetweenCluster2And3For03, tmpAngleBetweenCluster2And3For03, 1e-4f); - } - // - /** - * Tests the angle between cluster 3 and 4 for vigilance parameter 0.4f - */ - @Test - public void testAngleBetweenCluster3And4For04() { - float tmpTestAngleBetweenCluster3And4For04 = 90.000f; - float tmpAngleBetweenCluster3And4For04 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[3]; - Assertions.assertEquals(tmpTestAngleBetweenCluster3And4For04, tmpAngleBetweenCluster3And4For04, 1e-4f); - } - // - /** - * Tests the angle between cluster 4 and 5 for vigilance parameter 0.5f - */ - @Test - public void testAngleBetweenCluster4And5For05() { - float tmpTestAngleBetweenCluster4And5For05 = 71.56505f; - float tmpAngleBetweenCluster4And5For05 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[4]; - Assertions.assertEquals(tmpTestAngleBetweenCluster4And5For05, tmpAngleBetweenCluster4And5For05, 1e-4f); - } - // - /** - * Tests the angle between cluster 5 and 6 for vigilance parameter 0.6f - */ - @Test - public void testAngleBetweenCluster5And6For06() { - float tmpTestAngleBetweenCluster5And6For06 = 71.56505f; - float tmpAngleBetweenCluster5And6For06 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[5]; - Assertions.assertEquals(tmpTestAngleBetweenCluster5And6For06, tmpAngleBetweenCluster5And6For06, 1e-4f); - } - // - /** - * Tests the angle between cluster 6 and 7 for vigilance parameter 0.7f - */ - @Test - public void testAngleBetweenCluster6And7For07() { - float tmpTestAngleBetweenCluster6And7For07 = 75.03678f; - float tmpAngleBetweenCluster6And7For07 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[6]; - Assertions.assertEquals(tmpTestAngleBetweenCluster6And7For07, tmpAngleBetweenCluster6And7For07, 1e-4f); - } - // - /** - * Tests the angle between cluster 7 and 8 for vigilance parameter 0.8f - */ - @Test - public void testAngleBetweenCluster7And8For08() { - float tmpTestAngleBetweenCluster7And8For08 = 90.000f; - float tmpAngleBetweenCluster7And8For08 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[7]; - Assertions.assertEquals(tmpTestAngleBetweenCluster7And8For08, tmpAngleBetweenCluster7And8For08, 1e-4f); - } - // - /** - * Tests the angle between cluster 8 and 9 for vigilance parameter 0.9f - */ - @Test - public void testAngleBetweenCluster8And9For09() { - float tmpTestAngleBetweenCluster8And9For09 = 90.000f; - float tmpAngleBetweenCluster8And9For09 = Art2aFloatClusteringTest.clusterAnglesForAllVigilanceParameter[8]; - Assertions.assertEquals(tmpTestAngleBetweenCluster8And9For09, tmpAngleBetweenCluster8And9For09, 1e-4f); - } - // - // - // - /** - * Method tests whether the import of a data matrix from a text file works correctly. - * - * @throws NoSuchMethodException is thrown, if the private method is not found - */ - @Test - public void testImportFloatDataMatrix() throws NoSuchMethodException { - float[][] tmpImportFloatDataMatrix = FileUtil.importFloatDataMatrixFromTextFile("src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt", ','); - Method tmpImportMethod = FileUtil.class.getDeclaredMethod("importFloatDataMatrixFromTextFile", String.class, char.class); - tmpImportMethod.setAccessible(true); - float[][] tmpTestDataMatrix = new float[6][10]; - - tmpTestDataMatrix[0][0] = 1.0f; - tmpTestDataMatrix[0][1] = 0.0f; - tmpTestDataMatrix[0][2] = 0.0f; - tmpTestDataMatrix[0][3] = 0.0f; - tmpTestDataMatrix[0][4] = 0.0f; - tmpTestDataMatrix[0][5] = 0.0f; - tmpTestDataMatrix[0][6] = 0.0f; - tmpTestDataMatrix[0][7] = 0.0f; - tmpTestDataMatrix[0][8] = 1.0f; - tmpTestDataMatrix[0][9] = 0.0f; - - tmpTestDataMatrix[1][0] = 0.0f; - tmpTestDataMatrix[1][1] = 0.0f; - tmpTestDataMatrix[1][2] = 0.0f; - tmpTestDataMatrix[1][3] = 0.0f; - tmpTestDataMatrix[1][4] = 0.0f; - tmpTestDataMatrix[1][5] = 3.0f; - tmpTestDataMatrix[1][6] = 1.0f; - tmpTestDataMatrix[1][7] = 1.0f; - tmpTestDataMatrix[1][8] = 0.0f; - tmpTestDataMatrix[1][9] = 0.0f; - - tmpTestDataMatrix[2][0] = 0.0f; - tmpTestDataMatrix[2][1] = 0.0f; - tmpTestDataMatrix[2][2] = 0.0f; - tmpTestDataMatrix[2][3] = 0.0f; - tmpTestDataMatrix[2][4] = 0.0f; - tmpTestDataMatrix[2][5] = 0.0f; - tmpTestDataMatrix[2][6] = 0.0f; - tmpTestDataMatrix[2][7] = 0.0f; - tmpTestDataMatrix[2][8] = 0.0f; - tmpTestDataMatrix[2][9] = 0.0f; - - tmpTestDataMatrix[3][0] = 0.0f; - tmpTestDataMatrix[3][1] = 0.0f; - tmpTestDataMatrix[3][2] = 0.0f; - tmpTestDataMatrix[3][3] = 0.0f; - tmpTestDataMatrix[3][4] = 0.0f; - tmpTestDataMatrix[3][5] = 0.0f; - tmpTestDataMatrix[3][6] = 0.0f; - tmpTestDataMatrix[3][7] = 0.0f; - tmpTestDataMatrix[3][8] = 0.0f; - tmpTestDataMatrix[3][9] = 0.0f; - - tmpTestDataMatrix[4][0] = 0.0f; - tmpTestDataMatrix[4][1] = 0.0f; - tmpTestDataMatrix[4][2] = 0.0f; - tmpTestDataMatrix[4][3] = 0.0f; - tmpTestDataMatrix[4][4] = 1.0f; - tmpTestDataMatrix[4][5] = 0.0f; - tmpTestDataMatrix[4][6] = 0.0f; - tmpTestDataMatrix[4][7] = 0.0f; - tmpTestDataMatrix[4][8] = 0.0f; - tmpTestDataMatrix[4][9] = 0.0f; - - tmpTestDataMatrix[5][0] = 0.0f; - tmpTestDataMatrix[5][1] = 1.0f; - tmpTestDataMatrix[5][2] = 0.0f; - tmpTestDataMatrix[5][3] = 0.0f; - tmpTestDataMatrix[5][4] = 0.0f; - tmpTestDataMatrix[5][5] = 0.0f; - tmpTestDataMatrix[5][6] = 0.0f; - tmpTestDataMatrix[5][7] = 0.0f; - tmpTestDataMatrix[5][8] = 0.0f; - tmpTestDataMatrix[5][9] = 8.0f; - Assertions.assertArrayEquals(tmpTestDataMatrix,tmpImportFloatDataMatrix); - } - // - /** - * Method tests whether the checks and possible scaling in the data matrix work successfully. - * - * @throws NoSuchMethodException is thrown, if the private method is not found. - * @throws InvocationTargetException is thrown if an exception occurs during the call of the private method. - * It wraps the underlying exception that was thrown by the invoked method. - * @throws IllegalAccessException is thrown if the private method is inaccessible and cannot be called. - */ - @Test - public void testCheckAndScaleDataMatrix() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - float[][] tmpImportFloatDataMatrix = FileUtil.importFloatDataMatrixFromTextFile( - "src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt", ','); - Art2aFloatClustering tmpFloatClustering = new Art2aFloatClustering(tmpImportFloatDataMatrix, 10, - 0.1f, 0.99f, 0.1f); - Method tmpCheckAndScaleDataMatrix = Art2aFloatClustering.class.getDeclaredMethod( - "getCheckedAndScaledDataMatrix", float[][].class); - tmpCheckAndScaleDataMatrix.setAccessible(true); - tmpCheckAndScaleDataMatrix.invoke(tmpFloatClustering, (Object) tmpImportFloatDataMatrix); - - float[][] tmpTestDataMatrix = new float[6][10]; - - tmpTestDataMatrix[0][0] = 0.125f; - tmpTestDataMatrix[0][1] = 0.0f; - tmpTestDataMatrix[0][2] = 0.0f; - tmpTestDataMatrix[0][3] = 0.0f; - tmpTestDataMatrix[0][4] = 0.0f; - tmpTestDataMatrix[0][5] = 0.0f; - tmpTestDataMatrix[0][6] = 0.0f; - tmpTestDataMatrix[0][7] = 0.0f; - tmpTestDataMatrix[0][8] = 0.125f; - tmpTestDataMatrix[0][9] = 0.0f; - - tmpTestDataMatrix[1][0] = 0.0f; - tmpTestDataMatrix[1][1] = 0.0f; - tmpTestDataMatrix[1][2] = 0.0f; - tmpTestDataMatrix[1][3] = 0.0f; - tmpTestDataMatrix[1][4] = 0.0f; - tmpTestDataMatrix[1][5] = 0.375f; - tmpTestDataMatrix[1][6] = 0.125f; - tmpTestDataMatrix[1][7] = 0.125f; - tmpTestDataMatrix[1][8] = 0.0f; - tmpTestDataMatrix[1][9] = 0.0f; - - tmpTestDataMatrix[2][0] = 0.0f; - tmpTestDataMatrix[2][1] = 0.0f; - tmpTestDataMatrix[2][2] = 0.0f; - tmpTestDataMatrix[2][3] = 0.0f; - tmpTestDataMatrix[2][4] = 0.0f; - tmpTestDataMatrix[2][5] = 0.0f; - tmpTestDataMatrix[2][6] = 0.0f; - tmpTestDataMatrix[2][7] = 0.0f; - tmpTestDataMatrix[2][8] = 0.0f; - tmpTestDataMatrix[2][9] = 0.0f; - - tmpTestDataMatrix[3][0] = 0.0f; - tmpTestDataMatrix[3][1] = 0.0f; - tmpTestDataMatrix[3][2] = 0.0f; - tmpTestDataMatrix[3][3] = 0.0f; - tmpTestDataMatrix[3][4] = 0.0f; - tmpTestDataMatrix[3][5] = 0.0f; - tmpTestDataMatrix[3][6] = 0.0f; - tmpTestDataMatrix[3][7] = 0.0f; - tmpTestDataMatrix[3][8] = 0.0f; - tmpTestDataMatrix[3][9] = 0.0f; - - tmpTestDataMatrix[4][0] = 0.0f; - tmpTestDataMatrix[4][1] = 0.0f; - tmpTestDataMatrix[4][2] = 0.0f; - tmpTestDataMatrix[4][3] = 0.0f; - tmpTestDataMatrix[4][4] = 0.125f; - tmpTestDataMatrix[4][5] = 0.0f; - tmpTestDataMatrix[4][6] = 0.0f; - tmpTestDataMatrix[4][7] = 0.0f; - tmpTestDataMatrix[4][8] = 0.0f; - tmpTestDataMatrix[4][9] = 0.0f; - - tmpTestDataMatrix[5][0] = 0.0f; - tmpTestDataMatrix[5][1] = 0.125f; - tmpTestDataMatrix[5][2] = 0.0f; - tmpTestDataMatrix[5][3] = 0.0f; - tmpTestDataMatrix[5][4] = 0.0f; - tmpTestDataMatrix[5][5] = 0.0f; - tmpTestDataMatrix[5][6] = 0.0f; - tmpTestDataMatrix[5][7] = 0.0f; - tmpTestDataMatrix[5][8] = 0.0f; - tmpTestDataMatrix[5][9] = 1.0f; - Assertions.assertArrayEquals(tmpTestDataMatrix,tmpImportFloatDataMatrix); - } - // - -} diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java new file mode 100644 index 0000000..863c999 --- /dev/null +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -0,0 +1,948 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Test class for ART-2a clustering. + * + * @author Achim Zielesny + */ +public class Art2aTest { + + /** + * Test class for development purposes only + */ + @Test + public void test_Development_IrisFlowerData() { + System.out.println("---------------------------------"); + System.out.println("test_Development_IrisFlowerData()"); + System.out.println("---------------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + + // float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + float[] tmpVigilances = new float[] {0.1f}; + + int tmpMaximumNumberOfClusters = 150; + boolean tmpIsDataPreprocessing = false; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + for (float tmpVigilance : tmpVigilances) { + System.out.println(" Vigilance parameter = " + String.valueOf(tmpVigilance)); + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Assertions.assertNotNull(tmpArt2aKernel); + Art2aResult tmpArt2aResult = null; + try { + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + Assertions.assertNotNull(tmpArt2aResult); + int tmpNumberOfDetectedClusters = tmpArt2aResult.getNumberOfDetectedClusters(); + System.out.println(" - Number of detected clusters = " + String.valueOf(tmpArt2aResult.getNumberOfDetectedClusters())); + System.out.println(" - Number of epochs = " + String.valueOf(tmpArt2aResult.getNumberOfEpochs())); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + System.out.println(" - Cluster " + String.valueOf(i) + " of size " + String.valueOf(tmpArt2aResult.getClusterSize(i))); + int[] tmpDataVectorIndicesOfCluster = tmpArt2aResult.getDataVectorIndicesOfCluster(i); + System.out.println(" " + this.getStringFromIntArray(tmpDataVectorIndicesOfCluster)); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + System.out.println(" - Angle between cluster " + String.valueOf(i) + " and cluster " + String.valueOf(j) + " = " + String.valueOf(tmpArt2aResult.getAngleBetweenClusters(i, j))); + } + } + System.out.println(""); + } + } + + /** + * Test class for development purposes only + */ + @Test + public void test_Development_CombinedGaussianCouldData() { + System.out.println("--------------------------------------------"); + System.out.println("test_Development_CombinedGaussianCouldData()"); + System.out.println("--------------------------------------------"); + int tmpNumberOfDimensions = 10; + int tmpNumberOfGaussianCloudVectors = 100; + float tmpStandardDeviation = 0.1f; + Random tmpRandomNumberGenerator = new Random(1L); + float[][] tmpCombinedGaussianCloudDataMatrix = + this.getCombinedGaussianCloudMatrix( + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, + tmpRandomNumberGenerator + ); + + float tmpVigilance = 0.1f; + int tmpMaximumNumberOfClusters = 1000; + boolean tmpIsDataPreprocessing = false; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + long tmpStart = System.currentTimeMillis(); + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpCombinedGaussianCloudDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aResult tmpArt2aResult = null; + try { + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + long tmpEnd = System.currentTimeMillis(); + + System.out.println(" Number of data vectors = " + String.valueOf(tmpNumberOfDimensions * tmpNumberOfGaussianCloudVectors)); + System.out.println(" Elapsed time in ms = " + String.valueOf(tmpEnd - tmpStart)); + Assertions.assertNotNull(tmpArt2aKernel); + Assertions.assertNotNull(tmpArt2aResult); + int tmpNumberOfDetectedClusters = tmpArt2aResult.getNumberOfDetectedClusters(); + System.out.println(" Number of detected clusters = " + String.valueOf(tmpArt2aResult.getNumberOfDetectedClusters())); + System.out.println(" Number of epochs = " + String.valueOf(tmpArt2aResult.getNumberOfEpochs())); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + System.out.println(" Cluster " + String.valueOf(i) + " of size " + String.valueOf(tmpArt2aResult.getClusterSize(i))); + System.out.println(" - Representative = " + String.valueOf(tmpArt2aResult.getClusterRepresentativeIndex(i))); + System.out.println(" - Representatives = " + this.getStringFromIntArray(tmpArt2aResult.getClusterRepresentativeIndices(i))); + int[] tmpDataVectorIndicesOfCluster = tmpArt2aResult.getDataVectorIndicesOfCluster(i); + System.out.println(" " + this.getStringFromIntArray(tmpDataVectorIndicesOfCluster)); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + System.out.println(" Angle between cluster " + String.valueOf(i) + " and cluster " + String.valueOf(j) + " = " + String.valueOf(tmpArt2aResult.getAngleBetweenClusters(i, j))); + } + } + } + + /** + * Test class for development purposes only + */ + @Test + public void test_Development_GetRepresentatives() { + System.out.println("-------------------------------------"); + System.out.println("test_Development_GetRepresentatives()"); + System.out.println("-------------------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + boolean tmpIsDataPreprocessing = false; + + float tmpVigilanceMin = 0.0001f; + float tmpVigilanceMax = 0.9999f; + int tmpNumberOfTrialSteps = 32; + + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + + try { + int[] tmpBestRepresentatives = tmpArt2aKernel.getBestRepresentatives(tmpIrisFlowerDataMatrix, 2, tmpIrisFlowerDataMatrix.length); + Arrays.sort(tmpBestRepresentatives); + System.out.println( + String.valueOf(tmpBestRepresentatives.length) + " best representatives = " + this.getStringFromIntArray(tmpBestRepresentatives) + ); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + int[] tmpAllIndices = new int[150]; + for (int i = 0; i < 150; i++) { + tmpAllIndices[i] = i; + } + float tmpBaseMeanDistance = Art2aUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpAllIndices); + System.out.println( + "Base mean distance = " + String.valueOf(tmpBaseMeanDistance) + ); + for (int tmpNumberOfRepresentatives = 2; tmpNumberOfRepresentatives < tmpIrisFlowerDataMatrix.length; tmpNumberOfRepresentatives++) { + try { + int[] tmpRepresentatives = + tmpArt2aKernel.getRepresentatives( + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, + tmpNumberOfTrialSteps + ); + if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { + Arrays.sort(tmpRepresentatives); + float tmpMeanDistance = Art2aUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); + System.out.println( + String.valueOf(tmpNumberOfRepresentatives) + + " Representatives (Mean distance = " + + String.valueOf(tmpMeanDistance) + + ") = " + + this.getStringFromIntArray(tmpRepresentatives) + ); + } + } catch (Exception anException) { + Assertions.assertTrue(false); + } + } + } + + /** + * Tests Art2aKernel method getRepresentatives(). + */ + @Test + public void test_GetRepresentatives() { + System.out.println("-------------------------"); + System.out.println("test_GetRepresentatives()"); + System.out.println("-------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 0.5f; + long tmpRandomSeed = 1L; + boolean tmpIsDataPreprocessing = false; + + float tmpVigilanceMin = 0.0001f; + float tmpVigilanceMax = 0.9999f; + int tmpNumberOfRepresentatives = 7; + int tmpNumberOfTrialSteps = 32; + + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + try { + int[] tmpRepresentatives = + tmpArt2aKernel.getRepresentatives( + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, + tmpNumberOfTrialSteps + ); + Assertions.assertEquals(tmpRepresentatives.length, tmpNumberOfRepresentatives); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + } + + /** + * Test for perfect clustering + */ + @Test + public void test_PerfectClustering() { + System.out.println("------------------------"); + System.out.println("test_PerfectClustering()"); + System.out.println("------------------------"); + int tmpNumberOfDimensions = 10; + int tmpNumberOfGaussianCloudVectors = 1000; + float tmpStandardDeviation = 0.01f; + Random tmpRandomNumberGenerator = new Random(1L); + float[][] tmpCombinedGaussianCloudDataMatrix = + this.getCombinedGaussianCloudMatrix( + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, + tmpRandomNumberGenerator + ); + + float tmpVigilance = 0.1f; + int tmpMaximumNumberOfClusters = 100; + boolean tmpIsDataPreprocessing = false; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpCombinedGaussianCloudDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aResult tmpArt2aResult = null; + try { + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + Assertions.assertEquals(tmpArt2aResult.getNumberOfDetectedClusters(), tmpNumberOfDimensions); + Assertions.assertTrue(tmpArt2aResult.getNumberOfEpochs() < tmpMaximumNumberOfEpochs); + for (int i = 0; i < tmpArt2aResult.getNumberOfDetectedClusters(); i++) { + Assertions.assertEquals(tmpArt2aResult.getClusterSize(i), tmpNumberOfGaussianCloudVectors); + int[] tmpDataVectorIndicesOfCluster = tmpArt2aResult.getDataVectorIndicesOfCluster(i); + int[] tmpClusterRepresentativeIndices = tmpArt2aResult.getClusterRepresentativeIndices(i); + Assertions.assertEquals(tmpArt2aResult.getClusterRepresentativeIndex(i), tmpClusterRepresentativeIndices[0]); + Arrays.sort(tmpDataVectorIndicesOfCluster); + Arrays.sort(tmpClusterRepresentativeIndices); + Assertions.assertArrayEquals(tmpDataVectorIndicesOfCluster, tmpClusterRepresentativeIndices); + } + for (int i = 0; i < tmpArt2aResult.getNumberOfDetectedClusters(); i++) { + for (int j = i + 1; j < tmpArt2aResult.getNumberOfDetectedClusters(); j++) { + Assertions.assertEquals(tmpArt2aResult.getAngleBetweenClusters(i, j), 90.0, 0.001); + } + } + Assertions.assertFalse(tmpArt2aResult.isClusterOverflow()); + for (int i = 0; i < tmpArt2aResult.getNumberOfDetectedClusters(); i++) { + Assertions.assertEquals(tmpArt2aResult.getClusterRepresentativeIndex(i), tmpArt2aResult.getClusterRepresentativeIndices(i)[0]); + } + } + + /** + * Tests that clustering with and without preprocessing has identical + * results. + */ + @Test + public void test_Preprocessing() { + System.out.println("--------------------"); + System.out.println("test_Preprocessing()"); + System.out.println("--------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + for (float tmpVigilance : tmpVigilances) { + // No preprocessing + boolean tmpIsDataPreprocessing = false; + Art2aKernel tmpArt2aKernelWithoutPreprocessing = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aResult tmpArt2aResultWithoutPreprocessing = null; + try { + tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Preprocessing + tmpIsDataPreprocessing = true; + Art2aKernel tmpArt2aKernelWithPreprocessing = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aResult tmpArt2aResultWithPreprocessing = null; + try { + tmpArt2aResultWithPreprocessing = tmpArt2aKernelWithPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Assertions.assert that results without and with preprocessing are identical + Assertions.assertTrue( + tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters() == + tmpArt2aResultWithPreprocessing.getNumberOfDetectedClusters() + ); + Assertions.assertTrue( + tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs() == + tmpArt2aResultWithPreprocessing.getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + Assertions.assertArrayEquals( + tmpArt2aResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aResultWithPreprocessing.getDataVectorIndicesOfCluster(i) + ); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertTrue( + tmpArt2aResultWithoutPreprocessing.getAngleBetweenClusters(i, j) == + tmpArt2aResultWithPreprocessing.getAngleBetweenClusters(i, j) + ); + } + } + } + } + + /** + * Test that generated Art2aData object leads to identical clustering + * results. + */ + @Test + public void test_Art2aData() { + System.out.println("----------------"); + System.out.println("test_Art2aData()"); + System.out.println("----------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + for (float tmpVigilance : tmpVigilances) { + // No preprocessing + boolean tmpIsDataPreprocessing = false; + Art2aKernel tmpArt2aKernelWithoutPreprocessing = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aResult tmpArt2aResultWithoutPreprocessing = null; + try { + tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Preprocessed Art2aData + Art2aData tmpArt2aData = Art2aKernel.getArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + Art2aKernel tmpArt2aKernelWithArt2aData = + new Art2aKernel( + tmpArt2aData, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpRandomSeed + ); + Art2aResult tmpArt2aResultWithArt2aData = null; + try { + tmpArt2aResultWithArt2aData = tmpArt2aKernelWithArt2aData.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Assertions.assert that results without preprocessing and preprocessed + // Art2aData are identical + Assertions.assertTrue( + tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters() == + tmpArt2aResultWithArt2aData.getNumberOfDetectedClusters() + ); + Assertions.assertTrue( + tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs() == + tmpArt2aResultWithArt2aData.getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + Assertions.assertArrayEquals( + tmpArt2aResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aResultWithArt2aData.getDataVectorIndicesOfCluster(i) + ); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertTrue( + tmpArt2aResultWithoutPreprocessing.getAngleBetweenClusters(i, j) == + tmpArt2aResultWithArt2aData.getAngleBetweenClusters(i, j) + ); + } + } + } + } + + /** + * Tests that sequential and parallelized clustering leads to identical + * results. + */ + @Test + public void test_ParallelClustering() { + System.out.println("-------------------------"); + System.out.println("test_ParallelClustering()"); + System.out.println("-------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + // Sequential clustering one after another + Art2aResult[] tmpSequentialResults = new Art2aResult[tmpVigilances.length]; + int tmpIndex = 0; + for (float tmpVigilance : tmpVigilances) { + boolean tmpIsDataPreprocessing = false; + Art2aKernel tmpArt2aKernelWithoutPreprocessing = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + try { + tmpSequentialResults[tmpIndex++] = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + } + + // Concurrent (parallelized) clustering + LinkedList tmpArt2aTaskList = new LinkedList<>(); + Art2aData tmpArt2aData = Art2aKernel.getArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + for (float tmpVigilance : tmpVigilances) { + tmpArt2aTaskList.add( + new Art2aTask( + tmpArt2aData, + tmpVigilance, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpRandomSeed + ) + ); + } + ExecutorService tmpExecutorService = Executors.newFixedThreadPool(tmpVigilances.length); + List> tmpFutureList = null; + try { + tmpFutureList = tmpExecutorService.invokeAll(tmpArt2aTaskList); + } catch (InterruptedException e) { + System.out.println("test_ParallelClustering: InterruptedException occurred."); + } + tmpExecutorService.shutdown(); + Art2aResult[] tmpParallelResults = new Art2aResult[tmpVigilances.length]; + tmpIndex = 0; + for (Future tmpFuture : tmpFutureList) { + try { + tmpParallelResults[tmpIndex++] = tmpFuture.get(); + } catch (Exception e) { + System.out.println("test_ParallelClustering: Exception occurred."); + } + } + + // Assertions.assert that sequential results without preprocessing and concurrent + // results with preprocessed Art2aData are identical + for (int i = 0; i < tmpVigilances.length; i++) { + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfDetectedClusters() == + tmpParallelResults[i].getNumberOfDetectedClusters() + ); + + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfEpochs() == + tmpParallelResults[i].getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertArrayEquals( + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpParallelResults[i].getDataVectorIndicesOfCluster(j) + ); + } + + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { + Assertions.assertTrue( + tmpSequentialResults[i].getAngleBetweenClusters(j, k) == + tmpParallelResults[i].getAngleBetweenClusters(j, k) + ); + } + } + } + } + + /** + * Tests that sequential and parallelized clustering with + * Art2aKernel.getClusterResults() leads to identical results. + */ + @Test + public void test_ParallelClusteringWithGetGlusterResults() { + System.out.println("----------------------------------------------"); + System.out.println("test_ParallelClusteringWithGetGlusterResults()"); + System.out.println("----------------------------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + boolean tmpIsDataPreprocessing = false; + + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + + // Sequential clustering one after another + Art2aResult[] tmpSequentialResults = null; + try { + tmpSequentialResults = tmpArt2aKernel.getClusterResults(tmpVigilances, 0); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Concurrent (parallel) clustering + int tmpNumberOfParallelCalculationThreads = 2; + Art2aResult[] tmpParallelResults = null; + try { + tmpParallelResults = tmpArt2aKernel.getClusterResults(tmpVigilances, tmpNumberOfParallelCalculationThreads); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Assertions.assert that sequential results without preprocessing and concurrent + // results with preprocessed Art2aData are identical + for (int i = 0; i < tmpVigilances.length; i++) { + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfDetectedClusters() == + tmpParallelResults[i].getNumberOfDetectedClusters() + ); + + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfEpochs() == + tmpParallelResults[i].getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertArrayEquals( + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpParallelResults[i].getDataVectorIndicesOfCluster(j) + ); + } + + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { + Assertions.assertTrue( + tmpSequentialResults[i].getAngleBetweenClusters(j, k) == + tmpParallelResults[i].getAngleBetweenClusters(j, k) + ); + } + } + } + } + + // + /** + * Returns int array as a string. + * Note: No checks are performed. + * + * @param anIntArray Int array + * @return The int array as a string + */ + private String getStringFromIntArray( + int[] anIntArray + ) { + // Assumes 6 characters for int number plus comma plus space + StringBuilder tmpStringBuilder = new StringBuilder(anIntArray.length * 6); + tmpStringBuilder.append(String.valueOf(anIntArray[0])); + for (int i = 1; i < anIntArray.length; i++) { + tmpStringBuilder.append(", "); + tmpStringBuilder.append(String.valueOf(anIntArray[i])); + } + return tmpStringBuilder.toString(); + } + + /** + * Compares two arrays. + * Note: No checks are performed. + * + * @param anArray1 Array 1 + * @param anArray2 Array 2 + * @return True: Arrays have the same values in the same order, false: + * Otherwise + */ + private boolean compareArrays( + int[] anArray1, + int[] anArray2 + ) { + boolean isEqual = true; + if (anArray1.length != anArray2.length) { + return false; + } + for (int i = 0; i < anArray1.length; i++) { + if (anArray1[i] != anArray2[i]) { + return false; + } + } + return isEqual; + } + // + // + /** + * Returns Gaussian cloud matrix + * + * @param aCentroidVector Centroid vector (IS NOT CHANGED) + * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors + * @param aStandardDeviation Standard deviation of Gaussian distribution + * @param aRandomNumberGenerator Random number generator + * @return Gaussian cloud matrix + */ + private float[][] getGaussianCloudMatrix( + float[] aCentroidVector, + int aNumberOfGaussianCloudVectors, + float aStandardDeviation, + Random aRandomNumberGenerator + ) { + float[][] tmpGaussianCloudMatrix = new float[aNumberOfGaussianCloudVectors][]; + for (int i = 0; i < aNumberOfGaussianCloudVectors; i++) { + float[] tmpCloudVector = new float[aCentroidVector.length]; + for (int j = 0; j < aCentroidVector.length; j++) { + tmpCloudVector[j] = aCentroidVector[j] + (float) aRandomNumberGenerator.nextGaussian() * aStandardDeviation; + } + tmpGaussianCloudMatrix[i] = tmpCloudVector; + } + return tmpGaussianCloudMatrix; + } + + /** + * Returns combined Gaussian cloud matrix (see code) + * + * @param aNumberOfDimensions Number of dimensions + * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors + * @param aStandardDeviation Standard deviation of Gaussian distribution + * @param aRandomNumberGenerator Random number generator + * @return Combined Gaussian cloud matrix (see code) + */ + private float[][] getCombinedGaussianCloudMatrix( + int aNumberOfDimensions, + int aNumberOfGaussianCloudVectors, + float aStandardDeviation, + Random aRandomNumberGenerator + ) { + float[][] tmpCombinedGaussianCloudMatrix = new float[aNumberOfDimensions * aNumberOfGaussianCloudVectors][]; + int tmpIndex = 0; + for (int i = 0; i < aNumberOfDimensions; i++) { + float[] tmpCentroidVector = new float[aNumberOfDimensions]; + Arrays.fill(tmpCentroidVector, 0.0f); + tmpCentroidVector[i] = 1.0f; + float[][] tmpGaussianCloudMatrix = + this.getGaussianCloudMatrix( + tmpCentroidVector, + aNumberOfGaussianCloudVectors, + aStandardDeviation, + aRandomNumberGenerator + ); + for (int j = 0; j < tmpGaussianCloudMatrix.length; j++) { + tmpCombinedGaussianCloudMatrix[tmpIndex++] = tmpGaussianCloudMatrix[j]; + } + } + return tmpCombinedGaussianCloudMatrix; + } + // + // + /** + * Returns Iris flower data: Indices 0-49 = Iris setosa, indices 50-99 = + * Iris versicolor, indices 100-149 = Iris virginica + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris flower data + */ + private float[][] getIrisFlowerDataMatrix() { + float[][] tmpIrisSetosaData = this.getIrisSetosaDataMatrix(); + float[][] tmpIrisVersicolorData = this.getIrisVersicolorDataMatrix(); + float[][] tmpIrisVirginicaData = this.getIrisVirginicaDataMatrix(); + float[][] tmpIrisFlowerData = + new float[tmpIrisSetosaData.length + tmpIrisVersicolorData.length + tmpIrisVirginicaData.length][]; + int tmpIndex = 0; + for (int i = 0; i < tmpIrisSetosaData.length; i++) { + tmpIrisFlowerData[tmpIndex++] = tmpIrisSetosaData[i]; + } + for (int i = 0; i < tmpIrisVersicolorData.length; i++) { + tmpIrisFlowerData[tmpIndex++] = tmpIrisVersicolorData[i]; + } + for (int i = 0; i < tmpIrisVirginicaData.length; i++) { + tmpIrisFlowerData[tmpIndex++] = tmpIrisVirginicaData[i]; + } + return tmpIrisFlowerData; + } + + /** + * Returns Iris setosa data + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris setosa data + */ + private float[][] getIrisSetosaDataMatrix() { + return new + float[][] { + {49.0f, 30.0f, 14.0f, 2.0f}, {51.0f, 38.0f, 19.0f, 4.0f}, {52.0f, 41.0f, 15.0f, 1.0f}, {54.0f, 34.0f, 15.0f, 4.0f}, + {50.0f, 36.0f, 14.0f, 2.0f}, {57.0f, 44.0f, 15.0f, 4.0f}, {46.0f, 32.0f, 14.0f, 2.0f}, {50.0f, 34.0f, 16.0f, 4.0f}, + {51.0f, 35.0f, 14.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 2.0f}, {50.0f, 34.0f, 15.0f, 2.0f}, {58.0f, 40.0f, 12.0f, 2.0f}, + {43.0f, 30.0f, 11.0f, 1.0f}, {50.0f, 32.0f, 12.0f, 2.0f}, {50.0f, 30.0f, 16.0f, 2.0f}, {48.0f, 34.0f, 19.0f, 2.0f}, + {51.0f, 38.0f, 16.0f, 2.0f}, {48.0f, 30.0f, 14.0f, 3.0f}, {55.0f, 42.0f, 14.0f, 2.0f}, {44.0f, 30.0f, 13.0f, 2.0f}, + {54.0f, 39.0f, 17.0f, 4.0f}, {48.0f, 34.0f, 16.0f, 2.0f}, {51.0f, 35.0f, 14.0f, 3.0f}, {52.0f, 35.0f, 15.0f, 2.0f}, + {51.0f, 37.0f, 15.0f, 4.0f}, {54.0f, 34.0f, 17.0f, 2.0f}, {51.0f, 38.0f, 15.0f, 3.0f}, {57.0f, 38.0f, 17.0f, 3.0f}, + {45.0f, 23.0f, 13.0f, 3.0f}, {48.0f, 30.0f, 14.0f, 1.0f}, {53.0f, 37.0f, 15.0f, 2.0f}, {44.0f, 29.0f, 14.0f, 2.0f}, + {54.0f, 39.0f, 13.0f, 4.0f}, {54.0f, 37.0f, 15.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 1.0f}, {50.0f, 35.0f, 13.0f, 3.0f}, + {51.0f, 34.0f, 15.0f, 2.0f}, {46.0f, 31.0f, 15.0f, 2.0f}, {47.0f, 32.0f, 13.0f, 2.0f}, {47.0f, 32.0f, 16.0f, 2.0f}, + {50.0f, 33.0f, 14.0f, 2.0f}, {50.0f, 35.0f, 16.0f, 6.0f}, {55.0f, 35.0f, 13.0f, 2.0f}, {46.0f, 34.0f, 14.0f, 3.0f}, + {51.0f, 33.0f, 17.0f, 5.0f}, {52.0f, 34.0f, 14.0f, 2.0f}, {49.0f, 36.0f, 14.0f, 1.0f}, {48.0f, 31.0f, 16.0f, 2.0f}, + {46.0f, 36.0f, 10.0f, 2.0f}, {44.0f, 32.0f, 13.0f, 2.0f} + }; + } + + /** + * Returns Iris versicolor data + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris versicolor data + */ + private float[][] getIrisVersicolorDataMatrix() { + return new + float[][] { + {66.0f, 29.0f, 46.0f, 13.0f}, {61.0f, 29.0f, 47.0f, 14.0f}, {60.0f, 34.0f, 45.0f, 16.0f}, {52.0f, 27.0f, 39.0f, 14.0f}, + {49.0f, 24.0f, 33.0f, 10.0f}, {60.0f, 27.0f, 51.0f, 16.0f}, {56.0f, 27.0f, 42.0f, 13.0f}, {61.0f, 30.0f, 46.0f, 14.0f}, + {55.0f, 24.0f, 37.0f, 10.0f}, {57.0f, 30.0f, 42.0f, 12.0f}, {63.0f, 33.0f, 47.0f, 16.0f}, {69.0f, 31.0f, 49.0f, 15.0f}, + {57.0f, 28.0f, 45.0f, 13.0f}, {61.0f, 28.0f, 47.0f, 12.0f}, {64.0f, 29.0f, 43.0f, 13.0f}, {63.0f, 23.0f, 44.0f, 13.0f}, + {60.0f, 22.0f, 40.0f, 10.0f}, {56.0f, 30.0f, 41.0f, 13.0f}, {63.0f, 25.0f, 49.0f, 15.0f}, {50.0f, 20.0f, 35.0f, 10.0f}, + {59.0f, 30.0f, 42.0f, 15.0f}, {55.0f, 25.0f, 40.0f, 13.0f}, {62.0f, 29.0f, 43.0f, 13.0f}, {51.0f, 25.0f, 30.0f, 11.0f}, + {57.0f, 28.0f, 41.0f, 13.0f}, {58.0f, 27.0f, 39.0f, 12.0f}, {56.0f, 29.0f, 36.0f, 13.0f}, {67.0f, 31.0f, 47.0f, 15.0f}, + {67.0f, 31.0f, 44.0f, 14.0f}, {55.0f, 24.0f, 38.0f, 11.0f}, {56.0f, 30.0f, 45.0f, 15.0f}, {61.0f, 28.0f, 40.0f, 13.0f}, + {50.0f, 23.0f, 33.0f, 10.0f}, {55.0f, 26.0f, 44.0f, 12.0f}, {64.0f, 32.0f, 45.0f, 15.0f}, {55.0f, 23.0f, 40.0f, 13.0f}, + {66.0f, 30.0f, 44.0f, 14.0f}, {68.0f, 28.0f, 48.0f, 14.0f}, {58.0f, 27.0f, 41.0f, 10.0f}, {54.0f, 30.0f, 45.0f, 15.0f}, + {56.0f, 25.0f, 39.0f, 11.0f}, {62.0f, 22.0f, 45.0f, 15.0f}, {65.0f, 28.0f, 46.0f, 15.0f}, {58.0f, 26.0f, 40.0f, 12.0f}, + {57.0f, 29.0f, 42.0f, 13.0f}, {59.0f, 32.0f, 48.0f, 18.0f}, {70.0f, 32.0f, 47.0f, 14.0f}, {60.0f, 29.0f, 45.0f, 15.0f}, + {57.0f, 26.0f, 35.0f, 10.0f}, {67.0f, 30.0f, 50.0f, 17.0f} + }; + } + + /** + * Returns Iris virginica data + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris versicolor data + */ + private float[][] getIrisVirginicaDataMatrix() { + return new + float[][] { + {63.0f, 33.0f, 60.0f, 25.0f}, {65.0f, 30.0f, 52.0f, 20.0f}, {58.0f, 28.0f, 51.0f, 24.0f}, {68.0f, 30.0f, 55.0f, 21.0f}, + {67.0f, 31.0f, 56.0f, 24.0f}, {63.0f, 28.0f, 51.0f, 15.0f}, {69.0f, 31.0f, 51.0f, 23.0f}, {64.0f, 27.0f, 53.0f, 19.0f}, + {69.0f, 31.0f, 54.0f, 21.0f}, {72.0f, 36.0f, 61.0f, 25.0f}, {57.0f, 25.0f, 50.0f, 20.0f}, {65.0f, 32.0f, 51.0f, 20.0f}, + {65.0f, 30.0f, 58.0f, 22.0f}, {62.0f, 34.0f, 54.0f, 23.0f}, {64.0f, 28.0f, 56.0f, 21.0f}, {61.0f, 26.0f, 56.0f, 14.0f}, + {64.0f, 28.0f, 56.0f, 22.0f}, {77.0f, 30.0f, 61.0f, 23.0f}, {67.0f, 30.0f, 52.0f, 23.0f}, {62.0f, 28.0f, 48.0f, 18.0f}, + {59.0f, 30.0f, 51.0f, 18.0f}, {63.0f, 25.0f, 50.0f, 19.0f}, {72.0f, 30.0f, 58.0f, 16.0f}, {76.0f, 30.0f, 66.0f, 21.0f}, + {64.0f, 32.0f, 53.0f, 23.0f}, {61.0f, 30.0f, 49.0f, 18.0f}, {79.0f, 38.0f, 64.0f, 20.0f}, {72.0f, 32.0f, 60.0f, 18.0f}, + {63.0f, 27.0f, 49.0f, 18.0f}, {77.0f, 28.0f, 67.0f, 20.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {67.0f, 25.0f, 58.0f, 18.0f}, + {49.0f, 25.0f, 45.0f, 17.0f}, {67.0f, 33.0f, 57.0f, 21.0f}, {77.0f, 38.0f, 67.0f, 22.0f}, {56.0f, 28.0f, 49.0f, 20.0f}, + {65.0f, 30.0f, 55.0f, 18.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {74.0f, 28.0f, 61.0f, 19.0f}, {69.0f, 32.0f, 57.0f, 23.0f}, + {68.0f, 32.0f, 59.0f, 23.0f}, {73.0f, 29.0f, 63.0f, 18.0f}, {71.0f, 30.0f, 59.0f, 21.0f}, {60.0f, 22.0f, 50.0f, 15.0f}, + {77.0f, 26.0f, 69.0f, 23.0f}, {67.0f, 33.0f, 57.0f, 25.0f}, {63.0f, 29.0f, 56.0f, 18.0f}, {60.0f, 30.0f, 48.0f, 18.0f}, + {64.0f, 31.0f, 55.0f, 18.0f}, {63.0f, 34.0f, 56.0f, 24.0f} + }; + } + // + +} diff --git a/src/test/resources/de/unijena/cheminf/clustering/art2a/Bit_Fingerprints.txt b/src/test/resources/de/unijena/cheminf/clustering/art2a/Bit_Fingerprints.txt deleted file mode 100644 index d5b893b..0000000 --- a/src/test/resources/de/unijena/cheminf/clustering/art2a/Bit_Fingerprints.txt +++ /dev/null @@ -1,10 +0,0 @@ -1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1 -0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0 -0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 -0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 -0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 -0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1 -0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 -0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 -0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1 diff --git a/src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt b/src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt deleted file mode 100644 index 1a95e4e..0000000 --- a/src/test/resources/de/unijena/cheminf/clustering/art2a/Count_Fingerprints.txt +++ /dev/null @@ -1,6 +0,0 @@ -1, 0, 0, 0, 0, 0, 0, 0, 1, 0 -0, 0, 0, 0, 0, 3, 1, 1, 0, 0 -0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -0, 0, 0, 0, 1, 0, 0, 0, 0, 0 -0, 1, 0, 0, 0, 0, 0, 0, 0, 8 From 0bbc89e42bc6df0e858925b0629ef2f57b7e29d4 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Wed, 5 Feb 2025 17:55:29 +0100 Subject: [PATCH 02/18] Refactoring continued --- .../cheminf/clustering/art2a/Art2aData.java | 2 +- .../cheminf/clustering/art2a/Art2aKernel.java | 4 +- .../cheminf/clustering/art2a/Art2aUtils.java | 130 +++++++++--------- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java index 69fbb61..da31c2b 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java @@ -270,7 +270,7 @@ protected boolean[] getDataVectorZeroLengthFlags() { /** * Min-max components of original data matrix (see method - Art2aUtils.getMinMaxComponents() for data structure) + * Art2aUtils.getMinMaxComponents() for data structure) * * @return Min-max components of original data matrix */ diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 3cf4f12..f3f8472 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -1057,7 +1057,7 @@ public static boolean isDataMatrixValid( *
* Note: aDataMatrix could be set to null after this operation to release * its memory. - + * * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Art2aKernel.isDataMatrixValid() in advance) * @param anOffsetForContrastEnhancement Offset for contrast enhancement @@ -1107,7 +1107,7 @@ public static Art2aData getArt2aData( /** * Creates ART-2a data object with preprocessed data for maximum speed * of the clustering process. The ART-2a data object allocates about twice - * the memory of aDataMatrix. A default value of 0.5 is used for the offset + * the memory of aDataMatrix. A default value of 1.0 is used for the offset * for contrast enhancement. *
* Note: aDataMatrix could be set to null after this operation to release diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java index 64569ba..8921160 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -613,48 +613,6 @@ protected static boolean hasLengthOfZero( } return true; } - - /** - * Removes empty clusters from cluster matrix - * - * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, - * false: Cluster is empty and has to be removed (IS NOT CHANGED) - * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aClusterRemovalInfo Cluster removal info (is set according to the - * operations performed, IS CHANGED) - */ - protected static void removeEmptyClusters( - boolean[] aClusterUsageFlags, - float[][] aClusterMatrix, - int aNumberOfDetectedClusters, - ClusterRemovalInfo aClusterRemovalInfo - ) { - boolean tmpIsEmptyClusterRemoval = false; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if (!aClusterUsageFlags[i]) { - tmpIsEmptyClusterRemoval = true; - break; - } - } - if (tmpIsEmptyClusterRemoval) { - // Remove empty clusters from cluster matrix - LinkedList tmpClusterVectorList = new LinkedList<>(); - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if (aClusterUsageFlags[i]) { - tmpClusterVectorList.add(aClusterMatrix[i]); - aClusterMatrix[i] = null; - } - } - int tmpIndex = 0; - for (float[] tmpClusterVector : tmpClusterVectorList) { - aClusterMatrix[tmpIndex++] = tmpClusterVector; - } - aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); - } else { - aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); - } - } /** * Calculates contrast enhanced vector. @@ -806,6 +764,71 @@ protected static void normalizeVector( } } + /** + * Removes empty clusters from cluster matrix + * + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (IS NOT CHANGED) + * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterRemovalInfo Cluster removal info (is set according to the + * operations performed, IS CHANGED) + */ + protected static void removeEmptyClusters( + boolean[] aClusterUsageFlags, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + ClusterRemovalInfo aClusterRemovalInfo + ) { + boolean tmpIsEmptyClusterRemoval = false; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (!aClusterUsageFlags[i]) { + tmpIsEmptyClusterRemoval = true; + break; + } + } + if (tmpIsEmptyClusterRemoval) { + // Remove empty clusters from cluster matrix + LinkedList tmpClusterVectorList = new LinkedList<>(); + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (aClusterUsageFlags[i]) { + tmpClusterVectorList.add(aClusterMatrix[i]); + aClusterMatrix[i] = null; + } + } + int tmpIndex = 0; + for (float[] tmpClusterVector : tmpClusterVectorList) { + aClusterMatrix[tmpIndex++] = tmpClusterVector; + } + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); + } else { + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); + } + } + + /** + * Scales components of aVectorToBeScaled according to min-max components + * to interval [0,1] (see code and method getMinMaxComponents()). + * + * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) + * @param aMinMaxComponents Min-max components + */ + protected static void scaleVector( + float[] aVectorToBeScaled, + MinMaxValue[] aMinMaxComponents + ) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { + // Scale component to interval [0,1] + aVectorToBeScaled[i] = + (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); + } else { + // Shift component to zero + aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); + } + } + } + /** * Sets rho winner with the rho value and the cluster index of the winner * (see code). If the cluster index is negative the first scaled rho value @@ -897,29 +920,6 @@ protected static boolean setContrastEnhancedUnitVector( return false; } } - - /** - * Scales components of aVectorToBeScaled according to min-max components - * to interval [0,1] (see code and method getMinMaxComponents()). - * - * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) - * @param aMinMaxComponents Min-max components - */ - protected static void scaleVector( - float[] aVectorToBeScaled, - MinMaxValue[] aMinMaxComponents - ) { - for(int i = 0; i < aVectorToBeScaled.length; i++) { - if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { - // Scale component to interval [0,1] - aVectorToBeScaled[i] = - (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); - } else { - // Shift component to zero - aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); - } - } - } /** * Randomly shuffles indices from 0 to (anIndices.Length - 1) in From 4aacca2a4e5e2e907f3c20f3b01b7c197649105f Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Wed, 5 Feb 2025 18:14:39 +0100 Subject: [PATCH 03/18] ART-2A-Algorithm PDF document added and README edited --- ART-2A-Algorithm.pdf | Bin 0 -> 193838 bytes README.md | 30 ++++++------------------------ 2 files changed, 6 insertions(+), 24 deletions(-) create mode 100644 ART-2A-Algorithm.pdf diff --git a/ART-2A-Algorithm.pdf b/ART-2A-Algorithm.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7144f3884d8855eb670670199d38a2d5999b7adb GIT binary patch literal 193838 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

v0CJ! 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 *4*;X>5W`WhqpbsN{ z@Ro-#8?BT((G}@^pfjEJxWB%0WT=1Jn+LAExMQU|_Kwnb@u8l~j=e{^iT!iyH&7

(t=ziqWLu-ji=j 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%0D!|Hq^G1(SO{Qz$ zgE=evH*Kr@>hU?2m2Bm!O+h}*F(!tDY6n+``yN^YUINcb{w%=XpsXlm z`OoIOT}#u!MfSbmsDqBkRT?CGTUK?o-MuTDzJ0CHvSkS>JnXmyVNClclJ-U9S~~o)O)q|*ERmL_RuB_=a;{Q zB~*Vm)!*6r`_y&(y5sBLufKl%ap1bopZfQ(pCmJx1t!}efe5tnY{fEx$&d&9`+dAB;irz15r%<0v>=PaA!#V zush)fyq*>sCwt)D;bby{dh3lUINbS2{g0a#^hwgOdEr0tLO=$OFvafG6K@gNNMTaG!Q#ZnwPbTUscWucXr@aq^-Fg!2FR zr%SX*i^2gaR(%2h0P6G4DtWFuC7BdePW+}ad2i+IJt?oX!KO!5=JD1j_e)@ORPLz=~MnYj2OoOF715m z88EsZjNS)EmV@JEuw~t2j{i+SYBkD(?GouOr@&;gV}tui)L8fJJZfo9vv%h^kCv=gr+2+qSOC_}v~l-M(bi zjt+l@Ga9TO$`vFnsKTZVwx)w_3z6T}^P(|QN(U)hw$wYksx{agHVd3HimG%vOiP=I z;>!8WmVeFnHilW-qILJp3%7(!dW)O#L~yNEVKD0PB`U7AAfjX-^#uH zZiAgS8{Afd*Q}!$RHG#D{x6;=gZF^F;@y;e@R2WV=rDOgCZp3tx)?2GM%B1h;}Tk= zlKmL?0f?4xbI6-sbIY(4Wch@}oASEUq}FI8P@GgNmp*)El#X`iTJB!m#&R(;?O}Cc zn}*iw36)l_0@>l)H;xA&c289LRChq^z5{xt!-p?`=uy!1$j~Eb?h)|BonSi$9y};O zjPC7myWa44-gv{))5Y}kbe((>ta%cQJqezK2Zd-RGq>yIiRYhpl3MLW@Xk9PryXiy zyVKqUU+cfOdDEtieZ5EK9+^AWvwkD9e*MO8?F5hS1b6HNJ2&Pxg3v~=as7(LOV^(~ zxo_Va53gUjaqfcNu9hB8z~$_bf$e+aop+9GenWMRf99Eo`Q6A$et5xx7G4M#v?K{s zcA7*=;-z$25KHeuh+mopx+qYS7yk;_Z-jQ}>S?hKbTK^*ad{?*NQ*c6t@1SZ%Yhl3 zvojpXnS9u6#Mf{;)evGHUWX!FRckSm?l5k&-oluZwbd-x&rH<|)2zETdDCYK9KAx4^uS zX3Qqz1CEwTvUsJkfFsbr8I?v237aT^iV68F;?l2t1Qz_Rq_qa@BS&}j z=v)b}KjX7&ttLuITAIc-u3sL~hI(2S+_$`u@@4p7ns=J)CIh4qscK!Zb>niIf*|C) z;S^-j_aT!`Aa8-Pbhzuaq1VtuUk4|*fhV?s=XkK6dyGT>+rHhq+uW|VJf64OB#-o# zr%hto+N2&xB={y1sZ%=k$}6WJkHGO04(G{J_EX3S08X6noU${gPT6000d6Gt8vpy- z*RNmK(SEq|aA&7yd>u2su5XUq`O;hK+Fi~z znM#MK9j9gxO^={;6(oH8u!G&S4O0ivm-oEDMn-LWCcsa)F<<4_}^I8d$DeCq1 z?U_GsUVkLKGrBVx_4M^KeSQ5W7K2@j?_Z4Gwixs-28;WT^aD#j!23ae-<(WKUtdRm zR0wnZGDYk=SFYTIAdBbkRBhsyAssw~uh+>#$q1#C`0kaotfopZzCcDm&{C7>T4b3H zE48?CLm5>E`IL%EPXilZGM0nafVVmVBcpd}=|B{p!f;(Q%^tQ^DQhBTf zHo<;1$=$s5zOK?79qXdBRz{~aXypkE;f`jJ{i!cs`1JPNfr0k@y$&$|SwN=0WYybz zMmtaHbXGFbQ%sv`LpIJt*l)-#usiyft;y#GGIqhQcUbhKj!gMN19RJ~i6X!Y9YWXz z<@Mp6r*0RU!UmUFr`9thX`*xrr9xpTtelU2ywT&)Rl0v(d3nYBF`C=YZ%&~ zQW4><6AD1tJqxypV4DVvYe1)Fi3S}aKqmpd@zqBk9dR3Mwt$dmxGV^lFMCFYn30j8 zZ=D9up9Wt&4Gx_Kcb*38PJ{AkF!wY#4X^b!b##mkz4O*i}0)cQiFa%$_ zf4ujed-g0HJvnxAY|OKJ53_ssp4Sh8#}9%#4jwv)jvWL|2m2192ls^bfIYi6tX#YM z;NFv$Uw{3`k!LRN-nwUO#pqD~h|t&&9;u3+%cn1^RF_phdezA?1zr&|eWm%0LyJ-WyPXo+3hHq4K-Gr7H5LLcH10)YdWkSpY(rAb)<2og6VaumUTvMq>mg; zbb4ve4Fyu$_!oDLJLNjmg9w5RVV@#Cgh$RuP4BqC3B?5kO4S}U(pHDv&pUY=$6Mjc zY4vE-j7DwHYcy&v;-h@1&&NBJIFCDJX84sjqr`DF0+dR%-Nti%zuM{5=#{`q^FD(H zUOJDx3thEPx(Jz2eL)aI$pUNw#QA$^2=N#N-aeqH3Vf*EWdy&MK2R_7zX^3|r};t;7jHKu@2jH4dYA_e0NSTvP8a9`Y^1!U!|0lzOu zlmX-B0q*aTFxSP8tOWELqlhAC0&5g?A>%VMH0j* z!dLkY2``kXD|CUnQe%8gIhkroDd(#(S%I6~n9OK-7@+#VT~6z3+{3Xd$!iBHyn{^7 ziP^Q^AjwRL0QaxGuh-^Dw>GEsmezbIQIcp+bI({Qr4wCbB$Z8vNtGvAuth?DiuTS~ z+aIA~^INS+J#Q1*qG*82F;Nc}E%o`*^9o^`9)m$9)*+V4B_7wBbU12470~XnwxrV) zYY9@St50j=y(`j==9Cvj5rEA9>_bHxL~0&+L0WboKaoeb=RrIQ4n;sT0#5kBej|94 z2K#96DhYOw6C^sU8P}k%65yl~JgEftE5Q-u9D?S?^B|u`Y$^L3J8HLsqX^g}4<>n$ zo8&LCZZcFC=<)FIzT1JF!Q=USCJtfyGUI{T`;x4->w}I!C|4JPD3>pZHKdj=&G_2s zKY$C?Y8K=$4}n?tlesC0Jf#ZiGa5x~lEd6q*YeJuLp#b&9U;yuN4s{7FY!fPgWI23 zzwGeZCKch>_S}2h+*u){XF1B^Gy2fSf}`<5b8*}8+mZ_di??*TW8KSAXllcXV&fMc z8ufOBHotywE}_?}G#Zy5GXLB1dg~C)@^|_?DaF>Nccsw_F>o>l?u>%9QLrv@R|MUs z1p%0LXe=6ZKL#E_!9ElyWdn&x*nPoL$oYKrT1}7JZ5y$HE*l8hayHb)m@K{kZ((?& zCYRxDZXUT7&UF~i!f`EVm9JcDLr@D6cx~Xym#a?ttU=l|GcJ{#0xkOF&c9oqQQ>4* zT3T$sW6SznrJL*>F`rednhHdps--=!{0p~^cAM3Bd|-2@qs@$c(7y4@3*4=)WO;Nj z5Q&==e6btdU=L9V7kBkrX4-2r1d2EB8+8@z-FGg{U>E|bzTjqEQ5(6Q*+}v}6J%i0V{JQ=yP?Iklvlbh|yN43o;N%7E_7a0bmZ#8asTK4t_) zKx%oXIT|%PG1ARJACM{VNesqv8DpJ?38+~Eks#83w`K}(+K6Tw+n6*b*Ci%g{==HrHEC-t>TrcOy@mgDpf$_ zvTJXeZ{rtS{T&%E#$Dv|YIVMqz&`j$YtlrrUR`CABc5|r9y12%aIEqjvr})go0Ibk zpYi5!YX}{5M5u@hZ#J|S(Vh?yR%oJ%8-roGhKx;Z`^snYs zcys5yy;>^UO;qBf*cP>EzoDkm?F9NM`Wu6p^fm^~l})(2wXyPug{K>0m6*i(VLamP zLR$n4c$&>zfUA4P)zC9`A#Y2g!(HQD=ejUW4jjq?q7hsWz;OY5RRAXwuO-kY67rmZ zeGJ%P0u#D(I&`}h>_8466A0$_!bFV=b#`?@XBdh^JT?bovpIZTuUtRaL(8l;ry`*^ zXLDG*9FO>X7NV)EOKOsxqa{AuISTK|BiYr3Hh>`eNO6Yc)zk{wQFfQ&pSjCv%g*+e z*<5aVDNE=ErgN=&)?ZJ?M1_q@7!kEi!4;4Sma!?HjpPg|cJ58hl6XXb8G> zh2`(xw*KqmDFY>qZ9DX}CE~KQ-D}X9jXH%N{j$NXD~z^y2kt$#I5y9}a7(Y>#r~@f zroCa*>qWJ`=F~qY#p0R=7tZPE+`DB(XOLw`LT9p6=NUi@E1@@m>fj%xWj3od>{8=+ z0VOmV3(M+h+T%hQw+Ffh*DwkNP0NtMd@dk~R@_*=UPD#D}dA(ibarJvFmO*OXP=O-v2&REKwF zBfFaYisju?*Ob;+U3`h@`o-!7`jFfdoN@W?VAn#u{%sKA_0X@j#j)WIjV@i(fFEd5 zQp|1mI!QoLQOOb3Ofp6dRtkL@GSf~}lB75%YL|&1wh`d6Mr@0@bnU45_ue!U3~-z& z)+J8QsSWN(!d3F;ZNC1`q1PWAy-b_~&*sE5T3gwY0w;^w)Lf7ozRa2yC*-pM2 zS)lG^!2__14W*b%J3$hV>#&QB9Z?(kEP*H6d2_TFMT`Ch6A!y;7mEDkcf1V@SFJbv z&SxKDI@pgSGAQ|lL?V)e0UXCY!3Yx!Mpj2aB8(DdnOZeq$h_(P=D< zgLkp|Z;-mSUJdvs>8Jooyp*caEyS@#(%|s&FweY}ZWpU$W2ba^JtBV|(`3}lsQ0PS zR8X!Dw;oVXu>2s-EK1JRyet1-4DE{Hu6Y!$cc9%exjHxfCiDa$Zd( zL&OtC?LQ2wHDZTMx1)eg|@7j2Jyz(9*>$JXssvv?cs#%lUi}d%16?~& zGtE!2GnR(-hRIi0PU|wR8?XGHXQRb1D%H9Ud-?|S{(E6hGeD5Et5Gn9f<9FKVs06@ zn;dq=0T74%90Dvz5XmAK4&jQpoWO~ATu#R2au%GUPIQL@I2~U0Is2A32)U^cYI#(zUXh8{3E?A5;+I@yw#~!aMf?N__g8>@U8}t z-%BKnvqaYWnMrU7@6?o6E1ed?-oKBZC@=V~hZ2*@8B`I2=> z2lOP9J`|7nvuTba+Zw=vJ;Wj08|ERyHlVpkYl(@~O3?LKJ#AOyW@;I};N0=e@2(p; zv@Dq#+VQ|s1Jb_ro!Uz(4WTr~`|>@TN2It-r6SczQ>;@gjxXtG41vk@=lAtB-n4(< zrsp5KV@U8199k7#7BGs17N=P5jy8{NSiQt#O6Qv$;#gNC+EX51gVb_$>`jQm0CXIC zUp-5J=Lmqy9cxV_T=@Xcd!fRl48bg&m1U-vae3ExLAQ6_}Z)*naZwf^`TqQ=*p}sY9Vy!_M$Llsa4{el2+L5y`UdO;h^js7@4?tM9 zR9AC4)d1Pz5gHglXgCf4AcSW4VmrMaj~#MLjf?F>J3)9@JHy&n+kxF=WW_AsfCK1; z+a=!asRbe}L9r&D7E(MSYXQi<8u_LM=|pilo4lbB<)M6IL3WAuYN_L%OvzxVZIQg4hC(ikgY{jB}ch3L>DcY7@yML1^szu;+PxtG0_3Q4}Gnm@S z${N97IUAa~Rf6`EP0%F@Kh6_?mR=rsO4-xj)7sEFeZ1{y&2Ss<`s)>SeDCUvUux>m zD@@hkDoJksdtIYYO;xdVI~kAI~1}BH^0&4=jgy6Np)8uq~8ZE zoDIF?CS04M*xV5DRzUZP1$@Q=p0a=`%e@xzMFM=908bF$9Gv;86@x=VaP_S~8sYnW zzQBmdY_?h~cy}2O*onggN&k>PA?5#-^ovsR9_iy!@`Mzmq+m=6LQ>#`Lpjo50feOr zF4im_|8xE?`N{kJ7yRUgA1wL7K|ks9ktp{dwrdyU1x}RHEnbE4eLX#wkoxOtBEy|q_wM8X&UU% zk98X(&1&z#Grfjji>0bgS=~r}mRrr!@7GE+wOm(u^=TTyy~FnCa<5icEz}4> zOjs`rjyY>3wy@p5+^z}rjyOUKy?S_6CP7bzX5qTK5_^}KfJ?7K?^z_ZS``5((n+q8 zERqyUB$WbzMxhcbRH{0uLLgSu=_EWIP*u`SUo<+o-<%mR(|`0v&U`{1HfZ!(p{HeZs1i*j&Y4&*OE z3pBZ#+AE|YeS=)h0nP0?KLzFRe3J-AK}xpZObldZd!&=zcp*D0ZWxM1I3@xL>!I?k zDDkfQNp}nVlNR(P_8+*dU3xJ;SJkSs%_a`&_uoFzBoZ!G3cNuc_&csE2vrx*W<*@+wf!afFNO%b06m?@yC){q*F0?L_J!Q)qG6mqzpmn-COzt+b; z#V2`usf;J%BMsQ-J4|KrTD2I6#nR76!4puh0r`mhsGKF2$tx8askj30t93QBk(F$^ zfZRdvqGW}2=hpg?*+^C4GoymvouRi;aD0mEYC%zw=(QPImYrp6C9BJIP4!&T364-| zhn5G1c>YrZ*MD0(!md(THAqOLKxU8&>P6Ro0sfD@S0a~bwdAR#>rz&W-QOSpSRN1D zc(W`I<>4dV2yjj2D>iTuuDTJwXDFk=K=n0&Ci}~F@R%LkV+Zs06Lxaa4x)Bo7r{+& zp9W|Y8j-lR7B~c=&r{$s1<+&9Bh)Iz4u}9HqD1Y!R`&rXaJoL@dfD}L7wZWZaJkV- zZpt)Hcfd`$1r80LEwwu64$Ro`+mabuMA zATxwJfElI&U5f3u>iW#gxA0iTK2MAu^8?5Q(q@Skh_&0^I|I=3wk%YER8>;uIxbun z8$feqg;;5*=Yhv~+9m~$MfyOGtls9X1R1XDj-gGVrE_!A+Qkv84RuKLHB^&=!xKoV zuK#cFQnD-3P^Q%i6Xe{JkiP!XmV+7z zf`&l6fl}6rWiGed1}&1;&hvGcEHYb%sa|F)Q(EORo2-q?^FiD7l_^uUbsO1@woO$w z%Eo#XPN6(-KM%xsK*ZDV96Xi{Rz<3vl-a~G+obHI>?IjXCPVMBD}xic$?WeibtuZq zlx3z4lSWx4R+cH1zHl(m(c$&H>I0X2pwIWDkM#MxCasdPxpeMEUPnMCE(5&XtbTLH zRtz1OLRFnZ>xgqgbT|q>wuP-{@U`OmgG5!uY9f(nt0q)`6{A)9hm4tn)I#*GR?Vs_ zh8~PtG44)QYb&kWOl7CDTIl|qKq(WM3_Nh0C)*cKhp)`mSuAELZ*!8{;x6M1eW{_@ zV&`o>z?Hgs=lWis7AOoYMx@|IO$|cLWlDhIbE>d1lQYV(wJt4U$c~QbTDLE%0B@#;502&(LRvX_AZhqMe zE}4N@+h{b`35{AgrEBC_9g=De;0kr3oob@j(ru;ZGld;7%5Ha5Rp`vyL7#6IOA3mp ztKIVXLKb9a`>e@`zJ=#*<4yivW!FQwALPkgts%?iS2`<%e6>qoxoP5=_t~|=whdmb z!l;7tyjEqaZ`iyBgpJ*Ba)X0L)abWwKK;*Bi?~)V1kHxFta!-&KNpgV%dNM+i76Bjye1Qg#Vzl3_dszZ5 zNx%gOct8S{B&VTtYsKQv@&A@j@&WW80-Xek?XD;kv`*VXZ|hwlrupywCAS z9`7m3B@2141z38AqJWB5$zN~1SaCD5stwyQqdgf4*2~67D3-;bPax8 ztHQgHt5B40T*E(`b=6%(`wkC2_JRxG88>>QpNSM;ORd^=@>RMu2BnV;6z^fv7PrRe zh8}#9EiGIsE>qz3qRrMqLvCOnnZJDbE2k$etap13pB=S&m0XWrVK&;7)h!-xU`I*V?Y_9g&G^#Wi&jsZk?gO|yWoq19}wy#Av~YtXLXNPBIWe9j57 z;MIq|eQ&7c*k8_%Jhap)YtZq{HU*^sny%UK{!=5SLzjR3XzqJoxLd2W)pHH%T1B_W0f;|Jir(M#5u*e2kyK-7fAHBp!*F@nLcA8 z@6&=)T5!1$JdNWtzQ(>r63({^HQ*&7co9b_u2h1*M)x9sOXc7~IhZa_my`FGT`nUx z%D_tmXdyryVI;`rmKLI3p@91$Uln>Eb(NkVxVDxSw~eK!ua{6FxTZum{Nlqkw*&4> z@Llipx#TW49PDzI#v+SaD&Cq~Q-h=Yh1(7|B0WjkV+l|A{A2enbh?f{G}E1%o3=DI zrOrHlaP4pJkA+Wt>TCbB+MF<$T{TsjS~Kh0LXGU;sfcpm{{Qj2&7VDU-w)1@=zEf# zlA+tDT~n9-@QJ?!Ccygs)8mrLN`Acx{hAR$l>LAcBW8&I6?pQ}L9lV)z5`Dkc;x{5 zN&q|-0P6uT834_JNPsj3z}c>gUF1R+c%=h8X8=#@0dhEGiU%fnK+FRi9vI_3#wCO5 z01yR0AkZo50$p9L=*Q@fOido*YvuAo;}a7D{l3;%Yb>Td1jL8HlW^aEC^kOTW$m)I z`n3I0b#--rpg+)hXi7Cv(F)2@SJs+cokGYMJ;Gvb+D z?*oAfDIRSWw85ORFiZRvou{FJK1#N;cbqNbT@%x=Fql}k)m5rI=Ci+lH6xH4ryG4cd9<g2pQnVg?b9zAj-85>wi$I=7prL=fyDV<(SCYMgu5j>tf?AK|JEu|L^Ps}8b zst(r(I2@m+v(tE7sXSaUzpyYmtQtJFv}7C@gog6nYjmg_S^@I7=*R(u39jLIP}O&@ zLVLOX!`H7N6TxAGwvvrX!O51KTj1sPxe7WqUh>+J>)4Syk<8HU`r7^ql|r*mnT}ocTllM4eiz_C|)rOf_b$ z6q=;I-rBBi+wY#d>qwVD^v7?p-C9}O;huMXZl6>t8*9pRI-zHFj1^@6@mcoK+&i18 z&DWo8J@yQ=$Sa#i*&CcRaTxdm50oFOJ48;5fpeqa#3(p91U@wg&JI2?NM49uj*@4h zU?~dj>jRI6z_}2Z2!Ym6Z-`t7o(z%|E}4ro*+8EKv{^vfe4m-Tq5)nFIL!iS7C68H zQ5Luj#ioyuWIqW$IsW`Oxic&CYI2b=L5(oyvq0j;3`3+yTg6FK@ zf)y-VAGDGOtsrIvek-tBLA@1l;R?iVFdQ90RVllOqg2{Mq49z722TyIuCBpqB?dgf z;6At*@(>~ve10Qd*7pijH@&8oNYJ?~ovwH4vQ+4q=MXjit&QlP}IA%A5w z6z?&vQ?m+hEZ*h|VHJaKQy|Tu7;K9Vyqf367Z>Hz$ z9X)I3_Z!=N`z)lerk1PL)fkc{R=IuuN|*CcuT9Z$w0j`cshIi14>pgsCGQ()OZ8jD zl)lPlkttAWw0?Wt*I<~q&!Ct{d*JTUom?70|uw;QC~>uz$w4L;=pa2G!A0&JJq zMZRhQ&sl&s8Vp)>dc6Xj+$rR8b(2-x)MRZk8myh&a5_=wtVWf-qDk)HNTm)!C?r_u zIa)NqLqF1ncM(`^bVO~-NnB&LcCT-nV*njgEcrb%Iy?^;$G{A6V7O`aDf$KSkJ-O} z{(iHwL2Oc4o3xRIu5fDuf zOA|2N){n+en_Sm+)CF9wpliR2MYyV5l#AtZ$wR?l=fT51W~sRp~(Y#vrObe=fT zNzTp=i$y%1n2Xlg9;B--ye6y0;|o1>g8q<#&?tjyflGt8Yv>2mt35Mxe2=-=5#8&! zEuLJ^q?jy*{fx!6T1!i7Sy{>LquW$R(!V}fR$j6gM^Ql!YwHdwd~U@q49N1gwSCKd z=9#&9x9?!cz*3JKU25%L>DNVb4~F#*9(v=~LWNPTpj28FbnAWY@y^D{r@nJ$?7~1* zMIG0_5_c&aL8GoW?&(JnD_I#so6?mS*`JXyZX zIYz9)-PwIF&Vuu^pPnUOc7dcJZy+ZO;5>zbI2X9!0WL6dz!Pi`C8tSpK9x!g4_iY6 zAu^;Dp`WZxw6wN%E{Ybw!b0}wkw8~xqH~pKvO7Anj;uP-DNb}I5{_K6*Xw`-Sj*=- z5~=K>YF=KgR(s1$rumA=cz=Jh!`s=aY9{HshtMMN8naS(;~H~Ni@a`|qj126cezzU z6sW^-nyZDI-3t+&w`cIa9kDSm&mMUY*>aHOfU+hg0f*aO?|0mBnx4ZVD>vyjEpZIT@Sz?D^a6^X|mM zhesY<=#VvN(ORHEq3TR@qtlE7pZ>)o;47Z}J{4RE3+glyNvn19`>D%kXVpUVxN-1gmF&Q6e$uQ9jtZaw_Z+_Qx@?8V1@IB2!lpa(5Ge`J(?#Yy2O*KU5* z6f%hI@1bwSFQP}`CM~eB9tFJ3UmSeMGhcWMm9VPKr>S;mI!fL{6`88L55I@%fWR0^ zMoV8?KWtXj$?$vY`*dwhTA`j3^S%?l|4P$=P-}Q*b7rWFeoIxopXzcs^p?}$;b@as zCbb?{zup_a#(tqjQO(_Y%e}4XVb*_Fi!2=@Ui!CjE1aB%h&w?vaP^fl;9?$JUItGs z1Jg0E9=|6}PQ>TqBquJ3lNa}c@%@kQC#Czr<^EUu$qPR4pbtcS;4kjH)9tVW!s#Rk zjoT%5yIq$zKzail+yJo+u;HF}1Gh^el?r$~jg}H?wbTRJr?n&(J=F-O3u@r*v2-Fa z&>e~R+uGEF1LDEKfx+IMfy2q^>ET|2xbybE*f=@Z=BB((Xj$IjRhGYdpjQ=f7(toS zC6x%ODW%d?p*8Z#%87R|47B~9@wBzQ;r^i;g4H*z3*3c`XyHMzoz}ARxknDO{kVDd z`QSE$ea8n`Xh4?(|5}-oYh&3Pn?Jv06Pv8pC}-01`VYJ=i9{}3cnQPrmAajYKP#XFZ;I5Sf2{g#_5ZB-aqaWstmGMK zqHa?zlRsVmNc|&<{fd8AeopmW^+z=yrEY6D)cEI=zM=K*mULdd@0Qc^#q^JRX)o=i zy|kD1(q7t2ducE2rMKa8Taj_|L_#qOMB@T%9)~wIyd%mr^t-GCU|A{~9ztM4S-$-Y;^S51z|B`fS zFTH;XtZq+1>Gl*7?ViF@@1DBXy4NC>($v$}JMpKI`u0U1-%ERGFYTqh^k0_t0MFff-m%Nb!{fsoDlhr)XpfS~pV3=9a7_~#5v5@ixU1G5OLq>+Kygj8Z>U=G2T z3^H&TA(Koqa5>>G;;A5HwLf8CE)kX-XJA1&DR}{625c7WOQ`$@24)j><%<~RpmG(P zfl>L-FkFV@qjEDal!>Ya!{xaAFax9VP7GJz@<$jLmFF;weq_9nWoKYo{xnR>pN47q z(=aW68m8q>!?gTqn3g{c)AFZbTK+UF5Qd}i8H!6} zD8cn#U|>}LF$_y_`I`)k%4xaD5uT;W>d}3ogIPz^MGMG2Ddl{EmSU9xU^U68Tq@$iJe5uZj}B_$BrE zCH46wdf^xJa*3b_H{ry86fs6D5-IpSNMwl&{L2#?#40Wc!(0x+=raM!)3}BmUI`M* zFi}JtmMy^cd14LcQt&$kYoCChNnBF^Qy>1c^p`RGUz5#KsLp&73 zn}s;j#2kd@AiN5%<%<1KMe3b|2SSEou9dK18WIr0Dy&PveiN`Cgkz34#?*END%7QL`5G)i@>?xx zaElLgJrAi!!#kr)7WhhFDv?bjF%5`|W0>Fh4`oj~Yqq~H z9Z^j6GNTm}#E`~ZW{a{l6BAgTYek-0VNXt?xdbqCZfp%FVf_r03DV^}OlgL%Cipjs@ns;s6l_IXRu0PpVMQ96!1ePC z1u-mxB&Hcj+eNfNy}EZ91BmZ+NXIJXlIC|FW6fjzO=B5T*!nlHOlcY9i+U~8rf}~R z#-GC0ox+qQalKVW|2FI!GPs{r%mdwaj=@r(IDv7mV(qQKT6ug8wVK6r6!f`MH+iN7 zZ7I2%O6H3^*oyfr+qhXet2m#8ZIEYQ6l^nxZ9k7SH&?X%-8d-peluPE z5*t9A(7fbve+A!|!=q;d+Yfp(U zCmB1q*`w_o2`22>5ufdZ1Ev)A$f%EF*l(qcc ztd*=CXnoUGkYf5;z_y%W^lrmE=a_kh_C3fmB(VHxJs0elwyRZUeM0w;g;?o%B~!GN z1hF-r@APN?3A!jsD1muLa!WJ5KFN&LIf#9Q86#V391729%Z$yK3N*gEZV5E&ZJW`d zw#_9nO5#~&d7GcPnaB6QhrMqaw=2}%-MiU#c(;P=ceY-}tH$&Wz6*41t+uvC&DKm* z(4h_c`z-EdzL-yySP1f4w1(CowyhbKrg|3Bl4546^`b6IeF?2Y2cz*dJW7^}G!(}5 zc3Zl2sg}<0G?%3_&32pKlEg_Y$CVGM$HHuYR)ZNvGN}>@lK2z#xh0n+SYxha?#RD~ zKc+o@5_3?P@!Gd}@dU(}#U5w(^)PWCF3xU;9l$n>HdabD|)vOW#&Yh(lJ<06*=5aRTTGFpllH4 z;t+oyb2SKYM=?G`KkB~+!$UGQ}SZ@^fAH+P<)DB@D zdf;`Mmk6c+^=xN&rRyR2OfXlFju3q@n6|hIVj1*dYPRGThTkKQa)iGZULC_T#4v2x zjX52~(u*)ML)^x2Zi_2gQ(?>vk}8rxH-rb_UvE)%ar{Zs5--8FU3QcB`c^HPzaaA& z#*!PxIa(WGoEyWsLRW2!F5{SwoxM(CON`)JLCo=J(L#E#_0rTAtds6zxP%hAKcw4I zS_)Q8y~h}#u@tV4Gn&3xE=cM@EE7c6XtC#a!)(7qxt(q&HMW?d2D6!Leq%L7g|oTU zY%Y;cXESyxxV%io(+i9FH7cH3OXW_al6FcU=u6G!QYWe5)l_B-wTUG*vg>(jIlGXa zqvo=!8#&a5Lg<|y%7DIFZB#t5yt+vBB{Fl_xnr=jKf9Qr`qqWNK|LmtI8-;y#k8d?LNPW*!5yNTya2 zxnopz{@vwNC~V(}BbraZZYk4XdM=km)S4$!xi!>ttKHd*Ys1R8?#O5pU3+k>*asGF zcP?=w zry>9I*-YL}O=s7smBa?M4keLCGDpQ!K1;{&1L8^Ip0V*d~0OnB-HuR0PQY2?JmrbtEAA%c%1**g)0w&D^&~RA0`=L<@&;u}n`!NqREHSei(1+kkTi3twR{fB4eDa6 z1Jt~T%a0{sO2c0AsTHK_TpIS8%%03FXA{Zo(o4{yf<_40$-=JS=X!n>8gnv**hF;} zQ_HK{B?<>vW`n7Y6ao=K`7EYq(~xGnK!6NxKD)e}#RkBL+D6SL)*!9fOwr&AN-!XV=l-T|oJ%5hoY3EAPQ08V2jR3?wFn5hSzFF=JYmQgiu&fo<6_H2!2754?8T#1pgG z6RDDU1A0F+WH3EwkgaZ63Ug^~F#)-nO>G;wi4sP0sQWeODD!EkA~<;If&N|sK;x}1 zLX8ghj7U!{f1FoEjOAj|`7SVE5e+?ND^6Ck}gw z3`T~=?66l@MnxuIh8pb)#$vdu;5ei{j_C^zk4(p-y?tX;-*Buu0t-VCNLerxi_l#` zroypcbkIh12M2?_5!`ARVu|Bw40V%z5nKfO3&Q{5vFPv+Vk0~}G!}G_plYcO)2txJFUq(#r3D zHZ_NLh-iOx0+zgA?PIw1Y2xh*>Spn-a{c||B?Y_m`{Vm1~fU_v+WABT3gvf#fUJYk%L3$ZHQ>BTwF!X-4 zBan{M7*3$)EN=Yz@>g#(!1-tQe}MTmp{Fg9%b5iRmafroC;U?eb1NAe72e1#+o;}L z>X?m+CGwe@OHq85zQ*7N7(e{e-w(gVbXWR2opejHk{lUd*DU zRwqhM*A9ei>YL9T`MmYUZ~kBVRvi`P+U+N4q*I6Pd}nAWK|+uoNTX)@mW@bP8eR@B8|Mu^BCyKmI zp?q_ra=FJb6uz+DTGp9RDs=c%p=kz|+!DaTCIKw8Jp&2>Lm+e#AP~%8l$zZ}@)(!g z*FT^b5Srbc0hk;=&Tu3-KuQEPff3O`OwdR=fQBHD(2<$h`n$M#IR|)qA*q3VghFCE zVk1WfPj4>=BpYymP(nt>@bj8w&+Qaa0N^9&p>*tf^$w04XjkX29J;|_6%G`Bk^mbM zB?<}10Vt#_N*XC`ffrGD5xFY@0sofq?dqX_p$D*FuDuI|UwaxFlZ<# z(cPCtMgVtiqhf{Hl(Z?G|JAXxcX-DRS7~e8J=i(V+J>txcfs7{C0h7!*`eNI?c5k@ z9|)S3S%25I_t_C<@B3M-{N|Mu z=NpROnrE3s)8R(Vk1p;kKN{tF;0_D3VoJTRL_;6)!gp%%nV$c}L`S$Lctv~dWo*Zg zmdqI?TJWd_h;-2+QeWOnp_*TI>)h$U)ctQMPv;ITtc)xGZ&x-DJ0>c+%SHT|3T()20|Lb# zk!&p34!Vki_q5DNFf7-966C#5$As2Cj?q_@56S0*~1 zw8Db{b%L4(rUWSD90X*)ZwUEcZ5Sg!Pte2ZC{Yq9IVm||Nog5L83}-$&8DNa0 zpV_nteaHnepYqcyU*-(pM$Ie~0j7ip*kSsBE^stMD?>9@efPr3-p@nK^Lt-n_THZ2 zKJKoBQt{pULVt1mQ1C{JH(9*3+7PT&6p#@Gq{Z+L0T$n<3I@aU03AU4y9_{Lm3F%b z3JUsN7mj}avh4tXf-q(}Fn9+B0YJYTSSZ2#ZX;i+9VRi}2AMU4@FeBq_~MI$UpJ}e z0y2Ys$?#-jhKLg=+7CBkK0a?N)8Cbb<`n z3&>|sclS?hdMV5CIuV`qC%c!Fc(&tty?RBHZZtbohT>CC=Rrv!YFT;soYHG80~)8E z8!2;#Kecp?`QrFWbO&({sUVThujLQr616grK>2CYOBO4z>HNx^%j;snC4Ch`lU_mE zQ3eQ3>gz83MCOzwbYpq$DPZRk?`L8{h{QQ>C!=I^X5Hoxy|XRj3N7a{%vSHgDfOMe z6Jb{`knQ7v8AbI|M!)Or!&Eg!XsCNZb%g*dkq_S0nSXP0WVRGWI^X_l_p5iqoc*`B z4-^1=b02~~^+w*__=oU&>+0lcZyVsqp%NJ2;_c@eaO#Jn!+**E(kMU@K*|stozyo` ziXi^uj`;6f-kW_62B)0MpH6O(f^^bTi`GnC-l`3DHJa-e=aW^(w15p$(&vn_eWtPf$WwOS%1))({enDYAhGU2FzHsMlvjmeK1T928Wj9U-X zAfB4onfa|dSMhR4j#=mBUOZkX5ZTdJl0FSBx9=^XZ+YAj$47Q7g82Q;z7LVKQaZGG zwMLdJdBfx9$d3f|$7uFZS2fMuoDZ#(ls>eUg=)&0D6OMIzD-rS4dx7B7~Tp&d;HyR{SeEK3iJ>d1sfS2K@)Q3!ypxa zoKTYrMrbA$p!#w4;Q$o>^#>2vw`$s+G?!&%=hCg1dfsxN^BVKh|EY(=+b!N|@y5FA zg``DM68~Es?l0O705CTQgXVx?E&`Yf04C|j;)p>33$9h`9uK0*8K7hX9gnC=LWZk#WRBPy)=gA%NIj*nJ8wp?B#u#~&o4 z7zg4{J{P~itN{yV`huZP8DoMU81!;&lHMr7US5~n5fZt4k#>5juB$GkWm2+W1oL2& z4OH7B%mG(M>P7CA0{j`WpuA^FU zUJR3Y5hpFX(WjDQZuIqxDEdn+Dcm(KG17FazfE_sCy{3Os`GJF{Dw?S_h?{D+L&G+=k}oEH zP$liAJUPj28qCEfUE6j)-s?7EDrQ(y!Y(PNHCe*Om+esT6~|VtIaN;HxC5dmhGcLm zI3Y%=0b7MsmlH+{{hC)ZA`i_oV1ks=#?j2I1_;@U*V&vT==kNBg3m~I>OHY3Sy9wP zSBy`OklQK>4O|fEl(A4cq>9X8rzUAIO{?RvHiX=8JJrL~JN!5`kvNpsSFMo9ZRSPO zEADHT0vnN777T!83B#j(0B^Scu)F?CY`-UE$iUnqxSd@?CWRs?_U0t`k^Gd%k^6wX zS_VM;XGIv23&tTN=VLJ}YtmDaWGMpYyVFL@c&9Hhs?y`l=9rmfeS9aNwx>D;QW}uV zppT^e^Id4>fk;-uiT3|?q<=ZnzeFGy77TJr)owbugY;tu@$9{2bRA2wC2U!e#SFH^ zOtzTWVrFJMlEuu-%*@Ozi&+*kGcz;u9Q*dY4c~h+)9+hrezkN;Co*zpRAy9aRphGJ zl{+kU#^Rhd%z;!#fXyKb4qVbPBzz34Z!VK78du=cd(jC~{sa@P?5y1b4i2ssYE#0y zSKwH_l_ENueOlKaKNL21S424!_cyOKMryF2D13V5+Q-uyB=#xC58gXh+D^dSGFI6= zV5+Y+bxz5%6Z@z^7tto`)~P5{+&{z5a4PMt2y0+?@hlR;DWyZp4A&>S8w(HENs|7e zd0Yi6aA(81x6ot#_?0W*zU~%ArUZNMl&+y_d)Hr|7S^S!OrfkO69$5<{sm&Vs1GPh>SeG+(`=hW;@vkW4o&fJR zWlb{F&~Cc&cgnpz+(bE{DEHa4ke?p+^ig4$@Cn}9(x&fSQDGFjDo*fQctI@2nk9sA z34OdhVjM{h8pYnjO$c(acLr_%z@&-=8|P0p?Br;buE_WJ)ggTQshUI=%hHnI!PjG$ zjdH(!>tI0ErJJlqfAu5AnUw9msN9Rr`-)v6$-H*ZZ&uH;KArUcB!WN*g|M49?dh40`h*N$?FcrFUeCGS z2K*%ACJ@>TAJJoQiOJ9?(D#S>=^Pg)|!8SC~%K`lD2XCUhvi{ibsl;8q0N7vvt#R z?TwFF`QRk)jTyxoi{n3VDN=jDkV|i^>mj@LoKh`5hvgd-a|jcNz10CiIU8Pt_{!I0 z4?B`B@Ky0Z$wn~&g6p`MwMgd8>L^|cdohcuzCl7wT^Xhrv81p|ZEL7O`#zZ&0AjMt zI%wzO93{Mqk@z^Z05PQ~wUMRdte58tCYbo;`~H)3<0}KIK8Y4X<6XVhczrtS&vxg! z9{DtTBeNG2=8G<|aIqL!vL(e0IG2J5b56g6r*D|4STJ~JGu_9VjnZqPPb%d@l*Gm# zRRNze7U(nf2Ci-mWOkZVIzln?t5-jrn93=KeuooH$|Oe_GAnV~^2D}2Njzyf>X&u{ zNJp?RJ(pRv&J>h*SGU+YzxeofV@QEXlMM(mFFLrnvCX<8Ob*vH(xN1tN|3>%vn!)8YYG;J#IOJ_mqYWqW3jgnm5li%&TJ$$^ac4cgsr2ggPG0cxE^yA~oI z^Vv*mE$YFeqF$>ga1D`@sJ&~jAU%O^SfbR`TIw*wpnFcO?VHu$xG(glJv&F46%7I; z4Ndi1ZXMy~t=HjvRoU|GZh9nJ4@;G@{Qhh+a9IVSojm*jQYMOlJ#|iRk(d4QPS4;4 z8IUd z&+3@D>0vfv>52V07ZU9s+_-f2bsb_acGO{3Q?w=HQ!A&XWbT8Uvsp2g)wOTcN=yn4 zOBZsaniu?wFygz9Sre%o&aDmVC!5l@3o$!es$awQ1aqYC{GcnOplci5Wh4)!Zv0zN z9Hx0#V49X9d(9K92f$i7wm-p_e_rl};zh92ppx0mvYUrA*IvZ~q1i_uu%C|Fh$zmz1zMKv z6eR7lqm}Z_>5qa#rN!&<~2pQz1W;FA`GUFiHzm7*W=bB2+XpY<;1quI673y_TS)6Da5Cl zM1CiW?#C;#0h$YTE)k0t_-1+k+WejAhE2+_Za5<5Szht!-B?kwo7?_X8L=C z){b~4@*fJ8z5%!Q{1S1B0%(mek&>ekK6W_3O7HbHPf(U1muQE%5TDudZI&Pzs`= zmx<1aUa-PyMO7HU#WkQ9ea4Y;Tlb%^?@7fzE5uYtxVf|^XexPg3oSbRIV1$d8Z#h5 zY)#;AWZ|k#zLYEAh8#sH`<^idVCa}0T2JDafJqwW?vWwDO1j7qk z%8W>Ba`Um?n)T;T=77l=Ert7_2;T*M#fWA21b6DP9~8%SA+wg{@bDw2w_{UrP{N#e zL3xkDFGaF34}7o7GgE_Xggt$j@sA%~&=8I|<0Gd;d^u$8kaFBH%?fEJG`4z8bzp8S zY)09*@GOz(@H>y0BgZbPHee_hJX@IsN@}`TheBJ(Buy~!@K_n`DhJo;Sx4+=iNB`K!v;tRFDsP>F*nGvA+}bVl1K-o0S_1s7xXULW7l6o{iZaN&Xz%D; zhMrQIiBCK)&`JoOvWD`(19PXeZGxHP9R>|i_Pm)SDFti>X34ujmwC*f_7GuXu=joa z6Mai-j2%m7(@L+q^*Xz6P&KV!ix%FeaKP7KXR%iZ%%^n<;|$FmxC=4Ew5+L4D3Ki) zv4O3TiZ+%8#*!sqem{>>#IZkTdEt;Ay~0PcF2*~!G`-dIMS?Gxnd&yAPn3`+G!X?tZH+>i~}tcew(($*~x}n zrpjnwCvM9){NSWE1noV^^O%z{JYc#?etdFL`O9SlZWEDO&8-UJ-8Rw`=hmm0Nr~WV zo39NE5!>d8A7Dc$LS(d4Z9zAK%GwhHT?!MbA&qEWQnsVt4=JfGfS^XKk_z3Djeh^a zI&j1t`r@sN-BgoWlMJhQVhTO4I&*QAPX!BxL44*1i(wj*)XtGRZhGw0#kC&T@5U)H z(RWR=#uhNfQTv*17L?1Fr%g|(7l8lf1<54FXqgQzR?WUMg5@P+hz^o&ypS{3!o(7` zvNCPN0E7PJtZsNC{Ag|8sobV|c{uU9?Ta$yvu%D^z#G#-*BymqaJkw^ghI{FE;-|+ zxt#)v&xeC-an@1(6A_1a5mhQ#;*cNIy~l1%A8bGrv7Dl>Eo(>jj@b1rh~}JfQ~mnL zafiFLPCQ0#ZVB6q2`mo9zj@}VIW$cMoWthw&*$7SXW3PA8L#YpRD(w7#U`o>`E*$& zbVM?&$7!=M9pM(VTsVYCg1noT#3h+QR~1R+`YG~7RNBnob#1ueN5jDR6d{Q>%$X0b zau#gnQz@tI9nSL9VzQ5QF9I#AiEs}i@G>rl`mj9r zcVr^G5Pp?svdAUMckexYxLH4c`b7R75=6fl4)GM@JRJ?p%d-09HM8+a!bCIY+qmdi z5e&X`Fn1y6`!3J{JWyfCnau7atfVy$A3b{a&(x?sARE1{!5Niq*_9Yr-`U?gi1bs- zW;kJ1I@**M3GNzn$_Lzjar8bg;;ef_klMDGk;YhRh;_!=b{m%|NVO?7wDSNfQ#g7- zi)ug^ekKU%#~-u2u7V4ED&ytMl<}%$1?*Dom60^`qG7GZ; z+)cvtP~icvhKlSG9g zX+@Hy_=KNq1*4^(3z!cyAjJI}@hO7d?GB}Xz}plaR(nSeqn-aw94>UrhWMU0wv?ra zy1!!Cqi%iUGQm%8R#tx5+4D<@5kx{=S;`5BLo->Gsb7jxVA+f z6*1?QnkBwQ)eFAfp2WFOAB?0}k5bi{M9_a7l_(3Cb_A`?hK(<^-armhFAtm`J6d2- z=vVL9d+_CH%{|5#d{ONlNHCcckJkf8-b3b+UKnIh!tTK1TVO5KSQ=R9>Bkl8>qNCh zBjsWGpKPn1k~~%a$b~F$5CU_EBxqsjeG)nyQ5S`ulPa)bRxsPLjM%QAlD5BVFd(Q@ zMp5!@C|9;10;QY5W{*^zygJ3df2kQ}QBsCc^6;CSj=BoYskkz7l&B8ZXAXiHRf;k6 z!ylDiD;>5+P(s$5_-2{VhBxN;;8%uODf>X+Z z#6U3qK|yYqbrsDvAv8tqeCb|=QV`owS30*By^lYT@G=$j(d1_5+vSUCM7>Ex;la8s zk#2YoYD&{;Fvx&wO_rMa5mdPRVN3;b8%npBR_VHRDTSWcX)h*13f9VlKsS z?V66>sP9xu6x5_gq(!~93X%;W3!)B^oVuPKsu!%53_b-aL@pAeH&YAfR5R3(L&c44w{jk0gndn zx2T3kqlU-K%%XvZN3VuQ$4CPdXld2(=;`Ty9luJ9f9aY3l;~K0BYrhvQp01SrTrzb z03{}7pu_-d|A&qNi2Fmw$e@PDNJE3i$jAglFaohGz%%@TVgiY3^2{;C7E1=?f*_LH84_4n~-%U@{V`G2?iB?Ie$NLprKw}2h_qca+K z|L7RTzdHu#3}E3OfB)u`-?~3)|G$_&@xQQt%D=vWL;dv^*qOhV{;B?NHNSL!bJTC- z-`wV}b!-9r06hzR0JS*4+|c$r z9vvMc7Z)7#pL5PJRU@*~xQhnX|I8g4yP3~Q6}r{8WB=23W!cviP?oyr&*%$eqxcrw z3vk%t+CI;38TXWKMp~+uP}gxjCp4Yw1GJ+$Iw9j*`y$vBmaF+?U+UAu8+B~p`K*e&GNX7?&9~WQ zKh>Q2J=iSI23mf7*}&=j5MaZAzAzqGT%Ls@w+*9ByUi)<8t2`zjiF%>(gYO;L-r3CH=q2+)4jlXyQ< z30?;-tAUN-94>+F8y(j-VCucw8)-YsyS<~xH4(sUg2dYxYACM%OI}SDsfW{!+4+Qp zB|<^gch}V`$lgUfTuH+;#y5`E4-YEK6##3f)~AJ2On$hrZ{hkkwJn#}b(r@I$BlW0 zVV7fiYXp}ss5ULJV+)cGOANxOj=Z6JFoLfsyD)_O>3ev6BBc^8_cuu!m?Uq-;cd3th#qH|_=CM*!EzI(5GOu0^5&FgL`MJ9$wxROm zUoH<~*3*K6F5T?oINnv{ARU$73_(AD;vmQ+Zpxqnw)mz8{Cq{WmRy^qgLs}=ywQUC z7idmZjPu?u{63{WOPv$MalQrPx;*BDZxgno)_X5f-2A-Vx_#ASRxB%ZY5eJ+7;zlQ zWi@8|7AKZt#qq+z3T;&QaDIDiZ`~|Sg)$@NwFcq-(UJaKa6Jqmqnk3je;A!{l=3CA zs-m}E`9VzWy2qzNny~02^|kp1<4>pIRI;iuUTFg9R7j8_BrX-%2=P&Bzb)4o3rS+; z2O_DXfoOaF+fOu1TW&4A!1pRjvUH9f*f4XI1=aIqYdfd{rlAY`-ngxU!e)5 z^!*d2PaE^3sN$GNyz?%C9BEKDt0r$6^CoYCw8?M+;_A`KABPMAFl|6}Z`1dCR4X>j z5y2;>Y~WVN4z@vL58eq7Pk!f?jQoaUoYji#WD;nM6Y4z;b4{@?ZyXt5&Yjl#Li@+v0!u9;W)>*K-s z`N4(nwT9`BIummd*$I7(&lIbQG4pY|R&c}-M0lWCIWBxBSs!k%<;<8phME>h0^ku= zC@$99SGcaEYR*>kG@iK43$^X@Z|(Qv6vQqMeB?W!87tvGme@vOTURL>Ze`(N+EgXD z9?MagXVThF^l&YXls7a)HO(`tFY|j8UognAKShPSAa9LSF!HjEx$o$vej(n}^;4Bb zL@S*voITVn8|~%mTC53aoh~+OXz-e?DWSTRslTlY!8H20Wn+cn@}qZ3oRkBhd7k>* zr}5djQ#=pb{i{`~)Tg?nYL+%W*e^Q#_pxz-=U!1!QLCM=uU`Ad1QBJd;AtsL^&t3aJXRW>^8mMpLxod7f&N0;HB$+5vIp`VfpOf;Loy{UQ_;YI|J$_D0 zvX=}ODyP#Z*}QI&=gaeajXB!QPqvRqY+TLrUrvlaqc}Cj5&1#_ki{rV=XEz6gk5cC z)oC4%qs~-0!V)A)jOwxE6NrC;n3i?WV&Lq5k5)$*U->Ib2K`NI~zkd$Q49oNdjb0jOMx& zG_&^7HlxNP0al-W#oGLEv9?vVwk4BNRceRgTv7Bd@gfdNFf{}}G!`ygN*raDNSYc zm{rMcS0pz(&macM3h>Yu75Q)Bmd1@dZnE_=HF&N{8myo4QE_*zyvx@mEWki$wRS91 zyTzGgR&X~uX)*-hvUfI!#S(ss+H zpXpY2K_L0W?GlSCbg%}Y`3I!5!Qwgap{$*FK^@(gwdDJD6L7?uT98{3BZ`MoHTGwe z$RpE?RIw=dXDE8f)cE@!+r4*H`S~u9w&Tl`7K#Rq7F6yZ9Ee-B4p{oH_8)-{(UlA< z7R(#WF-(%%%@5hWA2#)`PGHKNtxivg5MpwVO6EB7P~iOd#8-1?6@SVw$T)~7fRjC} z5rLlfiL#D?y)h`?z2$JPR$)d3Qibd*Et7~{;g03W!#ILyUOMgp$HBM%`sy3T2iLcTde(Zfan)w@Z* z+Brw(m{8>5eKMdt|TsS<7(T46E>?3TC=QSNs zzWs!I7sQ(|xAMBGTLj(57INoJRT{|IkFcSfq&#|jc(HaKhqQwHHRJvIsO_E8a@d(h zEi9e``|Qb~)!O)2l01|!wUB#b-;u)^>q9kYW-;WDKa+585m7I>we4DQMr^FD$h zFMHh)xyPGlJ#h2kb|tvXE&^^yd8Mf4BgX|8r@0C zTz93ip6vLGDHxktrW(kMIbWQYQt6jd@u6!w-zJ|R;V*}^OYMJzH+-}RNJD)~Rm<#Ie^HIUZC#unFZ7qA zAZ3sNPLWs$aX{~Sa`+c#*EKpGYx!o6pNG`v=R}^So`=Hm^V}aEs2zgXl<6g|_AQze@w+Dh4r+e^TUb1^vCUfyq?k**~^4{Zl*&~)Si=mM-*KP$0YYj6h~nX+I# z-2x`13<|`?UsH#6ROMRtp$!!PJwGoW0C~hc0^vWsU0pQICF>}^OeO0?O-;$}8mgNzE-IuaCxM^SGJ1N+kPwP=EqhqI=3(e7l{{;qfT-I;lo&8K4)3$u>vIC_zf!7Pr; z(Yq@@)a33bnus#QLvnJOcRD&tQVi==_kZMp_jRA;sZKSi!1JqoRBIMwuHRft6auYu?zRT*eVhjQ_IcRuazELigjhOF z*UV~e6jRD`VunyCM#K7YIo|u(s+9-Co`ZymcR$+@Wi^ROkk(#wrXrmuUOK2$DO&(c zKO!`mK~1K>?|MT6GAG@y-(Lx)Q`5^{9Z3t%Smda%EqZq96@*sIB2I|=ea*Y0?uACi zQ{gZQu{R{?=*5B1GMy&|?8+rjC#PxtT;hg&vMmC|bgf8ktZn1c7w6CPz6gr=b*J&7 z5I-%d#=+YN_r00Eg4t`Xl*R(Si?P0^3TP$nsGW?Lo1zRd0ltIzAU^4|OtEHL&oqYg z!*pFzhH@)yTX_lHw@|I|V}-?|l+qie3XAw7K{dkp*@`4N*~O2eH3Eg-o}&%8{6eJF zp~qAw$e2fIrg(o);puNg1b?=3k(SL-sKy|mhTB(5_%enb)^ka15A9ECL>;fOkB&qy zZIq!_o-CJ6m~I{`^W0Za0Kb-ohn;k zXy=I5ai83f3`T_+6ZV4#mzH47?zl0Uvt(h4CnI$$kdTwJ9}S09_{TG0QDY*Ysm3CC zD&)u3WZFWFskMULUjExd)b=&=g*!tt(F>EI$RnjA`#DZ8-Oi9Z5pyebb49_KiCeRu zH4Vl@^d_vaQe?}^_M&g0Z{cMX9_vAIWN&&@Z@8#l+N5$bL!Bgs8bugoWG{3cZ8JkW zaTq`p>X4;Y6FXYAKJb!>u^s}RJ7f230Z zF>vpw@iD(Am&Y3K7X9u?!m#C&%!kFfi zP5N}yR+4uw%&06*qA-*s(q=T4tlwjZEIF>OvQDjVajrlCcPLK?|3qYF+t0zKx3xMn z+>pS;Jo)+V_XkrW%(RycSEEB=WXo82**hg|M~JoT!(29F&t&y{;h9!9dj4Q^4dD(s zNI$~2kT$7+vx)C5Mshq7JaA^bK3Nj9*JzkVqKh&B5vSm1yvf`?V;?)(`GDh(8a~(F zxoldLtuoOl8*3kT?~}DnvX*xiM_E5w`O>(UkRo^za3_JDRKlPTnC+?__HYENdZj)kFD&+v}F;ixM?y*!L(Fo;+C(jID6w>;CUJl zYBaiQUe&8`ciLkRmgRdi5P)$|+y!g?XwS~eJR5$1p@?w~G+W4ZT3F%P>vF@k+e9+`c4}^V|w>B zy(9K2kmlC{VF)F}KP|&|NrR?SOh9FgrXp5On5G|jOehtN=M?{(+9EY-1z3Jx?yWXZ zw#(M&J~|d=1!A7cK9IKYN-e4~Fn8heEb*N4EcKvu*}n4zQ}#;HNqO&!zmdZG%~2o` zbrcF8iT_JJ?*t`Isxyr|?jmBHU&&9i4fZa*#8tk~DQQeh{T!|mO9jqxulT8m17~r< zz?HJX2~Nv*RXO{dP&|$pW!D7_ZDUb_R+1|n%z#R99{>gPH?rbS^jgDl*_ftH1%oCF-v`J0qmV=o040izD{W6{K)O0 z6?=0RnD-`5+#U$+8E;G;p6wY3mj(BG;F5)?icp|GDSsxOXP!5l!vHhbOn?u=h{JWE z1Em7T0?!IJ<;jcb6k;gIilzw<9cJ)ZpxZsI95**n5E{ew2Uwm1TZUksB~Jy)lcuZ- z2B>J= zi4~Quqe~ZNPIT8Qpg*`sE&sK{gVdEeT(S~8zym9%$vP5xJPnQdG>Z7rY zBsQB5#Ra46B|-S*+1U+btAk6s36x}%ouI|28JX1v{J~naj3wyo!N&aM`IVXZ`I(je zwwJdLL1K!f%2BCz2V>AC#?UYtR3s3GY(cM@Ki%FcO?r5h`zb#*`QinFV-y(p8RX=v z{m{sHU+XU{gAs>H?47)2=Z%ig$y+QKmpM@g*>_ch;Y|cWu7Hh}hm1rc5qX7c1Jxqv z5Z#QElZ2a9k8)9j|He_A*Fk(VmPRWjPK2+!8)RHCrLysDzI(;*c7>U%zL_S-fT-M? zs2(~u(U zgKSowI=rg=SDc5bc$4_P`)pTLt!ubs(nhWEz);~2$Wq+7#d&xG+~t_)?xpqHcYty#0A_$p$eua@8$sNM|rou8;KX{%zakxxfT zI58h*$KFsExp{mV-^c2h@%zVOaS5sSQ=_I>HJ^PXmKM9{#w(gJeYuK|0fCi@f<9Hv zEOF<76!5=)cpt};+^)^D{7g5h=qWjX@p$ua0)Y|lCT2R;%!-QUOnl7?>3mXaIIz37 zhSvtQlM<-i`%>U13;Wp6snq6VArPLPVPKF{Zi&{u3r z2642H6JB6f1!tC2QrknH-k4*>z?qw2VS08pyC~APf<%E&@YN%i4kn9f&P7=z zQIW^n_`OgRzBy`Ac1)$>+733P%jM)sG^D1`RSsyYME4#~l8M2QFK>77~&9ZrmE;CSIyANoS zYSFswez_hmEnZZuSe!7S#Ihby?hOdH3)ktJ!{RMvq@*AuTuF{kjgrxp7#i(gFEu)Z zEm=*DPt=JkGpd6%p?SX9BC+fj@q-tNylzy_&jpJi9|+VlNC_3j2!pKb+7^zM09Vfn zIv)_}*}CqjaX!hJ&a3M^d!BMC+DO^nkj zt^OAKy65cO4TU7BTQWuA6oWLe5mVoQcm`nqwi=Podq7z=`FoO2e!4Y^85FfXgXoIVET9H|bGWqc|=JN6M>*j2S6YrZ5}*$<2YF60l~ z%^SVfEv2P&PM)~86{C;W3SMYjm}y0;R|hA9%^PDsDeu!`Xv1I1Od2I9hYrwTEWN++ zr(x(XkVk^?NJ518$-J-9SKekp^fgtf?1pL@j+TFaiyU*B+r)04M6U2YP9$WM_y8*F z`;T(PC)6Y`FELkwB^^n7RPcd{aMjB?dPGc;adDFChO3g236ON2W`QF{nrm@1V_yk= zn=AuD!C`}XuHD8J=Np?I0TD4l_1)5zT&KpmQl|x#$OK;mHBkff0g*aGkoglZp0)=A z*})Pb!k^03nJw}HYpM$rLA%S()hw8n*?S9{^g}krwXC!$ z<%RRHaT!XCi_eckd+=}^b;C{TQM+{-FclT%mStP*pNlIkTA0gRFD9#2XF-pk(KUEX zrP>8y9XO`mjso1@Ih#Cm#&JZOWCUQ6BFxo37`yq>6hm?2E{F-f5LsBOBCzWqBs) zT?63&Ikr&|5^I+m^twD?r^VOQn~sl-W2e(!A8CBNPrPjZ8VnW9pcEZjH=J5KtR9_m z6i6LKBs4%^hP|oJ6T+U2T>=|TOl1alMng5^FikW!tMjlAD=&vyO#dT`PM(Eibie38 zVw^I>Q|1hn2VTn)wcZ+Im#jmg%RqNF8;lQD3S3-OWJ(+q{AOxHAuPpX+NrSkYtHjv z?s4!<%GDL-@IlYj@xg=@IEGvjU?je67XZFH?UyEnKWC;|Q&)&j z*m_OXx=`P?uz^UVXIjdZoZvk7XAKbT=vzyhJn6!LZEVK zM_nj6&o0PhiY-X$Q2aW-NUqwpj-a`&KR%A&n=KtV8ytB-O!T+vv|0VM@bKC1c2&fI z`752=VqD!`HOnK8{WDPr5kv5N*0kW!ZTlqq&5z6cVEx9hf=C_4x2Hw<^HrHqJz?M7 zdKAz`*18N(C|6j6A7fm6UgAx~@x~BYqG`U_P#s zNVh3EdK?}yrIVPf8%`UM6e`Ke#10tVj-B$^ZjUhBTLSf^!YJH4&ivI>al6s zpY8N!=?b&LGy@~60ORnbE=K}G8%Ni@YzVMNr;sF*gyABd&NbJ9S4>wo$dQdJ@>!&v zb7zUXinYk<%C)GdDHqvGKYqW)YRGywZ`1&;N5w%x3Ib-z07d4LIaU|hMMkMF{S5Sttd;A)$R zlUo{Cc}}TVy<87}ia2%Q8u-SvysGiG+{OG-RRVa8iMqn=UBmrx#wN9mMsG+D8? zdXEexyLOr(DW&hEU$8*e_wlJRq<=@-npBtRkx(x;Xxtx70ZSmMx&M( z;a!o$wOd8^TyXrFJ@IznLa`r(J2q!ybx!Xd*aAHTOcBRSSnm57N=3>%K_do+;}4NU z9Q%q5463!F#L0G1o=YNk-q{)1aoMOzIV0vEwMjC&j}kkg+hJMx#K>-64qH0C&qqG_ zN}ZSitnSYmlpgxdzZ$ZFaIo$MciIH7s>YRuypHoq&E5(+FY|`&+uuyc;e9l%6Cp2W;BY<`_abNDG%cfibSpT+7T-Kt1W!t@gi=kPthQ81I zL9(xne0o~iqm95-uqojUR=jczFpt>U*)P!d35D@c-ps_7K@b)|73PJLeoBi0-#4La ztD7xT5){12qPraZiHJSC3-=yjGrJz|3SO!D7U2*<8mm-J&=m^(Y_4HcVhm0n4dLU*Yij9=SU(6A*Nug0+Im0&MrUl{`(?Y$ zSE=xzzP9vTuDXjDw9R12xel6z@hbM~RFBL@;V0qC$%%X>tv-z`ZbeE{Shvao9|3Vd zwpywuzvH1%WWOeutXSJdv3!CU*qB9-EUzvsjuH4MI!YQM3QAc>_~@=57#ExW$I9+4 zTE%r;VFv2&c1#JPM6Quvtv_$iCklBI9n)k@27SrfDaJG6q9DSQ;HkbS^h>9W+dsfk zOZJD##FZKzKoOadyo!}$J*Z3!O2&elb%6P9ZH!*4L>7?M5KQ#atupQ*uTG>|c%rgd zwy(Oc1L2bozC$u`IgrtCCh6oWEgJ5wb~epXu73#Mrf(!C;ATAOm{!GWhWwq5HCI1z8g=PRfT zNbe-AF`JQRI4G@<_Ax}Jz%OuM0X%PcGhaW<$Sq6YSyLr|)UZjQeqvp|VbhDHRd5Z) z`UauoHf`HQdmCn^+lxjEYMtr=F%}y&e)TBPAR>S|7SHL?`Q9gM`kP)M16Neb85eLL z?!INoHO^*pr_7Rj8dh=Jy)R1gX{2n{WVSnVZFqF(tf=N2ciGRX%Im>= zjtr={)yjDIjC=b@XdE$vz>Q2`L&hsTqpEwO_PGrBRxd%J4PhP~EnkvMloh08 zpHCNFkRx5jRsdgggNwjOA}*XPh0Kp0NLe%5C?2?3Ioee$XjzP@VhY$gbO`a1?DmS#T;cea%&J-TDpgCJn;US7NB8fd2 z{g%?-w@M%NXX^td$W5sL!_z7p2S4`3d)vb%z(d>8CJ$BUN({p)QWPpe`r8e1*OoSJ z9d`nD7&SB-vnuMxA`~v8+q_=eKINTpDW`CU{6|o?u_Y z!@4XLJW!+vIxjvp?F{Oib>Qt#QhlY4w!v^s_iF2GgZDT!w%C)dcUeyszIy(dt(zy- zm7LTlJ>E77;@~mYe@s!K&c42Scyto0WN&*f9kvSHaWRiSme-gR<>9XogL z+~Q-M<51GlRMEHcg&OnRxxOFAp?&P^6eo72^~Bm?P4UdLk8^({f!#HY4^%U+X@qnC zs{e(r73VE7yQsZl>genezF*Dlm1-d@#@4}wv$Uv!@6<{Ulgkyyp`K&^=vcx3;QH1X zqB7qD5+Cz=u*9{xV2E@5_Fm?IS4A_#4%-d%GIQ>%;Bfe}KRnI;g_Fw*m%QxiB6FqR zOe4g?HRsDpAx`npSxi*|M}K7wA}oGPg%Zv+VZBRcnirk&{LbB-5*wCUy*cG^khV%- z4olNB=SnyZ_lusr(Ac`&AdO$mt=4hX%u-mb<}AR#Zl^QL9d*29Sd*;&8ZOJ8@DkDZ zI75XTb%lQ2?g{C|eofn>-i0WmXr6mLq?Awr5$*I2{`&se9jn~T*JvqhXjQE5r$5hU z{7^(}TYuMNxpJ7zYlQ1s1gGZ*I!4*!6sae1DNk*)lS`*21Ccs3(wiISrhvuPlaIM) zM<+N@Hm;4;^#yQ%#*T18*QlD%*0gdOj6{!)zT@4WU^P#}$_lVvSh%B`cemqdOzT}L zOHXyT_IHg-d)a%6nY=&v?H&`|Zs8uD0-mj#6N*Kgnp##~Y>+5pO`L@Yj%|4?zjYT3 z&F%6fwa-g`A3y@hC}UyZ#fANb(2#}ctOF)yM#GzZ(Nptz$fM_p*9GOC_RP=T75*E$ z2Q~L7cUMOD6WAN-8=tbz%TtvhM4byw!iG-E+_#T8n3hCr$=pAQyO5dexm}v7R{L1= zOAmTq{e)+#o$nHGiarP(_PR38+Ui5_KQq9JV{1^hz1DEMLx^pCuD5XuiUcfkUIu$5DSG| zv7YCze$`%x95jSTBoT%(kRnq~SMX#{|+391uH?*!F2)3kb z>S=K(sB69POK;W_`D|vV$Ij1nS9^NnlGnS8-mzf|lZrX#lIq*iZy3C?7}LX06TI6YkexVL-6L5I`6E0VJ`pK z_2o3KA=yH64e=27<&W^s@?jIF624}BNOc7w!Wv6I! z_+G^qxVH?c1qw3amZ}hHZ=VAd&ue~abu&Qtf-^j(c%va3J zWU{9F1=p!F2DY|r6ZWgZ*5Myb+)8pix63xAq8IUQQG4KTA~HAhXS;@5E{2LW&Z$&& z9*^zgf-nnDbSk#wlc;-*L2ug=vY|RqsK8mn(ySdQy&B0ryh%W+Q9X>6C2^mJ zY_nbueRFZJF!V+LfX0?Fc|;ptaQF6(c{o)2zeykZm4p8~edu>`7}M|Euz#UD{30qa z{H6&2sSSTf^uX*fp!DYfq!9d~EHKgkqCGIv0W-_!fpv7hOF$9=6U{H$1j{cwbhLlk zq5;w@{$`f;FT1pKKrI8)Z_WiH(9YlJ7mPr90xQceCIu}M5X(&an})#*escsCv*a&|nPRTo20;nYbwmSMcwmQ^ucDlB|6v`6ff6JM|qo-x0 z|Hq6e4sJ(F9X%6(EuJpG(8!#V;HghTj<;A0j#+sjP$H6Y%C0Hf0pAZSy=1iiOb9Jas0yn9m7HW>sg!PhOaW#9b6Xo=hiHFyNMDc5*2vZr!1dcpJf^mP2&P8A zM#W~TV{XVv;7F+tFwn6xwI$%e+a*uQUBc(P|^buglsLW|F9+p zu=>+17r!+X9*^F4BQrczBVccA%$@$l*l+MJV}F_WAG!cISN`(BKV1+<9ZTT&X?~9% z2x8;=qp$K77N-B*HThQ<$6pEnKajA(MMp!&NJ&En{K(U=ve7cL(bIpSp=G0?;h_E( zT^J0pEICO$ek z0bW{ON&#Lv21*82T2@M49u`4L9%fnwCMI4MdS-g&-^2Xpw!a1n)QFhd*y@<;0sbe} z{-N!EF?hWH2S3pN56*%2pOgGsUjC1}{^PEH%LD%w_&>AjKkoXsJn(OU|1-P(J$LEMIwgdcH zlK^e}UdJ#p{KeG3=PifCYXv4q9GdphO3Zn*LXD*B{kob;eDRUxLLHMZq6vBnqqdz4yMqwA5(O z8M%-w3o}KWnu{4TN+zV3fNMczI)6+w!s)ckV`fEVJ1S`@hN4c(ut8>;BSF^@o!R!h zQ(NEre0KKN&c5HH9v+{2pZmGbz4vpU=ic{4(XPo;p1)c8;a+L@f7c)j|M_`nQ#key zW2u5l>qucy#^Z4xoc+tCK|2=@Tk&*QX?5Wv8@oM|xcsi_b+6{wQ{`Km8tvYPb4z}g z{`SP`)SG45rm1Pat@x<1$C~_x)SgSfYQMB~&a}BL7u~{d=bXHB+sUGR6HeBQU-jaH zZ+&s`p}G_2Huo43(cW?O>}N9v?pgQA_4i~i%}lCa*Jt{nFm?Lyf@5j#{Wr7`wqh@jQXth0juxqxB6t?)jcPMPMrRe zpqPbE@7kX}Iy>TcOPaanRCZI<zghVH?(_+h_a@7G(qdn~CHAqx*!2x*<7&+3R<60! z>u*W1UBW9$kCmq;q}^0EHLNge#UmR=WamDdle<@LKHO`?-+QlkrsC`5w^|A&ha`Cm z>S`7}dFJ!ICpU-B^x_N0)!sL`HDPkU`=^YkyVOC}C0XaZBOzH^I^R*gBlmb)#PQIj zXA_+xyU(W$UL5Jv=Pjx4-0Q@*XP=sOZpOZ2%U%wO=`?v#T)ADD@oalhYtEKO>k{(r zNT`V%Qu}pY$i4Gk9)11kl*iWv&&liA81{J8O^1q)j_z`L{)nKFokC02?!VzoS=#=X zr>eplJAG8;HFjygy-PvRxRj)tetnA%?+@-ZVP zbN2TO-j-bu6}+Q=LD+<{cw@(~rQPbs=k)JAVcz}m9S1a9bosUTB_pCb+bu6~_Z1wu zTzNL__T=~Hw+{b&>|+%nXWX8%DxVovf8yOai<*D7EOb%-sI@yDG&--{p18ib^hPZ|c@M@@)Oc zezo@$^joqmq%FQEs4hDpc|r81@WVTP@j$1)W)Ix3AY)ReqTs^!x{W_GD)0K1vg8YK z_V%e4I=;DW?8m{M#>YM}vTIApL(g}am@;qH%{%MIcTE}IGXHX3hjUFyZqS`oH*~E% z()Q!H(np(ODoc-#4%(U5@$Qtc#XANk&OewJyr(&?azSg8Q`4bmbK!;SZr=V}TetCL zFE;nQ5WR6*>xN%XeC50Bx>?6Bht1u7--X!k=FM#$Q4@TmIk>STyk=+UpSJvHOw{+^ z%*@Pm`e{^VdM5o=hlOLKMlHy>Demm`JQnd?7hJ z>Du?e5dS-5$myh;T@}ket;jlGaOUpU+RgT@Ki#zF!FPJ%HYGgRQ^+MV7&y|nqzZ}G}q}AX5uWLejr-J|KKjV9uY11Q|noL+kBbv;% zn4WYTVuV0F>G^rGK$)V~g)o`$2}IKqE)kL@Q#P;9F}R-V(SwbFWi8Xs=V>zOzePlo zS-j1bBUm3;p2O<1rALp;2G(a=9Gh*~yezO`JG?%}@E_U?wBabh`oM7cxm@OP8;-%oi|Lq#J`PMr*sOhyWIi_?i}k(f`tP6# z^rh)KY)+e=$=m0VleDtJu>DP8z}OI!U)S*p<a#unElYuIp;W%^H$mw*))vQMYnX6NLmxxJbxhqK!V_%n3vw?j>q(ExESur=Y_<*w z&ogvAX$V%IG;Bxr2d!WX2OT%CHcNVho7oV8t;JHv=+Nvt`gKp|#mEY@p{Ex?Rf@ z%m%R*n{SrsvV93@8Tz=lgl+0=wrJ7O=Q+J&j*T^cL&x+im#s|}EjhZ6ELuIvH%D15!VAa9Dsw?NmiEDNiG98hl?Wqz(I4B=@I8~ z8NlHJ94^2?`+Hs=z~KTMv@hXh0S<~Qn4Sl4cmRh7aM0xsULU|g+{5(%4vN*d9>+m% z&tZCk_P}~sI#dTTjzbt6ham3Z^>G|NPjfwvLlAEo4c12||;&Gi5d+Fx-!fWrhh zD0T@fYb%rOOAJGp00-@_0?U#f-8AAdfP?l7To2$NKIeJ>hXDQ%IyS$300+gt%m(d& zxeVX{`5^)LTmU}z<7sX~Z!>K{SZq%?=%yDd3;0|BJ{PnH=4F9D0G|uM=M>q3`hY)_ zj*V~tKKI{c%i2f#T`mJS74#4Mv;uKaN;B(rv0zH5O@HqvT zpe(=v_*~H5mbVZ1gK{2B5AeAk_i#OcgW?6Q2XN5-ndRNoY(0J{OdiV)X$&7l6+x7y@O1KLDQ#%8_xqz#o)r;d)?P zQO;7=lYq}Dd!y-5U$H!hVMxjq@cQ_;l7P=C8wAR79F)8PdH@IDb3Z@M+6VYtng9pj zbIMGC`T!0ZSKKbZ0r=d{m$0&c&n3O`oa+G`jJs{6$ZlXRl6*<|7hWI00r*@3KBuf3 zuMhCKq}&&?3;0|DK9_*cCE#<)_5mBfAAryO+%;<-;B(3p0zKdl#@*x(THBdF0H6Ek z2D~i*2jFuF_*?=$mmof;>?XI%IG)B3;ByJ$a|!sIUaJahfO!u1Tmn9qbne3X3h+5) zUx6N&=YY@sJUX`lZ~#7+fX^l1a|z;e%KCD1KBvP^pa*aOK9_*c{hTzj z>jHlOK9_*cCE#=Eg7pf-=MwO_f6m0(2l$+_;Xn`I0P#5;9)q&LA0R&W&tiD{fIk4A zE5PUUs{~kmigCO_N3(ofDG;A4z~>6^xdQRIpGViR`FUj~1AMLkpDV!U3h=oCe69eW zQ{WBS!u_FG%uf9W_*?-#r(lxX0DP`MeD3GMSzj@Zr?vn-S0FxDfX@}+a|QUEHqg8+ zfX@}+b2>o+W%>M|jVI6p{-ARq)-Qn172tElxSLLn$wxXT(`gpB0pfGTIG+3g_?(Cz z)CX_?K39Ox72tCM!fgOP_s{cK{{cQ%fX@}+a|QTZf%u#bz~}zCBx^I^b2`BUdSHG4KBtpTP!^0Uz~_o_w@qEI zpEnX-z~>6^Ih~yHwg5h-(^Q}bZ~#76fX@}+b9(%V+W_&o0(|a2Y{~ov;&VF11$qDn z<8JC_cD`>I{#_E?t_ApFM`uQ*QAK-Hf#OHM4t<~q3<@*x)^#NQD@Ht&*0D8b5fX|6{w6cVa-3y}I z2gIQP97T^1(*<)T^V_WHnI`qKF7sm&UB=G$O+lBkm>%85(e4_UbbCWzH%KOF`~}(| zFVdXWWv+e~%(NYs#<-^ExWucP%oY0izDZZ8^gAphb7-v#EK4H&j)O^8t(Xmx(Yo<{ z+5DN4rsqzX85vH`9$bCm!2aJqW^m;>f=M&;uiTlX-yTL+AR@yP5>hkLBEzp;q%|VL m|3A&UW%SG`PxQais$2YrF~;Qol794iPH#EgNVC+{mj4s<;M;uw literal 0 HcmV?d00001 diff --git a/README.md b/README.md index c7615c0..fb52f15 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,12 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.jonasschaub/ART2a-Clustering-for-Java/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.jonasschaub/ART2a-Clustering-for-Java) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=JonasSchaub_ART2a-Clustering-for-Java&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=JonasSchaub_ART2a-Clustering-for-Java) # ART2a-Clustering-for-Java -Implementation of the ART 2-A fingerprint clustering algorithm in Java. +Implementation of the ART 2-A clustering algorithm in Java. ## Description -Implementation of the ART 2-A count and bit fingerprint clustering algorithm -in Java for fast, stable unsupervised clustering for open categorical problems -in double or single machine precision. ART stands for "Adaptive Resonance Theory" and -represents a family of neural models. ART 2-A is a special form of ART that enables -rapid convergence in clustering. ART is able to adapt to changing environments. -For clustering, this means that after each assignment of an input to a cluster, -the model adapts the cluster to the new input. +Implementation of the "Adaptive Resonance Theory" ART 2-A clustering algorithm in Java with single machine precision for fast, +unsupervised clustering for open categorical problems (see references below). A general description of the algorithm is provided +in document "ART-2A-Algorithm.pdf". ## Example initialization and usage of ART2a-Clustering-for-Java See the wiki of this repository. @@ -47,19 +43,8 @@ all source code files including JUnit tests. ### Tests The test class - -Art2aDoubleClusteringTest tests the functionalities of Art-2a in double machine precision -and the test class - -Art2aFloatClusteringTest in single machine precision. -Methods for the clustering results are also tested. - -### Test resources -The test -"resources" subfolder -contains two text files. The text file named "Bit_Fingerprints.txt" contains 10 bit fingerprints, where each line represents -one bit fingerprint. And the file named "Count_Fingerprints.txt" contains 6 count fingerprints, where each line represents -one count fingerprint. + +Art2aDoubleClusteringTest provides test methods for ART-2A clustering. ## Dependencies for local installation **Needs to be pre-installed:** @@ -86,6 +71,3 @@ one count fingerprint. **An adaptive resonance theory based artificial neural network (ART-2a) for rapid identification of airborne particle shapes from their scanning electron microscopy images** * [D. Wienke et al., Chemoinformatics and Intelligent Laboratory Systems (1994) 367-387](https://www.sciencedirect.com/science/article/abs/pii/0169743994850542) - - - From c5e5926ab2d3073cfb5460bb4d2a5c8797e340ba Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Thu, 6 Feb 2025 09:01:24 +0100 Subject: [PATCH 04/18] Minor refactoring --- .../cheminf/clustering/art2a/Art2aKernel.java | 6 ++++++ .../cheminf/clustering/art2a/Art2aResult.java | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index f3f8472..7ec067c 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -703,6 +703,7 @@ public Art2aResult getClusterResult( tmpClusterMatrix, tmpDataVectorZeroLengthFlags, tmpIsClusterOverflow, + tmpIsConverged, this.art2aData ); } catch (Exception anException) { @@ -905,6 +906,8 @@ public int[] getRepresentatives( } /** + * Note: This is a purely experimental nonsense method. + * * Returns representatives whose mean distance is nearest to the mean * distance of all data vectors of specified original data matrix. * Note: This is a O(N^2) operation, N: Number of data vectors. @@ -1055,6 +1058,9 @@ public static boolean isDataMatrixValid( * of the clustering process. The ART-2a data object allocates about the * same memory as aDataMatrix. *
+ * Note: There a no checks! Check aDataMatrix in advance with method + * Art2aKernel.isDataMatrixValid(). + *
* Note: aDataMatrix could be set to null after this operation to release * its memory. * diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java index d240fbc..f030e85 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -92,6 +92,10 @@ public class Art2aResult { * True: Cluster overflow occurred, false: Otherwise */ private final boolean isClusterOverflow; + /** + * True: Clustering process converged, false: Otherwise + */ + private final boolean isConverged; /** * Art2aData object */ @@ -140,6 +144,7 @@ public int compareTo(IndexedValue anotherIndexedValue) { * case), false: Otherwise. * @param anIsClusterOverflow True: Cluster overflow occurred, false: * Otherwise + * @param anIsConverged True: Clustering process converged, false: Otherwise * @param anArt2aData Art2aData instance */ public Art2aResult( @@ -151,6 +156,7 @@ public Art2aResult( float[][] aClusterMatrix, boolean[] aDataVectorZeroLengthFlags, boolean anIsClusterOverflow, + boolean anIsConverged, Art2aData anArt2aData ) { this.vigilance = aVigilance; @@ -161,6 +167,7 @@ public Art2aResult( this.clusterMatrix = aClusterMatrix; this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; this.isClusterOverflow = anIsClusterOverflow; + this.isConverged = anIsConverged; this.art2aData = anArt2aData; } // @@ -349,6 +356,15 @@ public int getClusterSize( public boolean isClusterOverflow() { return this.isClusterOverflow; } + + /** + * Returns if clustering process converged. + * + * @return True: Clustering process converged, false: Otherwise + */ + public boolean isConverged() { + return this.isConverged; + } /** * Calculates index of representative data vector which is closest to the From ba31b711c3999d757ab85774c4a23a89b36963ff Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Fri, 7 Feb 2025 18:25:05 +0100 Subject: [PATCH 05/18] Bug removal and minor refactoring --- .../de/unijena/cheminf/clustering/art2a/Art2aKernel.java | 4 ++-- .../de/unijena/cheminf/clustering/art2a/Art2aTest.java | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 7ec067c..d5fea55 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -716,7 +716,7 @@ public Art2aResult getClusterResult( anException.toString(), anException ); - throw anException; + throw new Exception("Art2aKernel.getClusterResult: An exception occurred: This should never happen!"); } } @@ -1038,7 +1038,7 @@ public static boolean isDataMatrixValid( } for (float tmpValue : tmpDataVector) { - if (tmpValue == Float.NaN + if (!Float.isFinite(tmpValue) || tmpValue == Float.MIN_VALUE || tmpValue == Float.MAX_VALUE ) { diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 863c999..7e79047 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -35,7 +35,6 @@ import java.util.concurrent.Future; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; /** @@ -46,7 +45,7 @@ public class Art2aTest { /** - * Test class for development purposes only + * Test method for development purposes only */ @Test public void test_Development_IrisFlowerData() { @@ -105,7 +104,7 @@ public void test_Development_IrisFlowerData() { } /** - * Test class for development purposes only + * Test method for development purposes only */ @Test public void test_Development_CombinedGaussianCouldData() { @@ -175,7 +174,7 @@ public void test_Development_CombinedGaussianCouldData() { } /** - * Test class for development purposes only + * Test method for development purposes only */ @Test public void test_Development_GetRepresentatives() { From 96ccc01b28eaf40b3954edec5813c27b28579449 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Sun, 9 Feb 2025 12:22:02 +0100 Subject: [PATCH 06/18] ART-2a Euclid classes and test classes added (BUT NOT CONSOLIDATED) --- .../clustering/art2a/Art2aEuclidData.java | 302 +++++ .../clustering/art2a/Art2aEuclidKernel.java | 1175 +++++++++++++++++ .../clustering/art2a/Art2aEuclidResult.java | 522 ++++++++ .../clustering/art2a/Art2aEuclidTask.java | 305 +++++ .../clustering/art2a/Art2aEuclidUtils.java | 925 +++++++++++++ .../cheminf/clustering/art2a/Art2aKernel.java | 62 +- .../cheminf/clustering/art2a/Art2aUtils.java | 32 + .../clustering/art2a/Art2aEuclidTest.java | 974 ++++++++++++++ .../cheminf/clustering/art2a/Art2aTest.java | 56 + 9 files changed, 4337 insertions(+), 16 deletions(-) create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java create mode 100644 src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java new file mode 100644 index 0000000..d1df8fa --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java @@ -0,0 +1,302 @@ +/* + * ART-2a-Euclid Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Data class for ART-2a-Euclid clustering. + *

+ * Note: Art2aEuclidData objects are to be generated with + * Art2aEuclidKernel.getArt2aEuclidData() methods to obtain preprocessed data + * for faster ART-2a-Euclid clustering. + *

+ * Art2aEuclidData is also used for internal data preprocessing in class + * Art2aEuclidKernel. A private constructor ensures that original dataMatrix + * and preprocessed contrastEnhancedMatrix/dataVectorZeroLengthFlags are + * mutually exclusive. Use method hasPreprocessedData() to check wether + * preprocessed contrastEnhancedMatrix/dataVectorZeroLengthFlags are available. + *

+ * Note: Art2aEuclidData is a read-only class, i.e. thread-safe. The same + * Art2aEuclidData object may be distributed to several concurrently working + * Art2aEuclidTasks without any mutual interference problems. + * + * @author Achim Zielesny + */ +public class Art2aEuclidData { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aEuclidData.class.getName()); + // + // + /** + * Original data matrix with data row vectors + */ + private final float[][] dataMatrix; + /** + * Matrix of contrast enhanced unit vectors + */ + private final float[][] contrastEnhancedMatrix; + /** + * Flags array that indicates if scaled data row vectors have a length + * of zero (i.e. where all components are equal to zero, the corresponding + * contrast enhanced unit vector is set to null in this case). True: + * Scaled data row vector has a length of zero, false: Otherwise. + */ + private final boolean[] dataVectorZeroLengthFlags; + /** + * Min-max components of original data matrix (see method + * Art2aEuclidUtils.getMinMaxComponents() for data structure) + */ + private final Art2aEuclidUtils.MinMaxValue[] minMaxComponentsOfDataMatrix; + /** + * Offset for contrast enhancement + */ + private final float offsetForContrastEnhancement; + /** + * Returns if Art2aData object has preprocessed data, i.e. + * contrastEnhancedUnitMatrix and dataVectorZeroLengthFlags are defined: + * True: Art2aData object has preprocessed data, false: Otherwise + */ + private final boolean hasPreprocessedData; + // + + + // + /** + * Private constructor + * Note: No checks are necessary + * + * @param aDataMatrix Original data matrix with data row vectors (MAY BE NULL) + * @param aContrastEnhancedMatrix Matrix of contrast enhanced unit + * vectors (MAY BE NULL) + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @param aHasPreprocessedData True: Art2aData object has preprocessed data, + * false: Otherwise + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + private Art2aEuclidData ( + float[][] aDataMatrix, + float[][] aContrastEnhancedMatrix, + boolean[] aDataVectorZeroLengthFlags, + Art2aEuclidUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement, + boolean aHasPreprocessedData + ) { + this.dataMatrix = aDataMatrix; + this.contrastEnhancedMatrix = aContrastEnhancedMatrix; + this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; + this.minMaxComponentsOfDataMatrix = aMinMaxComponentsOfDataMatrix; + this.offsetForContrastEnhancement = anOffsetForContrastEnhancement; + this.hasPreprocessedData = aHasPreprocessedData; + } + // + // + /** + * Constructor + * + * @param aDataMatrix Original data matrix with data row vectors (NOT + * allowed to be null) + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + protected Art2aEuclidData ( + float[][] aDataMatrix, + Art2aEuclidUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement + ) { + this ( + aDataMatrix, + null, + null, + aMinMaxComponentsOfDataMatrix, + anOffsetForContrastEnhancement, + false + ); + if (!Art2aEuclidUtils.isMatrixValid(aDataMatrix)) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: aDataMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: aDataMatrix is invalid"); + } + if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aDataMatrix[0].length) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); + } + if (anOffsetForContrastEnhancement <= 0.0f) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero."); + } + } + + /** + * Constructor + * + * @param aContrastEnhancedMatrix Matrix of contrast enhanced unit + * vectors (NOT allowed to be null) + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + protected Art2aEuclidData ( + float[][] aContrastEnhancedMatrix, + boolean[] aDataVectorZeroLengthFlags, + Art2aEuclidUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement + ) { + this ( + null, + aContrastEnhancedMatrix, + aDataVectorZeroLengthFlags, + aMinMaxComponentsOfDataMatrix, + anOffsetForContrastEnhancement, + true + ); + if (!Art2aEuclidUtils.isMatrixValid(aContrastEnhancedMatrix)) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: aContrastEnhancedMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: aContrastEnhancedMatrix is invalid."); + } + if (aDataVectorZeroLengthFlags == null || aDataVectorZeroLengthFlags.length == 0 || aDataVectorZeroLengthFlags.length != aContrastEnhancedMatrix.length) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: aDataVectorZeroLengthFlags is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: aDataVectorZeroLengthFlags is illegal."); + } + if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aContrastEnhancedMatrix[0].length) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); + } + if (anOffsetForContrastEnhancement <= 0.0f) { + Art2aEuclidData.LOGGER.log( + Level.SEVERE, + "Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero."); + } + } + // + + // + /** + * Original data matrix with data row vectors + * + * @return Original data matrix with data row vectors or null if + * hasPreprocessedData() returns true + */ + protected float[][] getDataMatrix() { + return this.dataMatrix; + } + + /** + * Matrix of contrast enhanced unit vectors + * + * @return Matrix of contrast enhanced unit vectors or null if + * hasPreprocessedData() returns false + */ + protected float[][] getContrastEnhancedMatrix() { + return this.contrastEnhancedMatrix; + } + + /** + * Flags array that indicates if scaled data row vectors have a length + * of zero (i.e. where all components are equal to zero, the corresponding + * contrast enhanced unit vector is set to null in this case). True: + * Scaled data row vector has a length of zero, false: Otherwise. + * + * @return Array with flags or null if hasPreprocessedData() returns false + */ + protected boolean[] getDataVectorZeroLengthFlags() { + return this.dataVectorZeroLengthFlags; + } + + /** + * Min-max components of original data matrix (see method + Art2aEuclidUtils.getMinMaxComponents() for data structure) + * + * @return Min-max components of original data matrix + */ + protected Art2aEuclidUtils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { + return this.minMaxComponentsOfDataMatrix; + } + + /** + * Returns if Art2aEuclidData object has preprocessed data, i.e. + * contrastEnhancedMatrix and dataVectorZeroLengthFlags are defined. + * + * @return True: Art2aEuclidData object has preprocessed data, false: Otherwise + */ + protected boolean hasPreprocessedData() { + return this.hasPreprocessedData; + } + + /** + * Returns offset for contrast enhancement + * + * @return Offset for contrast enhancement + */ + protected float getOffsetForContrastEnhancement() { + return this.offsetForContrastEnhancement; + } + // + +} \ No newline at end of file diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java new file mode 100644 index 0000000..706e535 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -0,0 +1,1175 @@ +/* + * ART-2a-Euclid Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * ART-2a-Euclid algorithm implementation for unsupervised, open categorical + * clustering. + *

+ * Literature: G.A. Carpenter, S. Grossberg and D.B. Rosen, Neural Networks 4 + * (1991) 493-504; D. Wienke, Neural Resonance and Adaptation - Towards + * Nature’s Principles in Artificial Pattern Recognition, in L. Buydens and + * W. Melssen (Eds.), Chemometrics: Exploring and Exploiting Chemical + * Information, Catholic University Nijmegen, 1994. + *

+ * Use Art2aEuclidKernel for sequential clustering instances and Art2aEuclidTask + * for clustering instances to be executed concurrently (parallelized). See + * hints for ART-2a-Euclid clustering with minimal additional memory allocation + * or maximum speed below. + *

+ * Note: For clustering of the SAME data with DIFFERENT vigilance parameters use + * method getClusterResults() where the mode of calculation may be specified to + * be sequential or concurrent (parallelized). + *

+ * All numerical calculations are performed in single (float) precision. + *

+ * Note, that aDataMatrix may contain data vectors with all components being + * equal to zero (or some constant minimal value). These data vectors are + * removed from the clustering process and their indices are returned by method + * getZeroLengthDataVectorIndices() of an Art2aEuclidResult object. + *

+ * ART-2a-Euclid clustering with minimal memory allocation: + * If a data matrix with N data row vectors is used to construct a clustering + * instance without preprocessing (parameter isDataPreprocessing is set to + * false), minimal additional memory is allocated. The data matrix itself is not + * changed. The additional allocated memory can be controlled by the + * maximumNumberOfClusters parameter and estimated to be about + * (additional memory of ART-2a-Euclid instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 2 MByte of additionally + * allocated memory. Note, that memory for cluster vectors is only allocated if + * needed, e.g. if specified parameter maximumNumberOfClusters allows 150 + * clusters but only 27 are needed, then only memory for these 27 cluster + * vectors is allocated. The minimal memory allocation comes at the expense of + * clustering speed since preprocessing steps have to be executed repeatedly. + * This also decreases the performance of some methods of the Art2aEuclidResult + * object generated by the clustering process, e.g. getClusterRepresentatives(). + *

+ * ART-2a-Euclid clustering with maximum speed: + * If parameter isDataPreprocessing is set to true, preprocessing steps are + * calculated in advance for maximum clustering speed (as well as maximum speed + * of the Art2aResult methods). This requires an additional memory allocation + * for the preprocessed data for an ART-2a-Euclid clustering instance: + * (additional memory of ART-2a instance) = + * (1 + 2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 12 MByte of additionally + * allocated memory. + *

+ * CAUTION: Construction of several ART-2a-Euclid clustering instances with the + * SAME data matrix PLUS preprocessing is NOT advised due to the significant + * memory consumption of each instance. In this case, the data matrix should be + * checked with static method Art2aKernel.isDataMatrixValid() and then a priori + * converted into a preprocessed Art2aEuclidData object with static method + * Art2aEuclidKernel.getArt2aEuclidData(). The generated Art2aData object does + * NOT change or refer to the data matrix so that the data matrix memory could + * be released after conversion (by setting the data matrix object to null). + * The generated Art2aEuclidData object has additionally allocated about the + * same memory as the original data matrix, e.g., a 10 MByte data matrix is + * converted into a roughly 10 MByte Art2aData object. But this single + * Art2aEuclidData object can now be used to construct several ART-2a-Euclid + * clustering instances (Art2aEuclidKernel instances or Art2aEuclidTask + * instances for concurrent (parallelized) execution) where each of these + * ART-2a-Euclid clustering instances (and their generated Art2aEuclidResult + * object methods) performs with maximum speed and allocates only the minimal + * additional memory of + * (additional memory of ART-2a instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., for 9 constructed ART-2a-Euclid clustering instances for concurrent + * execution only 18 MBytes of additional memory are allocated in total. + * Compare this total additional allocated memory of only 10 + 18 = 28 MByte + * for an Art2aEuclidData object plus 9 ART-2a-Euclid clustering instances with + * the alternative 9 x 12 = 108 MByte of memory for 9 ART-2a-Euclid clustering + * instances constructed with the same data matrix plus independent + * preprocessing in each instance! (Just for completeness: For a minimal memory + * realization of these 9 ART-2a-Euclid clustering instances, each instance can + * be constructed with the same data matrix WITHOUT preprocessing, which would + * require only 18 MBytes of additional allocated memory in total.) + * + * @author Achim Zielesny + */ +public class Art2aEuclidKernel { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aEuclidKernel.class.getName()); + // + // + /** + * Value 1.0 + */ + private static final float ONE = 1.0f; + /** + * Default fraction of the (maximum) number of clusters relative to + * number of data vectors + */ + private static final float DEFAULT_FRACTION_OF_CLUSTERS = 0.2f; + /** + * Default seed value for random number generator + */ + private static final long DEFAULT_RANDOM_SEED = 1L; + /** + * Default maximum number of epochs + */ + private static final int DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS = 10; + /** + * Default value for the learning parameter + */ + private static final float DEFAULT_LEARNING_PARAMETER = 0.01f; + /** + * Default offset for contrast enhancement + */ + private static final float DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT = 1.0f; + /** + * Default value of the convergence threshold for cluster centroid + * distance + */ + private static final float DEFAULT_CONVERGENCE_THRESHOLD = 0.1f; + // + // + /** + * Maximum number of clusters in interval [2, number of data row vectors of getDataMatrix] + */ + private final int maximumNumberOfClusters; + /** + * Maximum number of epochs for training + */ + private final int maximumNumberOfEpochs; + /** + * Convergence threshold for cluster centroid distance + */ + private final float convergenceThreshold; + /** + * Learning parameter in interval (0,1) + */ + private final float learningParameter; + /** + * Random seed value + */ + private final long randomSeed; + /** + * Art2aEuclidData data object + */ + private final Art2aEuclidData art2aEuclidData; + // + // + /** + * Helper callable for a single getClusterResult() calculation task of + * Art2aEuclidKernel with a distinct vigilance parameter. + *

+ * Note: No checks are performed. + */ + private static class HelperTask implements Callable { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(HelperTask.class.getName()); + // + // + /** + * Art2aEuclidKernel + */ + private final Art2aEuclidKernel art2aKernel; + /** + * Vigilance parameter + */ + private final float vigilance; + // + + // + /** + * Constructor + */ + protected HelperTask( + Art2aEuclidKernel anArt2aEuclidKernel, + float aVigilance + ) { + this.art2aKernel = anArt2aEuclidKernel; + this.vigilance = aVigilance; + } + // + + // + /** + * Performs single getClusterResult() calculation task. + * + * @return Art2aEuclidResult or null if getClusterResult() calculation task + could not be performed. + */ + @Override + public Art2aEuclidResult call() { + try { + return this.art2aKernel.getClusterResult(this.vigilance); + } catch (Exception anException) { + HelperTask.LOGGER.log( + Level.SEVERE, + "SingleTask.call: Can not calculate a cluster result." + ); + HelperTask.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + return null; + } + } + // + + } + //
+ + // + /** + * Constructor. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * distance (must be greater zero) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * Otherwise. + * @throws IllegalArgumentException Thrown if an argument is illegal + * + */ + public Art2aEuclidKernel( + float[][] aDataMatrix, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + float anOffsetForContrastEnhancement, + long aRandomSeed, + boolean anIsDataPreprocessing + ) throws IllegalArgumentException { + // + if(!Art2aEuclidKernel.isDataMatrixValid(aDataMatrix)) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aDataMatrix is not valid." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aDataMatrix is not valid."); + } + if(aMaximumNumberOfEpochs <= 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); + } + if(aConvergenceThreshold <= 0.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero."); + } + if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)."); + } + if(anOffsetForContrastEnhancement <= 0.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: anOffsetForContrastEnhancement must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: anOffsetForContrastEnhancement must be greater zero."); + } + if(aRandomSeed <= 0L) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aRandomSeed must be greater 0." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aRandomSeed must be greater/equal 0."); + } + // + + if(anIsDataPreprocessing) { + this.art2aEuclidData = + Art2aEuclidKernel.getArt2aEuclidData( + aDataMatrix, + anOffsetForContrastEnhancement + ); + } else { + this.art2aEuclidData = + new Art2aEuclidData( + aDataMatrix, + Art2aEuclidUtils.getMinMaxComponents(aDataMatrix), + anOffsetForContrastEnhancement + ); + } + + this.maximumNumberOfClusters = aMaximumNumberOfClusters; + this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; + this.convergenceThreshold = aConvergenceThreshold; + this.learningParameter = aLearningParameter; + this.randomSeed = aRandomSeed; + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * (= 0.5) and RANDOM_SEED (= 1). + * Note: There is NO data preprocessing. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aEuclidKernel( + float[][] aDataMatrix + ) throws IllegalArgumentException { + this( + aDataMatrix, + (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? + (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) : + 2, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_LEARNING_PARAMETER, + DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT, + DEFAULT_RANDOM_SEED, + false + ); + } + + /** + * Constructor. + * + * @param anArt2aEuclidData ART-2a-Euclid data object created by method + * Art2aEuclidKernel.getArt2aEuclidData() + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * distance (must be greater zero) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + public Art2aEuclidKernel( + Art2aEuclidData anArt2aEuclidData, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + long aRandomSeed + ) throws IllegalArgumentException { + // + if(anArt2aEuclidData == null) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: anArt2aEuclidData is null." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: anArt2aEuclidData is null."); + } + if(aMaximumNumberOfEpochs <= 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); + } + if(aConvergenceThreshold <= 0.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero."); + } + if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)."); + } + if(aRandomSeed <= 0L) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aRandomSeed must be greater 0." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aRandomSeed must be greater/equal 0."); + } + // + + this.art2aEuclidData = anArt2aEuclidData; + this.maximumNumberOfClusters = aMaximumNumberOfClusters; + this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; + this.convergenceThreshold = aConvergenceThreshold; + this.learningParameter = aLearningParameter; + this.randomSeed = aRandomSeed; + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), + * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). + * + * @param anArt2aEuclidData ART-2a-Euclid data object created by method + * Art2aEuclidKernel.getArt2aEuclidData() + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aEuclidKernel( + Art2aEuclidData anArt2aEuclidData + ) throws IllegalArgumentException { + this( + anArt2aEuclidData, + (int) (anArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? + (int) (anArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) : + 2, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_LEARNING_PARAMETER, + DEFAULT_RANDOM_SEED + ); + } + // + + // + /** + * Performs ART-2a-Euclid clustering and returns corresponding + * Art2aEuclidResult. + * + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @return Art2aEuclidResult instance + * @throws IllegalArgumentException Thrown if argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public Art2aEuclidResult getClusterResult( + float aVigilance + ) throws Exception { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResult: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResult: aVigilance must be in interval (0,1)."); + } + // + + try { + Random tmpRandomNumberGenerator = new Random(this.randomSeed); + boolean tmpIsClusterOverflow = false; + + float[][] tmpDataMatrix = null; + float[][] tmpContrastEnhancedMatrix = null; + // Flags array that indicates if data row vectors have a length + // of zero (i.e. where all components are equal to zero). True: + // Data row vector has a length of zero, false: Otherwise. + boolean[] tmpDataVectorZeroLengthFlags = null; + int tmpNumberOfComponents = -1; + int tmpNumberOfDataVectors = -1; + if (this.art2aEuclidData.hasPreprocessedData()) { + tmpContrastEnhancedMatrix = (float[][]) this.art2aEuclidData.getContrastEnhancedMatrix(); + tmpDataVectorZeroLengthFlags = (boolean[]) this.art2aEuclidData.getDataVectorZeroLengthFlags(); + tmpNumberOfDataVectors = tmpContrastEnhancedMatrix.length; + tmpNumberOfComponents = tmpContrastEnhancedMatrix[0].length; + } else { + tmpDataMatrix = this.art2aEuclidData.getDataMatrix(); + tmpDataVectorZeroLengthFlags = new boolean[tmpDataMatrix.length]; + Art2aEuclidUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + tmpNumberOfDataVectors = tmpDataMatrix.length; + tmpNumberOfComponents = tmpDataMatrix[0].length; + } + Art2aEuclidUtils.MinMaxValue[] tmpMinMaxComponents = this.art2aEuclidData.getMinMaxComponentsOfDataMatrix(); + + // Set tmpRhoStar + float tmpRhoStar = tmpNumberOfComponents * (ONE - aVigilance); + + // Definitions + float tmpThresholdForContrastEnhancement = + Art2aEuclidUtils.getThresholdForContrastEnhancement( + tmpNumberOfComponents, + this.art2aEuclidData.getOffsetForContrastEnhancement() + ); + // Scaling factor alpha + float tmpScalingFactor = tmpThresholdForContrastEnhancement; + + // Initialize cluster matrix and that for previous epoch (old) with + // all row vectors being null + float[][] tmpClusterMatrix = new float[this.maximumNumberOfClusters][]; + float[][] tmpClusterMatrixOld = new float[this.maximumNumberOfClusters][]; + // Cluster usage flags. True: Cluster is used, false: Cluster is + // empty and can be removed. + boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; + + // Initialize cluster indices for data row vectors with -1 to + // indicate missing cluster assignment + int[] tmpClusterIndexOfDataVector = new int[tmpNumberOfDataVectors]; + Art2aEuclidUtils.fillVector(tmpClusterIndexOfDataVector, -1); + + // Initialize random indices + int[] tmpRandomIndices = new int[tmpNumberOfDataVectors]; + for(int i = 0; i < tmpRandomIndices.length; i++) { + tmpRandomIndices[i] = i; + } + + // Initialize buffer vector for vector operations + float[] tmpBufferVector = new float[tmpNumberOfComponents]; + + // Main clustering loop + int tmpCurrentNumberOfEpochs = 0; + int tmpNumberOfDetectedClusters = 0; + Art2aEuclidUtils.RhoWinner tmpRhoWinner = new Art2aEuclidUtils.RhoWinner(); + Art2aEuclidUtils.ClusterRemovalInfo tmpClusterRemovalInfo = new Art2aEuclidUtils.ClusterRemovalInfo(); + boolean tmpIsConverged = false; + while(!tmpIsConverged && tmpCurrentNumberOfEpochs < this.maximumNumberOfEpochs) { + tmpCurrentNumberOfEpochs++; + + // Get random sequence of indices for data row vectors + Art2aEuclidUtils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); + + Arrays.fill(tmpClusterUsageFlags, false); + for(int i = 0; i < tmpNumberOfDataVectors; i++) { + int tmpRandomIndex = tmpRandomIndices[i]; + + if (tmpDataVectorZeroLengthFlags[tmpRandomIndex]) { + // Shifted data row vector has length of zero: Ignore! + continue; + } + + if (this.art2aEuclidData.hasPreprocessedData()) { + Art2aEuclidUtils.copyVector(tmpContrastEnhancedMatrix[tmpRandomIndex], tmpBufferVector); + } else { + tmpDataVectorZeroLengthFlags[tmpRandomIndex] = + Art2aEuclidUtils.setContrastEnhancedVector( + tmpDataMatrix[tmpRandomIndex], + tmpBufferVector, + tmpMinMaxComponents, + tmpThresholdForContrastEnhancement + ); + if (tmpDataVectorZeroLengthFlags[tmpRandomIndex]) { + continue; + } + } + + if(tmpNumberOfDetectedClusters == 0) { + // Create first cluster + Art2aEuclidUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; + tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; + tmpNumberOfDetectedClusters++; + } else { + // Cluster number is greater than or equal to 1 + Art2aEuclidUtils.setRhoWinner( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoWinner + ); + // Assign to existing cluster or increment clusters + if(tmpRhoWinner.getIndexOfCluster() < 0 || tmpRhoWinner.getRhoValue() > tmpRhoStar) { + // Increment clusters (if possible) + if (tmpNumberOfDetectedClusters == this.maximumNumberOfClusters) { + tmpIsClusterOverflow = true; + } else { + // Increment clusters + Art2aEuclidUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; + tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; + tmpNumberOfDetectedClusters++; + } + } else { + // Assign to existing winner cluster with modification + // Note: tmpBufferVector (= contrast enhanced unit vector) + // is used for modification + Art2aEuclidUtils.modifyWinnerCluster( + tmpBufferVector, + tmpClusterMatrix[tmpRhoWinner.getIndexOfCluster()], + tmpThresholdForContrastEnhancement, + this.learningParameter + ); + tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpRhoWinner.getIndexOfCluster(); + tmpClusterUsageFlags[tmpRhoWinner.getIndexOfCluster()] = true; + } + } + } + + Art2aEuclidUtils.removeEmptyClusters( + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpClusterRemovalInfo + ); + if (tmpClusterRemovalInfo.isClusterRemoved()) { + tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); + tmpIsConverged = false; + } else { + tmpIsConverged = + Art2aEuclidUtils.isConverged( + tmpNumberOfDetectedClusters, + tmpCurrentNumberOfEpochs, + tmpClusterMatrix, + tmpClusterMatrixOld, + this.maximumNumberOfEpochs, + this.convergenceThreshold + ); + } + } + // Check if cluster overflow occurred + if (tmpIsClusterOverflow) { + // Cluster overflow occurred: Finally assign ALL data vectors + Art2aEuclidUtils.assignDataVectorsToClusters( + tmpNumberOfDetectedClusters, + tmpDataVectorZeroLengthFlags, + this.art2aEuclidData, + tmpBufferVector, + tmpThresholdForContrastEnhancement, + tmpClusterMatrix, + tmpClusterIndexOfDataVector, + tmpClusterUsageFlags + ); + // Remove possible empty clusters + Art2aEuclidUtils.removeEmptyClusters( + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpClusterRemovalInfo + ); + tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); + } + // Check if clusters were removed in last epoch and assure non-empty + // clusters in the cluster matrix + while (tmpClusterRemovalInfo.isClusterRemoved()) { + // Empty clusters are removed: Assign data vectors again + Art2aEuclidUtils.assignDataVectorsToClusters( + tmpNumberOfDetectedClusters, + tmpDataVectorZeroLengthFlags, + this.art2aEuclidData, + tmpBufferVector, + tmpThresholdForContrastEnhancement, + tmpClusterMatrix, + tmpClusterIndexOfDataVector, + tmpClusterUsageFlags + ); + Art2aEuclidUtils.removeEmptyClusters( + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpClusterRemovalInfo + ); + tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); + } + return new Art2aEuclidResult( + aVigilance, + tmpThresholdForContrastEnhancement, + tmpCurrentNumberOfEpochs, + tmpNumberOfDetectedClusters, + tmpClusterIndexOfDataVector, + tmpClusterMatrix, + tmpDataVectorZeroLengthFlags, + tmpIsClusterOverflow, + tmpIsConverged, + this.art2aEuclidData + ); + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResult: An exception occurred: This should never happen!" + ); + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + throw anException; + } + } + + /** + * Performs ART-2a-Euclid clustering for specified vigilance parameters and + * returns corresponding Art2aEuclidResult objects. + * + * @param aVigilances Vigilance parameters (must each be in interval (0,1)) + * @return Art2aEuclidResult objects or null if clustering result could + * not be calculated. + * @param aNumberOfConcurrentCalculationThreads Number of concurrent + * calculation threads for the different vigilance parameters to be + * calculated concurrently (in parallel). If zero, then the different + * vigilance parameters are calculated one after another (sequentially) + * @throws IllegalArgumentException Thrown if argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public Art2aEuclidResult[] getClusterResults( + float[] aVigilances, + int aNumberOfConcurrentCalculationThreads + ) throws Exception { + // + if (aVigilances == null || aVigilances.length == 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: aVigilances is null or has length 0." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: aVigilances is null or has length 0."); + } + for (float tmpVigilance : aVigilances) { + if(tmpVigilance <= 0.0f || tmpVigilance >= 1.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: Vigilance parameter must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: Vigilance parameter must be in interval (0,1)."); + } + } + if (aNumberOfConcurrentCalculationThreads < 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0."); + } + // + + if (aNumberOfConcurrentCalculationThreads > 0) { + LinkedList tmpSingleTaskList = new LinkedList<>(); + for (float tmpVigilance : aVigilances) { + tmpSingleTaskList.add(new HelperTask(this, tmpVigilance)); + } + ExecutorService tmpExecutorService = Executors.newFixedThreadPool(aNumberOfConcurrentCalculationThreads); + List> tmpFutureList = null; + try { + tmpFutureList = tmpExecutorService.invokeAll(tmpSingleTaskList); + } catch (InterruptedException anInterruptedException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: Interrupted exception during concurrent calculation: This should never happen." + ); + throw anInterruptedException; + } + tmpExecutorService.shutdown(); + Art2aEuclidResult[] tmpParallelResults = new Art2aEuclidResult[aVigilances.length]; + int tmpIndex = 0; + for (Future tmpFuture : tmpFutureList) { + try { + tmpParallelResults[tmpIndex++] = tmpFuture.get(); + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: Exception in tmpFuture.get()." + ); + throw anException; + } + } + return tmpParallelResults; + } else { + Art2aEuclidResult[] tmpSequentialResults = new Art2aEuclidResult[aVigilances.length]; + for (int i = 0; i < aVigilances.length; i++) { + tmpSequentialResults[i] = this.getClusterResult(aVigilances[i]); + } + return tmpSequentialResults; + } + } + + /** + * Nearest (smaller) indices of approximants to the desired number of + * representatives. + * + * @param aNumberOfRepresentatives Number of representatives (MUST be + * greater or equal to 2) + * @param aVigilanceMin Minimal vigilance parameter (must be in interval + * (0,1)) + * @param aVigilanceMax Maximal vigilance parameter (must be in interval + * (0,1)) + * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or + * equal to 1) + * @return Nearest (smaller) indices of approximants to the desired number + * of representatives. + * @throws IllegalArgumentException Thrown if an argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public int[] getRepresentatives( + int aNumberOfRepresentatives, + float aVigilanceMin, + float aVigilanceMax, + int aNumberOfTrialSteps + ) throws IllegalArgumentException, Exception { + // + if(aNumberOfRepresentatives < 2) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2."); + } + if(aVigilanceMin <= 0.0f || aVigilanceMin >= 1.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)."); + } + if(aVigilanceMax <= 0.0f || aVigilanceMax >= 1.0f) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)."); + } + if(aVigilanceMin >= aVigilanceMax) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax."); + } + if(aNumberOfTrialSteps < 1) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1."); + } + // + + try { + Art2aEuclidResult tmpArt2aEuclidResult = this.getClusterResult(aVigilanceMin); + int[] tmpRepresentativeIndicesOfClusters = tmpArt2aEuclidResult.getRepresentativeIndicesOfClusters(); + if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { + return tmpRepresentativeIndicesOfClusters; + } + tmpArt2aEuclidResult = this.getClusterResult(aVigilanceMax); + if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { + return tmpArt2aEuclidResult.getRepresentativeIndicesOfClusters(); + } + + float tmpVigilanceMin = aVigilanceMin; + float tmpVigilanceMax = aVigilanceMax; + for (int i = 0; i < aNumberOfTrialSteps; i++) { + float tmpVigilanceMean = (tmpVigilanceMin + tmpVigilanceMax) / 2.0f; + tmpArt2aEuclidResult = this.getClusterResult(tmpVigilanceMean); + if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { + tmpVigilanceMax = tmpVigilanceMean; + } else if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { + tmpVigilanceMin = tmpVigilanceMean; + tmpRepresentativeIndicesOfClusters = tmpArt2aEuclidResult.getRepresentativeIndicesOfClusters(); + } else { + return tmpArt2aEuclidResult.getRepresentativeIndicesOfClusters(); + } + } + return tmpRepresentativeIndicesOfClusters; + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getRepresentatives: An exception occurred: This should never happen!" + ); + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + throw anException; + } + } + + /** + * Note: This is a purely experimental nonsense method. + * + * Returns representatives whose mean distance is nearest to the mean + * distance of all data vectors of specified original data matrix. + * Note: This is a O(N^2) operation, N: Number of data vectors. + * + * @param aDataMatrix Original data matrix (IS NOT CHANGED and NOT properly + * CHECKED) + * @param aMinimumNumberOfRepresentatives Minimum number of representatives + * @param aMaximumNumberOfRepresentatives Maximum number of representatives + * @return Representatives whose mean distance is nearest to the mean + * distance of all data vectors of specified original data matrix. + * @throws IllegalArgumentException Thrown if an argument is illegal + * @throws Exception Thrown if exception occurs which should never happen + */ + public int[] getBestRepresentatives( + float[][] aDataMatrix, + int aMinimumNumberOfRepresentatives, + int aMaximumNumberOfRepresentatives + ) throws IllegalArgumentException, Exception { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getBestRepresentatives: aDataMatrix is null/has length zero." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getBestRepresentatives: aDataMatrix is null/has length zero."); + } + if(aMinimumNumberOfRepresentatives < 2 || aMinimumNumberOfRepresentatives > aDataMatrix.length - 1) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid."); + } + if(aMaximumNumberOfRepresentatives <= aMinimumNumberOfRepresentatives || aMaximumNumberOfRepresentatives > aDataMatrix.length) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid."); + } + // + + try { + int[] tmpAllIndices = new int[aDataMatrix.length]; + for (int i = 0; i < tmpAllIndices.length; i++) { + tmpAllIndices[i] = i; + } + float tmpBaseMeanDistance = Art2aEuclidUtils.getMeanDistance(aDataMatrix, tmpAllIndices); + + float tmpVigilanceMin = 0.0001f; + float tmpVigilanceMax = 0.9999f; + int tmpNumberOfTrialSteps = 32; + + float tmpMinimalDifference = Float.MAX_VALUE; + int[] tmpBestRepresentatives = null; + for (int i = aMinimumNumberOfRepresentatives; i < aMaximumNumberOfRepresentatives; i++) { + int[] tmpRepresentatives = + this.getRepresentatives( + i, + tmpVigilanceMin, + tmpVigilanceMax, + tmpNumberOfTrialSteps + ); + float tmpMeanDistance = Art2aEuclidUtils.getMeanDistance(aDataMatrix, tmpRepresentatives); + float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); + if (tmpDifference < tmpMinimalDifference) { + tmpMinimalDifference = tmpDifference; + tmpBestRepresentatives = tmpRepresentatives; + } + } + return tmpBestRepresentatives; + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getBestRepresentatives: An exception occurred: This should never happen!" + ); + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + throw anException; + } + } + // + // + /** + * Checks if aDataMatrix is valid. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @return True if aDataMatrix is valid, false otherwise. + */ + public static boolean isDataMatrixValid( + float[][] aDataMatrix + ) { + if(aDataMatrix == null || aDataMatrix.length == 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.isDataMatrixValid: aDataMatrixis is null or empty." + ); + return false; + } + + int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; + if(tmpNumberOfDataVectorComponents < 2) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.isDataMatrixValid: Data row vectors must have at least 2 components." + ); + return false; + } + + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.isDataMatrixValid: A data row vector of aDataMatrix is not allowed to be null or empty." + ); + return false; + } + + if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.isDataMatrixValid: Data row vectors in aDataMatrix must have the same length." + ); + return false; + } + } + if (Art2aEuclidUtils.hasNonFiniteComponent(aDataMatrix)) { + return false; + } + return true; + } + + /** + * Removes columns from data matrix with non-finite components. + * Note: If aDataMatrix is null, empty or has an invalid structure + * nothing is done and false is returned. + * + * @param aDataMatrix Data matrix with data row vectors (MAY BE CHANGED) + * @return True if aDataMatrix was changed (i.e. column removal was + * performed), false otherwise (i.e. data matrix is unchanged). + */ + public static boolean isNonFiniteComponentRemoval( + float[][] aDataMatrix + ) { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + + int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; + if(tmpNumberOfDataVectorComponents < 2) { + return false; + } + + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + + if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + return false; + } + } + // + + boolean tmpHasNonFiniteComponent = Art2aEuclidUtils.hasNonFiniteComponent(aDataMatrix); + if (tmpHasNonFiniteComponent) { + // TODO: Remove columns with non-finite components + } + return tmpHasNonFiniteComponent; + } + + /** + * Creates ART-2a-Euclid data object with preprocessed data for maximum + * speed of the clustering process. The ART-2a-Euclid data object allocates + * about the same memory as aDataMatrix. + *
+ * Note: There a no checks! Check aDataMatrix in advance with method + * Art2aEuclidKernel.isDataMatrixValid(). + *
+ * Note: aDataMatrix could be set to null after this operation to release + * its memory. + + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * with Art2aEuclidKernel.isDataMatrixValid() in advance) + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @return ART-2a-Euclid data object for maximum clustering speed but with + * additionally allocated memory (about the same memory as aDataMatrix) + */ + public static Art2aEuclidData getArt2aEuclidData( + float[][] aDataMatrix, + float anOffsetForContrastEnhancement + ) { + int tmpNumberOfComponents = aDataMatrix[0].length; + float tmpThresholdForContrastEnhancement = + Art2aEuclidUtils.getThresholdForContrastEnhancement( + tmpNumberOfComponents, + anOffsetForContrastEnhancement + ); + + // Initialize flags array for scaled data row vectors which have a + // length of zero (i.e. where all components are equal to zero) + boolean[] tmpDataVectorZeroLengthFlags = new boolean[aDataMatrix.length]; + Art2aEuclidUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + + float[][] tmpContrastEnhancedMatrix = new float[aDataMatrix.length][]; + + Art2aEuclidUtils.MinMaxValue[] tmpMinMaxComponents = Art2aEuclidUtils.getMinMaxComponents(aDataMatrix); + + for(int i = 0; i < aDataMatrix.length; i++) { + float[] tmpContrastEnhancedVector = new float[tmpNumberOfComponents]; + tmpDataVectorZeroLengthFlags[i] = + Art2aEuclidUtils.setContrastEnhancedVector( + aDataMatrix[i], + tmpContrastEnhancedVector, + tmpMinMaxComponents, + tmpThresholdForContrastEnhancement + ); + tmpContrastEnhancedMatrix[i] = tmpContrastEnhancedVector; + } + return new Art2aEuclidData( + tmpContrastEnhancedMatrix, + tmpDataVectorZeroLengthFlags, + tmpMinMaxComponents, + anOffsetForContrastEnhancement + ); + } + + /** + * Creates ART-2a-Euclid data object with preprocessed data for maximum + * speed of the clustering process. The ART-2a-Euclid data object allocates + * about twice the memory of aDataMatrix. A default value of 1.0 is used + * for the offset for contrast enhancement. + *
+ * Note: aDataMatrix could be set to null after this operation to release + * its memory. + + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * with Art2aEuclidKernel.isDataMatrixValid() in advance) + * @return ART-2a-Euclid data object for maximum clustering speed but with + * additionally allocated memory (about the same memory as aDataMatrix) + */ + public static Art2aEuclidData getArt2aEuclidData( + float[][] aDataMatrix + ) { + return Art2aEuclidKernel.getArt2aEuclidData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); + } + //
+ +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java new file mode 100644 index 0000000..44b93b0 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java @@ -0,0 +1,522 @@ +/* + * ART-2a-Euclid Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Result of an ART-2a-Euclid clustering process. + *

+ * Note: Art2aEuclidResult is a read-only class, i.e. thread-safe. In addition, + * there are NO internal calculated values cached, i.e. each method call + * performs a full calculation procedure. An Art2aEuclidResult object may be + * distributed to several concurrent (parallelized) evaluation tasks without + * any mutual interference problems. + * + * @author Betuel Sevindik, Achim Zielesny + */ +public class Art2aEuclidResult { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aEuclidResult.class.getName()); + // + // + /** + * Conversion constant from radiant to degree + */ + private static final float CONVERSION_TO_DEGREE = 180.0f / (float) Math.PI; + // + // + /** + * Cluster index of data vector + */ + private final int[] clusterIndexOfDataVector; + /** + * Vigilance parameter + */ + private final float vigilance; + /** + * Threshold for contrast enhancement + */ + private final float thresholdForContrastEnhancement; + /** + * Number of epochs + */ + private final int numberOfEpochs; + /** + * Number of detected clusters + */ + private final int numberOfDetectedClusters; + /** + * Cluster matrix + */ + private final float[][] clusterMatrix; + /** + * Array with flags. True: Scaled data vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise + */ + private final boolean[] dataVectorZeroLengthFlags; + /** + * True: Cluster overflow occurred, false: Otherwise + */ + private final boolean isClusterOverflow; + /** + * True: Clustering process converged, false: Otherwise + */ + private final boolean isConverged; + /** + * Art2aEuclidData object + */ + private final Art2aEuclidData art2aData; + // + // + /** + * Indexed value + */ + private record IndexedValue ( + int index, + float value + ) implements Comparable { + + /** + * Constructor + * + * @param index Index + * @param value Value + */ + public IndexedValue {} + + @Override + public int compareTo(IndexedValue anotherIndexedValue) { + return Float.compare(value, anotherIndexedValue.value()); + } + } + // + + // + /** + * Constructor. + * Note: No checks are performed. + * + * @param aVigilance Vigilance parameter in interval (0,1) + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @param aNumberOfEpochs Number of epochs used for clustering + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterIndexOfDataVector Cluster index of data vector + * @param aClusterMatrix Cluster matrix + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param anIsClusterOverflow True: Cluster overflow occurred, false: + * Otherwise + * @param anIsConverged True: Clustering process converged, false: Otherwise + * @param anArt2aEuclidData Art2aEuclidData instance + */ + public Art2aEuclidResult( + float aVigilance, + float aThresholdForContrastEnhancement, + int aNumberOfEpochs, + int aNumberOfDetectedClusters, + int[] aClusterIndexOfDataVector, + float[][] aClusterMatrix, + boolean[] aDataVectorZeroLengthFlags, + boolean anIsClusterOverflow, + boolean anIsConverged, + Art2aEuclidData anArt2aEuclidData + ) { + this.vigilance = aVigilance; + this.thresholdForContrastEnhancement = aThresholdForContrastEnhancement; + this.numberOfEpochs = aNumberOfEpochs; + this.numberOfDetectedClusters = aNumberOfDetectedClusters; + this.clusterIndexOfDataVector = aClusterIndexOfDataVector; + this.clusterMatrix = aClusterMatrix; + this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; + this.isClusterOverflow = anIsClusterOverflow; + this.isConverged = anIsConverged; + this.art2aData = anArt2aEuclidData; + } + // + + // + /** + * Returns specified cluster vector with index aClusterIndex in + * clusterMatrix. + * + * @param aClusterIndex Index of cluster vector in clusterMatrix + * @return Specified cluster vector + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public float[] getClusterVector( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getClusterVector: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidResult.getClusterVector: aClusterIndex is illegal."); + } + // + return this.clusterMatrix[aClusterIndex]; + } + + /** + * Returns specified cluster vector with index aClusterIndex in + * cluster matrix with components being scaled to interval [0,1]. + * Note: Cluster matrix is NOT changed. + * + * @param aClusterIndex Index of cluster vector in cluster matrix + * @return Specified scaled cluster vector + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public float[] getScaledClusterVector( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getClusterVector: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidResult.getClusterVector: aClusterIndex is illegal."); + } + // + return Art2aEuclidUtils.getScaledVector(this.clusterMatrix[aClusterIndex]); + } + + /** + * Returns indices of data vectors in original data matrix that belong to + * the specified cluster with index aClusterIndex. + * Note: The returned indices are cached for successive fast usage. + * + * @param aClusterIndex Index of cluster in cluster matrix + * @return Indices of data vectors in original data matrix that belong to + * the specified cluster with index aClusterIndex. + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public int[] getDataVectorIndicesOfCluster( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("rt2aClusteringResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal."); + } + // + + LinkedList tmpIndexListOfCluster = new LinkedList<>(); + for (int i = 0; i < this.clusterIndexOfDataVector.length; i++) { + if (this.clusterIndexOfDataVector[i] == aClusterIndex) { + tmpIndexListOfCluster.add(i); + } + } + return tmpIndexListOfCluster.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Returns all indices of (scaled) data vectors that have a length of + * zero. The indices refer to the original data matrix. + * Note: The returned indices are cached for successive fast usage. + * + * @return All indices of (scaled) data vectors that have a length of + * zero. The indices refer to the original data matrix. + */ + public int[] getZeroLengthDataVectorIndices() { + LinkedList tmpIndexList = new LinkedList<>(); + for (int i = 0; i < this.dataVectorZeroLengthFlags.length; i++) { + if (this.dataVectorZeroLengthFlags[i]) { + tmpIndexList.add(i); + } + } + return tmpIndexList.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Return distance between specified clusters with aClusterIndex1 and + * aClusterIndex2. + * + * @param aClusterIndex1 Index of cluster 1 in cluster matrix + * @param aClusterIndex2 Index of cluster 2 in cluster matrix + * @return Distance between specified clusters with aClusterIndex1 and + * aClusterIndex2. + * @throws IllegalArgumentException Thrown if an argument is illegal. + */ + public float getDistanceBetweenClusters( + int aClusterIndex1, + int aClusterIndex2 + ) throws IllegalArgumentException { + // + if(aClusterIndex1 < 0 || aClusterIndex1 >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex1 is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex1 is illegal."); + } + if(aClusterIndex2 < 0 || aClusterIndex2 >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex2 is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex2 is illegal."); + } + // + + if (aClusterIndex1 == aClusterIndex2) { + return 0.0f; + } else { + return + (float) Math.sqrt( + Art2aEuclidUtils.getSquaredDistance( + this.clusterMatrix[aClusterIndex1], + this.clusterMatrix[aClusterIndex2] + ) + ); + } + } + + /** + * Returns size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the + * cluster. + * Note: The internally evaluated indices of data vectors that belong to the + * specified cluster are cached for successive fast usage. + * + * @param aClusterIndex Index of cluster in cluster matrix + * @return Size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the + * cluster. + * @throws IllegalArgumentException Thrown if argument is illegal. + */ + public int getClusterSize( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getClusterSize: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("rt2aClusteringResult.getClusterSize: aClusterIndex is illegal."); + } + // + + int tmpCounter = 0; + for (int i = 0; i < this.clusterIndexOfDataVector.length; i++) { + if (this.clusterIndexOfDataVector[i] == aClusterIndex) { + tmpCounter++; + } + } + return tmpCounter; + } + + /** + * Returns if cluster overflow occurred. + * + * @return True: Cluster overflow occurred, false: Otherwise + */ + public boolean isClusterOverflow() { + return this.isClusterOverflow; + } + + /** + * Returns if clustering process converged. + * + * @return True: Clustering process converged, false: Otherwise + */ + public boolean isConverged() { + return this.isConverged; + } + + /** + * Calculates index of representative data vector which is closest to the + * specified cluster vector with index aClusterIndex. + * + * @param aClusterIndex Index of cluster vector in cluster matrix + * @return Index of representative data vector which is closest to the + * specified cluster vector with index aClusterIndex + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public int getClusterRepresentativeIndex( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getClusterRepresentativeIndex: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidResult.getClusterRepresentativeIndex: aClusterIndex is illegal."); + } + // + int[] tmpDataVectorIndicesOfCluster = this.getDataVectorIndicesOfCluster(aClusterIndex); + if (tmpDataVectorIndicesOfCluster.length == 1) { + return tmpDataVectorIndicesOfCluster[0]; + } + float[] tmpClusterVector = this.clusterMatrix[aClusterIndex]; + int tmpBestIndex = 0; + float tmpMinimumDistance = Float.MAX_VALUE; + float[] tmpContrastEnhancedVector = null; + if (!this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedVector = new float[tmpClusterVector.length]; + } + for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { + int tmpIndex = tmpDataVectorIndicesOfCluster[i]; + if (this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedVector = this.art2aData.getContrastEnhancedMatrix()[tmpIndex]; + } else { + // Check of length is NOT necessary + Art2aEuclidUtils.setContrastEnhancedVector( + this.art2aData.getDataMatrix()[tmpIndex], + tmpContrastEnhancedVector, + this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.thresholdForContrastEnhancement + ); + } + float tmpSquaredDistance = Art2aEuclidUtils.getSquaredDistance(tmpContrastEnhancedVector, tmpClusterVector); + if (tmpSquaredDistance < tmpMinimumDistance) { + tmpBestIndex = tmpIndex; + tmpMinimumDistance = tmpSquaredDistance; + } + } + return tmpBestIndex; + } + + /** + * Calculates array of indices of sorted representative data vectors of the + * specified cluster with index aClusterIndex. The data vector with index 0 + * is closest to the cluster vector, the one with index 1 is the second + * closest etc. + * + * @param aClusterIndex Index of cluster vector in cluster matrix + * @return Array of indices of sorted representative data vectors of the + * specified cluster + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public int[] getClusterRepresentativeIndices( + int aClusterIndex + ) throws IllegalArgumentException { + // + if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { + Art2aEuclidResult.LOGGER.log( + Level.SEVERE, + "Art2aEuclidResult.getClusterRepresentativeIndices: aClusterIndex is illegal." + ); + throw new IllegalArgumentException("Art2aEuclidResult.getClusterRepresentativeIndices: aClusterIndex is illegal."); + } + // + int[] tmpDataVectorIndicesOfCluster = this.getDataVectorIndicesOfCluster(aClusterIndex); + if (tmpDataVectorIndicesOfCluster.length == 1) { + return tmpDataVectorIndicesOfCluster; + } + float[] tmpClusterVector = this.clusterMatrix[aClusterIndex]; + IndexedValue[] tmpIndexedValues = new IndexedValue[tmpDataVectorIndicesOfCluster.length]; + float[] tmpContrastEnhancedVector = null; + if (!this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedVector = new float[tmpClusterVector.length]; + } + for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { + int tmpIndex = tmpDataVectorIndicesOfCluster[i]; + if (this.art2aData.hasPreprocessedData()) { + tmpContrastEnhancedVector = this.art2aData.getContrastEnhancedMatrix()[tmpIndex]; + } else { + // Check of length is NOT necessary + Art2aEuclidUtils.setContrastEnhancedVector( + this.art2aData.getDataMatrix()[tmpIndex], + tmpContrastEnhancedVector, + this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.thresholdForContrastEnhancement + ); + } + tmpIndexedValues[i] = new IndexedValue(tmpIndex, Art2aEuclidUtils.getSquaredDistance(tmpContrastEnhancedVector, tmpClusterVector)); + } + // NOTE: SMALLEST squared distance FIRST! + Arrays.sort(tmpIndexedValues); + int[] tmpClusterRepresentativeIndices = new int[tmpIndexedValues.length]; + for (int i = 0; i < tmpIndexedValues.length; i++) { + tmpClusterRepresentativeIndices[i] = tmpIndexedValues[i].index(); + } + return tmpClusterRepresentativeIndices; + } + + /** + * Returns data vector indices which are closest to their cluster vectors. + * + * @return Data vector indices which are closest to their cluster vectors + */ + public int[] getRepresentativeIndicesOfClusters() { + int[] tmpRepresentativeIndicesOfClusters = new int[this.numberOfDetectedClusters]; + for (int i = 0; i < this.numberOfDetectedClusters; i++) { + tmpRepresentativeIndicesOfClusters[i] = this.getClusterRepresentativeIndex(i); + } + return tmpRepresentativeIndicesOfClusters; + } + + /** + * Vigilance parameter + * + * @return Vigilance parameter + */ + public float getVigilance() { + return this.vigilance; + }; + + /** + * Number of epochs + * + * @return Number of epochs + */ + public int getNumberOfEpochs() { + return this.numberOfEpochs; + }; + + /** + * Number of detected clusters + * + * @return Number of detected clusters + */ + public int getNumberOfDetectedClusters() { + return this.numberOfDetectedClusters; + }; + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java new file mode 100644 index 0000000..40d066a --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -0,0 +1,305 @@ +/* + * ART-2a-Euclid Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Callable that wraps an Art2aEuclidKernel instance where the call() method + * returns an Art2aEuclidResult object. See Art2aEuclidKernel for further + * details. + * + * @author Betuel Sevindik, Achim Zielesny + */ +public class Art2aEuclidTask implements Callable { + + // + /** + * Logger of this class + */ + private static final Logger LOGGER = Logger.getLogger(Art2aEuclidTask.class.getName()); + // + // + /** + * ART-2a-Euclid clustering kernel instance + */ + private final Art2aEuclidKernel art2aClusteringKernel; + /** + * Vigilance parameter (must be in interval (0,1)) + */ + private final float vigilance; + // + + // + /** + * Constructor. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * similarity (must be in interval (0,1)) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * Otherwise. + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + public Art2aEuclidTask( + float[][] aDataMatrix, + float aVigilance, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + float anOffsetForContrastEnhancement, + long aRandomSeed, + boolean anIsDataPreprocessing + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aEuclidKernel( + aDataMatrix, + aMaximumNumberOfClusters, + aMaximumNumberOfEpochs, + aConvergenceThreshold, + aLearningParameter, + anOffsetForContrastEnhancement, + aRandomSeed, + anIsDataPreprocessing + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." + ); + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * (= 0.5) and RANDOM_SEED (= 1). + * Note: There is NO data preprocessing. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aEuclidTask( + float[][] aDataMatrix, + float aVigilance + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aEuclidKernel( + aDataMatrix + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." + ); + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + + /** + * Constructor. + * + * @param anArt2aEuclidData ART-2a-Euclid data object created by method + * Art2aEuclidKernel.getArt2aEuclidData() + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * (must be greater zero) + * @param aConvergenceThreshold Convergence threshold for cluster centroid + * similarity (must be in interval (0,1)) + * @param aLearningParameter Learning parameter (must be in interval (0,1)) + * @param aRandomSeed Random seed value for random number generator + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + public Art2aEuclidTask( + Art2aEuclidData anArt2aEuclidData, + float aVigilance, + int aMaximumNumberOfClusters, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, + float aLearningParameter, + long aRandomSeed + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aEuclidKernel( + anArt2aEuclidData, + aMaximumNumberOfClusters, + aMaximumNumberOfEpochs, + aConvergenceThreshold, + aLearningParameter, + aRandomSeed + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." + ); + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + + /** + * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), + * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). + * + * @param anArt2aEuclidData ART-2a-Euclid data object created by method + * Art2aEuclidKernel.getArt2aEuclidData() + * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @throws IllegalArgumentException Thrown if argument is illegal + */ + public Art2aEuclidTask( + Art2aEuclidData anArt2aEuclidData, + float aVigilance + ) throws IllegalArgumentException { + // + if(aVigilance <= 0.0f || aVigilance >= 1.0f) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." + ); + throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); + } + // + this.vigilance = aVigilance; + + try { + this.art2aClusteringKernel = + new Art2aEuclidKernel( + anArt2aEuclidData + ); + } catch (IllegalArgumentException anIllegalArgumentException) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." + ); + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + anIllegalArgumentException.toString(), + anIllegalArgumentException + ); + throw anIllegalArgumentException; + } + } + // + + // + /** + * Performs the clustering process. + * + * @return Clustering result or null if clustering process could not be + * performed. + */ + @Override + public Art2aEuclidResult call() { + try { + return this.art2aClusteringKernel.getClusterResult(this.vigilance); + } catch (Exception anException) { + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + "Art2aEuclidTask.call: Can not calculate a cluster result." + ); + Art2aEuclidTask.LOGGER.log( + Level.SEVERE, + anException.toString(), + anException + ); + return null; + } + } + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java new file mode 100644 index 0000000..1fd3aa6 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java @@ -0,0 +1,925 @@ +/* + * ART-2a-Euclid Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Random; + +/** + * Library of helper records, static helper classes and static, thread-safe + * (stateless) utility methods for ART-2a-Euclid clustering. + *

+ * Note: No checks are performed. + * + * @author Achim Zielesny + */ +public class Art2aEuclidUtils { + + // + /** + * Value 1.0 + */ + private static final float ONE = 1.0f; + // + // + /** + * Helper record: Minimum and maximum value. + *

+ * Note: No checks are performed. + * + * @param minValue Minimum value + * @param maxValue Maximum value + */ + protected record MinMaxValue(float minValue, float maxValue) { + + /** + * Constructor + * + * @param minValue Minimum value + * @param maxValue Maximum value + */ + public MinMaxValue {} + + } + //
+ // + /** + * Helper class: Rho winner. + *

+ * Note: No checks are performed. + */ + protected static class RhoWinner { + + // + /** + * Rho value + */ + private float rhoValue; + /** + * Index of cluster + */ + private int indexOfCluster; + // + + // + /** + * Constructor + */ + protected RhoWinner() {} + // + + // + /** + * Set rho winner + * + * @param aRhoValue Rho value + * @param anIndexOfCluster Index of cluster + */ + protected void setRhoWinner( + float aRhoValue, + int anIndexOfCluster + ) { + this.rhoValue = aRhoValue; + this.indexOfCluster = anIndexOfCluster; + } + + /** + * Rho value + * + * @return Rho value + */ + protected float getRhoValue() { + return this.rhoValue; + } + + /** + * Index of cluster + * + * @return Index of cluster + */ + protected int getIndexOfCluster() { + return this.indexOfCluster; + } + // + + } + + /** + * Helper class: Cluster removal info. + *

+ * Note: No checks are performed. + */ + protected static class ClusterRemovalInfo { + + // + /** + * True: Cluster is removed, false: Otherwise + */ + private boolean isClusterRemoved; + /** + * Number of detected clusters + */ + private int numberOfDetectedClusters; + // + + // + /** + * Constructor + */ + protected ClusterRemovalInfo() {} + // + + // + /** + * Set cluster removal info + * + * @param anIsClusterRemoved True: Cluster is removed, false: Otherwise + * @param aNumberOfDetectedClusters Number of detected clusters + */ + protected void setClusterRemovalInfo( + boolean anIsClusterRemoved, + int aNumberOfDetectedClusters + ) { + this.isClusterRemoved = anIsClusterRemoved; + this.numberOfDetectedClusters = aNumberOfDetectedClusters; + } + + /** + * True: Cluster is removed, false: Otherwise + * + * @return True: Cluster is removed, false: Otherwise + */ + protected boolean isClusterRemoved() { + return this.isClusterRemoved; + } + + /** + * Number of detected clusters + * + * @return Number of detected clusters + */ + protected int getNumberOfDetectedClusters() { + return this.numberOfDetectedClusters; + } + // + + } + //
+ + // + /** + * Constructor + */ + protected Art2aEuclidUtils() {} + // + + // + /** + * Assigns data vectors to clusters + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param anArt2aEuclidData Art2aEuclidData instance (IS NOT CHANGED) + * @param aBufferVector Buffer vector (MUST BE ALREADY INSTANTIATED) + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aClusterIndexOfDataVector Cluster index of data vector (MAY BE + * CHANGED and MUST ALREADY BE INSTANTIATED) + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (MAY BE CHANGED and MUST + * ALREADY BE INSTANTIATED) + */ + protected static void assignDataVectorsToClusters( + int aNumberOfDetectedClusters, + boolean[] aDataVectorZeroLengthFlags, + Art2aEuclidData anArt2aEuclidData, + float[] aBufferVector, + float aThresholdForContrastEnhancement, + float[][]aClusterMatrix, + int[] aClusterIndexOfDataVector, + boolean[] aClusterUsageFlags + ) { + // Assign data vectors to clusters (last pass) + Arrays.fill(aClusterUsageFlags, false); + for (int i = 0; i < aDataVectorZeroLengthFlags.length; i++) { + if (!aDataVectorZeroLengthFlags[i]) { + if (anArt2aEuclidData.hasPreprocessedData()) { + aBufferVector = anArt2aEuclidData.getContrastEnhancedMatrix()[i]; + } else { + // Check of length is NOT necessary + Art2aEuclidUtils.setContrastEnhancedVector( + anArt2aEuclidData.getDataMatrix()[i], + aBufferVector, + anArt2aEuclidData.getMinMaxComponentsOfDataMatrix(), + aThresholdForContrastEnhancement + ); + } + int tmpWinnerClusterIndex = + Art2aEuclidUtils.getClusterIndex( + aBufferVector, + aNumberOfDetectedClusters, + aClusterMatrix + ); + aClusterIndexOfDataVector[i] = tmpWinnerClusterIndex; + aClusterUsageFlags[tmpWinnerClusterIndex] = true; + } + } + } + + /** + * (Deep) Copies source matrix to destination matrix. Row vectors of + * destination matrix may not have been instantiated. + * + * @param aSourceMatrix Source matrix (IS NOT CHANGED) + * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + */ + protected static void copyMatrix( + float[][] aSourceMatrix, + float[][] aDestinationMatrix + ) { + for (int i = 0; i < aSourceMatrix.length; i++) { + if (aDestinationMatrix[i] == null) { + aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; + } + System.arraycopy( + aSourceMatrix[i], + 0, + aDestinationMatrix[i], + 0, + aSourceMatrix[i].length + ); + } + } + + /** + * (Deep) Copies specified number of rows of source matrix to destination + * matrix. Row vectors of destination matrix may not have been instantiated. + * + * @param aSourceMatrix Source matrix (IS NOT CHANGED) + * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + * @param aNumberOfRows Number of rows to be copied from source matrix to + * destination matrix + */ + protected static void copyRows( + float[][] aSourceMatrix, + float[][] aDestinationMatrix, + int aNumberOfRows + ) { + for (int i = 0; i < aNumberOfRows; i++) { + if (aDestinationMatrix[i] == null) { + aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; + } + System.arraycopy( + aSourceMatrix[i], + 0, + aDestinationMatrix[i], + 0, + aSourceMatrix[i].length + ); + } + } + + /** + * (Deep) Copies source vector to destination vector. + * + * @param aSourceVector Source vector (IS NOT CHANGED) + * @param aDestinationVector Destination vector (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + */ + protected static void copyVector( + float[] aSourceVector, + float[] aDestinationVector + ) { + System.arraycopy( + aSourceVector, + 0, + aDestinationVector, + 0, + aSourceVector.length + ); + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + */ + protected static void enhanceContrast( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + } + } + } + + /** + * Fills matrix with value. + * + * @param aMatrix Matrix (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillMatrix( + float[][] aMatrix, + float aValue + ) { + for (float [] tmpRowVector : aMatrix) { + Arrays.fill(tmpRowVector , aValue); + } + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + float[] aVector, + float aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + boolean[] aVector, + boolean aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + int[] aVector, + int aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Returns index of cluster for contrast enhanced vector + * + * @param aContrastEnhancedVector Contrast enhanced vector + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterMatrix Cluster matrix + * @return Index of cluster for contrast enhanced unit vector + */ + protected static int getClusterIndex( + float[] aContrastEnhancedVector, + int aNumberOfDetectedClusters, + float[][] aClusterMatrix + ) { + float tmpMinSquaredDistance = Float.MAX_VALUE; + int tmpWinnerClusterIndex = -1; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpSquaredDistance = Art2aEuclidUtils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); + if (tmpSquaredDistance < tmpMinSquaredDistance) { + tmpMinSquaredDistance = tmpSquaredDistance; + tmpWinnerClusterIndex = i; + } + } + return tmpWinnerClusterIndex; + } + + /** + * Returns mean distance of all specified row vectors. + * + * @param aMatrix Matrix with row vectors (IS NOT CHANGED) + * @param anIndicesOfRowVectors Indices of row vectors of aMatrix + * @return Mean squared distance of all specified row vectors. + */ + protected static float getMeanDistance( + float[][] aMatrix, + int[] anIndicesOfRowVectors + ) { + float tmpSum = 0.0f; + for (int i = 0; i < anIndicesOfRowVectors.length; i++) { + for (int j = i + 1; j < anIndicesOfRowVectors.length; j++) { + tmpSum += (float) Math.sqrt(Art2aEuclidUtils.getSquaredDistance(aMatrix[anIndicesOfRowVectors[i]], aMatrix[anIndicesOfRowVectors[j]])); + } + } + return tmpSum / (float) (anIndicesOfRowVectors.length * (anIndicesOfRowVectors.length - 1) / 2); + } + + /** + * Returns min-max components for matrix where MinMaxValue[j] + * corresponds to column j of the row vectors of the matrix. The min-max + * components may be used to scale row vectors to interval [0,1], see + * method scaleVector(). + * + * @param aMatrix Matrix (IS NOT CHANGED) + * @return Min-max components + */ + protected static MinMaxValue[] getMinMaxComponents( + float[][] aMatrix + ) { + MinMaxValue[] tmpMinMaxComponents = new MinMaxValue[aMatrix[0].length]; + for (int j = 0; j < aMatrix[0].length; j++) { + float tmpMinValue = aMatrix[0][j]; + float tmpMaxValue = aMatrix[0][j]; + for (int i = 1; i < aMatrix.length; i++) { + if (aMatrix[i][j] < tmpMinValue) { + tmpMinValue = aMatrix[i][j]; + } else if (aMatrix[i][j] > tmpMaxValue) { + tmpMaxValue = aMatrix[i][j]; + } + } + tmpMinMaxComponents[j] = new MinMaxValue(tmpMinValue, tmpMaxValue); + } + return tmpMinMaxComponents; + } + + /** + * Calculates the squared distance between aVector1 and aVector2. + * + * @param aVector1 Vector 1 (IS NOT CHANGED) + * @param aVector2 Vector 2 (IS NOT CHANGED) + * @return Squared distance + */ + protected static float getSquaredDistance( + float[] aVector1, + float[] aVector2 + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector1.length; i++) { + float tmpDelta = aVector1[i] - aVector2[i]; + // tmpSum += (aVector1[i] - aVector2[i])^2; + tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); + } + return tmpSum; + } + + /** + * Scales components of aVectorToBeScaled to interval [0,1]. + * + * @param aVectorToBeScaled Vector (IS NOT CHANGED) + * @return New scaled vector with components in interval [0,1] or new + * vector of length zero if all components of aVectorToBeScaled are the + * same. + */ + protected static float[] getScaledVector( + float[] aVectorToBeScaled + ) { + float tmpMinValue = aVectorToBeScaled[0]; + float tmpMaxValue = aVectorToBeScaled[0]; + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aVectorToBeScaled[i] < tmpMinValue) { + tmpMinValue = aVectorToBeScaled[i]; + } else if (aVectorToBeScaled[i] > tmpMaxValue) { + tmpMaxValue = aVectorToBeScaled[i]; + } + } + float[] tmpScaledVector = new float[aVectorToBeScaled.length]; + if (tmpMinValue == tmpMaxValue) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + tmpScaledVector[i] = aVectorToBeScaled[i] - tmpMinValue; + } + } else { + float tmpDenominator = tmpMaxValue - tmpMinValue; + for(int i = 0; i < aVectorToBeScaled.length; i++) { + tmpScaledVector[i] = (aVectorToBeScaled[i] - tmpMinValue) / tmpDenominator; + } + } + return tmpScaledVector; + } + + /** + * Calculates the sum of squared differences between the components of the + * specified vector and a value. + * + * @param aVector Vector (IS NOT CHANGED) + * @param aValue Value + * @return Sum of squared differences between the components of the + * specified vector and a value. + */ + protected static float getSumOfSquaredDifferences( + float[] aVector, + float aValue + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector.length; i++) { + float tmpDelta = aVector[i] - aValue; + // tmpSum += (aVector[i] - aValue)^2; + tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); + } + return tmpSum; + } + + /** + * Threshold for contrast enhancement + * + * @param aNumberOfComponents Number of components + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @return Threshold for contrast enhancement + */ + protected static float getThresholdForContrastEnhancement( + int aNumberOfComponents, + float anOffsetForContrastEnhancement + ) { + // Original code: + // return (float) (1.0 / Math.sqrt(aNumberOfComponents + 1.0)); + return (float) (1.0 / Math.sqrt(aNumberOfComponents + anOffsetForContrastEnhancement)); + } + + /** + * Checks if vector has a length of zero (i.e. if all components are equal + * to zero). + * + * @param aVector Vector (IS NOT CHANGED) + * @return True: Vector has a length of zero, false: Otherwise + */ + protected static boolean hasLengthOfZero( + float[] aVector + ) { + for(float tmpComponent : aVector) { + if (tmpComponent != 0.0f) { + return false; + } + } + return true; + } + + /** + * Checks if data matrix has a non-finite component. + * Note: If aDataMatrix is null or empty nothing is done and false is + * returned. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @return True: Data matrix has non-finite component, false: Otherwise + */ + protected static boolean hasNonFiniteComponent( + float[][] aDataMatrix + ) { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + } + // + + for(float[] tmpDataVector : aDataMatrix) { + for (float tmpComponent : tmpDataVector) { + if (!Float.isFinite(tmpComponent)) { + return true; + } + } + } + return false; + } + + /** + * Determines convergence of clustering process. + * Note: No checks are performed. + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param anEpoch Current epochs + * @param aClusterCentroidMatrix Cluster centroid matrix with centroid row + * vectors + * @param aClusterCentroidMatrixOld Cluster centroid matrix with + * centroid row vectors of the previous epoch + * @param aMaximumNumberOfEpochs Maximum number of epochs + * @param aConvergenceThreshold Convergence threshold + * @return True if clustering process has converged, false otherwise. + */ + protected static boolean isConverged( + int aNumberOfDetectedClusters, + int anEpoch, + float[][] aClusterCentroidMatrix, + float[][] aClusterCentroidMatrixOld, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold + ) { + if (anEpoch == 1) { + // Convergence check needs at least 2 epochs + Art2aEuclidUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + return false; + } else { + float tmpSquaredConvergenceThreshold = aConvergenceThreshold * aConvergenceThreshold; + boolean tmpIsConverged = false; + if(anEpoch < aMaximumNumberOfEpochs) { + // Check convergence by evaluating the similarity (scalar product) + // of the cluster vectors of this and the previous epoch + tmpIsConverged = true; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if ( + aClusterCentroidMatrixOld[i] == null || + Art2aEuclidUtils.getSquaredDistance( + aClusterCentroidMatrix[i], + aClusterCentroidMatrixOld[i] + ) > tmpSquaredConvergenceThreshold + ) { + tmpIsConverged = false; + break; + } + } + if(!tmpIsConverged) { + Art2aEuclidUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + } + } + return tmpIsConverged; + } + } + + /** + * Checks if matrix is valid. + * + * @param aMatrix Matrix + * @return True: Matrix is valid, false: Otherwise + */ + protected static boolean isMatrixValid( + float[][] aMatrix + ) { + if (aMatrix == null || aMatrix.length == 0) { + return false; + } + for (float[] tmpRowVector : aMatrix) { + if (tmpRowVector == null || tmpRowVector.length == 0) { + return false; + } + } + int tmpRowVectorLength = aMatrix[0].length; + for (int i = 1; i < aMatrix.length; i++) { + if (aMatrix[i].length != tmpRowVectorLength) { + return false; + } + } + return true; + } + + /** + * Modifies winner cluster (see code). + * Note: aContrastEnhancedVector is used for modification and may be + * changed. + * Note: No checks are performed. + * + * @param aContrastEnhancedVector Contrast enhanced unit vector for + * modification (MAY BE CHANGED) + * @param aWinnerClusterVector Winner cluster centroid vector (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + * @param aLearningParameter Learning parameter + */ + protected static void modifyWinnerCluster( + float[] aContrastEnhancedVector, + float[] aWinnerClusterVector, + float aThresholdForContrastEnhancement, + float aLearningParameter + ) { + // Note: aContrastEnhancedVector is used for modification + for(int j = 0; j < aWinnerClusterVector.length; j++) { + if(aWinnerClusterVector[j] <= aThresholdForContrastEnhancement) { + aContrastEnhancedVector[j] = 0.0f; + } + } + float tmpFactor = ONE - aLearningParameter; + for(int j = 0; j < aWinnerClusterVector.length; j++) { + aContrastEnhancedVector[j] = aLearningParameter * aContrastEnhancedVector[j] + tmpFactor * aWinnerClusterVector[j]; + } + Art2aEuclidUtils.copyVector(aContrastEnhancedVector, aWinnerClusterVector); + } + + /** + * Removes empty clusters from cluster matrix + * + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (IS NOT CHANGED) + * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterRemovalInfo Cluster removal info (is set according to the + * operations performed, IS CHANGED) + */ + protected static void removeEmptyClusters( + boolean[] aClusterUsageFlags, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + ClusterRemovalInfo aClusterRemovalInfo + ) { + boolean tmpIsEmptyClusterRemoval = false; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (!aClusterUsageFlags[i]) { + tmpIsEmptyClusterRemoval = true; + break; + } + } + if (tmpIsEmptyClusterRemoval) { + // Remove empty clusters from cluster matrix + LinkedList tmpClusterVectorList = new LinkedList<>(); + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (aClusterUsageFlags[i]) { + tmpClusterVectorList.add(aClusterMatrix[i]); + aClusterMatrix[i] = null; + } + } + int tmpIndex = 0; + for (float[] tmpClusterVector : tmpClusterVectorList) { + aClusterMatrix[tmpIndex++] = tmpClusterVector; + } + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); + } else { + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); + } + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + */ + protected static void setContrastEnhancement( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + } + } + } + + /** + * Transforms original data vector into corresponding contrast enhanced + * unit vector (see code). + * Note: No checks are performed. + * + * @param aDataVector Data vector (IS NOT CHANGED) + * @param aBufferVector Buffer vector for contrast enhanced unit vector + * derived from data vector (MUST ALREADY BE INSTANTIATED and is set within + * the method) + * @param aMinMaxComponents Min-max components of original data matrix + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @return True: Scaled data vector has a length of zero, false: Otherwise + */ + protected static boolean setContrastEnhancedVector( + float[] aDataVector, + float[] aBufferVector, + Art2aEuclidUtils.MinMaxValue[] aMinMaxComponents, + float aThresholdForContrastEnhancement + ) { + // Already allocated memory of aBufferVector is reused + Art2aEuclidUtils.copyVector(aDataVector, aBufferVector); + // Scale components of vector to interval [0,1] + Art2aEuclidUtils.scaleVector(aBufferVector, aMinMaxComponents); + // Check length + if (Art2aEuclidUtils.hasLengthOfZero(aBufferVector)) { + // True: Scaled source vector has a length of zero + return true; + } else { + // Enhance contrast + Art2aEuclidUtils.setContrastEnhancement(aBufferVector, aThresholdForContrastEnhancement); + // False: Scaled data vector has a length different from zero + return false; + } + } + + /** + * Sets rho winner with the rho value and the cluster index of the winner + * (see code). If the cluster index is negative the first scaled rho value + * is the winner. + * + * @param aContrastEnhancedVector Contrast enhanced unit vector (IS NOT + * CHANGED) + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aScalingFactor Scaling factor + * @param aRhoWinner Rho winner: Is set with the rho value and the cluster + * index of the winner. If the cluster index is negative the first scaled + * rho value is the winner. + */ + protected static void setRhoWinner( + float[] aContrastEnhancedVector, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + float aScalingFactor, + Art2aEuclidUtils.RhoWinner aRhoWinner + ) { + // Calculate first rho value + float tmpRhoValue = Art2aEuclidUtils.getSumOfSquaredDifferences(aContrastEnhancedVector, aScalingFactor); + // Set winner index to negative value + int tmpIndex = -1; + // Calculate other rho values + for(int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpRhoForCluster = Art2aEuclidUtils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); + if(tmpRhoForCluster < tmpRhoValue) { + tmpRhoValue = tmpRhoForCluster; + tmpIndex = i; + } + } + aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); + } + + /** + * Sets copied (!) row vector at index in matrix. + * + * @param aMatrix Matrix (MAY BE CHANGED) + * @param aRowVector Row vector (IS NOT CHANGED) + * @param anIndex Index of row vector in matrix + */ + protected static void setRowVector( + float[][] aMatrix, + float[] aRowVector, + int anIndex + ) { + float[] tmpNewMatrixRowVector = new float[aRowVector.length]; + Art2aEuclidUtils.copyVector(aRowVector, tmpNewMatrixRowVector); + aMatrix[anIndex] = tmpNewMatrixRowVector; + } + + /** + * Scales components of aVectorToBeScaled according to min-max components + * to interval [0,1] (see code and method getMinMaxComponents()). + * + * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) + * @param aMinMaxComponents Min-max components + */ + protected static void scaleVector( + float[] aVectorToBeScaled, + MinMaxValue[] aMinMaxComponents + ) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { + // Scale component to interval [0,1] + aVectorToBeScaled[i] = + (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); + } else { + // Shift component to zero + aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); + } + } + } + + /** + * Randomly shuffles indices from 0 to (anIndices.Length - 1) in + * anIndexArray using Fisher-Yates shuffling (i.e. the modern version + * introduced by Richard Durstenfeld). + * Note: No checks are performed. + * + * @param anIndexArray Array with indices from 0 to (anIndices.Length - 1) + * @param aRandomNumberGenerator Random number generator + */ + protected static void shuffleIndices( + int[] anIndexArray, + Random aRandomNumberGenerator + ) { + for (int i = anIndexArray.length - 1; i > 0; i--) { + // Generate a random index between 0 and i (inclusive) + int j = aRandomNumberGenerator.nextInt(i + 1); + // Swap the elements at indices i and j + int tmpIntBuffer = anIndexArray[i]; + anIndexArray[i] = anIndexArray[j]; + anIndexArray[j] = tmpIntBuffer; + } + } + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index d5fea55..c0ee1f7 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -142,7 +142,7 @@ public class Art2aKernel { /** * Default maximum number of epochs */ - private static final int DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS = 100; + private static final int DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS = 10; /** * Default value for the learning parameter */ @@ -351,7 +351,7 @@ public Art2aKernel( /** * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), - * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 1.0) and RANDOM_SEED (= 1). * Note: There is NO data preprocessing. @@ -448,7 +448,7 @@ public Art2aKernel( /** * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), - * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param anArt2aData ART-2a data object created by method @@ -716,7 +716,7 @@ public Art2aResult getClusterResult( anException.toString(), anException ); - throw new Exception("Art2aKernel.getClusterResult: An exception occurred: This should never happen!"); + throw anException; } } @@ -1036,21 +1036,51 @@ public static boolean isDataMatrixValid( ); return false; } + } + if (Art2aUtils.hasNonFiniteComponent(aDataMatrix)) { + return false; + } + return true; + } - for (float tmpValue : tmpDataVector) { - if (!Float.isFinite(tmpValue) - || tmpValue == Float.MIN_VALUE - || tmpValue == Float.MAX_VALUE - ) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.isDataMatrixValid: NaN/Float.MIN/Float.MAX are not allowed." - ); - return false; - } + /** + * Removes columns from data matrix with non-finite components. + * Note: If aDataMatrix is null, empty or has an invalid structure + * nothing is done and false is returned. + * + * @param aDataMatrix Data matrix with data row vectors (MAY BE CHANGED) + * @return True if aDataMatrix was changed (i.e. column removal was + * performed), false otherwise (i.e. data matrix is unchanged). + */ + public static boolean isNonFiniteComponentRemoval( + float[][] aDataMatrix + ) { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + + int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; + if(tmpNumberOfDataVectorComponents < 2) { + return false; + } + + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + + if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + return false; } } - return true; + // + + boolean tmpHasNonFiniteComponent = Art2aUtils.hasNonFiniteComponent(aDataMatrix); + if (tmpHasNonFiniteComponent) { + // TODO: Remove columns with non-finite components + } + return tmpHasNonFiniteComponent; } /** diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java index 8921160..9e9af97 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -613,6 +613,38 @@ protected static boolean hasLengthOfZero( } return true; } + + /** + * Checks if data matrix has a non-finite component. + * Note: If aDataMatrix is null or empty nothing is done and false is + * returned. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @return True: Data matrix has non-finite component, false: Otherwise + */ + protected static boolean hasNonFiniteComponent( + float[][] aDataMatrix + ) { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + } + // + + for(float[] tmpDataVector : aDataMatrix) { + for (float tmpComponent : tmpDataVector) { + if (!Float.isFinite(tmpComponent)) { + return true; + } + } + } + return false; + } /** * Calculates contrast enhanced vector. diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java new file mode 100644 index 0000000..19232cc --- /dev/null +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -0,0 +1,974 @@ +/* + * ART-2a-Euclid Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import de.unijena.cheminf.clustering.art2a.Art2aEuclidKernel; +import de.unijena.cheminf.clustering.art2a.Art2aEuclidResult; +import de.unijena.cheminf.clustering.art2a.Art2aEuclidTask; +import de.unijena.cheminf.clustering.art2a.Art2aEuclidData; +import de.unijena.cheminf.clustering.art2a.Art2aEuclidUtils; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test class for ART-2a-Euclid clustering. + * + * @author Achim Zielesny + */ +public class Art2aEuclidTest { + + /** + * Test method for development purposes only + */ + @Test + public void test_Development_IrisFlowerData() { + System.out.println("---------------------------------"); + System.out.println("test_Development_IrisFlowerData()"); + System.out.println("---------------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + + // float[] tmpVigilances = new float[] {0.01f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 0.99f}; + float[] tmpVigilances = new float[] {0.1f}; + boolean tmpIsClusterAnalysis = true; + int tmpMaximumNumberOfClusters = 150; + boolean tmpIsDataPreprocessing = false; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + for (float tmpVigilance : tmpVigilances) { + System.out.println(" Vigilance parameter = " + String.valueOf(tmpVigilance)); + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Assertions.assertNotNull(tmpArt2aEuclidKernel); + Art2aEuclidResult tmpArt2aEuclidResult = null; + try { + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + Assertions.assertNotNull(tmpArt2aEuclidResult); + int tmpNumberOfDetectedClusters = tmpArt2aEuclidResult.getNumberOfDetectedClusters(); + System.out.println(" - Number of detected clusters = " + String.valueOf(tmpArt2aEuclidResult.getNumberOfDetectedClusters())); + System.out.println(" - Number of epochs = " + String.valueOf(tmpArt2aEuclidResult.getNumberOfEpochs())); + if (tmpIsClusterAnalysis) { + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + System.out.println(" - Cluster " + String.valueOf(i) + " of size " + String.valueOf(tmpArt2aEuclidResult.getClusterSize(i))); + int[] tmpDataVectorIndicesOfCluster = tmpArt2aEuclidResult.getDataVectorIndicesOfCluster(i); + System.out.println(" " + this.getStringFromIntArray(tmpDataVectorIndicesOfCluster)); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + System.out.println(" - Distance between cluster " + String.valueOf(i) + " and cluster " + String.valueOf(j) + " = " + String.valueOf(tmpArt2aEuclidResult.getDistanceBetweenClusters(i, j))); + } + } + } + System.out.println(""); + } + } + + /** + * Test method for development purposes only + */ + @Test + public void test_Development_CombinedGaussianCouldData() { + System.out.println("--------------------------------------------"); + System.out.println("test_Development_CombinedGaussianCouldData()"); + System.out.println("--------------------------------------------"); + int tmpNumberOfDimensions = 10; + int tmpNumberOfGaussianCloudVectors = 100; + float tmpStandardDeviation = 0.1f; + Random tmpRandomNumberGenerator = new Random(1L); + float[][] tmpCombinedGaussianCloudDataMatrix = + this.getCombinedGaussianCloudMatrix( + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, + tmpRandomNumberGenerator + ); + + float tmpVigilance = 0.1f; + int tmpMaximumNumberOfClusters = 1000; + boolean tmpIsDataPreprocessing = false; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + long tmpStart = System.currentTimeMillis(); + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpCombinedGaussianCloudDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aEuclidResult tmpArt2aEuclidResult = null; + try { + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + long tmpEnd = System.currentTimeMillis(); + + System.out.println(" Number of data vectors = " + String.valueOf(tmpNumberOfDimensions * tmpNumberOfGaussianCloudVectors)); + System.out.println(" Elapsed time in ms = " + String.valueOf(tmpEnd - tmpStart)); + Assertions.assertNotNull(tmpArt2aEuclidKernel); + Assertions.assertNotNull(tmpArt2aEuclidResult); + int tmpNumberOfDetectedClusters = tmpArt2aEuclidResult.getNumberOfDetectedClusters(); + System.out.println(" Number of detected clusters = " + String.valueOf(tmpArt2aEuclidResult.getNumberOfDetectedClusters())); + System.out.println(" Number of epochs = " + String.valueOf(tmpArt2aEuclidResult.getNumberOfEpochs())); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + System.out.println(" Cluster " + String.valueOf(i) + " of size " + String.valueOf(tmpArt2aEuclidResult.getClusterSize(i))); + System.out.println(" - Representative = " + String.valueOf(tmpArt2aEuclidResult.getClusterRepresentativeIndex(i))); + System.out.println(" - Representatives = " + this.getStringFromIntArray(tmpArt2aEuclidResult.getClusterRepresentativeIndices(i))); + int[] tmpDataVectorIndicesOfCluster = tmpArt2aEuclidResult.getDataVectorIndicesOfCluster(i); + System.out.println(" " + this.getStringFromIntArray(tmpDataVectorIndicesOfCluster)); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + System.out.println(" Distance between cluster " + String.valueOf(i) + " and cluster " + String.valueOf(j) + " = " + String.valueOf(tmpArt2aEuclidResult.getDistanceBetweenClusters(i, j))); + } + } + } + + /** + * Test method for development purposes only + */ + @Test + public void test_Development_GetRepresentatives() { + System.out.println("-------------------------------------"); + System.out.println("test_Development_GetRepresentatives()"); + System.out.println("-------------------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + boolean tmpIsDataPreprocessing = false; + + float tmpVigilanceMin = 0.0001f; + float tmpVigilanceMax = 0.9999f; + int tmpNumberOfTrialSteps = 32; + + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + + try { + int[] tmpBestRepresentatives = tmpArt2aEuclidKernel.getBestRepresentatives(tmpIrisFlowerDataMatrix, 2, tmpIrisFlowerDataMatrix.length); + Arrays.sort(tmpBestRepresentatives); + System.out.println( + String.valueOf(tmpBestRepresentatives.length) + " best representatives = " + this.getStringFromIntArray(tmpBestRepresentatives) + ); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + int[] tmpAllIndices = new int[150]; + for (int i = 0; i < 150; i++) { + tmpAllIndices[i] = i; + } + float tmpBaseMeanDistance = Art2aEuclidUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpAllIndices); + System.out.println( + "Base mean distance = " + String.valueOf(tmpBaseMeanDistance) + ); + for (int tmpNumberOfRepresentatives = 2; tmpNumberOfRepresentatives < tmpIrisFlowerDataMatrix.length; tmpNumberOfRepresentatives++) { + try { + int[] tmpRepresentatives = + tmpArt2aEuclidKernel.getRepresentatives( + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, + tmpNumberOfTrialSteps + ); + if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { + Arrays.sort(tmpRepresentatives); + float tmpMeanDistance = Art2aEuclidUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); + System.out.println( + String.valueOf(tmpNumberOfRepresentatives) + + " Representatives (Mean distance = " + + String.valueOf(tmpMeanDistance) + + ") = " + + this.getStringFromIntArray(tmpRepresentatives) + ); + } + } catch (Exception anException) { + Assertions.assertTrue(false); + } + } + } + + /** + * Tests Art2aEuclidKernel method getRepresentatives(). + */ + @Test + public void test_GetRepresentatives() { + System.out.println("-------------------------"); + System.out.println("test_GetRepresentatives()"); + System.out.println("-------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + boolean tmpIsDataPreprocessing = false; + + int tmpNumberOfRepresentatives = 10; + float tmpVigilanceMin = 0.0001f; + float tmpVigilanceMax = 0.9999f; + int tmpNumberOfTrialSteps = 32; + + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + try { + int[] tmpRepresentatives = + tmpArt2aEuclidKernel.getRepresentatives( + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, + tmpNumberOfTrialSteps + ); + System.out.println( + String.valueOf(tmpNumberOfRepresentatives) + + " wanted Representatives, " + + String.valueOf(tmpRepresentatives.length) + + " generated = " + + this.getStringFromIntArray(tmpRepresentatives) + ); + for (int i = 0; i < tmpRepresentatives.length; i++) { + for (int j = i + 1; j < tmpRepresentatives.length; j++) { + System.out.println( + "Distance between representatives " + + String.valueOf(i) + + " and representative " + + String.valueOf(j) + + "= " + + String.valueOf( + Math.sqrt( + Art2aEuclidUtils.getSquaredDistance( + tmpIrisFlowerDataMatrix[tmpRepresentatives[i]], + tmpIrisFlowerDataMatrix[tmpRepresentatives[j]] + ) + ) + ) + ); + } + } + Assertions.assertEquals(tmpRepresentatives.length, tmpNumberOfRepresentatives); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + } + + /** + * Test for perfect clustering + */ + @Test + public void test_PerfectClustering() { + System.out.println("------------------------"); + System.out.println("test_PerfectClustering()"); + System.out.println("------------------------"); + int tmpNumberOfDimensions = 10; + int tmpNumberOfGaussianCloudVectors = 1000; + float tmpStandardDeviation = 0.01f; + Random tmpRandomNumberGenerator = new Random(1L); + float[][] tmpCombinedGaussianCloudDataMatrix = + this.getCombinedGaussianCloudMatrix( + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, + tmpRandomNumberGenerator + ); + + float tmpVigilance = 0.1f; + int tmpMaximumNumberOfClusters = 100; + boolean tmpIsDataPreprocessing = false; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpCombinedGaussianCloudDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aEuclidResult tmpArt2aEuclidResult = null; + try { + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + Assertions.assertEquals(tmpArt2aEuclidResult.getNumberOfDetectedClusters(), tmpNumberOfDimensions); + Assertions.assertTrue(tmpArt2aEuclidResult.getNumberOfEpochs() < tmpMaximumNumberOfEpochs); + for (int i = 0; i < tmpArt2aEuclidResult.getNumberOfDetectedClusters(); i++) { + Assertions.assertEquals(tmpArt2aEuclidResult.getClusterSize(i), tmpNumberOfGaussianCloudVectors); + int[] tmpDataVectorIndicesOfCluster = tmpArt2aEuclidResult.getDataVectorIndicesOfCluster(i); + int[] tmpClusterRepresentativeIndices = tmpArt2aEuclidResult.getClusterRepresentativeIndices(i); + Assertions.assertEquals(tmpArt2aEuclidResult.getClusterRepresentativeIndex(i), tmpClusterRepresentativeIndices[0]); + Arrays.sort(tmpDataVectorIndicesOfCluster); + Arrays.sort(tmpClusterRepresentativeIndices); + Assertions.assertArrayEquals(tmpDataVectorIndicesOfCluster, tmpClusterRepresentativeIndices); + } + Assertions.assertFalse(tmpArt2aEuclidResult.isClusterOverflow()); + for (int i = 0; i < tmpArt2aEuclidResult.getNumberOfDetectedClusters(); i++) { + Assertions.assertEquals(tmpArt2aEuclidResult.getClusterRepresentativeIndex(i), tmpArt2aEuclidResult.getClusterRepresentativeIndices(i)[0]); + } + } + + /** + * Tests that clustering with and without preprocessing has identical + * results. + */ + @Test + public void test_Preprocessing() { + System.out.println("--------------------"); + System.out.println("test_Preprocessing()"); + System.out.println("--------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + for (float tmpVigilance : tmpVigilances) { + // No preprocessing + boolean tmpIsDataPreprocessing = false; + Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aEuclidResult tmpArt2aEuclidResultWithoutPreprocessing = null; + try { + tmpArt2aEuclidResultWithoutPreprocessing = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Preprocessing + tmpIsDataPreprocessing = true; + Art2aEuclidKernel tmpArt2aEuclidKernelWithPreprocessing = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aEuclidResult tmpArt2aEuclidResultWithPreprocessing = null; + try { + tmpArt2aEuclidResultWithPreprocessing = tmpArt2aEuclidKernelWithPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Assert that results without and with preprocessing are identical + Assertions.assertTrue( + tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters() == + tmpArt2aEuclidResultWithPreprocessing.getNumberOfDetectedClusters() + ); + Assertions.assertTrue( + tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs() == + tmpArt2aEuclidResultWithPreprocessing.getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + Assertions.assertArrayEquals( + tmpArt2aEuclidResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aEuclidResultWithPreprocessing.getDataVectorIndicesOfCluster(i) + ); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertTrue( + tmpArt2aEuclidResultWithoutPreprocessing.getDistanceBetweenClusters(i, j) == + tmpArt2aEuclidResultWithPreprocessing.getDistanceBetweenClusters(i, j) + ); + } + } + } + } + + /** + * Test that generated Art2aEuclidData object leads to identical clustering + results. + */ + @Test + public void test_Art2aEuclidData() { + System.out.println("----------------"); + System.out.println("test_Art2aEuclidData()"); + System.out.println("----------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + for (float tmpVigilance : tmpVigilances) { + // No preprocessing + boolean tmpIsDataPreprocessing = false; + Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aEuclidResult tmpArt2aEuclidResultWithoutPreprocessing = null; + try { + tmpArt2aEuclidResultWithoutPreprocessing = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Preprocessed Art2aEuclidData + Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + Art2aEuclidKernel tmpArt2aEuclidKernelWithArt2aEuclidData = + new Art2aEuclidKernel( + tmpArt2aEuclidData, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpRandomSeed + ); + Art2aEuclidResult tmpArt2aEuclidResultWithArt2aEuclidData = null; + try { + tmpArt2aEuclidResultWithArt2aEuclidData = tmpArt2aEuclidKernelWithArt2aEuclidData.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Assert that results without preprocessing and preprocessed + // Art2aEuclidData are identical + Assertions.assertTrue( + tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters() == + tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfDetectedClusters() + ); + Assertions.assertTrue( + tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs() == + tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(); + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + Assertions.assertArrayEquals( + tmpArt2aEuclidResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aEuclidResultWithArt2aEuclidData.getDataVectorIndicesOfCluster(i) + ); + } + for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { + for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertTrue( + tmpArt2aEuclidResultWithoutPreprocessing.getDistanceBetweenClusters(i, j) == + tmpArt2aEuclidResultWithArt2aEuclidData.getDistanceBetweenClusters(i, j) + ); + } + } + } + } + + /** + * Tests that sequential and parallelized clustering leads to identical + * results. + */ + @Test + public void test_ParallelClustering() { + System.out.println("-------------------------"); + System.out.println("test_ParallelClustering()"); + System.out.println("-------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + // Sequential clustering one after another + Art2aEuclidResult[] tmpSequentialResults = new Art2aEuclidResult[tmpVigilances.length]; + int tmpIndex = 0; + for (float tmpVigilance : tmpVigilances) { + boolean tmpIsDataPreprocessing = false; + Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + try { + tmpSequentialResults[tmpIndex++] = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + } + + // Concurrent (parallelized) clustering + LinkedList tmpArt2aEuclidTaskList = new LinkedList<>(); + Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + for (float tmpVigilance : tmpVigilances) { + tmpArt2aEuclidTaskList.add(new Art2aEuclidTask( + tmpArt2aEuclidData, + tmpVigilance, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpRandomSeed + ) + ); + } + ExecutorService tmpExecutorService = Executors.newFixedThreadPool(tmpVigilances.length); + List> tmpFutureList = null; + try { + tmpFutureList = tmpExecutorService.invokeAll(tmpArt2aEuclidTaskList); + } catch (InterruptedException e) { + System.out.println("test_ParallelClustering: InterruptedException occurred."); + } + tmpExecutorService.shutdown(); + Art2aEuclidResult[] tmpParallelResults = new Art2aEuclidResult[tmpVigilances.length]; + tmpIndex = 0; + for (Future tmpFuture : tmpFutureList) { + try { + tmpParallelResults[tmpIndex++] = tmpFuture.get(); + } catch (Exception e) { + System.out.println("test_ParallelClustering: Exception occurred."); + } + } + + // Assert that sequential results without preprocessing and concurrent + // results with preprocessed Art2aEuclidData are identical + for (int i = 0; i < tmpVigilances.length; i++) { + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfDetectedClusters() == + tmpParallelResults[i].getNumberOfDetectedClusters() + ); + + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfEpochs() == + tmpParallelResults[i].getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertArrayEquals( + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpParallelResults[i].getDataVectorIndicesOfCluster(j) + ); + } + + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { + Assertions.assertTrue( + tmpSequentialResults[i].getDistanceBetweenClusters(j, k) == + tmpParallelResults[i].getDistanceBetweenClusters(j, k) + ); + } + } + } + } + + /** + * Tests that sequential and parallelized clustering with + Art2aEuclidKernel.getClusterResults() leads to identical results. + */ + @Test + public void test_ParallelClusteringWithGetGlusterResults() { + System.out.println("----------------------------------------------"); + System.out.println("test_ParallelClusteringWithGetGlusterResults()"); + System.out.println("----------------------------------------------"); + float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); + float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; + int tmpMaximumNumberOfClusters = 150; + int tmpMaximumNumberOfEpochs = 100; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + boolean tmpIsDataPreprocessing = false; + + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpIrisFlowerDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + + // Sequential clustering one after another + Art2aEuclidResult[] tmpSequentialResults = null; + try { + tmpSequentialResults = tmpArt2aEuclidKernel.getClusterResults(tmpVigilances, 0); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Concurrent (parallel) clustering + int tmpNumberOfParallelCalculationThreads = 2; + Art2aEuclidResult[] tmpParallelResults = null; + try { + tmpParallelResults = tmpArt2aEuclidKernel.getClusterResults(tmpVigilances, tmpNumberOfParallelCalculationThreads); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + + // Assert that sequential results without preprocessing and concurrent + // results with preprocessed Art2aEuclidData are identical + for (int i = 0; i < tmpVigilances.length; i++) { + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfDetectedClusters() == + tmpParallelResults[i].getNumberOfDetectedClusters() + ); + + Assertions.assertTrue( + tmpSequentialResults[i].getNumberOfEpochs() == + tmpParallelResults[i].getNumberOfEpochs() + ); + + int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + Assertions.assertArrayEquals( + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpParallelResults[i].getDataVectorIndicesOfCluster(j) + ); + } + + for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { + for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { + Assertions.assertTrue( + tmpSequentialResults[i].getDistanceBetweenClusters(j, k) == + tmpParallelResults[i].getDistanceBetweenClusters(j, k) + ); + } + } + } + } + + // + /** + * Returns int array as a string. + * Note: No checks are performed. + * + * @param anIntArray Int array + * @return The int array as a string + */ + private String getStringFromIntArray( + int[] anIntArray + ) { + // Assumes 6 characters for int number plus comma plus space + StringBuilder tmpStringBuilder = new StringBuilder(anIntArray.length * 6); + tmpStringBuilder.append(String.valueOf(anIntArray[0])); + for (int i = 1; i < anIntArray.length; i++) { + tmpStringBuilder.append(", "); + tmpStringBuilder.append(String.valueOf(anIntArray[i])); + } + return tmpStringBuilder.toString(); + } + + /** + * Compares two arrays. + * Note: No checks are performed. + * + * @param anArray1 Array 1 + * @param anArray2 Array 2 + * @return True: Arrays have the same values in the same order, false: + * Otherwise + */ + private boolean compareArrays( + int[] anArray1, + int[] anArray2 + ) { + boolean isEqual = true; + if (anArray1.length != anArray2.length) { + return false; + } + for (int i = 0; i < anArray1.length; i++) { + if (anArray1[i] != anArray2[i]) { + return false; + } + } + return isEqual; + } + // + // + /** + * Returns Gaussian cloud matrix + * + * @param aCentroidVector Centroid vector (IS NOT CHANGED) + * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors + * @param aStandardDeviation Standard deviation of Gaussian distribution + * @param aRandomNumberGenerator Random number generator + * @return Gaussian cloud matrix + */ + private float[][] getGaussianCloudMatrix( + float[] aCentroidVector, + int aNumberOfGaussianCloudVectors, + float aStandardDeviation, + Random aRandomNumberGenerator + ) { + float[][] tmpGaussianCloudMatrix = new float[aNumberOfGaussianCloudVectors][]; + for (int i = 0; i < aNumberOfGaussianCloudVectors; i++) { + float[] tmpCloudVector = new float[aCentroidVector.length]; + for (int j = 0; j < aCentroidVector.length; j++) { + tmpCloudVector[j] = aCentroidVector[j] + (float) aRandomNumberGenerator.nextGaussian() * aStandardDeviation; + } + tmpGaussianCloudMatrix[i] = tmpCloudVector; + } + return tmpGaussianCloudMatrix; + } + + /** + * Returns combined Gaussian cloud matrix (see code) + * + * @param aNumberOfDimensions Number of dimensions + * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors + * @param aStandardDeviation Standard deviation of Gaussian distribution + * @param aRandomNumberGenerator Random number generator + * @return Combined Gaussian cloud matrix (see code) + */ + private float[][] getCombinedGaussianCloudMatrix( + int aNumberOfDimensions, + int aNumberOfGaussianCloudVectors, + float aStandardDeviation, + Random aRandomNumberGenerator + ) { + float[][] tmpCombinedGaussianCloudMatrix = new float[aNumberOfDimensions * aNumberOfGaussianCloudVectors][]; + int tmpIndex = 0; + for (int i = 0; i < aNumberOfDimensions; i++) { + float[] tmpCentroidVector = new float[aNumberOfDimensions]; + Arrays.fill(tmpCentroidVector, 0.0f); + tmpCentroidVector[i] = 1.0f; + float[][] tmpGaussianCloudMatrix = + this.getGaussianCloudMatrix( + tmpCentroidVector, + aNumberOfGaussianCloudVectors, + aStandardDeviation, + aRandomNumberGenerator + ); + for (int j = 0; j < tmpGaussianCloudMatrix.length; j++) { + tmpCombinedGaussianCloudMatrix[tmpIndex++] = tmpGaussianCloudMatrix[j]; + } + } + return tmpCombinedGaussianCloudMatrix; + } + // + // + /** + * Returns Iris flower data: Indices 0-49 = Iris setosa, indices 50-99 = + * Iris versicolor, indices 100-149 = Iris virginica + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris flower data + */ + private float[][] getIrisFlowerDataMatrix() { + float[][] tmpIrisSetosaData = this.getIrisSetosaDataMatrix(); + float[][] tmpIrisVersicolorData = this.getIrisVersicolorDataMatrix(); + float[][] tmpIrisVirginicaData = this.getIrisVirginicaDataMatrix(); + float[][] tmpIrisFlowerData = + new float[tmpIrisSetosaData.length + tmpIrisVersicolorData.length + tmpIrisVirginicaData.length][]; + int tmpIndex = 0; + for (int i = 0; i < tmpIrisSetosaData.length; i++) { + tmpIrisFlowerData[tmpIndex++] = tmpIrisSetosaData[i]; + } + for (int i = 0; i < tmpIrisVersicolorData.length; i++) { + tmpIrisFlowerData[tmpIndex++] = tmpIrisVersicolorData[i]; + } + for (int i = 0; i < tmpIrisVirginicaData.length; i++) { + tmpIrisFlowerData[tmpIndex++] = tmpIrisVirginicaData[i]; + } + return tmpIrisFlowerData; + } + + /** + * Returns Iris setosa data + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris setosa data + */ + private float[][] getIrisSetosaDataMatrix() { + return new + float[][] { + {49.0f, 30.0f, 14.0f, 2.0f}, {51.0f, 38.0f, 19.0f, 4.0f}, {52.0f, 41.0f, 15.0f, 1.0f}, {54.0f, 34.0f, 15.0f, 4.0f}, + {50.0f, 36.0f, 14.0f, 2.0f}, {57.0f, 44.0f, 15.0f, 4.0f}, {46.0f, 32.0f, 14.0f, 2.0f}, {50.0f, 34.0f, 16.0f, 4.0f}, + {51.0f, 35.0f, 14.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 2.0f}, {50.0f, 34.0f, 15.0f, 2.0f}, {58.0f, 40.0f, 12.0f, 2.0f}, + {43.0f, 30.0f, 11.0f, 1.0f}, {50.0f, 32.0f, 12.0f, 2.0f}, {50.0f, 30.0f, 16.0f, 2.0f}, {48.0f, 34.0f, 19.0f, 2.0f}, + {51.0f, 38.0f, 16.0f, 2.0f}, {48.0f, 30.0f, 14.0f, 3.0f}, {55.0f, 42.0f, 14.0f, 2.0f}, {44.0f, 30.0f, 13.0f, 2.0f}, + {54.0f, 39.0f, 17.0f, 4.0f}, {48.0f, 34.0f, 16.0f, 2.0f}, {51.0f, 35.0f, 14.0f, 3.0f}, {52.0f, 35.0f, 15.0f, 2.0f}, + {51.0f, 37.0f, 15.0f, 4.0f}, {54.0f, 34.0f, 17.0f, 2.0f}, {51.0f, 38.0f, 15.0f, 3.0f}, {57.0f, 38.0f, 17.0f, 3.0f}, + {45.0f, 23.0f, 13.0f, 3.0f}, {48.0f, 30.0f, 14.0f, 1.0f}, {53.0f, 37.0f, 15.0f, 2.0f}, {44.0f, 29.0f, 14.0f, 2.0f}, + {54.0f, 39.0f, 13.0f, 4.0f}, {54.0f, 37.0f, 15.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 1.0f}, {50.0f, 35.0f, 13.0f, 3.0f}, + {51.0f, 34.0f, 15.0f, 2.0f}, {46.0f, 31.0f, 15.0f, 2.0f}, {47.0f, 32.0f, 13.0f, 2.0f}, {47.0f, 32.0f, 16.0f, 2.0f}, + {50.0f, 33.0f, 14.0f, 2.0f}, {50.0f, 35.0f, 16.0f, 6.0f}, {55.0f, 35.0f, 13.0f, 2.0f}, {46.0f, 34.0f, 14.0f, 3.0f}, + {51.0f, 33.0f, 17.0f, 5.0f}, {52.0f, 34.0f, 14.0f, 2.0f}, {49.0f, 36.0f, 14.0f, 1.0f}, {48.0f, 31.0f, 16.0f, 2.0f}, + {46.0f, 36.0f, 10.0f, 2.0f}, {44.0f, 32.0f, 13.0f, 2.0f} + }; + } + + /** + * Returns Iris versicolor data + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris versicolor data + */ + private float[][] getIrisVersicolorDataMatrix() { + return new + float[][] { + {66.0f, 29.0f, 46.0f, 13.0f}, {61.0f, 29.0f, 47.0f, 14.0f}, {60.0f, 34.0f, 45.0f, 16.0f}, {52.0f, 27.0f, 39.0f, 14.0f}, + {49.0f, 24.0f, 33.0f, 10.0f}, {60.0f, 27.0f, 51.0f, 16.0f}, {56.0f, 27.0f, 42.0f, 13.0f}, {61.0f, 30.0f, 46.0f, 14.0f}, + {55.0f, 24.0f, 37.0f, 10.0f}, {57.0f, 30.0f, 42.0f, 12.0f}, {63.0f, 33.0f, 47.0f, 16.0f}, {69.0f, 31.0f, 49.0f, 15.0f}, + {57.0f, 28.0f, 45.0f, 13.0f}, {61.0f, 28.0f, 47.0f, 12.0f}, {64.0f, 29.0f, 43.0f, 13.0f}, {63.0f, 23.0f, 44.0f, 13.0f}, + {60.0f, 22.0f, 40.0f, 10.0f}, {56.0f, 30.0f, 41.0f, 13.0f}, {63.0f, 25.0f, 49.0f, 15.0f}, {50.0f, 20.0f, 35.0f, 10.0f}, + {59.0f, 30.0f, 42.0f, 15.0f}, {55.0f, 25.0f, 40.0f, 13.0f}, {62.0f, 29.0f, 43.0f, 13.0f}, {51.0f, 25.0f, 30.0f, 11.0f}, + {57.0f, 28.0f, 41.0f, 13.0f}, {58.0f, 27.0f, 39.0f, 12.0f}, {56.0f, 29.0f, 36.0f, 13.0f}, {67.0f, 31.0f, 47.0f, 15.0f}, + {67.0f, 31.0f, 44.0f, 14.0f}, {55.0f, 24.0f, 38.0f, 11.0f}, {56.0f, 30.0f, 45.0f, 15.0f}, {61.0f, 28.0f, 40.0f, 13.0f}, + {50.0f, 23.0f, 33.0f, 10.0f}, {55.0f, 26.0f, 44.0f, 12.0f}, {64.0f, 32.0f, 45.0f, 15.0f}, {55.0f, 23.0f, 40.0f, 13.0f}, + {66.0f, 30.0f, 44.0f, 14.0f}, {68.0f, 28.0f, 48.0f, 14.0f}, {58.0f, 27.0f, 41.0f, 10.0f}, {54.0f, 30.0f, 45.0f, 15.0f}, + {56.0f, 25.0f, 39.0f, 11.0f}, {62.0f, 22.0f, 45.0f, 15.0f}, {65.0f, 28.0f, 46.0f, 15.0f}, {58.0f, 26.0f, 40.0f, 12.0f}, + {57.0f, 29.0f, 42.0f, 13.0f}, {59.0f, 32.0f, 48.0f, 18.0f}, {70.0f, 32.0f, 47.0f, 14.0f}, {60.0f, 29.0f, 45.0f, 15.0f}, + {57.0f, 26.0f, 35.0f, 10.0f}, {67.0f, 30.0f, 50.0f, 17.0f} + }; + } + + /** + * Returns Iris virginica data + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * + * @return Iris versicolor data + */ + private float[][] getIrisVirginicaDataMatrix() { + return new + float[][] { + {63.0f, 33.0f, 60.0f, 25.0f}, {65.0f, 30.0f, 52.0f, 20.0f}, {58.0f, 28.0f, 51.0f, 24.0f}, {68.0f, 30.0f, 55.0f, 21.0f}, + {67.0f, 31.0f, 56.0f, 24.0f}, {63.0f, 28.0f, 51.0f, 15.0f}, {69.0f, 31.0f, 51.0f, 23.0f}, {64.0f, 27.0f, 53.0f, 19.0f}, + {69.0f, 31.0f, 54.0f, 21.0f}, {72.0f, 36.0f, 61.0f, 25.0f}, {57.0f, 25.0f, 50.0f, 20.0f}, {65.0f, 32.0f, 51.0f, 20.0f}, + {65.0f, 30.0f, 58.0f, 22.0f}, {62.0f, 34.0f, 54.0f, 23.0f}, {64.0f, 28.0f, 56.0f, 21.0f}, {61.0f, 26.0f, 56.0f, 14.0f}, + {64.0f, 28.0f, 56.0f, 22.0f}, {77.0f, 30.0f, 61.0f, 23.0f}, {67.0f, 30.0f, 52.0f, 23.0f}, {62.0f, 28.0f, 48.0f, 18.0f}, + {59.0f, 30.0f, 51.0f, 18.0f}, {63.0f, 25.0f, 50.0f, 19.0f}, {72.0f, 30.0f, 58.0f, 16.0f}, {76.0f, 30.0f, 66.0f, 21.0f}, + {64.0f, 32.0f, 53.0f, 23.0f}, {61.0f, 30.0f, 49.0f, 18.0f}, {79.0f, 38.0f, 64.0f, 20.0f}, {72.0f, 32.0f, 60.0f, 18.0f}, + {63.0f, 27.0f, 49.0f, 18.0f}, {77.0f, 28.0f, 67.0f, 20.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {67.0f, 25.0f, 58.0f, 18.0f}, + {49.0f, 25.0f, 45.0f, 17.0f}, {67.0f, 33.0f, 57.0f, 21.0f}, {77.0f, 38.0f, 67.0f, 22.0f}, {56.0f, 28.0f, 49.0f, 20.0f}, + {65.0f, 30.0f, 55.0f, 18.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {74.0f, 28.0f, 61.0f, 19.0f}, {69.0f, 32.0f, 57.0f, 23.0f}, + {68.0f, 32.0f, 59.0f, 23.0f}, {73.0f, 29.0f, 63.0f, 18.0f}, {71.0f, 30.0f, 59.0f, 21.0f}, {60.0f, 22.0f, 50.0f, 15.0f}, + {77.0f, 26.0f, 69.0f, 23.0f}, {67.0f, 33.0f, 57.0f, 25.0f}, {63.0f, 29.0f, 56.0f, 18.0f}, {60.0f, 30.0f, 48.0f, 18.0f}, + {64.0f, 31.0f, 55.0f, 18.0f}, {63.0f, 34.0f, 56.0f, 24.0f} + }; + } + // + +} diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 7e79047..91e0d4f 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -173,6 +173,62 @@ public void test_Development_CombinedGaussianCouldData() { } } + /** + * Test method for development purposes only + */ + @Test + public void test_Development_CombinedGaussianCouldData_Performance() { + System.out.println("--------------------------------------------------------"); + System.out.println("test_Development_CombinedGaussianCouldData_Performance()"); + System.out.println("--------------------------------------------------------"); + int tmpNumberOfDimensions = 100; + int tmpNumberOfGaussianCloudVectors = 1000; + float tmpStandardDeviation = 0.01f; + Random tmpRandomNumberGenerator = new Random(1L); + float[][] tmpCombinedGaussianCloudDataMatrix = + this.getCombinedGaussianCloudMatrix( + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, + tmpRandomNumberGenerator + ); + + float tmpVigilance = 0.1f; + int tmpMaximumNumberOfClusters = 200; + boolean tmpIsDataPreprocessing = true; + int tmpMaximumNumberOfEpochs = 10; + float tmpConvergenceThreshold = 0.99f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + long tmpStart = System.currentTimeMillis(); + Art2aKernel tmpArt2aKernel = + new Art2aKernel( + tmpCombinedGaussianCloudDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aResult tmpArt2aResult = null; + try { + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + long tmpEnd = System.currentTimeMillis(); + + System.out.println(" Number of data vectors = " + String.valueOf(tmpNumberOfDimensions * tmpNumberOfGaussianCloudVectors)); + System.out.println(" Elapsed time in ms = " + String.valueOf(tmpEnd - tmpStart)); + int tmpNumberOfDetectedClusters = tmpArt2aResult.getNumberOfDetectedClusters(); + System.out.println(" Number of detected clusters = " + String.valueOf(tmpArt2aResult.getNumberOfDetectedClusters())); + System.out.println(" Number of epochs = " + String.valueOf(tmpArt2aResult.getNumberOfEpochs())); + } + /** * Test method for development purposes only */ From 28342d93a6f0f97228dc84587d5934c2dddd2258 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Sun, 9 Feb 2025 17:47:41 +0100 Subject: [PATCH 07/18] ART-2a and ART-2a-Euclid classes consolidated --- .../cheminf/clustering/art2a/Art2aData.java | 17 +- .../clustering/art2a/Art2aEuclidData.java | 19 +- .../clustering/art2a/Art2aEuclidKernel.java | 166 +--- .../clustering/art2a/Art2aEuclidResult.java | 21 +- .../clustering/art2a/Art2aEuclidUtils.java | 658 +------------- .../cheminf/clustering/art2a/Art2aKernel.java | 168 +--- .../cheminf/clustering/art2a/Art2aResult.java | 14 +- .../cheminf/clustering/art2a/Art2aUtils.java | 714 +-------------- .../cheminf/clustering/art2a/Utils.java | 848 ++++++++++++++++++ .../clustering/art2a/Art2aEuclidTest.java | 15 +- .../cheminf/clustering/art2a/Art2aTest.java | 8 +- 11 files changed, 995 insertions(+), 1653 deletions(-) create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java index da31c2b..b00b9cb 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java @@ -75,7 +75,7 @@ public class Art2aData { * Min-max components of original data matrix (see method * Art2aUtils.getMinMaxComponents() for data structure) */ - private final Art2aUtils.MinMaxValue[] minMaxComponentsOfDataMatrix; + private final Utils.MinMaxValue[] minMaxComponentsOfDataMatrix; /** * Offset for contrast enhancement */ @@ -114,7 +114,7 @@ private Art2aData ( float[][] aDataMatrix, float[][] aContrastEnhancedUnitMatrix, boolean[] aDataVectorZeroLengthFlags, - Art2aUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement, boolean aHasPreprocessedData ) { @@ -140,7 +140,7 @@ private Art2aData ( */ protected Art2aData ( float[][] aDataMatrix, - Art2aUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement ) { this ( @@ -151,7 +151,7 @@ protected Art2aData ( anOffsetForContrastEnhancement, false ); - if (!Art2aUtils.isMatrixValid(aDataMatrix)) { + if (!Utils.isMatrixValid(aDataMatrix)) { Art2aData.LOGGER.log( Level.SEVERE, "Art2aData.Constructor: aDataMatrix is invalid." @@ -193,7 +193,7 @@ protected Art2aData ( protected Art2aData ( float[][] aContrastEnhancedUnitMatrix, boolean[] aDataVectorZeroLengthFlags, - Art2aUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement ) { this ( @@ -204,7 +204,7 @@ protected Art2aData ( anOffsetForContrastEnhancement, true ); - if (!Art2aUtils.isMatrixValid(aContrastEnhancedUnitMatrix)) { + if (!Utils.isMatrixValid(aContrastEnhancedUnitMatrix)) { Art2aData.LOGGER.log( Level.SEVERE, "Art2aData.Constructor: aContrastEnhancedUnitMatrix is invalid." @@ -269,12 +269,11 @@ protected boolean[] getDataVectorZeroLengthFlags() { } /** - * Min-max components of original data matrix (see method - * Art2aUtils.getMinMaxComponents() for data structure) + * Min-max components of original data matrix (see method Utils.getMinMaxComponents() for data structure) * * @return Min-max components of original data matrix */ - protected Art2aUtils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { + protected Utils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { return this.minMaxComponentsOfDataMatrix; } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java index d1df8fa..4c436e3 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java @@ -74,9 +74,9 @@ public class Art2aEuclidData { private final boolean[] dataVectorZeroLengthFlags; /** * Min-max components of original data matrix (see method - * Art2aEuclidUtils.getMinMaxComponents() for data structure) + * Utils.getMinMaxComponents() for data structure) */ - private final Art2aEuclidUtils.MinMaxValue[] minMaxComponentsOfDataMatrix; + private final Utils.MinMaxValue[] minMaxComponentsOfDataMatrix; /** * Offset for contrast enhancement */ @@ -115,7 +115,7 @@ private Art2aEuclidData ( float[][] aDataMatrix, float[][] aContrastEnhancedMatrix, boolean[] aDataVectorZeroLengthFlags, - Art2aEuclidUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement, boolean aHasPreprocessedData ) { @@ -141,7 +141,7 @@ private Art2aEuclidData ( */ protected Art2aEuclidData ( float[][] aDataMatrix, - Art2aEuclidUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement ) { this ( @@ -152,7 +152,7 @@ protected Art2aEuclidData ( anOffsetForContrastEnhancement, false ); - if (!Art2aEuclidUtils.isMatrixValid(aDataMatrix)) { + if (!Utils.isMatrixValid(aDataMatrix)) { Art2aEuclidData.LOGGER.log( Level.SEVERE, "Art2aEuclidData.Constructor: aDataMatrix is invalid." @@ -194,7 +194,7 @@ protected Art2aEuclidData ( protected Art2aEuclidData ( float[][] aContrastEnhancedMatrix, boolean[] aDataVectorZeroLengthFlags, - Art2aEuclidUtils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement ) { this ( @@ -205,7 +205,7 @@ protected Art2aEuclidData ( anOffsetForContrastEnhancement, true ); - if (!Art2aEuclidUtils.isMatrixValid(aContrastEnhancedMatrix)) { + if (!Utils.isMatrixValid(aContrastEnhancedMatrix)) { Art2aEuclidData.LOGGER.log( Level.SEVERE, "Art2aEuclidData.Constructor: aContrastEnhancedMatrix is invalid." @@ -270,12 +270,11 @@ protected boolean[] getDataVectorZeroLengthFlags() { } /** - * Min-max components of original data matrix (see method - Art2aEuclidUtils.getMinMaxComponents() for data structure) + * Min-max components of original data matrix (see method Utils.getMinMaxComponents() for data structure) * * @return Min-max components of original data matrix */ - protected Art2aEuclidUtils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { + protected Utils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { return this.minMaxComponentsOfDataMatrix; } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index 706e535..dc1ffa1 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -32,11 +32,12 @@ import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; +import static java.util.concurrent.Executors.newFixedThreadPool; + /** * ART-2a-Euclid algorithm implementation for unsupervised, open categorical * clustering. @@ -290,7 +291,7 @@ public Art2aEuclidKernel( boolean anIsDataPreprocessing ) throws IllegalArgumentException { // - if(!Art2aEuclidKernel.isDataMatrixValid(aDataMatrix)) { + if(!Utils.isDataMatrixValid(aDataMatrix)) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, "Art2aEuclidKernel.Constructor: aDataMatrix is not valid." @@ -336,7 +337,7 @@ public Art2aEuclidKernel( if(anIsDataPreprocessing) { this.art2aEuclidData = - Art2aEuclidKernel.getArt2aEuclidData( + Art2aEuclidKernel.getPreprocessedArt2aEuclidData( aDataMatrix, anOffsetForContrastEnhancement ); @@ -344,7 +345,7 @@ public Art2aEuclidKernel( this.art2aEuclidData = new Art2aEuclidData( aDataMatrix, - Art2aEuclidUtils.getMinMaxComponents(aDataMatrix), + Utils.getMinMaxComponents(aDataMatrix), anOffsetForContrastEnhancement ); } @@ -370,10 +371,8 @@ public Art2aEuclidKernel( float[][] aDataMatrix ) throws IllegalArgumentException { this( - aDataMatrix, - (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? - (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) : - 2, + aDataMatrix, + Math.max((int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -466,10 +465,8 @@ public Art2aEuclidKernel( Art2aEuclidData anArt2aEuclidData ) throws IllegalArgumentException { this( - anArt2aEuclidData, - (int) (anArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? - (int) (anArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) : - 2, + anArt2aEuclidData, + Math.max((int) (anArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -514,25 +511,25 @@ public Art2aEuclidResult getClusterResult( int tmpNumberOfComponents = -1; int tmpNumberOfDataVectors = -1; if (this.art2aEuclidData.hasPreprocessedData()) { - tmpContrastEnhancedMatrix = (float[][]) this.art2aEuclidData.getContrastEnhancedMatrix(); - tmpDataVectorZeroLengthFlags = (boolean[]) this.art2aEuclidData.getDataVectorZeroLengthFlags(); + tmpContrastEnhancedMatrix = this.art2aEuclidData.getContrastEnhancedMatrix(); + tmpDataVectorZeroLengthFlags = this.art2aEuclidData.getDataVectorZeroLengthFlags(); tmpNumberOfDataVectors = tmpContrastEnhancedMatrix.length; tmpNumberOfComponents = tmpContrastEnhancedMatrix[0].length; } else { tmpDataMatrix = this.art2aEuclidData.getDataMatrix(); tmpDataVectorZeroLengthFlags = new boolean[tmpDataMatrix.length]; - Art2aEuclidUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + Utils.fillVector(tmpDataVectorZeroLengthFlags, false); tmpNumberOfDataVectors = tmpDataMatrix.length; tmpNumberOfComponents = tmpDataMatrix[0].length; } - Art2aEuclidUtils.MinMaxValue[] tmpMinMaxComponents = this.art2aEuclidData.getMinMaxComponentsOfDataMatrix(); + Utils.MinMaxValue[] tmpMinMaxComponents = this.art2aEuclidData.getMinMaxComponentsOfDataMatrix(); // Set tmpRhoStar float tmpRhoStar = tmpNumberOfComponents * (ONE - aVigilance); // Definitions float tmpThresholdForContrastEnhancement = - Art2aEuclidUtils.getThresholdForContrastEnhancement( + Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, this.art2aEuclidData.getOffsetForContrastEnhancement() ); @@ -550,7 +547,7 @@ public Art2aEuclidResult getClusterResult( // Initialize cluster indices for data row vectors with -1 to // indicate missing cluster assignment int[] tmpClusterIndexOfDataVector = new int[tmpNumberOfDataVectors]; - Art2aEuclidUtils.fillVector(tmpClusterIndexOfDataVector, -1); + Utils.fillVector(tmpClusterIndexOfDataVector, -1); // Initialize random indices int[] tmpRandomIndices = new int[tmpNumberOfDataVectors]; @@ -564,14 +561,14 @@ public Art2aEuclidResult getClusterResult( // Main clustering loop int tmpCurrentNumberOfEpochs = 0; int tmpNumberOfDetectedClusters = 0; - Art2aEuclidUtils.RhoWinner tmpRhoWinner = new Art2aEuclidUtils.RhoWinner(); - Art2aEuclidUtils.ClusterRemovalInfo tmpClusterRemovalInfo = new Art2aEuclidUtils.ClusterRemovalInfo(); + Utils.RhoWinner tmpRhoWinner = new Utils.RhoWinner(); + Utils.ClusterRemovalInfo tmpClusterRemovalInfo = new Utils.ClusterRemovalInfo(); boolean tmpIsConverged = false; while(!tmpIsConverged && tmpCurrentNumberOfEpochs < this.maximumNumberOfEpochs) { tmpCurrentNumberOfEpochs++; // Get random sequence of indices for data row vectors - Art2aEuclidUtils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); + Utils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); Arrays.fill(tmpClusterUsageFlags, false); for(int i = 0; i < tmpNumberOfDataVectors; i++) { @@ -583,7 +580,7 @@ public Art2aEuclidResult getClusterResult( } if (this.art2aEuclidData.hasPreprocessedData()) { - Art2aEuclidUtils.copyVector(tmpContrastEnhancedMatrix[tmpRandomIndex], tmpBufferVector); + Utils.copyVector(tmpContrastEnhancedMatrix[tmpRandomIndex], tmpBufferVector); } else { tmpDataVectorZeroLengthFlags[tmpRandomIndex] = Art2aEuclidUtils.setContrastEnhancedVector( @@ -599,7 +596,7 @@ public Art2aEuclidResult getClusterResult( if(tmpNumberOfDetectedClusters == 0) { // Create first cluster - Art2aEuclidUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + Utils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; tmpNumberOfDetectedClusters++; @@ -619,7 +616,7 @@ public Art2aEuclidResult getClusterResult( tmpIsClusterOverflow = true; } else { // Increment clusters - Art2aEuclidUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + Utils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; tmpNumberOfDetectedClusters++; @@ -640,7 +637,7 @@ public Art2aEuclidResult getClusterResult( } } - Art2aEuclidUtils.removeEmptyClusters( + Utils.removeEmptyClusters( tmpClusterUsageFlags, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -675,7 +672,7 @@ public Art2aEuclidResult getClusterResult( tmpClusterUsageFlags ); // Remove possible empty clusters - Art2aEuclidUtils.removeEmptyClusters( + Utils.removeEmptyClusters( tmpClusterUsageFlags, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -697,7 +694,7 @@ public Art2aEuclidResult getClusterResult( tmpClusterIndexOfDataVector, tmpClusterUsageFlags ); - Art2aEuclidUtils.removeEmptyClusters( + Utils.removeEmptyClusters( tmpClusterUsageFlags, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -727,7 +724,7 @@ public Art2aEuclidResult getClusterResult( anException.toString(), anException ); - throw anException; + throw new Exception("Art2aEuclidKernel.getClusterResult: An exception occurred: This should never happen!"); } } @@ -780,7 +777,7 @@ public Art2aEuclidResult[] getClusterResults( for (float tmpVigilance : aVigilances) { tmpSingleTaskList.add(new HelperTask(this, tmpVigilance)); } - ExecutorService tmpExecutorService = Executors.newFixedThreadPool(aNumberOfConcurrentCalculationThreads); + ExecutorService tmpExecutorService = newFixedThreadPool(aNumberOfConcurrentCalculationThreads); List> tmpFutureList = null; try { tmpFutureList = tmpExecutorService.invokeAll(tmpSingleTaskList); @@ -966,7 +963,7 @@ public int[] getBestRepresentatives( for (int i = 0; i < tmpAllIndices.length; i++) { tmpAllIndices[i] = i; } - float tmpBaseMeanDistance = Art2aEuclidUtils.getMeanDistance(aDataMatrix, tmpAllIndices); + float tmpBaseMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpAllIndices); float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -982,7 +979,7 @@ public int[] getBestRepresentatives( tmpVigilanceMax, tmpNumberOfTrialSteps ); - float tmpMeanDistance = Art2aEuclidUtils.getMeanDistance(aDataMatrix, tmpRepresentatives); + float tmpMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpRepresentatives); float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); if (tmpDifference < tmpMinimalDifference) { tmpMinimalDifference = tmpDifference; @@ -1005,120 +1002,31 @@ public int[] getBestRepresentatives( } // // - /** - * Checks if aDataMatrix is valid. - * - * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) - * @return True if aDataMatrix is valid, false otherwise. - */ - public static boolean isDataMatrixValid( - float[][] aDataMatrix - ) { - if(aDataMatrix == null || aDataMatrix.length == 0) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.isDataMatrixValid: aDataMatrixis is null or empty." - ); - return false; - } - - int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; - if(tmpNumberOfDataVectorComponents < 2) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.isDataMatrixValid: Data row vectors must have at least 2 components." - ); - return false; - } - - for(float[] tmpDataVector : aDataMatrix) { - if(tmpDataVector == null || tmpDataVector.length == 0) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.isDataMatrixValid: A data row vector of aDataMatrix is not allowed to be null or empty." - ); - return false; - } - - if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.isDataMatrixValid: Data row vectors in aDataMatrix must have the same length." - ); - return false; - } - } - if (Art2aEuclidUtils.hasNonFiniteComponent(aDataMatrix)) { - return false; - } - return true; - } - - /** - * Removes columns from data matrix with non-finite components. - * Note: If aDataMatrix is null, empty or has an invalid structure - * nothing is done and false is returned. - * - * @param aDataMatrix Data matrix with data row vectors (MAY BE CHANGED) - * @return True if aDataMatrix was changed (i.e. column removal was - * performed), false otherwise (i.e. data matrix is unchanged). - */ - public static boolean isNonFiniteComponentRemoval( - float[][] aDataMatrix - ) { - // - if(aDataMatrix == null || aDataMatrix.length == 0) { - return false; - } - - int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; - if(tmpNumberOfDataVectorComponents < 2) { - return false; - } - - for(float[] tmpDataVector : aDataMatrix) { - if(tmpDataVector == null || tmpDataVector.length == 0) { - return false; - } - - if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { - return false; - } - } - // - - boolean tmpHasNonFiniteComponent = Art2aEuclidUtils.hasNonFiniteComponent(aDataMatrix); - if (tmpHasNonFiniteComponent) { - // TODO: Remove columns with non-finite components - } - return tmpHasNonFiniteComponent; - } - /** * Creates ART-2a-Euclid data object with preprocessed data for maximum * speed of the clustering process. The ART-2a-Euclid data object allocates * about the same memory as aDataMatrix. *
* Note: There a no checks! Check aDataMatrix in advance with method - * Art2aEuclidKernel.isDataMatrixValid(). + * Utils.isDataMatrixValid(). *
* Note: aDataMatrix could be set to null after this operation to release * its memory. * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check - * with Art2aEuclidKernel.isDataMatrixValid() in advance) + * with Utils.isDataMatrixValid() in advance) * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @return ART-2a-Euclid data object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aEuclidData getArt2aEuclidData( + public static Art2aEuclidData getPreprocessedArt2aEuclidData( float[][] aDataMatrix, float anOffsetForContrastEnhancement ) { int tmpNumberOfComponents = aDataMatrix[0].length; float tmpThresholdForContrastEnhancement = - Art2aEuclidUtils.getThresholdForContrastEnhancement( + Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, anOffsetForContrastEnhancement ); @@ -1126,11 +1034,11 @@ public static Art2aEuclidData getArt2aEuclidData( // Initialize flags array for scaled data row vectors which have a // length of zero (i.e. where all components are equal to zero) boolean[] tmpDataVectorZeroLengthFlags = new boolean[aDataMatrix.length]; - Art2aEuclidUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + Utils.fillVector(tmpDataVectorZeroLengthFlags, false); float[][] tmpContrastEnhancedMatrix = new float[aDataMatrix.length][]; - Art2aEuclidUtils.MinMaxValue[] tmpMinMaxComponents = Art2aEuclidUtils.getMinMaxComponents(aDataMatrix); + Utils.MinMaxValue[] tmpMinMaxComponents = Utils.getMinMaxComponents(aDataMatrix); for(int i = 0; i < aDataMatrix.length; i++) { float[] tmpContrastEnhancedVector = new float[tmpNumberOfComponents]; @@ -1161,14 +1069,14 @@ public static Art2aEuclidData getArt2aEuclidData( * its memory. * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check - * with Art2aEuclidKernel.isDataMatrixValid() in advance) + * with Utils.isDataMatrixValid() in advance) * @return ART-2a-Euclid data object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aEuclidData getArt2aEuclidData( + public static Art2aEuclidData getPreprocessedArt2aEuclidData( float[][] aDataMatrix ) { - return Art2aEuclidKernel.getArt2aEuclidData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); + return Art2aEuclidKernel.getPreprocessedArt2aEuclidData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); } //
diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java index 44b93b0..16f02b7 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java @@ -27,7 +27,6 @@ package de.unijena.cheminf.clustering.art2a; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; @@ -51,12 +50,6 @@ public class Art2aEuclidResult { */ private static final Logger LOGGER = Logger.getLogger(Art2aEuclidResult.class.getName()); // - // - /** - * Conversion constant from radiant to degree - */ - private static final float CONVERSION_TO_DEGREE = 180.0f / (float) Math.PI; - // // /** * Cluster index of data vector @@ -217,7 +210,7 @@ public float[] getScaledClusterVector( throw new IllegalArgumentException("Art2aEuclidResult.getClusterVector: aClusterIndex is illegal."); } // - return Art2aEuclidUtils.getScaledVector(this.clusterMatrix[aClusterIndex]); + return Utils.getScaledVector(this.clusterMatrix[aClusterIndex]); } /** @@ -306,7 +299,7 @@ public float getDistanceBetweenClusters( } else { return (float) Math.sqrt( - Art2aEuclidUtils.getSquaredDistance( + Utils.getSquaredDistance( this.clusterMatrix[aClusterIndex1], this.clusterMatrix[aClusterIndex2] ) @@ -412,7 +405,7 @@ public int getClusterRepresentativeIndex( this.thresholdForContrastEnhancement ); } - float tmpSquaredDistance = Art2aEuclidUtils.getSquaredDistance(tmpContrastEnhancedVector, tmpClusterVector); + float tmpSquaredDistance = Utils.getSquaredDistance(tmpContrastEnhancedVector, tmpClusterVector); if (tmpSquaredDistance < tmpMinimumDistance) { tmpBestIndex = tmpIndex; tmpMinimumDistance = tmpSquaredDistance; @@ -467,7 +460,7 @@ public int[] getClusterRepresentativeIndices( this.thresholdForContrastEnhancement ); } - tmpIndexedValues[i] = new IndexedValue(tmpIndex, Art2aEuclidUtils.getSquaredDistance(tmpContrastEnhancedVector, tmpClusterVector)); + tmpIndexedValues[i] = new IndexedValue(tmpIndex, Utils.getSquaredDistance(tmpContrastEnhancedVector, tmpClusterVector)); } // NOTE: SMALLEST squared distance FIRST! Arrays.sort(tmpIndexedValues); @@ -498,7 +491,7 @@ public int[] getRepresentativeIndicesOfClusters() { */ public float getVigilance() { return this.vigilance; - }; + } /** * Number of epochs @@ -507,7 +500,7 @@ public float getVigilance() { */ public int getNumberOfEpochs() { return this.numberOfEpochs; - }; + } /** * Number of detected clusters @@ -516,7 +509,7 @@ public int getNumberOfEpochs() { */ public int getNumberOfDetectedClusters() { return this.numberOfDetectedClusters; - }; + } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java index 1fd3aa6..fe8a060 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java @@ -27,8 +27,6 @@ package de.unijena.cheminf.clustering.art2a; import java.util.Arrays; -import java.util.LinkedList; -import java.util.Random; /** * Library of helper records, static helper classes and static, thread-safe @@ -39,158 +37,14 @@ * @author Achim Zielesny */ public class Art2aEuclidUtils { - + // /** * Value 1.0 */ private static final float ONE = 1.0f; // - // - /** - * Helper record: Minimum and maximum value. - *

- * Note: No checks are performed. - * - * @param minValue Minimum value - * @param maxValue Maximum value - */ - protected record MinMaxValue(float minValue, float maxValue) { - - /** - * Constructor - * - * @param minValue Minimum value - * @param maxValue Maximum value - */ - public MinMaxValue {} - - } - //
- // - /** - * Helper class: Rho winner. - *

- * Note: No checks are performed. - */ - protected static class RhoWinner { - - // - /** - * Rho value - */ - private float rhoValue; - /** - * Index of cluster - */ - private int indexOfCluster; - // - - // - /** - * Constructor - */ - protected RhoWinner() {} - // - - // - /** - * Set rho winner - * - * @param aRhoValue Rho value - * @param anIndexOfCluster Index of cluster - */ - protected void setRhoWinner( - float aRhoValue, - int anIndexOfCluster - ) { - this.rhoValue = aRhoValue; - this.indexOfCluster = anIndexOfCluster; - } - - /** - * Rho value - * - * @return Rho value - */ - protected float getRhoValue() { - return this.rhoValue; - } - - /** - * Index of cluster - * - * @return Index of cluster - */ - protected int getIndexOfCluster() { - return this.indexOfCluster; - } - // - - } - - /** - * Helper class: Cluster removal info. - *

- * Note: No checks are performed. - */ - protected static class ClusterRemovalInfo { - - // - /** - * True: Cluster is removed, false: Otherwise - */ - private boolean isClusterRemoved; - /** - * Number of detected clusters - */ - private int numberOfDetectedClusters; - // - - // - /** - * Constructor - */ - protected ClusterRemovalInfo() {} - // - - // - /** - * Set cluster removal info - * - * @param anIsClusterRemoved True: Cluster is removed, false: Otherwise - * @param aNumberOfDetectedClusters Number of detected clusters - */ - protected void setClusterRemovalInfo( - boolean anIsClusterRemoved, - int aNumberOfDetectedClusters - ) { - this.isClusterRemoved = anIsClusterRemoved; - this.numberOfDetectedClusters = aNumberOfDetectedClusters; - } - - /** - * True: Cluster is removed, false: Otherwise - * - * @return True: Cluster is removed, false: Otherwise - */ - protected boolean isClusterRemoved() { - return this.isClusterRemoved; - } - /** - * Number of detected clusters - * - * @return Number of detected clusters - */ - protected int getNumberOfDetectedClusters() { - return this.numberOfDetectedClusters; - } - // - - } - //
- // /** * Constructor @@ -255,152 +109,6 @@ protected static void assignDataVectorsToClusters( } } } - - /** - * (Deep) Copies source matrix to destination matrix. Row vectors of - * destination matrix may not have been instantiated. - * - * @param aSourceMatrix Source matrix (IS NOT CHANGED) - * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN - * INSTANTIATED and MAY BE CHANGED) - */ - protected static void copyMatrix( - float[][] aSourceMatrix, - float[][] aDestinationMatrix - ) { - for (int i = 0; i < aSourceMatrix.length; i++) { - if (aDestinationMatrix[i] == null) { - aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; - } - System.arraycopy( - aSourceMatrix[i], - 0, - aDestinationMatrix[i], - 0, - aSourceMatrix[i].length - ); - } - } - - /** - * (Deep) Copies specified number of rows of source matrix to destination - * matrix. Row vectors of destination matrix may not have been instantiated. - * - * @param aSourceMatrix Source matrix (IS NOT CHANGED) - * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN - * INSTANTIATED and MAY BE CHANGED) - * @param aNumberOfRows Number of rows to be copied from source matrix to - * destination matrix - */ - protected static void copyRows( - float[][] aSourceMatrix, - float[][] aDestinationMatrix, - int aNumberOfRows - ) { - for (int i = 0; i < aNumberOfRows; i++) { - if (aDestinationMatrix[i] == null) { - aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; - } - System.arraycopy( - aSourceMatrix[i], - 0, - aDestinationMatrix[i], - 0, - aSourceMatrix[i].length - ); - } - } - - /** - * (Deep) Copies source vector to destination vector. - * - * @param aSourceVector Source vector (IS NOT CHANGED) - * @param aDestinationVector Destination vector (MUST HAVE BEEN - * INSTANTIATED and MAY BE CHANGED) - */ - protected static void copyVector( - float[] aSourceVector, - float[] aDestinationVector - ) { - System.arraycopy( - aSourceVector, - 0, - aDestinationVector, - 0, - aSourceVector.length - ); - } - - /** - * Calculates contrast enhanced vector. - * - * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) - * @param aThresholdForContrastEnhancement Threshold for contrast enhancement - */ - protected static void enhanceContrast( - float[] aVector, - float aThresholdForContrastEnhancement - ) { - for(int i = 0; i < aVector.length; i++) { - if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { - aVector[i] = 0.0f; - } - } - } - - /** - * Fills matrix with value. - * - * @param aMatrix Matrix (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillMatrix( - float[][] aMatrix, - float aValue - ) { - for (float [] tmpRowVector : aMatrix) { - Arrays.fill(tmpRowVector , aValue); - } - } - - /** - * Fills vector with value. - * - * @param aVector Vector (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillVector( - float[] aVector, - float aValue - ) { - Arrays.fill(aVector , aValue); - } - - /** - * Fills vector with value. - * - * @param aVector Vector (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillVector( - boolean[] aVector, - boolean aValue - ) { - Arrays.fill(aVector , aValue); - } - - /** - * Fills vector with value. - * - * @param aVector Vector (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillVector( - int[] aVector, - int aValue - ) { - Arrays.fill(aVector , aValue); - } /** * Returns index of cluster for contrast enhanced vector @@ -418,7 +126,7 @@ protected static int getClusterIndex( float tmpMinSquaredDistance = Float.MAX_VALUE; int tmpWinnerClusterIndex = -1; for (int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpSquaredDistance = Art2aEuclidUtils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); + float tmpSquaredDistance = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); if (tmpSquaredDistance < tmpMinSquaredDistance) { tmpMinSquaredDistance = tmpSquaredDistance; tmpWinnerClusterIndex = i; @@ -426,196 +134,6 @@ protected static int getClusterIndex( } return tmpWinnerClusterIndex; } - - /** - * Returns mean distance of all specified row vectors. - * - * @param aMatrix Matrix with row vectors (IS NOT CHANGED) - * @param anIndicesOfRowVectors Indices of row vectors of aMatrix - * @return Mean squared distance of all specified row vectors. - */ - protected static float getMeanDistance( - float[][] aMatrix, - int[] anIndicesOfRowVectors - ) { - float tmpSum = 0.0f; - for (int i = 0; i < anIndicesOfRowVectors.length; i++) { - for (int j = i + 1; j < anIndicesOfRowVectors.length; j++) { - tmpSum += (float) Math.sqrt(Art2aEuclidUtils.getSquaredDistance(aMatrix[anIndicesOfRowVectors[i]], aMatrix[anIndicesOfRowVectors[j]])); - } - } - return tmpSum / (float) (anIndicesOfRowVectors.length * (anIndicesOfRowVectors.length - 1) / 2); - } - - /** - * Returns min-max components for matrix where MinMaxValue[j] - * corresponds to column j of the row vectors of the matrix. The min-max - * components may be used to scale row vectors to interval [0,1], see - * method scaleVector(). - * - * @param aMatrix Matrix (IS NOT CHANGED) - * @return Min-max components - */ - protected static MinMaxValue[] getMinMaxComponents( - float[][] aMatrix - ) { - MinMaxValue[] tmpMinMaxComponents = new MinMaxValue[aMatrix[0].length]; - for (int j = 0; j < aMatrix[0].length; j++) { - float tmpMinValue = aMatrix[0][j]; - float tmpMaxValue = aMatrix[0][j]; - for (int i = 1; i < aMatrix.length; i++) { - if (aMatrix[i][j] < tmpMinValue) { - tmpMinValue = aMatrix[i][j]; - } else if (aMatrix[i][j] > tmpMaxValue) { - tmpMaxValue = aMatrix[i][j]; - } - } - tmpMinMaxComponents[j] = new MinMaxValue(tmpMinValue, tmpMaxValue); - } - return tmpMinMaxComponents; - } - - /** - * Calculates the squared distance between aVector1 and aVector2. - * - * @param aVector1 Vector 1 (IS NOT CHANGED) - * @param aVector2 Vector 2 (IS NOT CHANGED) - * @return Squared distance - */ - protected static float getSquaredDistance( - float[] aVector1, - float[] aVector2 - ) { - float tmpSum = 0.0f; - for (int i = 0; i < aVector1.length; i++) { - float tmpDelta = aVector1[i] - aVector2[i]; - // tmpSum += (aVector1[i] - aVector2[i])^2; - tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); - } - return tmpSum; - } - - /** - * Scales components of aVectorToBeScaled to interval [0,1]. - * - * @param aVectorToBeScaled Vector (IS NOT CHANGED) - * @return New scaled vector with components in interval [0,1] or new - * vector of length zero if all components of aVectorToBeScaled are the - * same. - */ - protected static float[] getScaledVector( - float[] aVectorToBeScaled - ) { - float tmpMinValue = aVectorToBeScaled[0]; - float tmpMaxValue = aVectorToBeScaled[0]; - for(int i = 0; i < aVectorToBeScaled.length; i++) { - if (aVectorToBeScaled[i] < tmpMinValue) { - tmpMinValue = aVectorToBeScaled[i]; - } else if (aVectorToBeScaled[i] > tmpMaxValue) { - tmpMaxValue = aVectorToBeScaled[i]; - } - } - float[] tmpScaledVector = new float[aVectorToBeScaled.length]; - if (tmpMinValue == tmpMaxValue) { - for(int i = 0; i < aVectorToBeScaled.length; i++) { - tmpScaledVector[i] = aVectorToBeScaled[i] - tmpMinValue; - } - } else { - float tmpDenominator = tmpMaxValue - tmpMinValue; - for(int i = 0; i < aVectorToBeScaled.length; i++) { - tmpScaledVector[i] = (aVectorToBeScaled[i] - tmpMinValue) / tmpDenominator; - } - } - return tmpScaledVector; - } - - /** - * Calculates the sum of squared differences between the components of the - * specified vector and a value. - * - * @param aVector Vector (IS NOT CHANGED) - * @param aValue Value - * @return Sum of squared differences between the components of the - * specified vector and a value. - */ - protected static float getSumOfSquaredDifferences( - float[] aVector, - float aValue - ) { - float tmpSum = 0.0f; - for (int i = 0; i < aVector.length; i++) { - float tmpDelta = aVector[i] - aValue; - // tmpSum += (aVector[i] - aValue)^2; - tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); - } - return tmpSum; - } - - /** - * Threshold for contrast enhancement - * - * @param aNumberOfComponents Number of components - * @param anOffsetForContrastEnhancement Offset for contrast enhancement - * @return Threshold for contrast enhancement - */ - protected static float getThresholdForContrastEnhancement( - int aNumberOfComponents, - float anOffsetForContrastEnhancement - ) { - // Original code: - // return (float) (1.0 / Math.sqrt(aNumberOfComponents + 1.0)); - return (float) (1.0 / Math.sqrt(aNumberOfComponents + anOffsetForContrastEnhancement)); - } - - /** - * Checks if vector has a length of zero (i.e. if all components are equal - * to zero). - * - * @param aVector Vector (IS NOT CHANGED) - * @return True: Vector has a length of zero, false: Otherwise - */ - protected static boolean hasLengthOfZero( - float[] aVector - ) { - for(float tmpComponent : aVector) { - if (tmpComponent != 0.0f) { - return false; - } - } - return true; - } - - /** - * Checks if data matrix has a non-finite component. - * Note: If aDataMatrix is null or empty nothing is done and false is - * returned. - * - * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) - * @return True: Data matrix has non-finite component, false: Otherwise - */ - protected static boolean hasNonFiniteComponent( - float[][] aDataMatrix - ) { - // - if(aDataMatrix == null || aDataMatrix.length == 0) { - return false; - } - for(float[] tmpDataVector : aDataMatrix) { - if(tmpDataVector == null || tmpDataVector.length == 0) { - return false; - } - } - // - - for(float[] tmpDataVector : aDataMatrix) { - for (float tmpComponent : tmpDataVector) { - if (!Float.isFinite(tmpComponent)) { - return true; - } - } - } - return false; - } /** * Determines convergence of clustering process. @@ -641,7 +159,7 @@ protected static boolean isConverged( ) { if (anEpoch == 1) { // Convergence check needs at least 2 epochs - Art2aEuclidUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); return false; } else { float tmpSquaredConvergenceThreshold = aConvergenceThreshold * aConvergenceThreshold; @@ -653,7 +171,7 @@ protected static boolean isConverged( for (int i = 0; i < aNumberOfDetectedClusters; i++) { if ( aClusterCentroidMatrixOld[i] == null || - Art2aEuclidUtils.getSquaredDistance( + Utils.getSquaredDistance( aClusterCentroidMatrix[i], aClusterCentroidMatrixOld[i] ) > tmpSquaredConvergenceThreshold @@ -663,38 +181,12 @@ protected static boolean isConverged( } } if(!tmpIsConverged) { - Art2aEuclidUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); } } return tmpIsConverged; } } - - /** - * Checks if matrix is valid. - * - * @param aMatrix Matrix - * @return True: Matrix is valid, false: Otherwise - */ - protected static boolean isMatrixValid( - float[][] aMatrix - ) { - if (aMatrix == null || aMatrix.length == 0) { - return false; - } - for (float[] tmpRowVector : aMatrix) { - if (tmpRowVector == null || tmpRowVector.length == 0) { - return false; - } - } - int tmpRowVectorLength = aMatrix[0].length; - for (int i = 1; i < aMatrix.length; i++) { - if (aMatrix[i].length != tmpRowVectorLength) { - return false; - } - } - return true; - } /** * Modifies winner cluster (see code). @@ -724,68 +216,9 @@ protected static void modifyWinnerCluster( for(int j = 0; j < aWinnerClusterVector.length; j++) { aContrastEnhancedVector[j] = aLearningParameter * aContrastEnhancedVector[j] + tmpFactor * aWinnerClusterVector[j]; } - Art2aEuclidUtils.copyVector(aContrastEnhancedVector, aWinnerClusterVector); - } - - /** - * Removes empty clusters from cluster matrix - * - * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, - * false: Cluster is empty and has to be removed (IS NOT CHANGED) - * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aClusterRemovalInfo Cluster removal info (is set according to the - * operations performed, IS CHANGED) - */ - protected static void removeEmptyClusters( - boolean[] aClusterUsageFlags, - float[][] aClusterMatrix, - int aNumberOfDetectedClusters, - ClusterRemovalInfo aClusterRemovalInfo - ) { - boolean tmpIsEmptyClusterRemoval = false; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if (!aClusterUsageFlags[i]) { - tmpIsEmptyClusterRemoval = true; - break; - } - } - if (tmpIsEmptyClusterRemoval) { - // Remove empty clusters from cluster matrix - LinkedList tmpClusterVectorList = new LinkedList<>(); - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if (aClusterUsageFlags[i]) { - tmpClusterVectorList.add(aClusterMatrix[i]); - aClusterMatrix[i] = null; - } - } - int tmpIndex = 0; - for (float[] tmpClusterVector : tmpClusterVectorList) { - aClusterMatrix[tmpIndex++] = tmpClusterVector; - } - aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); - } else { - aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); - } + Utils.copyVector(aContrastEnhancedVector, aWinnerClusterVector); } - /** - * Calculates contrast enhanced vector. - * - * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) - * @param aThresholdForContrastEnhancement Threshold for contrast enhancement - */ - protected static void setContrastEnhancement( - float[] aVector, - float aThresholdForContrastEnhancement - ) { - for(int i = 0; i < aVector.length; i++) { - if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { - aVector[i] = 0.0f; - } - } - } - /** * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). @@ -803,20 +236,20 @@ protected static void setContrastEnhancement( protected static boolean setContrastEnhancedVector( float[] aDataVector, float[] aBufferVector, - Art2aEuclidUtils.MinMaxValue[] aMinMaxComponents, + Utils.MinMaxValue[] aMinMaxComponents, float aThresholdForContrastEnhancement ) { // Already allocated memory of aBufferVector is reused - Art2aEuclidUtils.copyVector(aDataVector, aBufferVector); + Utils.copyVector(aDataVector, aBufferVector); // Scale components of vector to interval [0,1] - Art2aEuclidUtils.scaleVector(aBufferVector, aMinMaxComponents); + Utils.scaleVector(aBufferVector, aMinMaxComponents); // Check length - if (Art2aEuclidUtils.hasLengthOfZero(aBufferVector)) { + if (Utils.hasLengthOfZero(aBufferVector)) { // True: Scaled source vector has a length of zero return true; } else { // Enhance contrast - Art2aEuclidUtils.setContrastEnhancement(aBufferVector, aThresholdForContrastEnhancement); + Utils.setContrastEnhancement(aBufferVector, aThresholdForContrastEnhancement); // False: Scaled data vector has a length different from zero return false; } @@ -841,15 +274,15 @@ protected static void setRhoWinner( float[][] aClusterMatrix, int aNumberOfDetectedClusters, float aScalingFactor, - Art2aEuclidUtils.RhoWinner aRhoWinner + Utils.RhoWinner aRhoWinner ) { // Calculate first rho value - float tmpRhoValue = Art2aEuclidUtils.getSumOfSquaredDifferences(aContrastEnhancedVector, aScalingFactor); + float tmpRhoValue = Utils.getSumOfSquaredDifferences(aContrastEnhancedVector, aScalingFactor); // Set winner index to negative value int tmpIndex = -1; // Calculate other rho values for(int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpRhoForCluster = Art2aEuclidUtils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); + float tmpRhoForCluster = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); if(tmpRhoForCluster < tmpRhoValue) { tmpRhoValue = tmpRhoForCluster; tmpIndex = i; @@ -857,69 +290,6 @@ protected static void setRhoWinner( } aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); } - - /** - * Sets copied (!) row vector at index in matrix. - * - * @param aMatrix Matrix (MAY BE CHANGED) - * @param aRowVector Row vector (IS NOT CHANGED) - * @param anIndex Index of row vector in matrix - */ - protected static void setRowVector( - float[][] aMatrix, - float[] aRowVector, - int anIndex - ) { - float[] tmpNewMatrixRowVector = new float[aRowVector.length]; - Art2aEuclidUtils.copyVector(aRowVector, tmpNewMatrixRowVector); - aMatrix[anIndex] = tmpNewMatrixRowVector; - } - - /** - * Scales components of aVectorToBeScaled according to min-max components - * to interval [0,1] (see code and method getMinMaxComponents()). - * - * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) - * @param aMinMaxComponents Min-max components - */ - protected static void scaleVector( - float[] aVectorToBeScaled, - MinMaxValue[] aMinMaxComponents - ) { - for(int i = 0; i < aVectorToBeScaled.length; i++) { - if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { - // Scale component to interval [0,1] - aVectorToBeScaled[i] = - (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); - } else { - // Shift component to zero - aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); - } - } - } - - /** - * Randomly shuffles indices from 0 to (anIndices.Length - 1) in - * anIndexArray using Fisher-Yates shuffling (i.e. the modern version - * introduced by Richard Durstenfeld). - * Note: No checks are performed. - * - * @param anIndexArray Array with indices from 0 to (anIndices.Length - 1) - * @param aRandomNumberGenerator Random number generator - */ - protected static void shuffleIndices( - int[] anIndexArray, - Random aRandomNumberGenerator - ) { - for (int i = anIndexArray.length - 1; i > 0; i--) { - // Generate a random index between 0 and i (inclusive) - int j = aRandomNumberGenerator.nextInt(i + 1); - // Swap the elements at indices i and j - int tmpIntBuffer = anIndexArray[i]; - anIndexArray[i] = anIndexArray[j]; - anIndexArray[j] = tmpIntBuffer; - } - } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index c0ee1f7..0bdd70d 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -32,11 +32,12 @@ import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; +import static java.util.concurrent.Executors.newFixedThreadPool; + /** * ART-2a algorithm implementation for unsupervised, open categorical * clustering. @@ -93,7 +94,7 @@ * CAUTION: Construction of several ART-2a clustering instances with the SAME * data matrix PLUS preprocessing is NOT advised due to the significant memory * consumption of each instance. In this case, the data matrix should be - * checked with static method Art2aKernel.isDataMatrixValid() and then a priori + * checked with static method Utils.isDataMatrixValid() and then a priori * converted into a preprocessed Art2aData object with static method * Art2aKernel.getArt2aData(). The generated Art2aData object does NOT change * or refer to the data matrix so that the data matrix memory could be released @@ -283,7 +284,7 @@ public Art2aKernel( boolean anIsDataPreprocessing ) throws IllegalArgumentException { // - if(!Art2aKernel.isDataMatrixValid(aDataMatrix)) { + if(!Utils.isDataMatrixValid(aDataMatrix)) { Art2aKernel.LOGGER.log( Level.SEVERE, "Art2aKernel.Constructor: aDataMatrix is not valid." @@ -329,7 +330,7 @@ public Art2aKernel( if(anIsDataPreprocessing) { this.art2aData = - Art2aKernel.getArt2aData( + Art2aKernel.getPreprocessedArt2aData( aDataMatrix, anOffsetForContrastEnhancement ); @@ -337,7 +338,7 @@ public Art2aKernel( this.art2aData = new Art2aData( aDataMatrix, - Art2aUtils.getMinMaxComponents(aDataMatrix), + Utils.getMinMaxComponents(aDataMatrix), anOffsetForContrastEnhancement ); } @@ -363,10 +364,8 @@ public Art2aKernel( float[][] aDataMatrix ) throws IllegalArgumentException { this( - aDataMatrix, - (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? - (int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS) : - 2, + aDataMatrix, + Math.max((int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -459,10 +458,8 @@ public Art2aKernel( Art2aData anArt2aData ) throws IllegalArgumentException { this( - anArt2aData, - (int) (anArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) > 2 ? - (int) (anArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS) : - 2, + anArt2aData, + Math.max((int) (anArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -507,22 +504,22 @@ public Art2aResult getClusterResult( int tmpNumberOfComponents = -1; int tmpNumberOfDataVectors = -1; if (this.art2aData.hasPreprocessedData()) { - tmpContrastEnhancedUnitMatrix = (float[][]) this.art2aData.getContrastEnhancedUnitMatrix(); - tmpDataVectorZeroLengthFlags = (boolean[]) this.art2aData.getDataVectorZeroLengthFlags(); + tmpContrastEnhancedUnitMatrix = this.art2aData.getContrastEnhancedUnitMatrix(); + tmpDataVectorZeroLengthFlags = this.art2aData.getDataVectorZeroLengthFlags(); tmpNumberOfDataVectors = tmpContrastEnhancedUnitMatrix.length; tmpNumberOfComponents = tmpContrastEnhancedUnitMatrix[0].length; } else { tmpDataMatrix = this.art2aData.getDataMatrix(); tmpDataVectorZeroLengthFlags = new boolean[tmpDataMatrix.length]; - Art2aUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + Utils.fillVector(tmpDataVectorZeroLengthFlags, false); tmpNumberOfDataVectors = tmpDataMatrix.length; tmpNumberOfComponents = tmpDataMatrix[0].length; } - Art2aUtils.MinMaxValue[] tmpMinMaxComponents = this.art2aData.getMinMaxComponentsOfDataMatrix(); + Utils.MinMaxValue[] tmpMinMaxComponents = this.art2aData.getMinMaxComponentsOfDataMatrix(); // Definitions float tmpThresholdForContrastEnhancement = - Art2aUtils.getThresholdForContrastEnhancement( + Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, this.art2aData.getOffsetForContrastEnhancement() ); @@ -540,7 +537,7 @@ public Art2aResult getClusterResult( // Initialize cluster indices for data row vectors with -1 to // indicate missing cluster assignment int[] tmpClusterIndexOfDataVector = new int[tmpNumberOfDataVectors]; - Art2aUtils.fillVector(tmpClusterIndexOfDataVector, -1); + Utils.fillVector(tmpClusterIndexOfDataVector, -1); // Initialize random indices int[] tmpRandomIndices = new int[tmpNumberOfDataVectors]; @@ -554,14 +551,14 @@ public Art2aResult getClusterResult( // Main clustering loop int tmpCurrentNumberOfEpochs = 0; int tmpNumberOfDetectedClusters = 0; - Art2aUtils.RhoWinner tmpRhoWinner = new Art2aUtils.RhoWinner(); - Art2aUtils.ClusterRemovalInfo tmpClusterRemovalInfo = new Art2aUtils.ClusterRemovalInfo(); + Utils.RhoWinner tmpRhoWinner = new Utils.RhoWinner(); + Utils.ClusterRemovalInfo tmpClusterRemovalInfo = new Utils.ClusterRemovalInfo(); boolean tmpIsConverged = false; while(!tmpIsConverged && tmpCurrentNumberOfEpochs < this.maximumNumberOfEpochs) { tmpCurrentNumberOfEpochs++; // Get random sequence of indices for data row vectors - Art2aUtils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); + Utils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); Arrays.fill(tmpClusterUsageFlags, false); for(int i = 0; i < tmpNumberOfDataVectors; i++) { @@ -573,7 +570,7 @@ public Art2aResult getClusterResult( } if (this.art2aData.hasPreprocessedData()) { - Art2aUtils.copyVector(tmpContrastEnhancedUnitMatrix[tmpRandomIndex], tmpBufferVector); + Utils.copyVector(tmpContrastEnhancedUnitMatrix[tmpRandomIndex], tmpBufferVector); } else { tmpDataVectorZeroLengthFlags[tmpRandomIndex] = Art2aUtils.setContrastEnhancedUnitVector( @@ -589,7 +586,7 @@ public Art2aResult getClusterResult( if(tmpNumberOfDetectedClusters == 0) { // Create first cluster - Art2aUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + Utils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; tmpNumberOfDetectedClusters++; @@ -609,7 +606,7 @@ public Art2aResult getClusterResult( tmpIsClusterOverflow = true; } else { // Increment clusters - Art2aUtils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); + Utils.setRowVector(tmpClusterMatrix, tmpBufferVector, tmpNumberOfDetectedClusters); tmpClusterIndexOfDataVector[tmpRandomIndex] = tmpNumberOfDetectedClusters; tmpClusterUsageFlags[tmpNumberOfDetectedClusters] = true; tmpNumberOfDetectedClusters++; @@ -629,7 +626,7 @@ public Art2aResult getClusterResult( } } } - Art2aUtils.removeEmptyClusters( + Utils.removeEmptyClusters( tmpClusterUsageFlags, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -664,7 +661,7 @@ public Art2aResult getClusterResult( tmpClusterUsageFlags ); // Remove possible empty clusters - Art2aUtils.removeEmptyClusters( + Utils.removeEmptyClusters( tmpClusterUsageFlags, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -686,7 +683,7 @@ public Art2aResult getClusterResult( tmpClusterIndexOfDataVector, tmpClusterUsageFlags ); - Art2aUtils.removeEmptyClusters( + Utils.removeEmptyClusters( tmpClusterUsageFlags, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -716,7 +713,7 @@ public Art2aResult getClusterResult( anException.toString(), anException ); - throw anException; + throw new Exception("Art2aKernel.getClusterResult: An exception occurred: This should never happen!"); } } @@ -769,7 +766,7 @@ public Art2aResult[] getClusterResults( for (float tmpVigilance : aVigilances) { tmpSingleTaskList.add(new HelperTask(this, tmpVigilance)); } - ExecutorService tmpExecutorService = Executors.newFixedThreadPool(aNumberOfConcurrentCalculationThreads); + ExecutorService tmpExecutorService = newFixedThreadPool(aNumberOfConcurrentCalculationThreads); List> tmpFutureList = null; try { tmpFutureList = tmpExecutorService.invokeAll(tmpSingleTaskList); @@ -955,7 +952,7 @@ public int[] getBestRepresentatives( for (int i = 0; i < tmpAllIndices.length; i++) { tmpAllIndices[i] = i; } - float tmpBaseMeanDistance = Art2aUtils.getMeanDistance(aDataMatrix, tmpAllIndices); + float tmpBaseMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpAllIndices); float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -971,7 +968,7 @@ public int[] getBestRepresentatives( tmpVigilanceMax, tmpNumberOfTrialSteps ); - float tmpMeanDistance = Art2aUtils.getMeanDistance(aDataMatrix, tmpRepresentatives); + float tmpMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpRepresentatives); float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); if (tmpDifference < tmpMinimalDifference) { tmpMinimalDifference = tmpDifference; @@ -994,120 +991,31 @@ public int[] getBestRepresentatives( } // // - /** - * Checks if aDataMatrix is valid. - * - * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) - * @return True if aDataMatrix is valid, false otherwise. - */ - public static boolean isDataMatrixValid( - float[][] aDataMatrix - ) { - if(aDataMatrix == null || aDataMatrix.length == 0) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.isDataMatrixValid: aDataMatrixis is null or empty." - ); - return false; - } - - int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; - if(tmpNumberOfDataVectorComponents < 2) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.isDataMatrixValid: Data row vectors must have at least 2 components." - ); - return false; - } - - for(float[] tmpDataVector : aDataMatrix) { - if(tmpDataVector == null || tmpDataVector.length == 0) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.isDataMatrixValid: A data row vector of aDataMatrix is not allowed to be null or empty." - ); - return false; - } - - if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.isDataMatrixValid: Data row vectors in aDataMatrix must have the same length." - ); - return false; - } - } - if (Art2aUtils.hasNonFiniteComponent(aDataMatrix)) { - return false; - } - return true; - } - - /** - * Removes columns from data matrix with non-finite components. - * Note: If aDataMatrix is null, empty or has an invalid structure - * nothing is done and false is returned. - * - * @param aDataMatrix Data matrix with data row vectors (MAY BE CHANGED) - * @return True if aDataMatrix was changed (i.e. column removal was - * performed), false otherwise (i.e. data matrix is unchanged). - */ - public static boolean isNonFiniteComponentRemoval( - float[][] aDataMatrix - ) { - // - if(aDataMatrix == null || aDataMatrix.length == 0) { - return false; - } - - int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; - if(tmpNumberOfDataVectorComponents < 2) { - return false; - } - - for(float[] tmpDataVector : aDataMatrix) { - if(tmpDataVector == null || tmpDataVector.length == 0) { - return false; - } - - if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { - return false; - } - } - // - - boolean tmpHasNonFiniteComponent = Art2aUtils.hasNonFiniteComponent(aDataMatrix); - if (tmpHasNonFiniteComponent) { - // TODO: Remove columns with non-finite components - } - return tmpHasNonFiniteComponent; - } - /** * Creates ART-2a data object with preprocessed data for maximum speed * of the clustering process. The ART-2a data object allocates about the * same memory as aDataMatrix. *
* Note: There a no checks! Check aDataMatrix in advance with method - * Art2aKernel.isDataMatrixValid(). + * Utils.isDataMatrixValid(). *
* Note: aDataMatrix could be set to null after this operation to release * its memory. * * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check - * with Art2aKernel.isDataMatrixValid() in advance) + * with Utils.isDataMatrixValid() in advance) * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @return ART-2a data object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aData getArt2aData( + public static Art2aData getPreprocessedArt2aData( float[][] aDataMatrix, float anOffsetForContrastEnhancement ) { int tmpNumberOfComponents = aDataMatrix[0].length; float tmpThresholdForContrastEnhancement = - Art2aUtils.getThresholdForContrastEnhancement( + Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, anOffsetForContrastEnhancement ); @@ -1115,11 +1023,11 @@ public static Art2aData getArt2aData( // Initialize flags array for scaled data row vectors which have a // length of zero (i.e. where all components are equal to zero) boolean[] tmpDataVectorZeroLengthFlags = new boolean[aDataMatrix.length]; - Art2aUtils.fillVector(tmpDataVectorZeroLengthFlags, false); + Utils.fillVector(tmpDataVectorZeroLengthFlags, false); float[][] tmpContrastEnhancedUnitMatrix = new float[aDataMatrix.length][]; - Art2aUtils.MinMaxValue[] tmpMinMaxComponents = Art2aUtils.getMinMaxComponents(aDataMatrix); + Utils.MinMaxValue[] tmpMinMaxComponents = Utils.getMinMaxComponents(aDataMatrix); for(int i = 0; i < aDataMatrix.length; i++) { float[] tmpContrastEnhancedUnitVector = new float[tmpNumberOfComponents]; @@ -1150,14 +1058,14 @@ public static Art2aData getArt2aData( * its memory. * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check - * with Art2aKernel.isDataMatrixValid() in advance) + * with Utils.isDataMatrixValid() in advance) * @return ART-2a data object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aData getArt2aData( + public static Art2aData getPreprocessedArt2aData( float[][] aDataMatrix ) { - return Art2aKernel.getArt2aData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); + return Art2aKernel.getPreprocessedArt2aData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); } //
diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java index f030e85..8b134ae 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -217,7 +217,7 @@ public float[] getScaledClusterVector( throw new IllegalArgumentException("Art2aResult.getClusterVector: aClusterIndex is illegal."); } // - return Art2aUtils.getScaledVector(this.clusterMatrix[aClusterIndex]); + return Utils.getScaledVector(this.clusterMatrix[aClusterIndex]); } /** @@ -305,7 +305,7 @@ public float getAngleBetweenClusters( } else { return (float) Math.acos( - Art2aUtils.getScalarProduct( + Utils.getScalarProduct( this.clusterMatrix[aClusterIndex1], this.clusterMatrix[aClusterIndex2] ) @@ -411,7 +411,7 @@ public int getClusterRepresentativeIndex( this.thresholdForContrastEnhancement ); } - float tmpScalarProduct = Art2aUtils.getScalarProduct(tmpContrastEnhancedUnitVector, tmpClusterVector); + float tmpScalarProduct = Utils.getScalarProduct(tmpContrastEnhancedUnitVector, tmpClusterVector); if (tmpScalarProduct > tmpMaximumScalarProduct) { tmpBestIndex = tmpIndex; tmpMaximumScalarProduct = tmpScalarProduct; @@ -466,7 +466,7 @@ public int[] getClusterRepresentativeIndices( this.thresholdForContrastEnhancement ); } - tmpIndexedValues[i] = new IndexedValue(tmpIndex, Art2aUtils.getScalarProduct(tmpContrastEnhancedUnitVector, tmpClusterVector)); + tmpIndexedValues[i] = new IndexedValue(tmpIndex, Utils.getScalarProduct(tmpContrastEnhancedUnitVector, tmpClusterVector)); } // NOTE: LARGEST scalar product FIRST! Arrays.sort(tmpIndexedValues, Collections.reverseOrder()); @@ -497,7 +497,7 @@ public int[] getRepresentativeIndicesOfClusters() { */ public float getVigilance() { return this.vigilance; - }; + } /** * Number of epochs @@ -506,7 +506,7 @@ public float getVigilance() { */ public int getNumberOfEpochs() { return this.numberOfEpochs; - }; + } /** * Number of detected clusters @@ -515,7 +515,7 @@ public int getNumberOfEpochs() { */ public int getNumberOfDetectedClusters() { return this.numberOfDetectedClusters; - }; + } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java index 9e9af97..a9f3e3a 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -27,8 +27,6 @@ package de.unijena.cheminf.clustering.art2a; import java.util.Arrays; -import java.util.LinkedList; -import java.util.Random; /** * Library of helper records, static helper classes and static, thread-safe @@ -39,158 +37,14 @@ * @author Achim Zielesny */ public class Art2aUtils { - + // /** * Value 1.0 */ private static final float ONE = 1.0f; // - // - /** - * Helper record: Minimum and maximum value. - *

- * Note: No checks are performed. - * - * @param minValue Minimum value - * @param maxValue Maximum value - */ - protected record MinMaxValue(float minValue, float maxValue) { - - /** - * Constructor - * - * @param minValue Minimum value - * @param maxValue Maximum value - */ - public MinMaxValue {} - - } - //
- // - /** - * Helper class: Rho winner. - *

- * Note: No checks are performed. - */ - protected static class RhoWinner { - - // - /** - * Rho value - */ - private float rhoValue; - /** - * Index of cluster - */ - private int indexOfCluster; - // - - // - /** - * Constructor - */ - protected RhoWinner() {} - // - - // - /** - * Set rho winner - * - * @param aRhoValue Rho value - * @param anIndexOfCluster Index of cluster - */ - protected void setRhoWinner( - float aRhoValue, - int anIndexOfCluster - ) { - this.rhoValue = aRhoValue; - this.indexOfCluster = anIndexOfCluster; - } - - /** - * Rho value - * - * @return Rho value - */ - protected float getRhoValue() { - return this.rhoValue; - } - - /** - * Index of cluster - * - * @return Index of cluster - */ - protected int getIndexOfCluster() { - return this.indexOfCluster; - } - // - - } - - /** - * Helper class: Cluster removal info. - *

- * Note: No checks are performed. - */ - protected static class ClusterRemovalInfo { - - // - /** - * True: Cluster is removed, false: Otherwise - */ - private boolean isClusterRemoved; - /** - * Number of detected clusters - */ - private int numberOfDetectedClusters; - // - - // - /** - * Constructor - */ - protected ClusterRemovalInfo() {} - // - - // - /** - * Set cluster removal info - * - * @param anIsClusterRemoved True: Cluster is removed, false: Otherwise - * @param aNumberOfDetectedClusters Number of detected clusters - */ - protected void setClusterRemovalInfo( - boolean anIsClusterRemoved, - int aNumberOfDetectedClusters - ) { - this.isClusterRemoved = anIsClusterRemoved; - this.numberOfDetectedClusters = aNumberOfDetectedClusters; - } - - /** - * True: Cluster is removed, false: Otherwise - * - * @return True: Cluster is removed, false: Otherwise - */ - protected boolean isClusterRemoved() { - return this.isClusterRemoved; - } - /** - * Number of detected clusters - * - * @return Number of detected clusters - */ - protected int getNumberOfDetectedClusters() { - return this.numberOfDetectedClusters; - } - // - - } - //
- // /** * Constructor @@ -254,172 +108,6 @@ protected static void assignDataVectorsToClusters( } } } - - /** - * (Deep) Copies source matrix to destination matrix. Row vectors of - * destination matrix may not have been instantiated. - * - * @param aSourceMatrix Source matrix (IS NOT CHANGED) - * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN - * INSTANTIATED and MAY BE CHANGED) - */ - protected static void copyMatrix( - float[][] aSourceMatrix, - float[][] aDestinationMatrix - ) { - for (int i = 0; i < aSourceMatrix.length; i++) { - if (aDestinationMatrix[i] == null) { - aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; - } - System.arraycopy( - aSourceMatrix[i], - 0, - aDestinationMatrix[i], - 0, - aSourceMatrix[i].length - ); - } - } - - /** - * (Deep) Copies specified number of rows of source matrix to destination - * matrix. Row vectors of destination matrix may not have been instantiated. - * - * @param aSourceMatrix Source matrix (IS NOT CHANGED) - * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN - * INSTANTIATED and MAY BE CHANGED) - * @param aNumberOfRows Number of rows to be copied from source matrix to - * destination matrix - */ - protected static void copyRows( - float[][] aSourceMatrix, - float[][] aDestinationMatrix, - int aNumberOfRows - ) { - for (int i = 0; i < aNumberOfRows; i++) { - if (aDestinationMatrix[i] == null) { - aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; - } - System.arraycopy( - aSourceMatrix[i], - 0, - aDestinationMatrix[i], - 0, - aSourceMatrix[i].length - ); - } - } - - /** - * (Deep) Copies source vector to destination vector. - * - * @param aSourceVector Source vector (IS NOT CHANGED) - * @param aDestinationVector Destination vector (MUST HAVE BEEN - * INSTANTIATED and MAY BE CHANGED) - */ - protected static void copyVector( - float[] aSourceVector, - float[] aDestinationVector - ) { - System.arraycopy( - aSourceVector, - 0, - aDestinationVector, - 0, - aSourceVector.length - ); - } - - /** - * Calculates contrast enhanced vector. - * - * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) - * @param aThresholdForContrastEnhancement Threshold for contrast enhancement - */ - protected static void enhanceContrast( - float[] aVector, - float aThresholdForContrastEnhancement - ) { - for(int i = 0; i < aVector.length; i++) { - if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { - aVector[i] = 0.0f; - } - } - } - - /** - * Fills matrix with value. - * - * @param aMatrix Matrix (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillMatrix( - float[][] aMatrix, - float aValue - ) { - for (float [] tmpRowVector : aMatrix) { - Arrays.fill(tmpRowVector , aValue); - } - } - - /** - * Fills vector with value. - * - * @param aVector Vector (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillVector( - float[] aVector, - float aValue - ) { - Arrays.fill(aVector , aValue); - } - - /** - * Fills vector with value. - * - * @param aVector Vector (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillVector( - boolean[] aVector, - boolean aValue - ) { - Arrays.fill(aVector , aValue); - } - - /** - * Fills vector with value. - * - * @param aVector Vector (MAY BE CHANGED) - * @param aValue Value - */ - protected static void fillVector( - int[] aVector, - int aValue - ) { - Arrays.fill(aVector , aValue); - } - - /** - * Returns mean distance of all specified row vectors. - * - * @param aMatrix Matrix with row vectors (IS NOT CHANGED) - * @param anIndicesOfRowVectors Indices of row vectors of aMatrix - * @return Mean squared distance of all specified row vectors. - */ - protected static float getMeanDistance( - float[][] aMatrix, - int[] anIndicesOfRowVectors - ) { - float tmpSum = 0.0f; - for (int i = 0; i < anIndicesOfRowVectors.length; i++) { - for (int j = i + 1; j < anIndicesOfRowVectors.length; j++) { - tmpSum += (float) Math.sqrt(Art2aUtils.getSquaredDistance(aMatrix[anIndicesOfRowVectors[i]], aMatrix[anIndicesOfRowVectors[j]])); - } - } - return tmpSum / (float) (anIndicesOfRowVectors.length * (anIndicesOfRowVectors.length - 1) / 2); - } /** * Returns index of cluster for contrast enhanced unit vector @@ -437,7 +125,7 @@ protected static int getClusterIndex( float tmpMaxScalarProduct = Float.MIN_VALUE; int tmpWinnerClusterIndex = -1; for (int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpScalarProduct = Art2aUtils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); + float tmpScalarProduct = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); if (tmpScalarProduct > tmpMaxScalarProduct) { tmpMaxScalarProduct = tmpScalarProduct; tmpWinnerClusterIndex = i; @@ -445,227 +133,6 @@ protected static int getClusterIndex( } return tmpWinnerClusterIndex; } - - /** - * Returns min-max components for matrix where MinMaxValue[j] - * corresponds to column j of the row vectors of the matrix. The min-max - * components may be used to scale row vectors to interval [0,1], see - * method scaleVector(). - * - * @param aMatrix Matrix (IS NOT CHANGED) - * @return Min-max components - */ - protected static MinMaxValue[] getMinMaxComponents( - float[][] aMatrix - ) { - MinMaxValue[] tmpMinMaxComponents = new MinMaxValue[aMatrix[0].length]; - for (int j = 0; j < aMatrix[0].length; j++) { - float tmpMinValue = aMatrix[0][j]; - float tmpMaxValue = aMatrix[0][j]; - for (int i = 1; i < aMatrix.length; i++) { - if (aMatrix[i][j] < tmpMinValue) { - tmpMinValue = aMatrix[i][j]; - } else if (aMatrix[i][j] > tmpMaxValue) { - tmpMaxValue = aMatrix[i][j]; - } - } - tmpMinMaxComponents[j] = new MinMaxValue(tmpMinValue, tmpMaxValue); - } - return tmpMinMaxComponents; - } - - /** - * Calculates the scalar product (dot product) of aVector1 and aVector2. - * - * @param aVector1 Vector 1 (IS NOT CHANGED) - * @param aVector2 Vector 2 (IS NOT CHANGED) - * @return Scalar product (dot product) - */ - protected static float getScalarProduct( - float[] aVector1, - float[] aVector2 - ) { - float tmpSum = 0.0f; - for (int i = 0; i < aVector1.length; i++) { - // tmpSum += aVector1[i] * aVector2[i]; - tmpSum = Math.fma(aVector1[i], aVector2[i], tmpSum); - } - return tmpSum; - } - - /** - * Scales components of aVectorToBeScaled to interval [0,1]. - * - * @param aVectorToBeScaled Vector (IS NOT CHANGED) - * @return New scaled vector with components in interval [0,1] or new - * vector of length zero if all components of aVectorToBeScaled are the - * same. - */ - protected static float[] getScaledVector( - float[] aVectorToBeScaled - ) { - float tmpMinValue = aVectorToBeScaled[0]; - float tmpMaxValue = aVectorToBeScaled[0]; - for(int i = 0; i < aVectorToBeScaled.length; i++) { - if (aVectorToBeScaled[i] < tmpMinValue) { - tmpMinValue = aVectorToBeScaled[i]; - } else if (aVectorToBeScaled[i] > tmpMaxValue) { - tmpMaxValue = aVectorToBeScaled[i]; - } - } - float[] tmpScaledVector = new float[aVectorToBeScaled.length]; - if (tmpMinValue == tmpMaxValue) { - for(int i = 0; i < aVectorToBeScaled.length; i++) { - tmpScaledVector[i] = aVectorToBeScaled[i] - tmpMinValue; - } - } else { - float tmpDenominator = tmpMaxValue - tmpMinValue; - for(int i = 0; i < aVectorToBeScaled.length; i++) { - tmpScaledVector[i] = (aVectorToBeScaled[i] - tmpMinValue) / tmpDenominator; - } - } - return tmpScaledVector; - } - - /** - * Calculates the squared distance between aVector1 and aVector2. - * - * @param aVector1 Vector 1 (IS NOT CHANGED) - * @param aVector2 Vector 2 (IS NOT CHANGED) - * @return Squared distance - */ - protected static float getSquaredDistance( - float[] aVector1, - float[] aVector2 - ) { - float tmpSum = 0.0f; - for (int i = 0; i < aVector1.length; i++) { - float tmpDelta = aVector1[i] - aVector2[i]; - // tmpSum += (aVector1[i] - aVector2[i])^2; - tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); - } - return tmpSum; - } - - /** - * Calculates the sum of components of aVector. - * - * @param aVector Vector (IS NOT CHANGED) - * @return Sum of components - */ - protected static float getSumOfComponents( - float[] aVector - ) { - float tmpSum = 0.0f; - for (float tmpComponent : aVector) { - tmpSum += tmpComponent; - } - return tmpSum; - } - - /** - * Threshold for contrast enhancement - * - * @param aNumberOfComponents Number of components - * @param anOffsetForContrastEnhancement Offset for contrast enhancement - * @return Threshold for contrast enhancement - */ - protected static float getThresholdForContrastEnhancement( - int aNumberOfComponents, - float anOffsetForContrastEnhancement - ) { - // Original code: - // return (float) (1.0 / Math.sqrt(aNumberOfComponents + 1.0)); - return (float) (1.0 / Math.sqrt(aNumberOfComponents + anOffsetForContrastEnhancement)); - } - - /** - * Calculates the length of aVector. - * - * @param aVector Vector (IS NOT CHANGED) - * @return Length of vector - */ - protected static float getVectorLength( - float[] aVector - ) { - float tmpSum = 0.0f; - for (float tmpComponent : aVector) { - // tmpSum += tmpComponent * tmpComponent; - tmpSum = Math.fma(tmpComponent, tmpComponent, tmpSum); - } - return (float) Math.sqrt(tmpSum); - } - - /** - * Checks if vector has a length of zero (i.e. if all components are equal - * to zero). - * - * @param aVector Vector (IS NOT CHANGED) - * @return True: Vector has a length of zero, false: Otherwise - */ - protected static boolean hasLengthOfZero( - float[] aVector - ) { - for(float tmpComponent : aVector) { - if (tmpComponent != 0.0f) { - return false; - } - } - return true; - } - - /** - * Checks if data matrix has a non-finite component. - * Note: If aDataMatrix is null or empty nothing is done and false is - * returned. - * - * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) - * @return True: Data matrix has non-finite component, false: Otherwise - */ - protected static boolean hasNonFiniteComponent( - float[][] aDataMatrix - ) { - // - if(aDataMatrix == null || aDataMatrix.length == 0) { - return false; - } - for(float[] tmpDataVector : aDataMatrix) { - if(tmpDataVector == null || tmpDataVector.length == 0) { - return false; - } - } - // - - for(float[] tmpDataVector : aDataMatrix) { - for (float tmpComponent : tmpDataVector) { - if (!Float.isFinite(tmpComponent)) { - return true; - } - } - } - return false; - } - - /** - * Calculates contrast enhanced vector. - * - * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) - * @param aThresholdForContrastEnhancement Threshold for contrast enhancement - * @return True if aVector is changed by contrast enhancement, false otherwise. - */ - protected static boolean isContrastEnhanced( - float[] aVector, - float aThresholdForContrastEnhancement - ) { - boolean tmpIsVectorChanged = false; - for(int i = 0; i < aVector.length; i++) { - if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { - aVector[i] = 0.0f; - tmpIsVectorChanged = true; - } - } - return tmpIsVectorChanged; - } /** * Determines convergence of clustering process. @@ -691,7 +158,7 @@ protected static boolean isConverged( ) { if (anEpoch == 1) { // Convergence check needs at least 2 epochs - Art2aUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); return false; } else { boolean tmpIsConverged = false; @@ -702,45 +169,19 @@ protected static boolean isConverged( for (int i = 0; i < aNumberOfDetectedClusters; i++) { if ( aClusterCentroidMatrixOld[i] == null || - Art2aUtils.getScalarProduct(aClusterCentroidMatrix[i], aClusterCentroidMatrixOld[i]) < aConvergenceThreshold + Utils.getScalarProduct(aClusterCentroidMatrix[i], aClusterCentroidMatrixOld[i]) < aConvergenceThreshold ) { tmpIsConverged = false; break; } } if(!tmpIsConverged) { - Art2aUtils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); } } return tmpIsConverged; } } - - /** - * Checks if matrix is valid. - * - * @param aMatrix Matrix - * @return True: Matrix is valid, false: Otherwise - */ - protected static boolean isMatrixValid( - float[][] aMatrix - ) { - if (aMatrix == null || aMatrix.length == 0) { - return false; - } - for (float[] tmpRowVector : aMatrix) { - if (tmpRowVector == null || tmpRowVector.length == 0) { - return false; - } - } - int tmpRowVectorLength = aMatrix[0].length; - for (int i = 1; i < aMatrix.length; i++) { - if (aMatrix[i].length != tmpRowVectorLength) { - return false; - } - } - return true; - } /** * Modifies winner cluster (see code). @@ -770,7 +211,7 @@ protected static void modifyWinnerCluster( } float tmpFactor1; if (tmpIsChanged) { - tmpFactor1 = aLearningParameter / Art2aUtils.getVectorLength(aContrastEnhancedUnitVector); + tmpFactor1 = aLearningParameter / Utils.getVectorLength(aContrastEnhancedUnitVector); } else { tmpFactor1 = aLearningParameter; } @@ -778,87 +219,8 @@ protected static void modifyWinnerCluster( for(int j = 0; j < aWinnerClusterVector.length; j++) { aContrastEnhancedUnitVector[j] = tmpFactor1 * aContrastEnhancedUnitVector[j] + tmpFactor2 * aWinnerClusterVector[j]; } - Art2aUtils.normalizeVector(aContrastEnhancedUnitVector); - Art2aUtils.copyVector(aContrastEnhancedUnitVector, aWinnerClusterVector); - } - - /** - * Calculates normalized (unit) vector of length 1. - * - * @param aVector Vector to be normalized (MAY BE CHANGED) - */ - protected static void normalizeVector( - float[] aVector - ) { - float tmpInverseVectorLength = ONE / Art2aUtils.getVectorLength(aVector); - for(int i = 0; i < aVector.length; i++) { - aVector[i] *= tmpInverseVectorLength; - } - } - - /** - * Removes empty clusters from cluster matrix - * - * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, - * false: Cluster is empty and has to be removed (IS NOT CHANGED) - * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aClusterRemovalInfo Cluster removal info (is set according to the - * operations performed, IS CHANGED) - */ - protected static void removeEmptyClusters( - boolean[] aClusterUsageFlags, - float[][] aClusterMatrix, - int aNumberOfDetectedClusters, - ClusterRemovalInfo aClusterRemovalInfo - ) { - boolean tmpIsEmptyClusterRemoval = false; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if (!aClusterUsageFlags[i]) { - tmpIsEmptyClusterRemoval = true; - break; - } - } - if (tmpIsEmptyClusterRemoval) { - // Remove empty clusters from cluster matrix - LinkedList tmpClusterVectorList = new LinkedList<>(); - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if (aClusterUsageFlags[i]) { - tmpClusterVectorList.add(aClusterMatrix[i]); - aClusterMatrix[i] = null; - } - } - int tmpIndex = 0; - for (float[] tmpClusterVector : tmpClusterVectorList) { - aClusterMatrix[tmpIndex++] = tmpClusterVector; - } - aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); - } else { - aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); - } - } - - /** - * Scales components of aVectorToBeScaled according to min-max components - * to interval [0,1] (see code and method getMinMaxComponents()). - * - * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) - * @param aMinMaxComponents Min-max components - */ - protected static void scaleVector( - float[] aVectorToBeScaled, - MinMaxValue[] aMinMaxComponents - ) { - for(int i = 0; i < aVectorToBeScaled.length; i++) { - if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { - // Scale component to interval [0,1] - aVectorToBeScaled[i] = - (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); - } else { - // Shift component to zero - aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); - } - } + Utils.normalizeVector(aContrastEnhancedUnitVector); + Utils.copyVector(aContrastEnhancedUnitVector, aWinnerClusterVector); } /** @@ -880,15 +242,15 @@ protected static void setRhoWinner( float[][] aClusterMatrix, int aNumberOfDetectedClusters, float aScalingFactor, - Art2aUtils.RhoWinner aRhoWinner + Utils.RhoWinner aRhoWinner ) { // Calculate first rho value - float tmpRhoValue = aScalingFactor * Art2aUtils.getSumOfComponents(aContrastEnhancedUnitVector); + float tmpRhoValue = aScalingFactor * Utils.getSumOfComponents(aContrastEnhancedUnitVector); // Set winner index to negative value int tmpIndex = -1; // Calculate other rho values for(int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpRhoForCluster = Art2aUtils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); + float tmpRhoForCluster = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); if(tmpRhoForCluster > tmpRhoValue) { tmpRhoValue = tmpRhoForCluster; tmpIndex = i; @@ -897,23 +259,6 @@ protected static void setRhoWinner( aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); } - /** - * Sets copied (!) row vector at index in matrix. - * - * @param aMatrix Matrix (MAY BE CHANGED) - * @param aRowVector Row vector (IS NOT CHANGED) - * @param anIndex Index of row vector in matrix - */ - protected static void setRowVector( - float[][] aMatrix, - float[] aRowVector, - int anIndex - ) { - float[] tmpNewMatrixRowVector = new float[aRowVector.length]; - Art2aUtils.copyVector(aRowVector, tmpNewMatrixRowVector); - aMatrix[anIndex] = tmpNewMatrixRowVector; - } - /** * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). @@ -931,50 +276,27 @@ protected static void setRowVector( protected static boolean setContrastEnhancedUnitVector( float[] aDataVector, float[] aBufferVector, - Art2aUtils.MinMaxValue[] aMinMaxComponents, + Utils.MinMaxValue[] aMinMaxComponents, float aThresholdForContrastEnhancement ) { // Already allocated memory of aBufferVector is reused - Art2aUtils.copyVector(aDataVector, aBufferVector); + Utils.copyVector(aDataVector, aBufferVector); // Scale components of vector to interval [0,1] - Art2aUtils.scaleVector(aBufferVector, aMinMaxComponents); + Utils.scaleVector(aBufferVector, aMinMaxComponents); // Check length - if (Art2aUtils.hasLengthOfZero(aBufferVector)) { + if (Utils.hasLengthOfZero(aBufferVector)) { // True: Scaled source vector has a length of zero return true; } else { - Art2aUtils.normalizeVector(aBufferVector); + Utils.normalizeVector(aBufferVector); // Enhance contrast - if (Art2aUtils.isContrastEnhanced(aBufferVector, aThresholdForContrastEnhancement)) { - Art2aUtils.normalizeVector(aBufferVector); + if (Utils.isContrastEnhanced(aBufferVector, aThresholdForContrastEnhancement)) { + Utils.normalizeVector(aBufferVector); } // False: Scaled data vector has a length different from zero return false; } } - - /** - * Randomly shuffles indices from 0 to (anIndices.Length - 1) in - * anIndexArray using Fisher-Yates shuffling (i.e. the modern version - * introduced by Richard Durstenfeld). - * Note: No checks are performed. - * - * @param anIndexArray Array with indices from 0 to (anIndices.Length - 1) - * @param aRandomNumberGenerator Random number generator - */ - protected static void shuffleIndices( - int[] anIndexArray, - Random aRandomNumberGenerator - ) { - for (int i = anIndexArray.length - 1; i > 0; i--) { - // Generate a random index between 0 and i (inclusive) - int j = aRandomNumberGenerator.nextInt(i + 1); - // Swap the elements at indices i and j - int tmpIntBuffer = anIndexArray[i]; - anIndexArray[i] = anIndexArray[j]; - anIndexArray[j] = tmpIntBuffer; - } - } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java new file mode 100644 index 0000000..b52d6b1 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java @@ -0,0 +1,848 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Random; + +/** + * Library of helper records, static helper classes and static, thread-safe + * (stateless) utility methods for ART-2a and ART-2a-Euclid clustering. + *

+ * Note: No checks are performed. + * + * @author Achim Zielesny + */ +public class Utils { + + // + /** + * Value 1.0 + */ + private static final float ONE = 1.0f; + // + // + /** + * Helper record: Minimum and maximum value. + *

+ * Note: No checks are performed. + * + * @param minValue Minimum value + * @param maxValue Maximum value + */ + protected record MinMaxValue(float minValue, float maxValue) { + + /** + * Constructor + * + * @param minValue Minimum value + * @param maxValue Maximum value + */ + public MinMaxValue {} + + } + //
+ // + /** + * Helper class: Rho winner. + *

+ * Note: No checks are performed. + */ + protected static class RhoWinner { + + // + /** + * Rho value + */ + private float rhoValue; + /** + * Index of cluster + */ + private int indexOfCluster; + // + + // + /** + * Constructor + */ + protected RhoWinner() {} + // + + // + /** + * Set rho winner + * + * @param aRhoValue Rho value + * @param anIndexOfCluster Index of cluster + */ + protected void setRhoWinner( + float aRhoValue, + int anIndexOfCluster + ) { + this.rhoValue = aRhoValue; + this.indexOfCluster = anIndexOfCluster; + } + + /** + * Rho value + * + * @return Rho value + */ + protected float getRhoValue() { + return this.rhoValue; + } + + /** + * Index of cluster + * + * @return Index of cluster + */ + protected int getIndexOfCluster() { + return this.indexOfCluster; + } + // + + } + + /** + * Helper class: Cluster removal info. + *

+ * Note: No checks are performed. + */ + protected static class ClusterRemovalInfo { + + // + /** + * True: Cluster is removed, false: Otherwise + */ + private boolean isClusterRemoved; + /** + * Number of detected clusters + */ + private int numberOfDetectedClusters; + // + + // + /** + * Constructor + */ + protected ClusterRemovalInfo() {} + // + + // + /** + * Set cluster removal info + * + * @param anIsClusterRemoved True: Cluster is removed, false: Otherwise + * @param aNumberOfDetectedClusters Number of detected clusters + */ + protected void setClusterRemovalInfo( + boolean anIsClusterRemoved, + int aNumberOfDetectedClusters + ) { + this.isClusterRemoved = anIsClusterRemoved; + this.numberOfDetectedClusters = aNumberOfDetectedClusters; + } + + /** + * True: Cluster is removed, false: Otherwise + * + * @return True: Cluster is removed, false: Otherwise + */ + protected boolean isClusterRemoved() { + return this.isClusterRemoved; + } + + /** + * Number of detected clusters + * + * @return Number of detected clusters + */ + protected int getNumberOfDetectedClusters() { + return this.numberOfDetectedClusters; + } + // + + } + //
+ + // + /** + * Constructor + */ + protected Utils() {} + // + + // + /** + * Checks if aDataMatrix is valid. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @return True if aDataMatrix is valid, false otherwise. + */ + public static boolean isDataMatrixValid( + float[][] aDataMatrix + ) { + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + + int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; + if(tmpNumberOfDataVectorComponents < 2) { + return false; + } + + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + + if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + return false; + } + } + if (Utils.hasNonFiniteComponent(aDataMatrix)) { + return false; + } + return true; + } + + /** + * Removes columns from data matrix with non-finite components. + * Note: If aDataMatrix is null, empty or has an invalid structure + * nothing is done and false is returned. + * + * @param aDataMatrix Data matrix with data row vectors (MAY BE CHANGED) + * @return True if aDataMatrix was changed (i.e. column removal was + * performed), false otherwise (i.e. data matrix is unchanged). + */ + public static boolean isNonFiniteComponentRemoval( + float[][] aDataMatrix + ) { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + + int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; + if(tmpNumberOfDataVectorComponents < 2) { + return false; + } + + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + + if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + return false; + } + } + // + + boolean tmpHasNonFiniteComponent = Utils.hasNonFiniteComponent(aDataMatrix); + if (tmpHasNonFiniteComponent) { + // TODO: Remove columns with non-finite components + } + return tmpHasNonFiniteComponent; + } + // + // + /** + * (Deep) Copies source matrix to destination matrix. Row vectors of + * destination matrix may not have been instantiated. + * + * @param aSourceMatrix Source matrix (IS NOT CHANGED) + * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + */ + protected static void copyMatrix( + float[][] aSourceMatrix, + float[][] aDestinationMatrix + ) { + for (int i = 0; i < aSourceMatrix.length; i++) { + if (aDestinationMatrix[i] == null) { + aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; + } + System.arraycopy( + aSourceMatrix[i], + 0, + aDestinationMatrix[i], + 0, + aSourceMatrix[i].length + ); + } + } + + /** + * (Deep) Copies specified number of rows of source matrix to destination + * matrix. Row vectors of destination matrix may not have been instantiated. + * + * @param aSourceMatrix Source matrix (IS NOT CHANGED) + * @param aDestinationMatrix Destination matrix (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + * @param aNumberOfRows Number of rows to be copied from source matrix to + * destination matrix + */ + protected static void copyRows( + float[][] aSourceMatrix, + float[][] aDestinationMatrix, + int aNumberOfRows + ) { + for (int i = 0; i < aNumberOfRows; i++) { + if (aDestinationMatrix[i] == null) { + aDestinationMatrix[i] = new float[aSourceMatrix[i].length]; + } + System.arraycopy( + aSourceMatrix[i], + 0, + aDestinationMatrix[i], + 0, + aSourceMatrix[i].length + ); + } + } + + /** + * (Deep) Copies source vector to destination vector. + * + * @param aSourceVector Source vector (IS NOT CHANGED) + * @param aDestinationVector Destination vector (MUST HAVE BEEN + * INSTANTIATED and MAY BE CHANGED) + */ + protected static void copyVector( + float[] aSourceVector, + float[] aDestinationVector + ) { + System.arraycopy( + aSourceVector, + 0, + aDestinationVector, + 0, + aSourceVector.length + ); + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + */ + protected static void enhanceContrast( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + } + } + } + + /** + * Fills matrix with value. + * + * @param aMatrix Matrix (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillMatrix( + float[][] aMatrix, + float aValue + ) { + for (float [] tmpRowVector : aMatrix) { + Arrays.fill(tmpRowVector , aValue); + } + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + float[] aVector, + float aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + boolean[] aVector, + boolean aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Fills vector with value. + * + * @param aVector Vector (MAY BE CHANGED) + * @param aValue Value + */ + protected static void fillVector( + int[] aVector, + int aValue + ) { + Arrays.fill(aVector , aValue); + } + + /** + * Returns mean distance of all specified row vectors. + * + * @param aMatrix Matrix with row vectors (IS NOT CHANGED) + * @param anIndicesOfRowVectors Indices of row vectors of aMatrix + * @return Mean squared distance of all specified row vectors. + */ + protected static float getMeanDistance( + float[][] aMatrix, + int[] anIndicesOfRowVectors + ) { + float tmpSum = 0.0f; + for (int i = 0; i < anIndicesOfRowVectors.length; i++) { + for (int j = i + 1; j < anIndicesOfRowVectors.length; j++) { + tmpSum += (float) Math.sqrt(Utils.getSquaredDistance(aMatrix[anIndicesOfRowVectors[i]], aMatrix[anIndicesOfRowVectors[j]])); + } + } + return tmpSum / (float) (anIndicesOfRowVectors.length * (anIndicesOfRowVectors.length - 1) / 2); + } + + /** + * Returns min-max components for matrix where MinMaxValue[j] + * corresponds to column j of the row vectors of the matrix. The min-max + * components may be used to scale row vectors to interval [0,1], see + * method scaleVector(). + * + * @param aMatrix Matrix (IS NOT CHANGED) + * @return Min-max components + */ + protected static MinMaxValue[] getMinMaxComponents( + float[][] aMatrix + ) { + MinMaxValue[] tmpMinMaxComponents = new MinMaxValue[aMatrix[0].length]; + for (int j = 0; j < aMatrix[0].length; j++) { + float tmpMinValue = aMatrix[0][j]; + float tmpMaxValue = aMatrix[0][j]; + for (int i = 1; i < aMatrix.length; i++) { + if (aMatrix[i][j] < tmpMinValue) { + tmpMinValue = aMatrix[i][j]; + } else if (aMatrix[i][j] > tmpMaxValue) { + tmpMaxValue = aMatrix[i][j]; + } + } + tmpMinMaxComponents[j] = new MinMaxValue(tmpMinValue, tmpMaxValue); + } + return tmpMinMaxComponents; + } + + /** + * Calculates the scalar product (dot product) of aVector1 and aVector2. + * + * @param aVector1 Vector 1 (IS NOT CHANGED) + * @param aVector2 Vector 2 (IS NOT CHANGED) + * @return Scalar product (dot product) + */ + protected static float getScalarProduct( + float[] aVector1, + float[] aVector2 + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector1.length; i++) { + // tmpSum += aVector1[i] * aVector2[i]; + tmpSum = Math.fma(aVector1[i], aVector2[i], tmpSum); + } + return tmpSum; + } + + /** + * Scales components of aVectorToBeScaled to interval [0,1]. + * + * @param aVectorToBeScaled Vector (IS NOT CHANGED) + * @return New scaled vector with components in interval [0,1] or new + * vector of length zero if all components of aVectorToBeScaled are the + * same. + */ + protected static float[] getScaledVector( + float[] aVectorToBeScaled + ) { + float tmpMinValue = aVectorToBeScaled[0]; + float tmpMaxValue = aVectorToBeScaled[0]; + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aVectorToBeScaled[i] < tmpMinValue) { + tmpMinValue = aVectorToBeScaled[i]; + } else if (aVectorToBeScaled[i] > tmpMaxValue) { + tmpMaxValue = aVectorToBeScaled[i]; + } + } + float[] tmpScaledVector = new float[aVectorToBeScaled.length]; + if (tmpMinValue == tmpMaxValue) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + tmpScaledVector[i] = aVectorToBeScaled[i] - tmpMinValue; + } + } else { + float tmpDenominator = tmpMaxValue - tmpMinValue; + for(int i = 0; i < aVectorToBeScaled.length; i++) { + tmpScaledVector[i] = (aVectorToBeScaled[i] - tmpMinValue) / tmpDenominator; + } + } + return tmpScaledVector; + } + + /** + * Calculates the squared distance between aVector1 and aVector2. + * + * @param aVector1 Vector 1 (IS NOT CHANGED) + * @param aVector2 Vector 2 (IS NOT CHANGED) + * @return Squared distance + */ + protected static float getSquaredDistance( + float[] aVector1, + float[] aVector2 + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector1.length; i++) { + float tmpDelta = aVector1[i] - aVector2[i]; + // tmpSum += (aVector1[i] - aVector2[i])^2; + tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); + } + return tmpSum; + } + + /** + * Calculates the sum of components of aVector. + * + * @param aVector Vector (IS NOT CHANGED) + * @return Sum of components + */ + protected static float getSumOfComponents( + float[] aVector + ) { + float tmpSum = 0.0f; + for (float tmpComponent : aVector) { + tmpSum += tmpComponent; + } + return tmpSum; + } + + /** + * Calculates the sum of squared differences between the components of the + * specified vector and a value. + * + * @param aVector Vector (IS NOT CHANGED) + * @param aValue Value + * @return Sum of squared differences between the components of the + * specified vector and a value. + */ + protected static float getSumOfSquaredDifferences( + float[] aVector, + float aValue + ) { + float tmpSum = 0.0f; + for (int i = 0; i < aVector.length; i++) { + float tmpDelta = aVector[i] - aValue; + // tmpSum += (aVector[i] - aValue)^2; + tmpSum = Math.fma(tmpDelta, tmpDelta, tmpSum); + } + return tmpSum; + } + + /** + * Threshold for contrast enhancement + * + * @param aNumberOfComponents Number of components + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @return Threshold for contrast enhancement + */ + protected static float getThresholdForContrastEnhancement( + int aNumberOfComponents, + float anOffsetForContrastEnhancement + ) { + // Original code: + // return (float) (1.0 / Math.sqrt(aNumberOfComponents + 1.0)); + return (float) (1.0 / Math.sqrt(aNumberOfComponents + anOffsetForContrastEnhancement)); + } + + /** + * Calculates the length of aVector. + * + * @param aVector Vector (IS NOT CHANGED) + * @return Length of vector + */ + protected static float getVectorLength( + float[] aVector + ) { + float tmpSum = 0.0f; + for (float tmpComponent : aVector) { + // tmpSum += tmpComponent * tmpComponent; + tmpSum = Math.fma(tmpComponent, tmpComponent, tmpSum); + } + return (float) Math.sqrt(tmpSum); + } + + /** + * Checks if vector has a length of zero (i.e. if all components are equal + * to zero). + * + * @param aVector Vector (IS NOT CHANGED) + * @return True: Vector has a length of zero, false: Otherwise + */ + protected static boolean hasLengthOfZero( + float[] aVector + ) { + for(float tmpComponent : aVector) { + if (tmpComponent != 0.0f) { + return false; + } + } + return true; + } + + /** + * Checks if data matrix has a non-finite component. + * Note: If aDataMatrix is null or empty nothing is done and false is + * returned. + * + * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @return True: Data matrix has non-finite component, false: Otherwise + */ + protected static boolean hasNonFiniteComponent( + float[][] aDataMatrix + ) { + // + if(aDataMatrix == null || aDataMatrix.length == 0) { + return false; + } + for(float[] tmpDataVector : aDataMatrix) { + if(tmpDataVector == null || tmpDataVector.length == 0) { + return false; + } + } + // + + for(float[] tmpDataVector : aDataMatrix) { + for (float tmpComponent : tmpDataVector) { + if (!Float.isFinite(tmpComponent)) { + return true; + } + } + } + return false; + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + * @return True if aVector is changed by contrast enhancement, false otherwise. + */ + protected static boolean isContrastEnhanced( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + boolean tmpIsVectorChanged = false; + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + tmpIsVectorChanged = true; + } + } + return tmpIsVectorChanged; + } + + /** + * Checks if matrix is valid. + * + * @param aMatrix Matrix + * @return True: Matrix is valid, false: Otherwise + */ + protected static boolean isMatrixValid( + float[][] aMatrix + ) { + if (aMatrix == null || aMatrix.length == 0) { + return false; + } + for (float[] tmpRowVector : aMatrix) { + if (tmpRowVector == null || tmpRowVector.length == 0) { + return false; + } + } + int tmpRowVectorLength = aMatrix[0].length; + for (int i = 1; i < aMatrix.length; i++) { + if (aMatrix[i].length != tmpRowVectorLength) { + return false; + } + } + return true; + } + + /** + * Calculates normalized (unit) vector of length 1. + * + * @param aVector Vector to be normalized (MAY BE CHANGED) + */ + protected static void normalizeVector( + float[] aVector + ) { + float tmpInverseVectorLength = ONE / Utils.getVectorLength(aVector); + for(int i = 0; i < aVector.length; i++) { + aVector[i] *= tmpInverseVectorLength; + } + } + + /** + * Removes empty clusters from cluster matrix + * + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (IS NOT CHANGED) + * @param aClusterMatrix Cluster matrix (MAY BE CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterRemovalInfo Cluster removal info (is set according to the + * operations performed, IS CHANGED) + */ + protected static void removeEmptyClusters( + boolean[] aClusterUsageFlags, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + ClusterRemovalInfo aClusterRemovalInfo + ) { + boolean tmpIsEmptyClusterRemoval = false; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (!aClusterUsageFlags[i]) { + tmpIsEmptyClusterRemoval = true; + break; + } + } + if (tmpIsEmptyClusterRemoval) { + // Remove empty clusters from cluster matrix + LinkedList tmpClusterVectorList = new LinkedList<>(); + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if (aClusterUsageFlags[i]) { + tmpClusterVectorList.add(aClusterMatrix[i]); + aClusterMatrix[i] = null; + } + } + int tmpIndex = 0; + for (float[] tmpClusterVector : tmpClusterVectorList) { + aClusterMatrix[tmpIndex++] = tmpClusterVector; + } + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, tmpClusterVectorList.size()); + } else { + aClusterRemovalInfo.setClusterRemovalInfo(tmpIsEmptyClusterRemoval, aNumberOfDetectedClusters); + } + } + + /** + * Scales components of aVectorToBeScaled according to min-max components + * to interval [0,1] (see code and method getMinMaxComponents()). + * + * @param aVectorToBeScaled Vector to be scaled (MAY BE CHANGED) + * @param aMinMaxComponents Min-max components + */ + protected static void scaleVector( + float[] aVectorToBeScaled, + MinMaxValue[] aMinMaxComponents + ) { + for(int i = 0; i < aVectorToBeScaled.length; i++) { + if (aMinMaxComponents[i].minValue() < aMinMaxComponents[i].maxValue()) { + // Scale component to interval [0,1] + aVectorToBeScaled[i] = + (aVectorToBeScaled[i] - aMinMaxComponents[i].minValue()) / (aMinMaxComponents[i].maxValue() - aMinMaxComponents[i].minValue()); + } else { + // Shift component to zero + aVectorToBeScaled[i] -= aMinMaxComponents[i].minValue(); + } + } + } + + /** + * Calculates contrast enhanced vector. + * + * @param aVector Vector to be contrast enhanced (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + */ + protected static void setContrastEnhancement( + float[] aVector, + float aThresholdForContrastEnhancement + ) { + for(int i = 0; i < aVector.length; i++) { + if(aVector[i] != 0.0f && aVector[i] <= aThresholdForContrastEnhancement) { + aVector[i] = 0.0f; + } + } + } + + /** + * Sets copied (!) row vector at index in matrix. + * + * @param aMatrix Matrix (MAY BE CHANGED) + * @param aRowVector Row vector (IS NOT CHANGED) + * @param anIndex Index of row vector in matrix + */ + protected static void setRowVector( + float[][] aMatrix, + float[] aRowVector, + int anIndex + ) { + float[] tmpNewMatrixRowVector = new float[aRowVector.length]; + Utils.copyVector(aRowVector, tmpNewMatrixRowVector); + aMatrix[anIndex] = tmpNewMatrixRowVector; + } + + /** + * Randomly shuffles indices from 0 to (anIndices.Length - 1) in + * anIndexArray using Fisher-Yates shuffling (i.e. the modern version + * introduced by Richard Durstenfeld). + * Note: No checks are performed. + * + * @param anIndexArray Array with indices from 0 to (anIndices.Length - 1) + * @param aRandomNumberGenerator Random number generator + */ + protected static void shuffleIndices( + int[] anIndexArray, + Random aRandomNumberGenerator + ) { + for (int i = anIndexArray.length - 1; i > 0; i--) { + // Generate a random index between 0 and i (inclusive) + int j = aRandomNumberGenerator.nextInt(i + 1); + // Swap the elements at indices i and j + int tmpIntBuffer = anIndexArray[i]; + anIndexArray[i] = anIndexArray[j]; + anIndexArray[j] = tmpIntBuffer; + } + } + // + +} diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java index 19232cc..6747482 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -26,11 +26,6 @@ package de.unijena.cheminf.clustering.art2a; -import de.unijena.cheminf.clustering.art2a.Art2aEuclidKernel; -import de.unijena.cheminf.clustering.art2a.Art2aEuclidResult; -import de.unijena.cheminf.clustering.art2a.Art2aEuclidTask; -import de.unijena.cheminf.clustering.art2a.Art2aEuclidData; -import de.unijena.cheminf.clustering.art2a.Art2aEuclidUtils; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -227,7 +222,7 @@ public void test_Development_GetRepresentatives() { for (int i = 0; i < 150; i++) { tmpAllIndices[i] = i; } - float tmpBaseMeanDistance = Art2aEuclidUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpAllIndices); + float tmpBaseMeanDistance = Utils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpAllIndices); System.out.println( "Base mean distance = " + String.valueOf(tmpBaseMeanDistance) ); @@ -242,7 +237,7 @@ public void test_Development_GetRepresentatives() { ); if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { Arrays.sort(tmpRepresentatives); - float tmpMeanDistance = Art2aEuclidUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); + float tmpMeanDistance = Utils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); System.out.println( String.valueOf(tmpNumberOfRepresentatives) + " Representatives (Mean distance = " + @@ -315,7 +310,7 @@ public void test_GetRepresentatives() { "= " + String.valueOf( Math.sqrt( - Art2aEuclidUtils.getSquaredDistance( + Utils.getSquaredDistance( tmpIrisFlowerDataMatrix[tmpRepresentatives[i]], tmpIrisFlowerDataMatrix[tmpRepresentatives[j]] ) @@ -520,7 +515,7 @@ public void test_Art2aEuclidData() { } // Preprocessed Art2aEuclidData - Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); Art2aEuclidKernel tmpArt2aEuclidKernelWithArt2aEuclidData = new Art2aEuclidKernel( tmpArt2aEuclidData, @@ -609,7 +604,7 @@ public void test_ParallelClustering() { // Concurrent (parallelized) clustering LinkedList tmpArt2aEuclidTaskList = new LinkedList<>(); - Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); for (float tmpVigilance : tmpVigilances) { tmpArt2aEuclidTaskList.add(new Art2aEuclidTask( tmpArt2aEuclidData, diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 91e0d4f..f3eef05 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -276,7 +276,7 @@ public void test_Development_GetRepresentatives() { for (int i = 0; i < 150; i++) { tmpAllIndices[i] = i; } - float tmpBaseMeanDistance = Art2aUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpAllIndices); + float tmpBaseMeanDistance = Utils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpAllIndices); System.out.println( "Base mean distance = " + String.valueOf(tmpBaseMeanDistance) ); @@ -291,7 +291,7 @@ public void test_Development_GetRepresentatives() { ); if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { Arrays.sort(tmpRepresentatives); - float tmpMeanDistance = Art2aUtils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); + float tmpMeanDistance = Utils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); System.out.println( String.valueOf(tmpNumberOfRepresentatives) + " Representatives (Mean distance = " + @@ -548,7 +548,7 @@ public void test_Art2aData() { } // Preprocessed Art2aData - Art2aData tmpArt2aData = Art2aKernel.getArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + Art2aData tmpArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); Art2aKernel tmpArt2aKernelWithArt2aData = new Art2aKernel( tmpArt2aData, @@ -637,7 +637,7 @@ public void test_ParallelClustering() { // Concurrent (parallelized) clustering LinkedList tmpArt2aTaskList = new LinkedList<>(); - Art2aData tmpArt2aData = Art2aKernel.getArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + Art2aData tmpArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); for (float tmpVigilance : tmpVigilances) { tmpArt2aTaskList.add( new Art2aTask( From ddf3bc6e4050d585db0ab6dcaf9d9b809f15508e Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Sun, 9 Feb 2025 18:21:57 +0100 Subject: [PATCH 08/18] Removal method for non-finite values in data matrix implemented --- .../clustering/art2a/Art2aEuclidKernel.java | 23 ++++++------ .../clustering/art2a/Art2aEuclidTask.java | 16 ++++----- .../cheminf/clustering/art2a/Art2aKernel.java | 23 ++++++------ .../cheminf/clustering/art2a/Art2aTask.java | 16 ++++----- .../cheminf/clustering/art2a/Utils.java | 35 ++++++++++++++++--- 5 files changed, 71 insertions(+), 42 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index dc1ffa1..a9c1519 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -96,7 +96,8 @@ * CAUTION: Construction of several ART-2a-Euclid clustering instances with the * SAME data matrix PLUS preprocessing is NOT advised due to the significant * memory consumption of each instance. In this case, the data matrix should be - * checked with static method Art2aKernel.isDataMatrixValid() and then a priori + * checked with static method Art2aKernel.isDataMatrixValid() (where possible NaN + * values can be removed with Utils.isNonFiniteComponentRemoval()) and then a priori * converted into a preprocessed Art2aEuclidData object with static method * Art2aEuclidKernel.getArt2aEuclidData(). The generated Art2aData object does * NOT change or refer to the data matrix so that the data matrix memory could @@ -385,8 +386,8 @@ public Art2aEuclidKernel( /** * Constructor. * - * @param anArt2aEuclidData ART-2a-Euclid data object created by method - * Art2aEuclidKernel.getArt2aEuclidData() + * @param aPreprocessedArt2aEuclidData Preprocessed ART-2a-Euclid data object + * created by method Art2aEuclidKernel.getPreprocessedArt2aEuclidData() * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) * @param aMaximumNumberOfEpochs Maximum number of epochs for training @@ -399,7 +400,7 @@ public Art2aEuclidKernel( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidKernel( - Art2aEuclidData anArt2aEuclidData, + Art2aEuclidData aPreprocessedArt2aEuclidData, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, float aConvergenceThreshold, @@ -407,7 +408,7 @@ public Art2aEuclidKernel( long aRandomSeed ) throws IllegalArgumentException { // - if(anArt2aEuclidData == null) { + if(aPreprocessedArt2aEuclidData == null) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, "Art2aEuclidKernel.Constructor: anArt2aEuclidData is null." @@ -444,7 +445,7 @@ public Art2aEuclidKernel( } // - this.art2aEuclidData = anArt2aEuclidData; + this.art2aEuclidData = aPreprocessedArt2aEuclidData; this.maximumNumberOfClusters = aMaximumNumberOfClusters; this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; this.convergenceThreshold = aConvergenceThreshold; @@ -457,16 +458,16 @@ public Art2aEuclidKernel( * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param anArt2aEuclidData ART-2a-Euclid data object created by method - * Art2aEuclidKernel.getArt2aEuclidData() + * @param aPreprocessedArt2aEuclidData Preprocessed ART-2a-Euclid data object created by method + * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidKernel( - Art2aEuclidData anArt2aEuclidData + Art2aEuclidData aPreprocessedArt2aEuclidData ) throws IllegalArgumentException { this( - anArt2aEuclidData, - Math.max((int) (anArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + aPreprocessedArt2aEuclidData, + Math.max((int) (aPreprocessedArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java index 40d066a..71a7180 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -173,8 +173,8 @@ public Art2aEuclidTask( /** * Constructor. * - * @param anArt2aEuclidData ART-2a-Euclid data object created by method - * Art2aEuclidKernel.getArt2aEuclidData() + * @param aPreprocessedArt2aEuclidData PreprocessedART-2a-Euclid data object created by method + * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) @@ -188,7 +188,7 @@ public Art2aEuclidTask( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidTask( - Art2aEuclidData anArt2aEuclidData, + Art2aEuclidData aPreprocessedArt2aEuclidData, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, @@ -210,7 +210,7 @@ public Art2aEuclidTask( try { this.art2aClusteringKernel = new Art2aEuclidKernel( - anArt2aEuclidData, + aPreprocessedArt2aEuclidData, aMaximumNumberOfClusters, aMaximumNumberOfEpochs, aConvergenceThreshold, @@ -236,13 +236,13 @@ public Art2aEuclidTask( * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param anArt2aEuclidData ART-2a-Euclid data object created by method - * Art2aEuclidKernel.getArt2aEuclidData() + * @param aPreprocessedArt2aEuclidData Preprocessed ART-2a-Euclid data object created by method + * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidTask( - Art2aEuclidData anArt2aEuclidData, + Art2aEuclidData aPreprocessedArt2aEuclidData, float aVigilance ) throws IllegalArgumentException { // @@ -259,7 +259,7 @@ public Art2aEuclidTask( try { this.art2aClusteringKernel = new Art2aEuclidKernel( - anArt2aEuclidData + aPreprocessedArt2aEuclidData ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 0bdd70d..9145979 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -94,7 +94,8 @@ * CAUTION: Construction of several ART-2a clustering instances with the SAME * data matrix PLUS preprocessing is NOT advised due to the significant memory * consumption of each instance. In this case, the data matrix should be - * checked with static method Utils.isDataMatrixValid() and then a priori + * checked with static method Utils.isDataMatrixValid() (where possible NaN + * values can be removed with Utils.isNonFiniteComponentRemoval()) and then a priori * converted into a preprocessed Art2aData object with static method * Art2aKernel.getArt2aData(). The generated Art2aData object does NOT change * or refer to the data matrix so that the data matrix memory could be released @@ -378,8 +379,8 @@ public Art2aKernel( /** * Constructor. * - * @param anArt2aData ART-2a data object created by method - * Art2aKernel.getArt2aData() + * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method + * Art2aKernel.getPreprocessedArt2aData() * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) * @param aMaximumNumberOfEpochs Maximum number of epochs for training @@ -392,7 +393,7 @@ public Art2aKernel( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aKernel( - Art2aData anArt2aData, + Art2aData aPreprocessedArt2aData, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, float aConvergenceThreshold, @@ -400,7 +401,7 @@ public Art2aKernel( long aRandomSeed ) throws IllegalArgumentException { // - if(anArt2aData == null) { + if(aPreprocessedArt2aData == null) { Art2aKernel.LOGGER.log( Level.SEVERE, "Art2aKernel.Constructor: anArt2aData is null." @@ -437,7 +438,7 @@ public Art2aKernel( } // - this.art2aData = anArt2aData; + this.art2aData = aPreprocessedArt2aData; this.maximumNumberOfClusters = aMaximumNumberOfClusters; this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; this.convergenceThreshold = aConvergenceThreshold; @@ -450,16 +451,16 @@ public Art2aKernel( * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param anArt2aData ART-2a data object created by method - * Art2aKernel.getArt2aData() + * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method + * Art2aKernel.getPreprocessedArt2aData() * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aKernel( - Art2aData anArt2aData + Art2aData aPreprocessedArt2aData ) throws IllegalArgumentException { this( - anArt2aData, - Math.max((int) (anArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + aPreprocessedArt2aData, + Math.max((int) (aPreprocessedArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java index 1077f97..8e3778d 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -172,8 +172,8 @@ public Art2aTask( /** * Constructor. * - * @param anArt2aData ART-2a data object created by method - * Art2aKernel.getArt2aData() + * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method + * Art2aKernel.getPreprocessedArt2aData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) @@ -187,7 +187,7 @@ public Art2aTask( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aTask( - Art2aData anArt2aData, + Art2aData aPreprocessedArt2aData, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, @@ -209,7 +209,7 @@ public Art2aTask( try { this.art2aClusteringKernel = new Art2aKernel( - anArt2aData, + aPreprocessedArt2aData, aMaximumNumberOfClusters, aMaximumNumberOfEpochs, aConvergenceThreshold, @@ -235,13 +235,13 @@ public Art2aTask( * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param anArt2aData ART-2a data object created by method - * Art2aKernel.getArt2aData() + * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method + * Art2aKernel.getPreprocessedArt2aData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aTask( - Art2aData anArt2aData, + Art2aData aPreprocessedArt2aData, float aVigilance ) throws IllegalArgumentException { // @@ -258,7 +258,7 @@ public Art2aTask( try { this.art2aClusteringKernel = new Art2aKernel( - anArt2aData + aPreprocessedArt2aData ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java index b52d6b1..16ff0d3 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java @@ -198,6 +198,8 @@ protected int getNumberOfDetectedClusters() { protected Utils() {} // + // TODO: Make tests for public methods + // /** * Checks if aDataMatrix is valid. @@ -249,8 +251,7 @@ public static boolean isNonFiniteComponentRemoval( return false; } - int tmpNumberOfDataVectorComponents = aDataMatrix[0].length; - if(tmpNumberOfDataVectorComponents < 2) { + if(aDataMatrix[0].length < 2) { return false; } @@ -259,7 +260,7 @@ public static boolean isNonFiniteComponentRemoval( return false; } - if(tmpNumberOfDataVectorComponents != tmpDataVector.length) { + if(aDataMatrix[0].length != tmpDataVector.length) { return false; } } @@ -267,7 +268,33 @@ public static boolean isNonFiniteComponentRemoval( boolean tmpHasNonFiniteComponent = Utils.hasNonFiniteComponent(aDataMatrix); if (tmpHasNonFiniteComponent) { - // TODO: Remove columns with non-finite components + // Remove columns with non-finite components + boolean[] tmpColumnsToBeRemoved = new boolean[aDataMatrix[0].length]; + Arrays.fill(tmpColumnsToBeRemoved, false); + for (float[] tmpDataVector : aDataMatrix) { + for (int i = 0; i < tmpDataVector.length; i++) { + if (!Float.isFinite(tmpDataVector[i])) { + tmpColumnsToBeRemoved[i] = true; + } + } + } + int tmpNumberOfColumnsToBeRemoved = 0; + for (boolean tmpColumnToBeRemoved : tmpColumnsToBeRemoved) { + if (tmpColumnToBeRemoved) { + tmpNumberOfColumnsToBeRemoved++; + } + } + for (int i = 0; i < aDataMatrix.length; i++) { + float[] tmpOldDataVector = aDataMatrix[i]; + float[] tmpNewDataVector = new float[tmpOldDataVector.length - tmpNumberOfColumnsToBeRemoved]; + int tmpIndex = 0; + for (int j = 0; j < tmpOldDataVector.length; j++) { + if (!tmpColumnsToBeRemoved[j]) { + tmpNewDataVector[tmpIndex++] = tmpOldDataVector[j]; + } + } + aDataMatrix[i] = tmpNewDataVector; + } } return tmpHasNonFiniteComponent; } From 5a46164133d567589df1e6fb42267f9c553194d2 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Sun, 9 Feb 2025 20:30:09 +0100 Subject: [PATCH 09/18] Further consolidation between ART-2a and ART-2a-Euclid --- .../clustering/art2a/Art2aEuclidData.java | 301 ----------------- .../clustering/art2a/Art2aEuclidKernel.java | 294 ++++++++++++++--- .../clustering/art2a/Art2aEuclidResult.java | 30 +- .../clustering/art2a/Art2aEuclidTask.java | 12 +- .../clustering/art2a/Art2aEuclidUtils.java | 214 +----------- .../cheminf/clustering/art2a/Art2aKernel.java | 310 +++++++++++++++--- .../cheminf/clustering/art2a/Art2aResult.java | 30 +- .../cheminf/clustering/art2a/Art2aTask.java | 12 +- .../cheminf/clustering/art2a/Art2aUtils.java | 240 +------------- .../{Art2aData.java => PreprocessedData.java} | 145 ++++---- .../clustering/art2a/Art2aEuclidTest.java | 8 +- .../cheminf/clustering/art2a/Art2aTest.java | 8 +- 12 files changed, 668 insertions(+), 936 deletions(-) delete mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java rename src/main/java/de/unijena/cheminf/clustering/art2a/{Art2aData.java => PreprocessedData.java} (62%) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java deleted file mode 100644 index 4c436e3..0000000 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidData.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * ART-2a-Euclid Clustering for Java - * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny - * - * Source code is available at - * - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package de.unijena.cheminf.clustering.art2a; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Data class for ART-2a-Euclid clustering. - *

- * Note: Art2aEuclidData objects are to be generated with - * Art2aEuclidKernel.getArt2aEuclidData() methods to obtain preprocessed data - * for faster ART-2a-Euclid clustering. - *

- * Art2aEuclidData is also used for internal data preprocessing in class - * Art2aEuclidKernel. A private constructor ensures that original dataMatrix - * and preprocessed contrastEnhancedMatrix/dataVectorZeroLengthFlags are - * mutually exclusive. Use method hasPreprocessedData() to check wether - * preprocessed contrastEnhancedMatrix/dataVectorZeroLengthFlags are available. - *

- * Note: Art2aEuclidData is a read-only class, i.e. thread-safe. The same - * Art2aEuclidData object may be distributed to several concurrently working - * Art2aEuclidTasks without any mutual interference problems. - * - * @author Achim Zielesny - */ -public class Art2aEuclidData { - - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(Art2aEuclidData.class.getName()); - // - // - /** - * Original data matrix with data row vectors - */ - private final float[][] dataMatrix; - /** - * Matrix of contrast enhanced unit vectors - */ - private final float[][] contrastEnhancedMatrix; - /** - * Flags array that indicates if scaled data row vectors have a length - * of zero (i.e. where all components are equal to zero, the corresponding - * contrast enhanced unit vector is set to null in this case). True: - * Scaled data row vector has a length of zero, false: Otherwise. - */ - private final boolean[] dataVectorZeroLengthFlags; - /** - * Min-max components of original data matrix (see method - * Utils.getMinMaxComponents() for data structure) - */ - private final Utils.MinMaxValue[] minMaxComponentsOfDataMatrix; - /** - * Offset for contrast enhancement - */ - private final float offsetForContrastEnhancement; - /** - * Returns if Art2aData object has preprocessed data, i.e. - * contrastEnhancedUnitMatrix and dataVectorZeroLengthFlags are defined: - * True: Art2aData object has preprocessed data, false: Otherwise - */ - private final boolean hasPreprocessedData; - // - - - // - /** - * Private constructor - * Note: No checks are necessary - * - * @param aDataMatrix Original data matrix with data row vectors (MAY BE NULL) - * @param aContrastEnhancedMatrix Matrix of contrast enhanced unit - * vectors (MAY BE NULL) - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this - * case), false: Otherwise. - * @param aMinMaxComponentsOfDataMatrix Min-max components of original data - * matrix - * @param anOffsetForContrastEnhancement Offset for contrast enhancement - * (must be greater zero) - * @param aHasPreprocessedData True: Art2aData object has preprocessed data, - * false: Otherwise - * @throws IllegalArgumentException Thrown if an argument is illegal - */ - private Art2aEuclidData ( - float[][] aDataMatrix, - float[][] aContrastEnhancedMatrix, - boolean[] aDataVectorZeroLengthFlags, - Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, - float anOffsetForContrastEnhancement, - boolean aHasPreprocessedData - ) { - this.dataMatrix = aDataMatrix; - this.contrastEnhancedMatrix = aContrastEnhancedMatrix; - this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; - this.minMaxComponentsOfDataMatrix = aMinMaxComponentsOfDataMatrix; - this.offsetForContrastEnhancement = anOffsetForContrastEnhancement; - this.hasPreprocessedData = aHasPreprocessedData; - } - // - // - /** - * Constructor - * - * @param aDataMatrix Original data matrix with data row vectors (NOT - * allowed to be null) - * @param aMinMaxComponentsOfDataMatrix Min-max components of original data - * matrix - * @param anOffsetForContrastEnhancement Offset for contrast enhancement - * (must be greater zero) - * @throws IllegalArgumentException Thrown if an argument is illegal - */ - protected Art2aEuclidData ( - float[][] aDataMatrix, - Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, - float anOffsetForContrastEnhancement - ) { - this ( - aDataMatrix, - null, - null, - aMinMaxComponentsOfDataMatrix, - anOffsetForContrastEnhancement, - false - ); - if (!Utils.isMatrixValid(aDataMatrix)) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: aDataMatrix is invalid." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: aDataMatrix is invalid"); - } - if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aDataMatrix[0].length) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); - } - if (anOffsetForContrastEnhancement <= 0.0f) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero."); - } - } - - /** - * Constructor - * - * @param aContrastEnhancedMatrix Matrix of contrast enhanced unit - * vectors (NOT allowed to be null) - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this - * case), false: Otherwise. - * @param aMinMaxComponentsOfDataMatrix Min-max components of original data - * matrix - * @param anOffsetForContrastEnhancement Offset for contrast enhancement - * (must be greater zero) - * @throws IllegalArgumentException Thrown if an argument is illegal - */ - protected Art2aEuclidData ( - float[][] aContrastEnhancedMatrix, - boolean[] aDataVectorZeroLengthFlags, - Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, - float anOffsetForContrastEnhancement - ) { - this ( - null, - aContrastEnhancedMatrix, - aDataVectorZeroLengthFlags, - aMinMaxComponentsOfDataMatrix, - anOffsetForContrastEnhancement, - true - ); - if (!Utils.isMatrixValid(aContrastEnhancedMatrix)) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: aContrastEnhancedMatrix is invalid." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: aContrastEnhancedMatrix is invalid."); - } - if (aDataVectorZeroLengthFlags == null || aDataVectorZeroLengthFlags.length == 0 || aDataVectorZeroLengthFlags.length != aContrastEnhancedMatrix.length) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: aDataVectorZeroLengthFlags is illegal." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: aDataVectorZeroLengthFlags is illegal."); - } - if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aContrastEnhancedMatrix[0].length) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); - } - if (anOffsetForContrastEnhancement <= 0.0f) { - Art2aEuclidData.LOGGER.log( - Level.SEVERE, - "Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero." - ); - throw new IllegalArgumentException("Art2aEuclidData.Constructor: anOffsetForContrastEnhancement must be greater zero."); - } - } - // - - // - /** - * Original data matrix with data row vectors - * - * @return Original data matrix with data row vectors or null if - * hasPreprocessedData() returns true - */ - protected float[][] getDataMatrix() { - return this.dataMatrix; - } - - /** - * Matrix of contrast enhanced unit vectors - * - * @return Matrix of contrast enhanced unit vectors or null if - * hasPreprocessedData() returns false - */ - protected float[][] getContrastEnhancedMatrix() { - return this.contrastEnhancedMatrix; - } - - /** - * Flags array that indicates if scaled data row vectors have a length - * of zero (i.e. where all components are equal to zero, the corresponding - * contrast enhanced unit vector is set to null in this case). True: - * Scaled data row vector has a length of zero, false: Otherwise. - * - * @return Array with flags or null if hasPreprocessedData() returns false - */ - protected boolean[] getDataVectorZeroLengthFlags() { - return this.dataVectorZeroLengthFlags; - } - - /** - * Min-max components of original data matrix (see method Utils.getMinMaxComponents() for data structure) - * - * @return Min-max components of original data matrix - */ - protected Utils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { - return this.minMaxComponentsOfDataMatrix; - } - - /** - * Returns if Art2aEuclidData object has preprocessed data, i.e. - * contrastEnhancedMatrix and dataVectorZeroLengthFlags are defined. - * - * @return True: Art2aEuclidData object has preprocessed data, false: Otherwise - */ - protected boolean hasPreprocessedData() { - return this.hasPreprocessedData; - } - - /** - * Returns offset for contrast enhancement - * - * @return Offset for contrast enhancement - */ - protected float getOffsetForContrastEnhancement() { - return this.offsetForContrastEnhancement; - } - // - -} \ No newline at end of file diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index a9c1519..ecf5a5f 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -134,11 +134,13 @@ public class Art2aEuclidKernel { */ private static final Logger LOGGER = Logger.getLogger(Art2aEuclidKernel.class.getName()); //
- // + // /** * Value 1.0 */ private static final float ONE = 1.0f; + // + // /** * Default fraction of the (maximum) number of clusters relative to * number of data vectors @@ -188,9 +190,9 @@ public class Art2aEuclidKernel { */ private final long randomSeed; /** - * Art2aEuclidData data object + * PreprocessedData object */ - private final Art2aEuclidData art2aEuclidData; + private final PreprocessedData preprocessedData; // // /** @@ -337,14 +339,14 @@ public Art2aEuclidKernel( // if(anIsDataPreprocessing) { - this.art2aEuclidData = + this.preprocessedData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData( aDataMatrix, anOffsetForContrastEnhancement ); } else { - this.art2aEuclidData = - new Art2aEuclidData( + this.preprocessedData = + new PreprocessedData( aDataMatrix, Utils.getMinMaxComponents(aDataMatrix), anOffsetForContrastEnhancement @@ -386,7 +388,7 @@ public Art2aEuclidKernel( /** * Constructor. * - * @param aPreprocessedArt2aEuclidData Preprocessed ART-2a-Euclid data object + * @param aPreprocessedArt2aEuclidData PreprocessedData object * created by method Art2aEuclidKernel.getPreprocessedArt2aEuclidData() * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) @@ -400,7 +402,7 @@ public Art2aEuclidKernel( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidKernel( - Art2aEuclidData aPreprocessedArt2aEuclidData, + PreprocessedData aPreprocessedArt2aEuclidData, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, float aConvergenceThreshold, @@ -411,9 +413,16 @@ public Art2aEuclidKernel( if(aPreprocessedArt2aEuclidData == null) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, - "Art2aEuclidKernel.Constructor: anArt2aEuclidData is null." + "Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData is null." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData is null."); + } + if(aPreprocessedArt2aEuclidData.hasArt2aPreprocessedData()) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData does not have ART-2a-Euclid preprocessed data." ); - throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: anArt2aEuclidData is null."); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData does not have ART-2a-Euclid preprocessed data."); } if(aMaximumNumberOfEpochs <= 0) { Art2aEuclidKernel.LOGGER.log( @@ -445,7 +454,7 @@ public Art2aEuclidKernel( } // - this.art2aEuclidData = aPreprocessedArt2aEuclidData; + this.preprocessedData = aPreprocessedArt2aEuclidData; this.maximumNumberOfClusters = aMaximumNumberOfClusters; this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; this.convergenceThreshold = aConvergenceThreshold; @@ -458,16 +467,16 @@ public Art2aEuclidKernel( * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param aPreprocessedArt2aEuclidData Preprocessed ART-2a-Euclid data object created by method + * @param aPreprocessedArt2aEuclidData PreprocessedData object created by method * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidKernel( - Art2aEuclidData aPreprocessedArt2aEuclidData + PreprocessedData aPreprocessedArt2aEuclidData ) throws IllegalArgumentException { this( aPreprocessedArt2aEuclidData, - Math.max((int) (aPreprocessedArt2aEuclidData.getContrastEnhancedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + Math.max((int) (aPreprocessedArt2aEuclidData.getPreprocessedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -511,19 +520,19 @@ public Art2aEuclidResult getClusterResult( boolean[] tmpDataVectorZeroLengthFlags = null; int tmpNumberOfComponents = -1; int tmpNumberOfDataVectors = -1; - if (this.art2aEuclidData.hasPreprocessedData()) { - tmpContrastEnhancedMatrix = this.art2aEuclidData.getContrastEnhancedMatrix(); - tmpDataVectorZeroLengthFlags = this.art2aEuclidData.getDataVectorZeroLengthFlags(); + if (this.preprocessedData.hasPreprocessedData()) { + tmpContrastEnhancedMatrix = this.preprocessedData.getPreprocessedMatrix(); + tmpDataVectorZeroLengthFlags = this.preprocessedData.getDataVectorZeroLengthFlags(); tmpNumberOfDataVectors = tmpContrastEnhancedMatrix.length; tmpNumberOfComponents = tmpContrastEnhancedMatrix[0].length; } else { - tmpDataMatrix = this.art2aEuclidData.getDataMatrix(); + tmpDataMatrix = this.preprocessedData.getDataMatrix(); tmpDataVectorZeroLengthFlags = new boolean[tmpDataMatrix.length]; Utils.fillVector(tmpDataVectorZeroLengthFlags, false); tmpNumberOfDataVectors = tmpDataMatrix.length; tmpNumberOfComponents = tmpDataMatrix[0].length; } - Utils.MinMaxValue[] tmpMinMaxComponents = this.art2aEuclidData.getMinMaxComponentsOfDataMatrix(); + Utils.MinMaxValue[] tmpMinMaxComponents = this.preprocessedData.getMinMaxComponentsOfDataMatrix(); // Set tmpRhoStar float tmpRhoStar = tmpNumberOfComponents * (ONE - aVigilance); @@ -532,7 +541,7 @@ public Art2aEuclidResult getClusterResult( float tmpThresholdForContrastEnhancement = Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, - this.art2aEuclidData.getOffsetForContrastEnhancement() + this.preprocessedData.getOffsetForContrastEnhancement() ); // Scaling factor alpha float tmpScalingFactor = tmpThresholdForContrastEnhancement; @@ -580,7 +589,7 @@ public Art2aEuclidResult getClusterResult( continue; } - if (this.art2aEuclidData.hasPreprocessedData()) { + if (this.preprocessedData.hasPreprocessedData()) { Utils.copyVector(tmpContrastEnhancedMatrix[tmpRandomIndex], tmpBufferVector); } else { tmpDataVectorZeroLengthFlags[tmpRandomIndex] = @@ -603,7 +612,7 @@ public Art2aEuclidResult getClusterResult( tmpNumberOfDetectedClusters++; } else { // Cluster number is greater than or equal to 1 - Art2aEuclidUtils.setRhoWinner( + Art2aEuclidKernel.setRhoWinner( tmpBufferVector, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -626,7 +635,7 @@ public Art2aEuclidResult getClusterResult( // Assign to existing winner cluster with modification // Note: tmpBufferVector (= contrast enhanced unit vector) // is used for modification - Art2aEuclidUtils.modifyWinnerCluster( + Art2aEuclidKernel.modifyWinnerCluster( tmpBufferVector, tmpClusterMatrix[tmpRhoWinner.getIndexOfCluster()], tmpThresholdForContrastEnhancement, @@ -648,8 +657,8 @@ public Art2aEuclidResult getClusterResult( tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); tmpIsConverged = false; } else { - tmpIsConverged = - Art2aEuclidUtils.isConverged( + tmpIsConverged = + Art2aEuclidKernel.isConverged( tmpNumberOfDetectedClusters, tmpCurrentNumberOfEpochs, tmpClusterMatrix, @@ -662,10 +671,10 @@ public Art2aEuclidResult getClusterResult( // Check if cluster overflow occurred if (tmpIsClusterOverflow) { // Cluster overflow occurred: Finally assign ALL data vectors - Art2aEuclidUtils.assignDataVectorsToClusters( + Art2aEuclidKernel.assignDataVectorsToClusters( tmpNumberOfDetectedClusters, tmpDataVectorZeroLengthFlags, - this.art2aEuclidData, + this.preprocessedData, tmpBufferVector, tmpThresholdForContrastEnhancement, tmpClusterMatrix, @@ -685,10 +694,10 @@ public Art2aEuclidResult getClusterResult( // clusters in the cluster matrix while (tmpClusterRemovalInfo.isClusterRemoved()) { // Empty clusters are removed: Assign data vectors again - Art2aEuclidUtils.assignDataVectorsToClusters( + Art2aEuclidKernel.assignDataVectorsToClusters( tmpNumberOfDetectedClusters, tmpDataVectorZeroLengthFlags, - this.art2aEuclidData, + this.preprocessedData, tmpBufferVector, tmpThresholdForContrastEnhancement, tmpClusterMatrix, @@ -713,7 +722,7 @@ public Art2aEuclidResult getClusterResult( tmpDataVectorZeroLengthFlags, tmpIsClusterOverflow, tmpIsConverged, - this.art2aEuclidData + this.preprocessedData ); } catch (Exception anException) { Art2aEuclidKernel.LOGGER.log( @@ -1004,8 +1013,8 @@ public int[] getBestRepresentatives( //
// /** - * Creates ART-2a-Euclid data object with preprocessed data for maximum - * speed of the clustering process. The ART-2a-Euclid data object allocates + * Creates PreprocessedData object with preprocessed ART-2a-Euclid data for maximum + * speed of the clustering process. The PreprocessedData object allocates * about the same memory as aDataMatrix. *
* Note: There a no checks! Check aDataMatrix in advance with method @@ -1018,10 +1027,10 @@ public int[] getBestRepresentatives( * with Utils.isDataMatrixValid() in advance) * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @return ART-2a-Euclid data object for maximum clustering speed but with + * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aEuclidData getPreprocessedArt2aEuclidData( + public static PreprocessedData getPreprocessedArt2aEuclidData( float[][] aDataMatrix, float anOffsetForContrastEnhancement ) { @@ -1052,17 +1061,18 @@ public static Art2aEuclidData getPreprocessedArt2aEuclidData( ); tmpContrastEnhancedMatrix[i] = tmpContrastEnhancedVector; } - return new Art2aEuclidData( + return new PreprocessedData( tmpContrastEnhancedMatrix, tmpDataVectorZeroLengthFlags, tmpMinMaxComponents, - anOffsetForContrastEnhancement + anOffsetForContrastEnhancement, + false ); } /** - * Creates ART-2a-Euclid data object with preprocessed data for maximum - * speed of the clustering process. The ART-2a-Euclid data object allocates + * Creates PreprocessedData object with preprocessed ART-2a-Euclid data for maximum + * speed of the clustering process. The PreprocessedData object allocates * about twice the memory of aDataMatrix. A default value of 1.0 is used * for the offset for contrast enhancement. *
@@ -1071,14 +1081,218 @@ public static Art2aEuclidData getPreprocessedArt2aEuclidData( * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Utils.isDataMatrixValid() in advance) - * @return ART-2a-Euclid data object for maximum clustering speed but with + * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aEuclidData getPreprocessedArt2aEuclidData( + public static PreprocessedData getPreprocessedArt2aEuclidData( float[][] aDataMatrix ) { return Art2aEuclidKernel.getPreprocessedArt2aEuclidData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); } //
+ // + /** + * Assigns data vectors to clusters + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param aPreprocessedArt2aEuclidData PreprocessedData instance (IS NOT CHANGED) + * @param aBufferVector Buffer vector (MUST BE ALREADY INSTANTIATED) + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aClusterIndexOfDataVector Cluster index of data vector (MAY BE + * CHANGED and MUST ALREADY BE INSTANTIATED) + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (MAY BE CHANGED and MUST + * ALREADY BE INSTANTIATED) + */ + private static void assignDataVectorsToClusters( + int aNumberOfDetectedClusters, + boolean[] aDataVectorZeroLengthFlags, + PreprocessedData aPreprocessedArt2aEuclidData, + float[] aBufferVector, + float aThresholdForContrastEnhancement, + float[][]aClusterMatrix, + int[] aClusterIndexOfDataVector, + boolean[] aClusterUsageFlags + ) { + // Assign data vectors to clusters (last pass) + Arrays.fill(aClusterUsageFlags, false); + for (int i = 0; i < aDataVectorZeroLengthFlags.length; i++) { + if (!aDataVectorZeroLengthFlags[i]) { + if (aPreprocessedArt2aEuclidData.hasPreprocessedData()) { + aBufferVector = aPreprocessedArt2aEuclidData.getPreprocessedMatrix()[i]; + } else { + // Check of length is NOT necessary + Art2aEuclidUtils.setContrastEnhancedVector( + aPreprocessedArt2aEuclidData.getDataMatrix()[i], + aBufferVector, + aPreprocessedArt2aEuclidData.getMinMaxComponentsOfDataMatrix(), + aThresholdForContrastEnhancement + ); + } + int tmpWinnerClusterIndex = + Art2aEuclidKernel.getClusterIndex( + aBufferVector, + aNumberOfDetectedClusters, + aClusterMatrix + ); + aClusterIndexOfDataVector[i] = tmpWinnerClusterIndex; + aClusterUsageFlags[tmpWinnerClusterIndex] = true; + } + } + } + + /** + * Returns index of cluster for contrast enhanced vector + * + * @param aContrastEnhancedVector Contrast enhanced vector + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterMatrix Cluster matrix + * @return Index of cluster for contrast enhanced unit vector + */ + private static int getClusterIndex( + float[] aContrastEnhancedVector, + int aNumberOfDetectedClusters, + float[][] aClusterMatrix + ) { + float tmpMinSquaredDistance = Float.MAX_VALUE; + int tmpWinnerClusterIndex = -1; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpSquaredDistance = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); + if (tmpSquaredDistance < tmpMinSquaredDistance) { + tmpMinSquaredDistance = tmpSquaredDistance; + tmpWinnerClusterIndex = i; + } + } + return tmpWinnerClusterIndex; + } + + /** + * Determines convergence of clustering process. + * Note: No checks are performed. + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param anEpoch Current epochs + * @param aClusterCentroidMatrix Cluster centroid matrix with centroid row + * vectors + * @param aClusterCentroidMatrixOld Cluster centroid matrix with + * centroid row vectors of the previous epoch + * @param aMaximumNumberOfEpochs Maximum number of epochs + * @param aConvergenceThreshold Convergence threshold + * @return True if clustering process has converged, false otherwise. + */ + private static boolean isConverged( + int aNumberOfDetectedClusters, + int anEpoch, + float[][] aClusterCentroidMatrix, + float[][] aClusterCentroidMatrixOld, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold + ) { + if (anEpoch == 1) { + // Convergence check needs at least 2 epochs + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + return false; + } else { + float tmpSquaredConvergenceThreshold = aConvergenceThreshold * aConvergenceThreshold; + boolean tmpIsConverged = false; + if(anEpoch < aMaximumNumberOfEpochs) { + // Check convergence by evaluating the similarity (scalar product) + // of the cluster vectors of this and the previous epoch + tmpIsConverged = true; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if ( + aClusterCentroidMatrixOld[i] == null || + Utils.getSquaredDistance( + aClusterCentroidMatrix[i], + aClusterCentroidMatrixOld[i] + ) > tmpSquaredConvergenceThreshold + ) { + tmpIsConverged = false; + break; + } + } + if(!tmpIsConverged) { + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + } + } + return tmpIsConverged; + } + } + + /** + * Modifies winner cluster (see code). + * Note: aContrastEnhancedVector is used for modification and may be + * changed. + * Note: No checks are performed. + * + * @param aContrastEnhancedVector Contrast enhanced unit vector for + * modification (MAY BE CHANGED) + * @param aWinnerClusterVector Winner cluster centroid vector (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + * @param aLearningParameter Learning parameter + */ + private static void modifyWinnerCluster( + float[] aContrastEnhancedVector, + float[] aWinnerClusterVector, + float aThresholdForContrastEnhancement, + float aLearningParameter + ) { + // Note: aContrastEnhancedVector is used for modification + for(int j = 0; j < aWinnerClusterVector.length; j++) { + if(aWinnerClusterVector[j] <= aThresholdForContrastEnhancement) { + aContrastEnhancedVector[j] = 0.0f; + } + } + float tmpFactor = ONE - aLearningParameter; + for(int j = 0; j < aWinnerClusterVector.length; j++) { + aContrastEnhancedVector[j] = aLearningParameter * aContrastEnhancedVector[j] + tmpFactor * aWinnerClusterVector[j]; + } + Utils.copyVector(aContrastEnhancedVector, aWinnerClusterVector); + } + + /** + * Sets rho winner with the rho value and the cluster index of the winner + * (see code). If the cluster index is negative the first scaled rho value + * is the winner. + * + * @param aContrastEnhancedVector Contrast enhanced unit vector (IS NOT + * CHANGED) + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aScalingFactor Scaling factor + * @param aRhoWinner Rho winner: Is set with the rho value and the cluster + * index of the winner. If the cluster index is negative the first scaled + * rho value is the winner. + */ + private static void setRhoWinner( + float[] aContrastEnhancedVector, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + float aScalingFactor, + Utils.RhoWinner aRhoWinner + ) { + // Calculate first rho value + float tmpRhoValue = Utils.getSumOfSquaredDifferences(aContrastEnhancedVector, aScalingFactor); + // Set winner index to negative value + int tmpIndex = -1; + // Calculate other rho values + for(int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpRhoForCluster = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); + if(tmpRhoForCluster < tmpRhoValue) { + tmpRhoValue = tmpRhoForCluster; + tmpIndex = i; + } + } + aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); + } + // + } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java index 16f02b7..d63d2e0 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java @@ -90,9 +90,9 @@ public class Art2aEuclidResult { */ private final boolean isConverged; /** - * Art2aEuclidData object + * PreprocessedData object */ - private final Art2aEuclidData art2aData; + private final PreprocessedData preprocessedArt2aEuclidData; // // /** @@ -138,7 +138,7 @@ public int compareTo(IndexedValue anotherIndexedValue) { * @param anIsClusterOverflow True: Cluster overflow occurred, false: * Otherwise * @param anIsConverged True: Clustering process converged, false: Otherwise - * @param anArt2aEuclidData Art2aEuclidData instance + * @param aPreprocessedArt2aEuclidData PreprocessedData instance */ public Art2aEuclidResult( float aVigilance, @@ -150,7 +150,7 @@ public Art2aEuclidResult( boolean[] aDataVectorZeroLengthFlags, boolean anIsClusterOverflow, boolean anIsConverged, - Art2aEuclidData anArt2aEuclidData + PreprocessedData aPreprocessedArt2aEuclidData ) { this.vigilance = aVigilance; this.thresholdForContrastEnhancement = aThresholdForContrastEnhancement; @@ -161,7 +161,7 @@ public Art2aEuclidResult( this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; this.isClusterOverflow = anIsClusterOverflow; this.isConverged = anIsConverged; - this.art2aData = anArt2aEuclidData; + this.preprocessedArt2aEuclidData = aPreprocessedArt2aEuclidData; } // @@ -389,19 +389,19 @@ public int getClusterRepresentativeIndex( int tmpBestIndex = 0; float tmpMinimumDistance = Float.MAX_VALUE; float[] tmpContrastEnhancedVector = null; - if (!this.art2aData.hasPreprocessedData()) { + if (!this.preprocessedArt2aEuclidData.hasPreprocessedData()) { tmpContrastEnhancedVector = new float[tmpClusterVector.length]; } for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { int tmpIndex = tmpDataVectorIndicesOfCluster[i]; - if (this.art2aData.hasPreprocessedData()) { - tmpContrastEnhancedVector = this.art2aData.getContrastEnhancedMatrix()[tmpIndex]; + if (this.preprocessedArt2aEuclidData.hasPreprocessedData()) { + tmpContrastEnhancedVector = this.preprocessedArt2aEuclidData.getPreprocessedMatrix()[tmpIndex]; } else { // Check of length is NOT necessary Art2aEuclidUtils.setContrastEnhancedVector( - this.art2aData.getDataMatrix()[tmpIndex], + this.preprocessedArt2aEuclidData.getDataMatrix()[tmpIndex], tmpContrastEnhancedVector, - this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.preprocessedArt2aEuclidData.getMinMaxComponentsOfDataMatrix(), this.thresholdForContrastEnhancement ); } @@ -444,19 +444,19 @@ public int[] getClusterRepresentativeIndices( float[] tmpClusterVector = this.clusterMatrix[aClusterIndex]; IndexedValue[] tmpIndexedValues = new IndexedValue[tmpDataVectorIndicesOfCluster.length]; float[] tmpContrastEnhancedVector = null; - if (!this.art2aData.hasPreprocessedData()) { + if (!this.preprocessedArt2aEuclidData.hasPreprocessedData()) { tmpContrastEnhancedVector = new float[tmpClusterVector.length]; } for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { int tmpIndex = tmpDataVectorIndicesOfCluster[i]; - if (this.art2aData.hasPreprocessedData()) { - tmpContrastEnhancedVector = this.art2aData.getContrastEnhancedMatrix()[tmpIndex]; + if (this.preprocessedArt2aEuclidData.hasPreprocessedData()) { + tmpContrastEnhancedVector = this.preprocessedArt2aEuclidData.getPreprocessedMatrix()[tmpIndex]; } else { // Check of length is NOT necessary Art2aEuclidUtils.setContrastEnhancedVector( - this.art2aData.getDataMatrix()[tmpIndex], + this.preprocessedArt2aEuclidData.getDataMatrix()[tmpIndex], tmpContrastEnhancedVector, - this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.preprocessedArt2aEuclidData.getMinMaxComponentsOfDataMatrix(), this.thresholdForContrastEnhancement ); } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java index 71a7180..381f59a 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -173,8 +173,8 @@ public Art2aEuclidTask( /** * Constructor. * - * @param aPreprocessedArt2aEuclidData PreprocessedART-2a-Euclid data object created by method - * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() + * @param aPreprocessedArt2aEuclidData PreprocessedData data object created by method + * Art2aEuclidKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) @@ -188,7 +188,7 @@ public Art2aEuclidTask( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidTask( - Art2aEuclidData aPreprocessedArt2aEuclidData, + PreprocessedData aPreprocessedArt2aEuclidData, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, @@ -236,13 +236,13 @@ public Art2aEuclidTask( * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param aPreprocessedArt2aEuclidData Preprocessed ART-2a-Euclid data object created by method - * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() + * @param aPreprocessedArt2aEuclidData PreprocessedData object created by method + * Art2aEuclidKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidTask( - Art2aEuclidData aPreprocessedArt2aEuclidData, + PreprocessedData aPreprocessedArt2aEuclidData, float aVigilance ) throws IllegalArgumentException { // diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java index fe8a060..b0fd252 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java @@ -26,11 +26,8 @@ package de.unijena.cheminf.clustering.art2a; -import java.util.Arrays; - /** - * Library of helper records, static helper classes and static, thread-safe - * (stateless) utility methods for ART-2a-Euclid clustering. + * Library of static, thread-safe (stateless) utility methods for ART-2a-Euclid clustering. *

* Note: No checks are performed. * @@ -38,13 +35,6 @@ */ public class Art2aEuclidUtils { - // - /** - * Value 1.0 - */ - private static final float ONE = 1.0f; - // - // /** * Constructor @@ -53,172 +43,6 @@ protected Art2aEuclidUtils() {} // // - /** - * Assigns data vectors to clusters - * - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this - * case), false: Otherwise. - * @param anArt2aEuclidData Art2aEuclidData instance (IS NOT CHANGED) - * @param aBufferVector Buffer vector (MUST BE ALREADY INSTANTIATED) - * @param aThresholdForContrastEnhancement Threshold for contrast - * enhancement - * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) - * @param aClusterIndexOfDataVector Cluster index of data vector (MAY BE - * CHANGED and MUST ALREADY BE INSTANTIATED) - * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, - * false: Cluster is empty and has to be removed (MAY BE CHANGED and MUST - * ALREADY BE INSTANTIATED) - */ - protected static void assignDataVectorsToClusters( - int aNumberOfDetectedClusters, - boolean[] aDataVectorZeroLengthFlags, - Art2aEuclidData anArt2aEuclidData, - float[] aBufferVector, - float aThresholdForContrastEnhancement, - float[][]aClusterMatrix, - int[] aClusterIndexOfDataVector, - boolean[] aClusterUsageFlags - ) { - // Assign data vectors to clusters (last pass) - Arrays.fill(aClusterUsageFlags, false); - for (int i = 0; i < aDataVectorZeroLengthFlags.length; i++) { - if (!aDataVectorZeroLengthFlags[i]) { - if (anArt2aEuclidData.hasPreprocessedData()) { - aBufferVector = anArt2aEuclidData.getContrastEnhancedMatrix()[i]; - } else { - // Check of length is NOT necessary - Art2aEuclidUtils.setContrastEnhancedVector( - anArt2aEuclidData.getDataMatrix()[i], - aBufferVector, - anArt2aEuclidData.getMinMaxComponentsOfDataMatrix(), - aThresholdForContrastEnhancement - ); - } - int tmpWinnerClusterIndex = - Art2aEuclidUtils.getClusterIndex( - aBufferVector, - aNumberOfDetectedClusters, - aClusterMatrix - ); - aClusterIndexOfDataVector[i] = tmpWinnerClusterIndex; - aClusterUsageFlags[tmpWinnerClusterIndex] = true; - } - } - } - - /** - * Returns index of cluster for contrast enhanced vector - * - * @param aContrastEnhancedVector Contrast enhanced vector - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aClusterMatrix Cluster matrix - * @return Index of cluster for contrast enhanced unit vector - */ - protected static int getClusterIndex( - float[] aContrastEnhancedVector, - int aNumberOfDetectedClusters, - float[][] aClusterMatrix - ) { - float tmpMinSquaredDistance = Float.MAX_VALUE; - int tmpWinnerClusterIndex = -1; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpSquaredDistance = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); - if (tmpSquaredDistance < tmpMinSquaredDistance) { - tmpMinSquaredDistance = tmpSquaredDistance; - tmpWinnerClusterIndex = i; - } - } - return tmpWinnerClusterIndex; - } - - /** - * Determines convergence of clustering process. - * Note: No checks are performed. - * - * @param aNumberOfDetectedClusters Number of detected clusters - * @param anEpoch Current epochs - * @param aClusterCentroidMatrix Cluster centroid matrix with centroid row - * vectors - * @param aClusterCentroidMatrixOld Cluster centroid matrix with - * centroid row vectors of the previous epoch - * @param aMaximumNumberOfEpochs Maximum number of epochs - * @param aConvergenceThreshold Convergence threshold - * @return True if clustering process has converged, false otherwise. - */ - protected static boolean isConverged( - int aNumberOfDetectedClusters, - int anEpoch, - float[][] aClusterCentroidMatrix, - float[][] aClusterCentroidMatrixOld, - int aMaximumNumberOfEpochs, - float aConvergenceThreshold - ) { - if (anEpoch == 1) { - // Convergence check needs at least 2 epochs - Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); - return false; - } else { - float tmpSquaredConvergenceThreshold = aConvergenceThreshold * aConvergenceThreshold; - boolean tmpIsConverged = false; - if(anEpoch < aMaximumNumberOfEpochs) { - // Check convergence by evaluating the similarity (scalar product) - // of the cluster vectors of this and the previous epoch - tmpIsConverged = true; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if ( - aClusterCentroidMatrixOld[i] == null || - Utils.getSquaredDistance( - aClusterCentroidMatrix[i], - aClusterCentroidMatrixOld[i] - ) > tmpSquaredConvergenceThreshold - ) { - tmpIsConverged = false; - break; - } - } - if(!tmpIsConverged) { - Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); - } - } - return tmpIsConverged; - } - } - - /** - * Modifies winner cluster (see code). - * Note: aContrastEnhancedVector is used for modification and may be - * changed. - * Note: No checks are performed. - * - * @param aContrastEnhancedVector Contrast enhanced unit vector for - * modification (MAY BE CHANGED) - * @param aWinnerClusterVector Winner cluster centroid vector (MAY BE CHANGED) - * @param aThresholdForContrastEnhancement Threshold for contrast enhancement - * @param aLearningParameter Learning parameter - */ - protected static void modifyWinnerCluster( - float[] aContrastEnhancedVector, - float[] aWinnerClusterVector, - float aThresholdForContrastEnhancement, - float aLearningParameter - ) { - // Note: aContrastEnhancedVector is used for modification - for(int j = 0; j < aWinnerClusterVector.length; j++) { - if(aWinnerClusterVector[j] <= aThresholdForContrastEnhancement) { - aContrastEnhancedVector[j] = 0.0f; - } - } - float tmpFactor = ONE - aLearningParameter; - for(int j = 0; j < aWinnerClusterVector.length; j++) { - aContrastEnhancedVector[j] = aLearningParameter * aContrastEnhancedVector[j] + tmpFactor * aWinnerClusterVector[j]; - } - Utils.copyVector(aContrastEnhancedVector, aWinnerClusterVector); - } - /** * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). @@ -254,42 +78,6 @@ protected static boolean setContrastEnhancedVector( return false; } } - - /** - * Sets rho winner with the rho value and the cluster index of the winner - * (see code). If the cluster index is negative the first scaled rho value - * is the winner. - * - * @param aContrastEnhancedVector Contrast enhanced unit vector (IS NOT - * CHANGED) - * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aScalingFactor Scaling factor - * @param aRhoWinner Rho winner: Is set with the rho value and the cluster - * index of the winner. If the cluster index is negative the first scaled - * rho value is the winner. - */ - protected static void setRhoWinner( - float[] aContrastEnhancedVector, - float[][] aClusterMatrix, - int aNumberOfDetectedClusters, - float aScalingFactor, - Utils.RhoWinner aRhoWinner - ) { - // Calculate first rho value - float tmpRhoValue = Utils.getSumOfSquaredDifferences(aContrastEnhancedVector, aScalingFactor); - // Set winner index to negative value - int tmpIndex = -1; - // Calculate other rho values - for(int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpRhoForCluster = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]); - if(tmpRhoForCluster < tmpRhoValue) { - tmpRhoValue = tmpRhoForCluster; - tmpIndex = i; - } - } - aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); - } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 9145979..7b932bf 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -131,7 +131,13 @@ public class Art2aKernel { */ private static final Logger LOGGER = Logger.getLogger(Art2aKernel.class.getName()); //
- // + // + /** + * Value 1.0 + */ + private static final float ONE = 1.0f; + // + // /** * Default fraction of the (maximum) number of clusters relative to * number of data vectors @@ -181,9 +187,9 @@ public class Art2aKernel { */ private final long randomSeed; /** - * Art2aData data object + * PreprocessedData object */ - private final Art2aData art2aData; + private final PreprocessedData preprocessedData; // // /** @@ -330,14 +336,14 @@ public Art2aKernel( // if(anIsDataPreprocessing) { - this.art2aData = + this.preprocessedData = Art2aKernel.getPreprocessedArt2aData( aDataMatrix, anOffsetForContrastEnhancement ); } else { - this.art2aData = - new Art2aData( + this.preprocessedData = + new PreprocessedData( aDataMatrix, Utils.getMinMaxComponents(aDataMatrix), anOffsetForContrastEnhancement @@ -379,8 +385,8 @@ public Art2aKernel( /** * Constructor. * - * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method - * Art2aKernel.getPreprocessedArt2aData() + * @param aPreprocessedArt2aData PreprocessedData object created by static + * method Art2aKernel.getPreprocessedArt2aData() * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) * @param aMaximumNumberOfEpochs Maximum number of epochs for training @@ -393,7 +399,7 @@ public Art2aKernel( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aKernel( - Art2aData aPreprocessedArt2aData, + PreprocessedData aPreprocessedArt2aData, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, float aConvergenceThreshold, @@ -404,9 +410,16 @@ public Art2aKernel( if(aPreprocessedArt2aData == null) { Art2aKernel.LOGGER.log( Level.SEVERE, - "Art2aKernel.Constructor: anArt2aData is null." + "Art2aKernel.Constructor: aPreprocessedArt2aData is null." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aPreprocessedArt2aData is null."); + } + if(!aPreprocessedArt2aData.hasArt2aPreprocessedData()) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aPreprocessedArt2aData does not have ART-2a preprocessed data." ); - throw new IllegalArgumentException("Art2aKernel.Constructor: anArt2aData is null."); + throw new IllegalArgumentException("Art2aKernel.Constructor: aPreprocessedArt2aData does not have ART-2a preprocessed data."); } if(aMaximumNumberOfEpochs <= 0) { Art2aKernel.LOGGER.log( @@ -438,7 +451,7 @@ public Art2aKernel( } // - this.art2aData = aPreprocessedArt2aData; + this.preprocessedData = aPreprocessedArt2aData; this.maximumNumberOfClusters = aMaximumNumberOfClusters; this.maximumNumberOfEpochs = aMaximumNumberOfEpochs; this.convergenceThreshold = aConvergenceThreshold; @@ -451,16 +464,16 @@ public Art2aKernel( * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method - * Art2aKernel.getPreprocessedArt2aData() + * @param aPreprocessedArt2aData PreprocessedData object created by static + * method Art2aKernel.getPreprocessedArt2aData() * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aKernel( - Art2aData aPreprocessedArt2aData + PreprocessedData aPreprocessedArt2aData ) throws IllegalArgumentException { this( aPreprocessedArt2aData, - Math.max((int) (aPreprocessedArt2aData.getContrastEnhancedUnitMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + Math.max((int) (aPreprocessedArt2aData.getPreprocessedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -504,25 +517,25 @@ public Art2aResult getClusterResult( boolean[] tmpDataVectorZeroLengthFlags = null; int tmpNumberOfComponents = -1; int tmpNumberOfDataVectors = -1; - if (this.art2aData.hasPreprocessedData()) { - tmpContrastEnhancedUnitMatrix = this.art2aData.getContrastEnhancedUnitMatrix(); - tmpDataVectorZeroLengthFlags = this.art2aData.getDataVectorZeroLengthFlags(); + if (this.preprocessedData.hasPreprocessedData()) { + tmpContrastEnhancedUnitMatrix = this.preprocessedData.getPreprocessedMatrix(); + tmpDataVectorZeroLengthFlags = this.preprocessedData.getDataVectorZeroLengthFlags(); tmpNumberOfDataVectors = tmpContrastEnhancedUnitMatrix.length; tmpNumberOfComponents = tmpContrastEnhancedUnitMatrix[0].length; } else { - tmpDataMatrix = this.art2aData.getDataMatrix(); + tmpDataMatrix = this.preprocessedData.getDataMatrix(); tmpDataVectorZeroLengthFlags = new boolean[tmpDataMatrix.length]; Utils.fillVector(tmpDataVectorZeroLengthFlags, false); tmpNumberOfDataVectors = tmpDataMatrix.length; tmpNumberOfComponents = tmpDataMatrix[0].length; } - Utils.MinMaxValue[] tmpMinMaxComponents = this.art2aData.getMinMaxComponentsOfDataMatrix(); + Utils.MinMaxValue[] tmpMinMaxComponents = this.preprocessedData.getMinMaxComponentsOfDataMatrix(); // Definitions float tmpThresholdForContrastEnhancement = Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, - this.art2aData.getOffsetForContrastEnhancement() + this.preprocessedData.getOffsetForContrastEnhancement() ); // Scaling factor alpha float tmpScalingFactor = tmpThresholdForContrastEnhancement; @@ -570,10 +583,10 @@ public Art2aResult getClusterResult( continue; } - if (this.art2aData.hasPreprocessedData()) { + if (this.preprocessedData.hasPreprocessedData()) { Utils.copyVector(tmpContrastEnhancedUnitMatrix[tmpRandomIndex], tmpBufferVector); } else { - tmpDataVectorZeroLengthFlags[tmpRandomIndex] = + tmpDataVectorZeroLengthFlags[tmpRandomIndex] = Art2aUtils.setContrastEnhancedUnitVector( tmpDataMatrix[tmpRandomIndex], tmpBufferVector, @@ -593,7 +606,7 @@ public Art2aResult getClusterResult( tmpNumberOfDetectedClusters++; } else { // Cluster number is greater than or equal to 1 - Art2aUtils.setRhoWinner( + Art2aKernel.setRhoWinner( tmpBufferVector, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -616,7 +629,7 @@ public Art2aResult getClusterResult( // Assign to existing winner cluster with modification // Note: tmpBufferVector (= contrast enhanced unit vector) // is used for modification - Art2aUtils.modifyWinnerCluster( + Art2aKernel.modifyWinnerCluster( tmpBufferVector, tmpClusterMatrix[tmpRhoWinner.getIndexOfCluster()], tmpThresholdForContrastEnhancement, @@ -637,8 +650,8 @@ public Art2aResult getClusterResult( tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); tmpIsConverged = false; } else { - tmpIsConverged = - Art2aUtils.isConverged( + tmpIsConverged = + Art2aKernel.isConverged( tmpNumberOfDetectedClusters, tmpCurrentNumberOfEpochs, tmpClusterMatrix, @@ -651,10 +664,10 @@ public Art2aResult getClusterResult( // Check if cluster overflow occurred if (tmpIsClusterOverflow) { // Cluster overflow occurred: Finally assign ALL data vectors - Art2aUtils.assignDataVectorsToClusters( + Art2aKernel.assignDataVectorsToClusters( tmpNumberOfDetectedClusters, tmpDataVectorZeroLengthFlags, - this.art2aData, + this.preprocessedData, tmpBufferVector, tmpThresholdForContrastEnhancement, tmpClusterMatrix, @@ -674,10 +687,10 @@ public Art2aResult getClusterResult( // clusters in the cluster matrix while (tmpClusterRemovalInfo.isClusterRemoved()) { // Empty clusters are removed: Assign data vectors again - Art2aUtils.assignDataVectorsToClusters( + Art2aKernel.assignDataVectorsToClusters( tmpNumberOfDetectedClusters, tmpDataVectorZeroLengthFlags, - this.art2aData, + this.preprocessedData, tmpBufferVector, tmpThresholdForContrastEnhancement, tmpClusterMatrix, @@ -702,7 +715,7 @@ public Art2aResult getClusterResult( tmpDataVectorZeroLengthFlags, tmpIsClusterOverflow, tmpIsConverged, - this.art2aData + this.preprocessedData ); } catch (Exception anException) { Art2aKernel.LOGGER.log( @@ -993,8 +1006,8 @@ public int[] getBestRepresentatives( // // /** - * Creates ART-2a data object with preprocessed data for maximum speed - * of the clustering process. The ART-2a data object allocates about the + * Creates PreprocessedData object with preprocessed ART-2a data for maximum speed + * of the clustering process. The PreprocessedData object allocates about the * same memory as aDataMatrix. *
* Note: There a no checks! Check aDataMatrix in advance with method @@ -1007,10 +1020,10 @@ public int[] getBestRepresentatives( * with Utils.isDataMatrixValid() in advance) * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @return ART-2a data object for maximum clustering speed but with + * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aData getPreprocessedArt2aData( + public static PreprocessedData getPreprocessedArt2aData( float[][] aDataMatrix, float anOffsetForContrastEnhancement ) { @@ -1032,7 +1045,7 @@ public static Art2aData getPreprocessedArt2aData( for(int i = 0; i < aDataMatrix.length; i++) { float[] tmpContrastEnhancedUnitVector = new float[tmpNumberOfComponents]; - tmpDataVectorZeroLengthFlags[i] = + tmpDataVectorZeroLengthFlags[i] = Art2aUtils.setContrastEnhancedUnitVector( aDataMatrix[i], tmpContrastEnhancedUnitVector, @@ -1041,17 +1054,18 @@ public static Art2aData getPreprocessedArt2aData( ); tmpContrastEnhancedUnitMatrix[i] = tmpContrastEnhancedUnitVector; } - return new Art2aData( + return new PreprocessedData( tmpContrastEnhancedUnitMatrix, tmpDataVectorZeroLengthFlags, tmpMinMaxComponents, - anOffsetForContrastEnhancement + anOffsetForContrastEnhancement, + true ); } /** - * Creates ART-2a data object with preprocessed data for maximum speed - * of the clustering process. The ART-2a data object allocates about twice + * Creates PreprocessedData object with preprocessed ART-2a data for maximum speed + * of the clustering process. The PreprocessedData object allocates about twice * the memory of aDataMatrix. A default value of 1.0 is used for the offset * for contrast enhancement. *
@@ -1060,14 +1074,222 @@ public static Art2aData getPreprocessedArt2aData( * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Utils.isDataMatrixValid() in advance) - * @return ART-2a data object for maximum clustering speed but with + * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static Art2aData getPreprocessedArt2aData( + public static PreprocessedData getPreprocessedArt2aData( float[][] aDataMatrix ) { return Art2aKernel.getPreprocessedArt2aData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); } //
+ // + /** + * Assigns data vectors to clusters + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this + * case), false: Otherwise. + * @param aPreprocessedArt2aData PreprocessedData instance (IS NOT CHANGED) + * @param aBufferVector Buffer vector (MUST BE ALREADY INSTANTIATED) + * @param aThresholdForContrastEnhancement Threshold for contrast + * enhancement + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aClusterIndexOfDataVector Cluster index of data vector (MAY BE + * CHANGED and MUST ALREADY BE INSTANTIATED) + * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, + * false: Cluster is empty and has to be removed (MAY BE CHANGED and MUST + * ALREADY BE INSTANTIATED) + */ + private static void assignDataVectorsToClusters( + int aNumberOfDetectedClusters, + boolean[] aDataVectorZeroLengthFlags, + PreprocessedData aPreprocessedArt2aData, + float[] aBufferVector, + float aThresholdForContrastEnhancement, + float[][]aClusterMatrix, + int[] aClusterIndexOfDataVector, + boolean[] aClusterUsageFlags + ) { + Arrays.fill(aClusterUsageFlags, false); + for (int i = 0; i < aDataVectorZeroLengthFlags.length; i++) { + if (!aDataVectorZeroLengthFlags[i]) { + if (aPreprocessedArt2aData.hasPreprocessedData()) { + aBufferVector = aPreprocessedArt2aData.getPreprocessedMatrix()[i]; + } else { + // Check of length is NOT necessary + Art2aUtils.setContrastEnhancedUnitVector( + aPreprocessedArt2aData.getDataMatrix()[i], + aBufferVector, + aPreprocessedArt2aData.getMinMaxComponentsOfDataMatrix(), + aThresholdForContrastEnhancement + ); + } + int tmpWinnerClusterIndex = + Art2aKernel.getClusterIndex( + aBufferVector, + aNumberOfDetectedClusters, + aClusterMatrix + ); + aClusterIndexOfDataVector[i] = tmpWinnerClusterIndex; + aClusterUsageFlags[tmpWinnerClusterIndex] = true; + } + } + } + + /** + * Returns index of cluster for contrast enhanced unit vector + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aClusterMatrix Cluster matrix + * @return Index of cluster for contrast enhanced unit vector + */ + private static int getClusterIndex( + float[] aContrastEnhancedUnitVector, + int aNumberOfDetectedClusters, + float[][] aClusterMatrix + ) { + float tmpMaxScalarProduct = Float.MIN_VALUE; + int tmpWinnerClusterIndex = -1; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpScalarProduct = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); + if (tmpScalarProduct > tmpMaxScalarProduct) { + tmpMaxScalarProduct = tmpScalarProduct; + tmpWinnerClusterIndex = i; + } + } + return tmpWinnerClusterIndex; + } + + /** + * Determines convergence of clustering process. + * Note: No checks are performed. + * + * @param aNumberOfDetectedClusters Number of detected clusters + * @param anEpoch Current epochs + * @param aClusterCentroidMatrix Cluster centroid matrix with centroid row + * vectors + * @param aClusterCentroidMatrixOld Cluster centroid matrix with + * centroid row vectors of the previous epoch + * @param aMaximumNumberOfEpochs Maximum number of epochs + * @param aConvergenceThreshold Convergence threshold + * @return True if clustering process has converged, false otherwise. + */ + private static boolean isConverged( + int aNumberOfDetectedClusters, + int anEpoch, + float[][] aClusterCentroidMatrix, + float[][] aClusterCentroidMatrixOld, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold + ) { + if (anEpoch == 1) { + // Convergence check needs at least 2 epochs + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + return false; + } else { + boolean tmpIsConverged = false; + if(anEpoch < aMaximumNumberOfEpochs) { + // Check convergence by evaluating the similarity (scalar product) + // of the cluster vectors of this and the previous epoch + tmpIsConverged = true; + for (int i = 0; i < aNumberOfDetectedClusters; i++) { + if ( + aClusterCentroidMatrixOld[i] == null || + Utils.getScalarProduct(aClusterCentroidMatrix[i], aClusterCentroidMatrixOld[i]) < aConvergenceThreshold + ) { + tmpIsConverged = false; + break; + } + } + if(!tmpIsConverged) { + Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); + } + } + return tmpIsConverged; + } + } + + /** + * Modifies winner cluster (see code). + * Note: aContrastEnhancedUnitVector is used for modification and may be + * changed. + * Note: No checks are performed. + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector for + * modification (MAY BE CHANGED) + * @param aWinnerClusterVector Winner cluster centroid vector (MAY BE CHANGED) + * @param aThresholdForContrastEnhancement Threshold for contrast enhancement + * @param aLearningParameter Learning parameter + */ + private static void modifyWinnerCluster( + float[] aContrastEnhancedUnitVector, + float[] aWinnerClusterVector, + float aThresholdForContrastEnhancement, + float aLearningParameter + ) { + // Note: aContrastEnhancedUnitVector is used for modification + boolean tmpIsChanged = false; + for(int j = 0; j < aWinnerClusterVector.length; j++) { + if(aWinnerClusterVector[j] <= aThresholdForContrastEnhancement) { + aContrastEnhancedUnitVector[j] = 0.0f; + tmpIsChanged = true; + } + } + float tmpFactor1; + if (tmpIsChanged) { + tmpFactor1 = aLearningParameter / Utils.getVectorLength(aContrastEnhancedUnitVector); + } else { + tmpFactor1 = aLearningParameter; + } + float tmpFactor2 = ONE - aLearningParameter; + for(int j = 0; j < aWinnerClusterVector.length; j++) { + aContrastEnhancedUnitVector[j] = tmpFactor1 * aContrastEnhancedUnitVector[j] + tmpFactor2 * aWinnerClusterVector[j]; + } + Utils.normalizeVector(aContrastEnhancedUnitVector); + Utils.copyVector(aContrastEnhancedUnitVector, aWinnerClusterVector); + } + + /** + * Sets rho winner with the rho value and the cluster index of the winner + * (see code). If the cluster index is negative the first scaled rho value + * is the winner. + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector (IS NOT + * CHANGED) + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aScalingFactor Scaling factor + * @param aRhoWinner Rho winner: Is set with the rho value and the cluster + * index of the winner. If the cluster index is negative the first scaled + * rho value is the winner. + */ + private static void setRhoWinner( + float[] aContrastEnhancedUnitVector, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + float aScalingFactor, + Utils.RhoWinner aRhoWinner + ) { + // Calculate first rho value + float tmpRhoValue = aScalingFactor * Utils.getSumOfComponents(aContrastEnhancedUnitVector); + // Set winner index to negative value + int tmpIndex = -1; + // Calculate other rho values + for(int i = 0; i < aNumberOfDetectedClusters; i++) { + float tmpRhoForCluster = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); + if(tmpRhoForCluster > tmpRhoValue) { + tmpRhoValue = tmpRhoForCluster; + tmpIndex = i; + } + } + aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); + } + // + } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java index 8b134ae..9b1837d 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -97,9 +97,9 @@ public class Art2aResult { */ private final boolean isConverged; /** - * Art2aData object + * PreprocessedData object */ - private final Art2aData art2aData; + private final PreprocessedData preprocessedArt2aData; // // /** @@ -145,7 +145,7 @@ public int compareTo(IndexedValue anotherIndexedValue) { * @param anIsClusterOverflow True: Cluster overflow occurred, false: * Otherwise * @param anIsConverged True: Clustering process converged, false: Otherwise - * @param anArt2aData Art2aData instance + * @param aPreprocessedArt2aData PreprocessedData instance */ public Art2aResult( float aVigilance, @@ -157,7 +157,7 @@ public Art2aResult( boolean[] aDataVectorZeroLengthFlags, boolean anIsClusterOverflow, boolean anIsConverged, - Art2aData anArt2aData + PreprocessedData aPreprocessedArt2aData ) { this.vigilance = aVigilance; this.thresholdForContrastEnhancement = aThresholdForContrastEnhancement; @@ -168,7 +168,7 @@ public Art2aResult( this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; this.isClusterOverflow = anIsClusterOverflow; this.isConverged = anIsConverged; - this.art2aData = anArt2aData; + this.preprocessedArt2aData = aPreprocessedArt2aData; } // @@ -395,19 +395,19 @@ public int getClusterRepresentativeIndex( int tmpBestIndex = 0; float tmpMaximumScalarProduct = Float.MIN_VALUE; float[] tmpContrastEnhancedUnitVector = null; - if (!this.art2aData.hasPreprocessedData()) { + if (!this.preprocessedArt2aData.hasPreprocessedData()) { tmpContrastEnhancedUnitVector = new float[tmpClusterVector.length]; } for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { int tmpIndex = tmpDataVectorIndicesOfCluster[i]; - if (this.art2aData.hasPreprocessedData()) { - tmpContrastEnhancedUnitVector = this.art2aData.getContrastEnhancedUnitMatrix()[tmpIndex]; + if (this.preprocessedArt2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitVector = this.preprocessedArt2aData.getPreprocessedMatrix()[tmpIndex]; } else { // Check of length is NOT necessary Art2aUtils.setContrastEnhancedUnitVector( - this.art2aData.getDataMatrix()[tmpIndex], + this.preprocessedArt2aData.getDataMatrix()[tmpIndex], tmpContrastEnhancedUnitVector, - this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.preprocessedArt2aData.getMinMaxComponentsOfDataMatrix(), this.thresholdForContrastEnhancement ); } @@ -450,19 +450,19 @@ public int[] getClusterRepresentativeIndices( float[] tmpClusterVector = this.clusterMatrix[aClusterIndex]; IndexedValue[] tmpIndexedValues = new IndexedValue[tmpDataVectorIndicesOfCluster.length]; float[] tmpContrastEnhancedUnitVector = null; - if (!this.art2aData.hasPreprocessedData()) { + if (!this.preprocessedArt2aData.hasPreprocessedData()) { tmpContrastEnhancedUnitVector = new float[tmpClusterVector.length]; } for (int i = 0; i < tmpDataVectorIndicesOfCluster.length; i++) { int tmpIndex = tmpDataVectorIndicesOfCluster[i]; - if (this.art2aData.hasPreprocessedData()) { - tmpContrastEnhancedUnitVector = this.art2aData.getContrastEnhancedUnitMatrix()[tmpIndex]; + if (this.preprocessedArt2aData.hasPreprocessedData()) { + tmpContrastEnhancedUnitVector = this.preprocessedArt2aData.getPreprocessedMatrix()[tmpIndex]; } else { // Check of length is NOT necessary Art2aUtils.setContrastEnhancedUnitVector( - this.art2aData.getDataMatrix()[tmpIndex], + this.preprocessedArt2aData.getDataMatrix()[tmpIndex], tmpContrastEnhancedUnitVector, - this.art2aData.getMinMaxComponentsOfDataMatrix(), + this.preprocessedArt2aData.getMinMaxComponentsOfDataMatrix(), this.thresholdForContrastEnhancement ); } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java index 8e3778d..93bb277 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -172,8 +172,8 @@ public Art2aTask( /** * Constructor. * - * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method - * Art2aKernel.getPreprocessedArt2aData() + * @param aPreprocessedArt2aData PreprocessedData object created by method + * Art2aKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) @@ -187,7 +187,7 @@ public Art2aTask( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aTask( - Art2aData aPreprocessedArt2aData, + PreprocessedData aPreprocessedArt2aData, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, @@ -235,13 +235,13 @@ public Art2aTask( * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * - * @param aPreprocessedArt2aData Preprocessed ART-2a data object created by method - * Art2aKernel.getPreprocessedArt2aData() + * @param aPreprocessedArt2aData PreprocessedData object created by method + * Art2aKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aTask( - Art2aData aPreprocessedArt2aData, + PreprocessedData aPreprocessedArt2aData, float aVigilance ) throws IllegalArgumentException { // diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java index a9f3e3a..438366c 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -26,11 +26,8 @@ package de.unijena.cheminf.clustering.art2a; -import java.util.Arrays; - /** - * Library of helper records, static helper classes and static, thread-safe - * (stateless) utility methods for ART-2a clustering. + * Library of static, thread-safe (stateless) utility methods for ART-2a clustering. *

* Note: No checks are performed. * @@ -38,13 +35,6 @@ */ public class Art2aUtils { - // - /** - * Value 1.0 - */ - private static final float ONE = 1.0f; - // - // /** * Constructor @@ -52,232 +42,26 @@ public class Art2aUtils { protected Art2aUtils() {} // - // - /** - * Assigns data vectors to clusters - * - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this - * case), false: Otherwise. - * @param anArt2aData Art2aData instance (IS NOT CHANGED) - * @param aBufferVector Buffer vector (MUST BE ALREADY INSTANTIATED) - * @param aThresholdForContrastEnhancement Threshold for contrast - * enhancement - * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) - * @param aClusterIndexOfDataVector Cluster index of data vector (MAY BE - * CHANGED and MUST ALREADY BE INSTANTIATED) - * @param aClusterUsageFlags Flags for cluster usage. True: Cluster is used, - * false: Cluster is empty and has to be removed (MAY BE CHANGED and MUST - * ALREADY BE INSTANTIATED) - */ - protected static void assignDataVectorsToClusters( - int aNumberOfDetectedClusters, - boolean[] aDataVectorZeroLengthFlags, - Art2aData anArt2aData, - float[] aBufferVector, - float aThresholdForContrastEnhancement, - float[][]aClusterMatrix, - int[] aClusterIndexOfDataVector, - boolean[] aClusterUsageFlags - ) { - Arrays.fill(aClusterUsageFlags, false); - for (int i = 0; i < aDataVectorZeroLengthFlags.length; i++) { - if (!aDataVectorZeroLengthFlags[i]) { - if (anArt2aData.hasPreprocessedData()) { - aBufferVector = anArt2aData.getContrastEnhancedUnitMatrix()[i]; - } else { - // Check of length is NOT necessary - Art2aUtils.setContrastEnhancedUnitVector( - anArt2aData.getDataMatrix()[i], - aBufferVector, - anArt2aData.getMinMaxComponentsOfDataMatrix(), - aThresholdForContrastEnhancement - ); - } - int tmpWinnerClusterIndex = - Art2aUtils.getClusterIndex( - aBufferVector, - aNumberOfDetectedClusters, - aClusterMatrix - ); - aClusterIndexOfDataVector[i] = tmpWinnerClusterIndex; - aClusterUsageFlags[tmpWinnerClusterIndex] = true; - } - } - } - + // /** - * Returns index of cluster for contrast enhanced unit vector - * - * @param aContrastEnhancedUnitVector Contrast enhanced unit vector - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aClusterMatrix Cluster matrix - * @return Index of cluster for contrast enhanced unit vector - */ - protected static int getClusterIndex( - float[] aContrastEnhancedUnitVector, - int aNumberOfDetectedClusters, - float[][] aClusterMatrix - ) { - float tmpMaxScalarProduct = Float.MIN_VALUE; - int tmpWinnerClusterIndex = -1; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpScalarProduct = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); - if (tmpScalarProduct > tmpMaxScalarProduct) { - tmpMaxScalarProduct = tmpScalarProduct; - tmpWinnerClusterIndex = i; - } - } - return tmpWinnerClusterIndex; - } - - /** - * Determines convergence of clustering process. - * Note: No checks are performed. - * - * @param aNumberOfDetectedClusters Number of detected clusters - * @param anEpoch Current epochs - * @param aClusterCentroidMatrix Cluster centroid matrix with centroid row - * vectors - * @param aClusterCentroidMatrixOld Cluster centroid matrix with - * centroid row vectors of the previous epoch - * @param aMaximumNumberOfEpochs Maximum number of epochs - * @param aConvergenceThreshold Convergence threshold - * @return True if clustering process has converged, false otherwise. - */ - protected static boolean isConverged( - int aNumberOfDetectedClusters, - int anEpoch, - float[][] aClusterCentroidMatrix, - float[][] aClusterCentroidMatrixOld, - int aMaximumNumberOfEpochs, - float aConvergenceThreshold - ) { - if (anEpoch == 1) { - // Convergence check needs at least 2 epochs - Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); - return false; - } else { - boolean tmpIsConverged = false; - if(anEpoch < aMaximumNumberOfEpochs) { - // Check convergence by evaluating the similarity (scalar product) - // of the cluster vectors of this and the previous epoch - tmpIsConverged = true; - for (int i = 0; i < aNumberOfDetectedClusters; i++) { - if ( - aClusterCentroidMatrixOld[i] == null || - Utils.getScalarProduct(aClusterCentroidMatrix[i], aClusterCentroidMatrixOld[i]) < aConvergenceThreshold - ) { - tmpIsConverged = false; - break; - } - } - if(!tmpIsConverged) { - Utils.copyRows(aClusterCentroidMatrix, aClusterCentroidMatrixOld, aNumberOfDetectedClusters); - } - } - return tmpIsConverged; - } - } - - /** - * Modifies winner cluster (see code). - * Note: aContrastEnhancedUnitVector is used for modification and may be - * changed. - * Note: No checks are performed. - * - * @param aContrastEnhancedUnitVector Contrast enhanced unit vector for - * modification (MAY BE CHANGED) - * @param aWinnerClusterVector Winner cluster centroid vector (MAY BE CHANGED) - * @param aThresholdForContrastEnhancement Threshold for contrast enhancement - * @param aLearningParameter Learning parameter - */ - protected static void modifyWinnerCluster( - float[] aContrastEnhancedUnitVector, - float[] aWinnerClusterVector, - float aThresholdForContrastEnhancement, - float aLearningParameter - ) { - // Note: aContrastEnhancedUnitVector is used for modification - boolean tmpIsChanged = false; - for(int j = 0; j < aWinnerClusterVector.length; j++) { - if(aWinnerClusterVector[j] <= aThresholdForContrastEnhancement) { - aContrastEnhancedUnitVector[j] = 0.0f; - tmpIsChanged = true; - } - } - float tmpFactor1; - if (tmpIsChanged) { - tmpFactor1 = aLearningParameter / Utils.getVectorLength(aContrastEnhancedUnitVector); - } else { - tmpFactor1 = aLearningParameter; - } - float tmpFactor2 = ONE - aLearningParameter; - for(int j = 0; j < aWinnerClusterVector.length; j++) { - aContrastEnhancedUnitVector[j] = tmpFactor1 * aContrastEnhancedUnitVector[j] + tmpFactor2 * aWinnerClusterVector[j]; - } - Utils.normalizeVector(aContrastEnhancedUnitVector); - Utils.copyVector(aContrastEnhancedUnitVector, aWinnerClusterVector); - } - - /** - * Sets rho winner with the rho value and the cluster index of the winner - * (see code). If the cluster index is negative the first scaled rho value - * is the winner. - * - * @param aContrastEnhancedUnitVector Contrast enhanced unit vector (IS NOT - * CHANGED) - * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) - * @param aNumberOfDetectedClusters Number of detected clusters - * @param aScalingFactor Scaling factor - * @param aRhoWinner Rho winner: Is set with the rho value and the cluster - * index of the winner. If the cluster index is negative the first scaled - * rho value is the winner. - */ - protected static void setRhoWinner( - float[] aContrastEnhancedUnitVector, - float[][] aClusterMatrix, - int aNumberOfDetectedClusters, - float aScalingFactor, - Utils.RhoWinner aRhoWinner - ) { - // Calculate first rho value - float tmpRhoValue = aScalingFactor * Utils.getSumOfComponents(aContrastEnhancedUnitVector); - // Set winner index to negative value - int tmpIndex = -1; - // Calculate other rho values - for(int i = 0; i < aNumberOfDetectedClusters; i++) { - float tmpRhoForCluster = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); - if(tmpRhoForCluster > tmpRhoValue) { - tmpRhoValue = tmpRhoForCluster; - tmpIndex = i; - } - } - aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); - } - - /** - * Transforms original data vector into corresponding contrast enhanced + * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). * Note: No checks are performed. - * + * * @param aDataVector Data vector (IS NOT CHANGED) - * @param aBufferVector Buffer vector for contrast enhanced unit vector - * derived from data vector (MUST ALREADY BE INSTANTIATED and is set within + * @param aBufferVector Buffer vector for contrast enhanced unit vector + * derived from data vector (MUST ALREADY BE INSTANTIATED and is set within * the method) * @param aMinMaxComponents Min-max components of original data matrix - * @param aThresholdForContrastEnhancement Threshold for contrast + * @param aThresholdForContrastEnhancement Threshold for contrast * enhancement * @return True: Scaled data vector has a length of zero, false: Otherwise */ protected static boolean setContrastEnhancedUnitVector( - float[] aDataVector, - float[] aBufferVector, - Utils.MinMaxValue[] aMinMaxComponents, - float aThresholdForContrastEnhancement + float[] aDataVector, + float[] aBufferVector, + Utils.MinMaxValue[] aMinMaxComponents, + float aThresholdForContrastEnhancement ) { // Already allocated memory of aBufferVector is reused Utils.copyVector(aDataVector, aBufferVector); @@ -298,5 +82,5 @@ protected static boolean setContrastEnhancedUnitVector( } } // - + } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java similarity index 62% rename from src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java rename to src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java index b00b9cb..6877474 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java @@ -32,28 +32,32 @@ /** * Data class for ART-2a clustering. *

- * Note: Art2aData objects are to be generated with Art2aKernel.getArt2aData() - * methods to obtain preprocessed data for faster ART-2a clustering. + * Note: PreprocessedData objects are to be generated with static getPreprocessedData() + * methods to obtain preprocessed data for faster clustering. PreprocessedData objects can + * refer to Art2aKernel or Art2aEuclidKernel instances: An internal safeguard is + * implemented to avoid incorrect assignments, i.e. a PreprocessedData instance knows if + * it was created by Art2aKernel or Art2aEuclidKernel if preprocessed data are present. *

- * Art2aData is also used for internal data preprocessing in class Art2aKernel. - * A private constructor ensures that original dataMatrix and preprocessed - * contrastEnhancedUnitMatrix/dataVectorZeroLengthFlags are mutually exclusive. - * Use method hasPreprocessedData() to check wether preprocessed - * contrastEnhancedUnitMatrix/dataVectorZeroLengthFlags are available. + * Note: PreprocessedData is also used for internal data preprocessing, i.e. it + * may not necessarily contain preprocessed data. + * A private constructor ensures that original dataMatrix and + * preprocessedMatrix/dataVectorZeroLengthFlags are mutually exclusive. + * Use method hasPreprocessedData() to check whether preprocessed + * preprocessedMatrix/dataVectorZeroLengthFlags are available. *

- * Note: Art2aData is a read-only class, i.e. thread-safe. The same Art2aData - * object may be distributed to several concurrently working Art2aTasks without + * Note: PreprocessedData is a read-only class, i.e. thread-safe. The same PreprocessedData + * object may be distributed to several concurrently working clustering tasks without * any mutual interference problems. * * @author Achim Zielesny */ -public class Art2aData { +public class PreprocessedData { // /** * Logger of this class */ - private static final Logger LOGGER = Logger.getLogger(Art2aData.class.getName()); + private static final Logger LOGGER = Logger.getLogger(PreprocessedData.class.getName()); // // /** @@ -61,19 +65,19 @@ public class Art2aData { */ private final float[][] dataMatrix; /** - * Matrix of contrast enhanced unit vectors + * Preprocessed matrix */ - private final float[][] contrastEnhancedUnitMatrix; + private final float[][] preprocessedMatrix; /** * Flags array that indicates if scaled data row vectors have a length * of zero (i.e. where all components are equal to zero, the corresponding - * contrast enhanced unit vector is set to null in this case). True: + * preprocessed vector is set to null in this case). True: * Scaled data row vector has a length of zero, false: Otherwise. */ private final boolean[] dataVectorZeroLengthFlags; /** * Min-max components of original data matrix (see method - * Art2aUtils.getMinMaxComponents() for data structure) + * Utils.getMinMaxComponents() for data structure) */ private final Utils.MinMaxValue[] minMaxComponentsOfDataMatrix; /** @@ -81,11 +85,14 @@ public class Art2aData { */ private final float offsetForContrastEnhancement; /** - * Returns if Art2aData object has preprocessed data, i.e. - * contrastEnhancedUnitMatrix and dataVectorZeroLengthFlags are defined: - * True: Art2aData object has preprocessed data, false: Otherwise + * True: PreprocessedData object has preprocessed data, false: Otherwise */ private final boolean hasPreprocessedData; + /** + * True: Preprocessed data were generated with Art2aKernel, + * false: Preprocessed data were generated with Art2aEuclidKernel + */ + private final boolean hasArt2aPreprocessedData; // @@ -95,8 +102,7 @@ public class Art2aData { * Note: No checks are necessary * * @param aDataMatrix Original data matrix with data row vectors (MAY BE NULL) - * @param aContrastEnhancedUnitMatrix Matrix of contrast enhanced unit - * vectors (MAY BE NULL) + * @param aPreprocessedMatrix Preprocessed matrix (MAY BE NULL) * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled * data row vectors have a length of zero (i.e. where all components are * equal to zero). True: Scaled data row vector has a length of zero @@ -106,24 +112,28 @@ public class Art2aData { * matrix * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @param aHasPreprocessedData True: Art2aData object has preprocessed data, + * @param aHasPreprocessedData True: PreprocessedData object has preprocessed data, * false: Otherwise + * @param aHasArt2aPreprocessedData True: Preprocessed data were generated with Art2aKernel, + * false: Preprocessed data were generated with Art2aEuclidKernel * @throws IllegalArgumentException Thrown if an argument is illegal */ - private Art2aData ( + private PreprocessedData ( float[][] aDataMatrix, - float[][] aContrastEnhancedUnitMatrix, + float[][] aPreprocessedMatrix, boolean[] aDataVectorZeroLengthFlags, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement, - boolean aHasPreprocessedData + boolean aHasPreprocessedData, + boolean aHasArt2aPreprocessedData ) { this.dataMatrix = aDataMatrix; - this.contrastEnhancedUnitMatrix = aContrastEnhancedUnitMatrix; + this.preprocessedMatrix = aPreprocessedMatrix; this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; this.minMaxComponentsOfDataMatrix = aMinMaxComponentsOfDataMatrix; this.offsetForContrastEnhancement = anOffsetForContrastEnhancement; this.hasPreprocessedData = aHasPreprocessedData; + this.hasArt2aPreprocessedData = aHasArt2aPreprocessedData; } //
// @@ -138,7 +148,7 @@ private Art2aData ( * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ - protected Art2aData ( + protected PreprocessedData ( float[][] aDataMatrix, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement @@ -149,36 +159,36 @@ protected Art2aData ( null, aMinMaxComponentsOfDataMatrix, anOffsetForContrastEnhancement, + false, false ); if (!Utils.isMatrixValid(aDataMatrix)) { - Art2aData.LOGGER.log( + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: aDataMatrix is invalid." + "PreprocessedData.Constructor: aDataMatrix is invalid." ); - throw new IllegalArgumentException("Art2aData.Constructor: aDataMatrix is invalid"); + throw new IllegalArgumentException("PreprocessedData.Constructor: aDataMatrix is invalid"); } if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aDataMatrix[0].length) { - Art2aData.LOGGER.log( + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." + "PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." ); - throw new IllegalArgumentException("Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); + throw new IllegalArgumentException("PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); } if (anOffsetForContrastEnhancement <= 0.0f) { - Art2aData.LOGGER.log( + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero." + "PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero." ); - throw new IllegalArgumentException("Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero."); + throw new IllegalArgumentException("PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero."); } } /** * Constructor * - * @param aContrastEnhancedUnitMatrix Matrix of contrast enhanced unit - * vectors (NOT allowed to be null) + * @param aPreprocessedMatrix Preprocessed matrix (NOT allowed to be null) * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled * data row vectors have a length of zero (i.e. where all components are * equal to zero). True: Scaled data row vector has a length of zero @@ -188,49 +198,53 @@ protected Art2aData ( * matrix * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) + * @param aHasArt2aPreprocessedData True: Preprocessed data were generated with Art2aKernel, + * false: Preprocessed data were generated with Art2aEuclidKernel * @throws IllegalArgumentException Thrown if an argument is illegal */ - protected Art2aData ( - float[][] aContrastEnhancedUnitMatrix, + protected PreprocessedData ( + float[][] aPreprocessedMatrix, boolean[] aDataVectorZeroLengthFlags, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, - float anOffsetForContrastEnhancement + float anOffsetForContrastEnhancement, + boolean aHasArt2aPreprocessedData ) { this ( null, - aContrastEnhancedUnitMatrix, + aPreprocessedMatrix, aDataVectorZeroLengthFlags, aMinMaxComponentsOfDataMatrix, anOffsetForContrastEnhancement, - true + true, + aHasArt2aPreprocessedData ); - if (!Utils.isMatrixValid(aContrastEnhancedUnitMatrix)) { - Art2aData.LOGGER.log( + if (!Utils.isMatrixValid(aPreprocessedMatrix)) { + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: aContrastEnhancedUnitMatrix is invalid." + "PreprocessedData.Constructor: aPreprocessedMatrix is invalid." ); - throw new IllegalArgumentException("Art2aData.Constructor: aContrastEnhancedUnitMatrix is invalid."); + throw new IllegalArgumentException("PreprocessedData.Constructor: aPreprocessedMatrix is invalid."); } - if (aDataVectorZeroLengthFlags == null || aDataVectorZeroLengthFlags.length == 0 || aDataVectorZeroLengthFlags.length != aContrastEnhancedUnitMatrix.length) { - Art2aData.LOGGER.log( + if (aDataVectorZeroLengthFlags == null || aDataVectorZeroLengthFlags.length == 0 || aDataVectorZeroLengthFlags.length != aPreprocessedMatrix.length) { + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: aDataVectorZeroLengthFlags is illegal." + "PreprocessedData.Constructor: aDataVectorZeroLengthFlags is illegal." ); - throw new IllegalArgumentException("Art2aData.Constructor: aDataVectorZeroLengthFlags is illegal."); + throw new IllegalArgumentException("PreprocessedData.Constructor: aDataVectorZeroLengthFlags is illegal."); } - if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aContrastEnhancedUnitMatrix[0].length) { - Art2aData.LOGGER.log( + if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aPreprocessedMatrix[0].length) { + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." + "PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." ); - throw new IllegalArgumentException("Art2aData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); + throw new IllegalArgumentException("PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); } if (anOffsetForContrastEnhancement <= 0.0f) { - Art2aData.LOGGER.log( + PreprocessedData.LOGGER.log( Level.SEVERE, - "Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero." + "PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero." ); - throw new IllegalArgumentException("Art2aData.Constructor: anOffsetForContrastEnhancement must be greater zero."); + throw new IllegalArgumentException("PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero."); } } // @@ -252,8 +266,8 @@ protected float[][] getDataMatrix() { * @return Matrix of contrast enhanced unit vectors or null if * hasPreprocessedData() returns false */ - protected float[][] getContrastEnhancedUnitMatrix() { - return this.contrastEnhancedUnitMatrix; + protected float[][] getPreprocessedMatrix() { + return this.preprocessedMatrix; } /** @@ -286,7 +300,18 @@ protected Utils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { protected boolean hasPreprocessedData() { return this.hasPreprocessedData; } - + + /** + * True: Preprocessed data were generated with Art2aKernel, + * false: Preprocessed data were generated with Art2aEuclidKernel + * + * @return True: Preprocessed data were generated with Art2aKernel, + * false: Preprocessed data were generated with Art2aEuclidKernel + */ + protected boolean hasArt2aPreprocessedData() { + return this.hasArt2aPreprocessedData; + } + /** * Returns offset for contrast enhancement * diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java index 6747482..17d362e 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -515,10 +515,10 @@ public void test_Art2aEuclidData() { } // Preprocessed Art2aEuclidData - Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); Art2aEuclidKernel tmpArt2aEuclidKernelWithArt2aEuclidData = new Art2aEuclidKernel( - tmpArt2aEuclidData, + tmpPreprocessedArt2aEuclidData, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -604,10 +604,10 @@ public void test_ParallelClustering() { // Concurrent (parallelized) clustering LinkedList tmpArt2aEuclidTaskList = new LinkedList<>(); - Art2aEuclidData tmpArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); for (float tmpVigilance : tmpVigilances) { tmpArt2aEuclidTaskList.add(new Art2aEuclidTask( - tmpArt2aEuclidData, + tmpPreprocessedArt2aEuclidData, tmpVigilance, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index f3eef05..5818c66 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -548,10 +548,10 @@ public void test_Art2aData() { } // Preprocessed Art2aData - Art2aData tmpArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); Art2aKernel tmpArt2aKernelWithArt2aData = new Art2aKernel( - tmpArt2aData, + tmpPreprocessedArt2aData, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -637,11 +637,11 @@ public void test_ParallelClustering() { // Concurrent (parallelized) clustering LinkedList tmpArt2aTaskList = new LinkedList<>(); - Art2aData tmpArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); for (float tmpVigilance : tmpVigilances) { tmpArt2aTaskList.add( new Art2aTask( - tmpArt2aData, + tmpPreprocessedArt2aData, tmpVigilance, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, From 70936e4fd9b5c280019de3f9de17fece53e0b6b5 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Sun, 9 Feb 2025 20:59:38 +0100 Subject: [PATCH 10/18] More expressive preprocessing data structures to avoid possible subtle errors --- .../clustering/art2a/Art2aEuclidKernel.java | 20 ++---- .../clustering/art2a/Art2aEuclidTask.java | 4 +- .../cheminf/clustering/art2a/Art2aKernel.java | 20 ++---- .../cheminf/clustering/art2a/Art2aTask.java | 4 +- .../art2a/PreprocessedArt2aData.java | 65 +++++++++++++++++++ .../art2a/PreprocessedArt2aEuclidData.java | 65 +++++++++++++++++++ .../clustering/art2a/PreprocessedData.java | 42 ++---------- .../clustering/art2a/Art2aEuclidTest.java | 4 +- .../cheminf/clustering/art2a/Art2aTest.java | 4 +- 9 files changed, 157 insertions(+), 71 deletions(-) create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java create mode 100644 src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index ecf5a5f..5c7e999 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -402,7 +402,7 @@ public Art2aEuclidKernel( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidKernel( - PreprocessedData aPreprocessedArt2aEuclidData, + PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, float aConvergenceThreshold, @@ -417,13 +417,6 @@ public Art2aEuclidKernel( ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData is null."); } - if(aPreprocessedArt2aEuclidData.hasArt2aPreprocessedData()) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData does not have ART-2a-Euclid preprocessed data." - ); - throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData does not have ART-2a-Euclid preprocessed data."); - } if(aMaximumNumberOfEpochs <= 0) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -472,7 +465,7 @@ public Art2aEuclidKernel( * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidKernel( - PreprocessedData aPreprocessedArt2aEuclidData + PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData ) throws IllegalArgumentException { this( aPreprocessedArt2aEuclidData, @@ -1030,7 +1023,7 @@ public int[] getBestRepresentatives( * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static PreprocessedData getPreprocessedArt2aEuclidData( + public static PreprocessedArt2aEuclidData getPreprocessedArt2aEuclidData( float[][] aDataMatrix, float anOffsetForContrastEnhancement ) { @@ -1061,12 +1054,11 @@ public static PreprocessedData getPreprocessedArt2aEuclidData( ); tmpContrastEnhancedMatrix[i] = tmpContrastEnhancedVector; } - return new PreprocessedData( + return new PreprocessedArt2aEuclidData( tmpContrastEnhancedMatrix, tmpDataVectorZeroLengthFlags, tmpMinMaxComponents, - anOffsetForContrastEnhancement, - false + anOffsetForContrastEnhancement ); } @@ -1084,7 +1076,7 @@ public static PreprocessedData getPreprocessedArt2aEuclidData( * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static PreprocessedData getPreprocessedArt2aEuclidData( + public static PreprocessedArt2aEuclidData getPreprocessedArt2aEuclidData( float[][] aDataMatrix ) { return Art2aEuclidKernel.getPreprocessedArt2aEuclidData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java index 381f59a..8b83650 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -188,7 +188,7 @@ public Art2aEuclidTask( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidTask( - PreprocessedData aPreprocessedArt2aEuclidData, + PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, @@ -242,7 +242,7 @@ public Art2aEuclidTask( * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidTask( - PreprocessedData aPreprocessedArt2aEuclidData, + PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData, float aVigilance ) throws IllegalArgumentException { // diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 7b932bf..bfdfaf5 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -399,7 +399,7 @@ public Art2aKernel( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aKernel( - PreprocessedData aPreprocessedArt2aData, + PreprocessedArt2aData aPreprocessedArt2aData, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, float aConvergenceThreshold, @@ -414,13 +414,6 @@ public Art2aKernel( ); throw new IllegalArgumentException("Art2aKernel.Constructor: aPreprocessedArt2aData is null."); } - if(!aPreprocessedArt2aData.hasArt2aPreprocessedData()) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.Constructor: aPreprocessedArt2aData does not have ART-2a preprocessed data." - ); - throw new IllegalArgumentException("Art2aKernel.Constructor: aPreprocessedArt2aData does not have ART-2a preprocessed data."); - } if(aMaximumNumberOfEpochs <= 0) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -469,7 +462,7 @@ public Art2aKernel( * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aKernel( - PreprocessedData aPreprocessedArt2aData + PreprocessedArt2aData aPreprocessedArt2aData ) throws IllegalArgumentException { this( aPreprocessedArt2aData, @@ -1023,7 +1016,7 @@ public int[] getBestRepresentatives( * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static PreprocessedData getPreprocessedArt2aData( + public static PreprocessedArt2aData getPreprocessedArt2aData( float[][] aDataMatrix, float anOffsetForContrastEnhancement ) { @@ -1054,12 +1047,11 @@ public static PreprocessedData getPreprocessedArt2aData( ); tmpContrastEnhancedUnitMatrix[i] = tmpContrastEnhancedUnitVector; } - return new PreprocessedData( + return new PreprocessedArt2aData( tmpContrastEnhancedUnitMatrix, tmpDataVectorZeroLengthFlags, tmpMinMaxComponents, - anOffsetForContrastEnhancement, - true + anOffsetForContrastEnhancement ); } @@ -1077,7 +1069,7 @@ public static PreprocessedData getPreprocessedArt2aData( * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) */ - public static PreprocessedData getPreprocessedArt2aData( + public static PreprocessedArt2aData getPreprocessedArt2aData( float[][] aDataMatrix ) { return Art2aKernel.getPreprocessedArt2aData(aDataMatrix, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT); diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java index 93bb277..b739c61 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -187,7 +187,7 @@ public Art2aTask( * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aTask( - PreprocessedData aPreprocessedArt2aData, + PreprocessedArt2aData aPreprocessedArt2aData, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, @@ -241,7 +241,7 @@ public Art2aTask( * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aTask( - PreprocessedData aPreprocessedArt2aData, + PreprocessedArt2aData aPreprocessedArt2aData, float aVigilance ) throws IllegalArgumentException { // diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java new file mode 100644 index 0000000..fc7e190 --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java @@ -0,0 +1,65 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +/** + * Class for preprocessed ART-2a data + */ +public class PreprocessedArt2aData extends PreprocessedData { + + // + /** + * Constructor + * + * @param aPreprocessedMatrix Preprocessed matrix (NOT allowed to be null) + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding preprocessed vector is set to null in this + * case), false: Otherwise. + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + protected PreprocessedArt2aData ( + float[][] aPreprocessedMatrix, + boolean[] aDataVectorZeroLengthFlags, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement + ) { + super ( + aPreprocessedMatrix, + aDataVectorZeroLengthFlags, + aMinMaxComponentsOfDataMatrix, + anOffsetForContrastEnhancement + ); + } + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java new file mode 100644 index 0000000..ddfddea --- /dev/null +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java @@ -0,0 +1,65 @@ +/* + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny + * + * Source code is available at + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package de.unijena.cheminf.clustering.art2a; + +/** + * Class for preprocessed ART-2a-Euclid data + */ +public class PreprocessedArt2aEuclidData extends PreprocessedData { + + // + /** + * Constructor + * + * @param aPreprocessedMatrix Preprocessed matrix (NOT allowed to be null) + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding preprocessed vector is set to null in this + * case), false: Otherwise. + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * matrix + * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * (must be greater zero) + * @throws IllegalArgumentException Thrown if an argument is illegal + */ + protected PreprocessedArt2aEuclidData ( + float[][] aPreprocessedMatrix, + boolean[] aDataVectorZeroLengthFlags, + Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, + float anOffsetForContrastEnhancement + ) { + super ( + aPreprocessedMatrix, + aDataVectorZeroLengthFlags, + aMinMaxComponentsOfDataMatrix, + anOffsetForContrastEnhancement + ); + } + // + +} diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java index 6877474..2e04bc9 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java @@ -30,13 +30,10 @@ import java.util.logging.Logger; /** - * Data class for ART-2a clustering. + * Class for preprocessed data. *

- * Note: PreprocessedData objects are to be generated with static getPreprocessedData() - * methods to obtain preprocessed data for faster clustering. PreprocessedData objects can - * refer to Art2aKernel or Art2aEuclidKernel instances: An internal safeguard is - * implemented to avoid incorrect assignments, i.e. a PreprocessedData instance knows if - * it was created by Art2aKernel or Art2aEuclidKernel if preprocessed data are present. + * Note: PreprocessedData instances are to be generated with static getPreprocessedData() + * methods to obtain preprocessed data for faster clustering. *

* Note: PreprocessedData is also used for internal data preprocessing, i.e. it * may not necessarily contain preprocessed data. @@ -88,11 +85,6 @@ public class PreprocessedData { * True: PreprocessedData object has preprocessed data, false: Otherwise */ private final boolean hasPreprocessedData; - /** - * True: Preprocessed data were generated with Art2aKernel, - * false: Preprocessed data were generated with Art2aEuclidKernel - */ - private final boolean hasArt2aPreprocessedData; //
@@ -114,8 +106,6 @@ public class PreprocessedData { * (must be greater zero) * @param aHasPreprocessedData True: PreprocessedData object has preprocessed data, * false: Otherwise - * @param aHasArt2aPreprocessedData True: Preprocessed data were generated with Art2aKernel, - * false: Preprocessed data were generated with Art2aEuclidKernel * @throws IllegalArgumentException Thrown if an argument is illegal */ private PreprocessedData ( @@ -124,8 +114,7 @@ private PreprocessedData ( boolean[] aDataVectorZeroLengthFlags, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement, - boolean aHasPreprocessedData, - boolean aHasArt2aPreprocessedData + boolean aHasPreprocessedData ) { this.dataMatrix = aDataMatrix; this.preprocessedMatrix = aPreprocessedMatrix; @@ -133,7 +122,6 @@ private PreprocessedData ( this.minMaxComponentsOfDataMatrix = aMinMaxComponentsOfDataMatrix; this.offsetForContrastEnhancement = anOffsetForContrastEnhancement; this.hasPreprocessedData = aHasPreprocessedData; - this.hasArt2aPreprocessedData = aHasArt2aPreprocessedData; } //
// @@ -159,7 +147,6 @@ protected PreprocessedData ( null, aMinMaxComponentsOfDataMatrix, anOffsetForContrastEnhancement, - false, false ); if (!Utils.isMatrixValid(aDataMatrix)) { @@ -192,22 +179,19 @@ protected PreprocessedData ( * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled * data row vectors have a length of zero (i.e. where all components are * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this + * (corresponding preprocessed vector is set to null in this * case), false: Otherwise. * @param aMinMaxComponentsOfDataMatrix Min-max components of original data * matrix * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @param aHasArt2aPreprocessedData True: Preprocessed data were generated with Art2aKernel, - * false: Preprocessed data were generated with Art2aEuclidKernel * @throws IllegalArgumentException Thrown if an argument is illegal */ protected PreprocessedData ( float[][] aPreprocessedMatrix, boolean[] aDataVectorZeroLengthFlags, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, - float anOffsetForContrastEnhancement, - boolean aHasArt2aPreprocessedData + float anOffsetForContrastEnhancement ) { this ( null, @@ -215,8 +199,7 @@ protected PreprocessedData ( aDataVectorZeroLengthFlags, aMinMaxComponentsOfDataMatrix, anOffsetForContrastEnhancement, - true, - aHasArt2aPreprocessedData + true ); if (!Utils.isMatrixValid(aPreprocessedMatrix)) { PreprocessedData.LOGGER.log( @@ -301,17 +284,6 @@ protected boolean hasPreprocessedData() { return this.hasPreprocessedData; } - /** - * True: Preprocessed data were generated with Art2aKernel, - * false: Preprocessed data were generated with Art2aEuclidKernel - * - * @return True: Preprocessed data were generated with Art2aKernel, - * false: Preprocessed data were generated with Art2aEuclidKernel - */ - protected boolean hasArt2aPreprocessedData() { - return this.hasArt2aPreprocessedData; - } - /** * Returns offset for contrast enhancement * diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java index 17d362e..50d249a 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -515,7 +515,7 @@ public void test_Art2aEuclidData() { } // Preprocessed Art2aEuclidData - PreprocessedData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedArt2aEuclidData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); Art2aEuclidKernel tmpArt2aEuclidKernelWithArt2aEuclidData = new Art2aEuclidKernel( tmpPreprocessedArt2aEuclidData, @@ -604,7 +604,7 @@ public void test_ParallelClustering() { // Concurrent (parallelized) clustering LinkedList tmpArt2aEuclidTaskList = new LinkedList<>(); - PreprocessedData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedArt2aEuclidData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); for (float tmpVigilance : tmpVigilances) { tmpArt2aEuclidTaskList.add(new Art2aEuclidTask( tmpPreprocessedArt2aEuclidData, diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 5818c66..80d4d4e 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -548,7 +548,7 @@ public void test_Art2aData() { } // Preprocessed Art2aData - PreprocessedData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedArt2aData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); Art2aKernel tmpArt2aKernelWithArt2aData = new Art2aKernel( tmpPreprocessedArt2aData, @@ -637,7 +637,7 @@ public void test_ParallelClustering() { // Concurrent (parallelized) clustering LinkedList tmpArt2aTaskList = new LinkedList<>(); - PreprocessedData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); + PreprocessedArt2aData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); for (float tmpVigilance : tmpVigilances) { tmpArt2aTaskList.add( new Art2aTask( From bc2fb9f3a5d1d27fceeac44c4a7af4113dd3a174 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Tue, 11 Feb 2025 14:32:41 +0100 Subject: [PATCH 11/18] Rho winner calculation parallelization for ART-2a, minor refactoring --- .../clustering/art2a/Art2aEuclidKernel.java | 91 +++++--- .../clustering/art2a/Art2aEuclidResult.java | 24 +- .../clustering/art2a/Art2aEuclidTask.java | 43 ++-- .../clustering/art2a/Art2aEuclidUtils.java | 4 +- .../cheminf/clustering/art2a/Art2aKernel.java | 211 +++++++++++++----- .../cheminf/clustering/art2a/Art2aResult.java | 26 +-- .../cheminf/clustering/art2a/Art2aTask.java | 47 ++-- .../cheminf/clustering/art2a/Art2aUtils.java | 4 +- .../art2a/PreprocessedArt2aData.java | 2 +- .../art2a/PreprocessedArt2aEuclidData.java | 2 +- .../clustering/art2a/PreprocessedData.java | 10 +- .../cheminf/clustering/art2a/Utils.java | 28 +-- .../clustering/art2a/Art2aEuclidTest.java | 57 +++++ .../cheminf/clustering/art2a/Art2aTest.java | 87 ++++---- 14 files changed, 422 insertions(+), 214 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index 5c7e999..9809a98 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -128,24 +128,19 @@ */ public class Art2aEuclidKernel { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(Art2aEuclidKernel.class.getName()); // - // + // /** * Value 1.0 */ private static final float ONE = 1.0f; // - // - /** - * Default fraction of the (maximum) number of clusters relative to - * number of data vectors - */ - private static final float DEFAULT_FRACTION_OF_CLUSTERS = 0.2f; + // /** * Default seed value for random number generator */ @@ -168,7 +163,7 @@ public class Art2aEuclidKernel { */ private static final float DEFAULT_CONVERGENCE_THRESHOLD = 0.1f; // - // + // /** * Maximum number of clusters in interval [2, number of data row vectors of getDataMatrix] */ @@ -194,7 +189,7 @@ public class Art2aEuclidKernel { */ private final PreprocessedData preprocessedData; // - // + // /** * Helper callable for a single getClusterResult() calculation task of * Art2aEuclidKernel with a distinct vigilance parameter. @@ -203,13 +198,13 @@ public class Art2aEuclidKernel { */ private static class HelperTask implements Callable { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(HelperTask.class.getName()); // - // + // /** * Art2aEuclidKernel */ @@ -220,7 +215,7 @@ private static class HelperTask implements Callable { private final float vigilance; // - // + // /** * Constructor */ @@ -233,7 +228,7 @@ protected HelperTask( } // - // + // /** * Performs single getClusterResult() calculation task. * @@ -262,7 +257,7 @@ public Art2aEuclidResult call() { } // - // + // /** * Constructor. * @@ -278,7 +273,7 @@ public Art2aEuclidResult call() { * (must be greater zero) * @param aRandomSeed Random seed value for random number generator * (must be greater zero) - * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. * @throws IllegalArgumentException Thrown if an argument is illegal * @@ -293,7 +288,7 @@ public Art2aEuclidKernel( long aRandomSeed, boolean anIsDataPreprocessing ) throws IllegalArgumentException { - // + // if(!Utils.isDataMatrixValid(aDataMatrix)) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -301,6 +296,16 @@ public Art2aEuclidKernel( ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aDataMatrix is not valid."); } + if(aMaximumNumberOfClusters < 2) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aMaximumNumberOfClusters must be greater 1." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aMaximumNumberOfClusters must be greater 1."); + } + if(aMaximumNumberOfClusters > aDataMatrix.length) { + aMaximumNumberOfClusters = aDataMatrix.length; + } if(aMaximumNumberOfEpochs <= 0) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -361,27 +366,32 @@ public Art2aEuclidKernel( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 0.5) and RANDOM_SEED (= 1). - * Note: There is NO data preprocessing. - * + * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: + * Otherwise. * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidKernel( - float[][] aDataMatrix + float[][] aDataMatrix, + int aMaximumNumberOfClusters, + boolean anIsDataPreprocessing ) throws IllegalArgumentException { this( aDataMatrix, - Math.max((int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + aMaximumNumberOfClusters, DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT, DEFAULT_RANDOM_SEED, - false + anIsDataPreprocessing ); } @@ -409,7 +419,7 @@ public Art2aEuclidKernel( float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { - // + // if(aPreprocessedArt2aEuclidData == null) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -417,6 +427,16 @@ public Art2aEuclidKernel( ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData is null."); } + if(aMaximumNumberOfClusters < 2) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.Constructor: aMaximumNumberOfClusters must be greater 1." + ); + throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aMaximumNumberOfClusters must be greater 1."); + } + if(aMaximumNumberOfClusters > aPreprocessedArt2aEuclidData.getPreprocessedMatrix().length) { + aMaximumNumberOfClusters = aPreprocessedArt2aEuclidData.getPreprocessedMatrix().length; + } if(aMaximumNumberOfEpochs <= 0) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -456,20 +476,23 @@ public Art2aEuclidKernel( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aEuclidData PreprocessedData object created by method * Art2aEuclidKernel.getPreprocessedArt2aEuclidData() + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidKernel( - PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData + PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData, + int aMaximumNumberOfClusters ) throws IllegalArgumentException { this( aPreprocessedArt2aEuclidData, - Math.max((int) (aPreprocessedArt2aEuclidData.getPreprocessedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + aMaximumNumberOfClusters, DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -478,7 +501,7 @@ public Art2aEuclidKernel( } // - // + // /** * Performs ART-2a-Euclid clustering and returns corresponding * Art2aEuclidResult. @@ -491,7 +514,7 @@ public Art2aEuclidKernel( public Art2aEuclidResult getClusterResult( float aVigilance ) throws Exception { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -749,7 +772,7 @@ public Art2aEuclidResult[] getClusterResults( float[] aVigilances, int aNumberOfConcurrentCalculationThreads ) throws Exception { - // + // if (aVigilances == null || aVigilances.length == 0) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -838,7 +861,7 @@ public int[] getRepresentatives( float aVigilanceMax, int aNumberOfTrialSteps ) throws IllegalArgumentException, Exception { - // + // if(aNumberOfRepresentatives < 2) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -937,7 +960,7 @@ public int[] getBestRepresentatives( int aMinimumNumberOfRepresentatives, int aMaximumNumberOfRepresentatives ) throws IllegalArgumentException, Exception { - // + // if(aDataMatrix == null || aDataMatrix.length == 0) { Art2aEuclidKernel.LOGGER.log( Level.SEVERE, @@ -1004,7 +1027,7 @@ public int[] getBestRepresentatives( } } // - // + // /** * Creates PreprocessedData object with preprocessed ART-2a-Euclid data for maximum * speed of the clustering process. The PreprocessedData object allocates @@ -1083,7 +1106,7 @@ public static PreprocessedArt2aEuclidData getPreprocessedArt2aEuclidData( } // - // + // /** * Assigns data vectors to clusters * diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java index d63d2e0..9d09071 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java @@ -44,13 +44,13 @@ */ public class Art2aEuclidResult { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(Art2aEuclidResult.class.getName()); // - // + // /** * Cluster index of data vector */ @@ -94,7 +94,7 @@ public class Art2aEuclidResult { */ private final PreprocessedData preprocessedArt2aEuclidData; // - // + // /** * Indexed value */ @@ -118,7 +118,7 @@ public int compareTo(IndexedValue anotherIndexedValue) { } // - // + // /** * Constructor. * Note: No checks are performed. @@ -165,7 +165,7 @@ public Art2aEuclidResult( } // - // + // /** * Returns specified cluster vector with index aClusterIndex in * clusterMatrix. @@ -177,7 +177,7 @@ public Art2aEuclidResult( public float[] getClusterVector( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, @@ -201,7 +201,7 @@ public float[] getClusterVector( public float[] getScaledClusterVector( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, @@ -226,7 +226,7 @@ public float[] getScaledClusterVector( public int[] getDataVectorIndicesOfCluster( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, @@ -277,7 +277,7 @@ public float getDistanceBetweenClusters( int aClusterIndex1, int aClusterIndex2 ) throws IllegalArgumentException { - // + // if(aClusterIndex1 < 0 || aClusterIndex1 >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, @@ -323,7 +323,7 @@ public float getDistanceBetweenClusters( public int getClusterSize( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, @@ -372,7 +372,7 @@ public boolean isConverged() { public int getClusterRepresentativeIndex( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, @@ -428,7 +428,7 @@ public int getClusterRepresentativeIndex( public int[] getClusterRepresentativeIndices( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( Level.SEVERE, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java index 8b83650..a4ef058 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -39,13 +39,13 @@ */ public class Art2aEuclidTask implements Callable { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(Art2aEuclidTask.class.getName()); // - // + // /** * ART-2a-Euclid clustering kernel instance */ @@ -56,7 +56,7 @@ public class Art2aEuclidTask implements Callable { private final float vigilance; // - // + // /** * Constructor. * @@ -73,7 +73,7 @@ public class Art2aEuclidTask implements Callable { * (must be greater zero) * @param aRandomSeed Random seed value for random number generator * (must be greater zero) - * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. * @throws IllegalArgumentException Thrown if an argument is illegal */ @@ -88,7 +88,7 @@ public Art2aEuclidTask( long aRandomSeed, boolean anIsDataPreprocessing ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( Level.SEVERE, @@ -126,21 +126,26 @@ public Art2aEuclidTask( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 0.5) and RANDOM_SEED (= 1). - * Note: There is NO data preprocessing. * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: + * Otherwise. * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidTask( float[][] aDataMatrix, - float aVigilance + float aVigilance, + int aMaximumNumberOfClusters, + boolean anIsDataPreprocessing ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( Level.SEVERE, @@ -154,7 +159,9 @@ public Art2aEuclidTask( try { this.art2aClusteringKernel = new Art2aEuclidKernel( - aDataMatrix + aDataMatrix, + aMaximumNumberOfClusters, + anIsDataPreprocessing ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( @@ -196,7 +203,7 @@ public Art2aEuclidTask( float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( Level.SEVERE, @@ -232,20 +239,23 @@ public Art2aEuclidTask( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aEuclidData PreprocessedData object created by method * Art2aEuclidKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aEuclidTask( PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData, - float aVigilance + float aVigilance, + int aMaximumNumberOfClusters ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( Level.SEVERE, @@ -259,7 +269,8 @@ public Art2aEuclidTask( try { this.art2aClusteringKernel = new Art2aEuclidKernel( - aPreprocessedArt2aEuclidData + aPreprocessedArt2aEuclidData, + aMaximumNumberOfClusters ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( @@ -276,7 +287,7 @@ public Art2aEuclidTask( } // - // + // /** * Performs the clustering process. * diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java index b0fd252..a641dd1 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java @@ -35,14 +35,14 @@ */ public class Art2aEuclidUtils { - // + // /** * Constructor */ protected Art2aEuclidUtils() {} // - // + // /** * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index bfdfaf5..dc671b6 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -35,6 +35,7 @@ import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.IntStream; import static java.util.concurrent.Executors.newFixedThreadPool; @@ -125,24 +126,19 @@ */ public class Art2aKernel { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(Art2aKernel.class.getName()); // - // + // /** * Value 1.0 */ private static final float ONE = 1.0f; // - // - /** - * Default fraction of the (maximum) number of clusters relative to - * number of data vectors - */ - private static final float DEFAULT_FRACTION_OF_CLUSTERS = 0.2f; + // /** * Default seed value for random number generator */ @@ -165,7 +161,7 @@ public class Art2aKernel { */ private static final float DEFAULT_CONVERGENCE_THRESHOLD = 0.99f; // - // + // /** * Maximum number of clusters in interval [2, number of data row vectors of getDataMatrix] */ @@ -191,22 +187,23 @@ public class Art2aKernel { */ private final PreprocessedData preprocessedData; // - // + // /** * Helper callable for a single getClusterResult() calculation task of * Art2aKernel with a distinct vigilance parameter. + * Note: Parallel Rho winner evaluations is disabled. *

* Note: No checks are performed. */ private static class HelperTask implements Callable { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(HelperTask.class.getName()); // - // + // /** * Art2aKernel */ @@ -217,7 +214,7 @@ private static class HelperTask implements Callable { private final float vigilance; // - // + // /** * Constructor */ @@ -230,9 +227,10 @@ protected HelperTask( } // - // + // /** * Performs single getClusterResult() calculation task. + * Note: Parallel Rho winner evaluations is disabled. * * @return Art2aResult or null if getClusterResult() calculation task * could not be performed. @@ -240,7 +238,8 @@ protected HelperTask( @Override public Art2aResult call() { try { - return this.art2aKernel.getClusterResult(this.vigilance); + // Note: Parallel Rho winner evaluations is disabled: Parameter false. + return this.art2aKernel.getClusterResult(this.vigilance, false); } catch (Exception anException) { HelperTask.LOGGER.log( Level.SEVERE, @@ -259,7 +258,7 @@ public Art2aResult call() { } // - // + // /** * Constructor. * @@ -275,7 +274,7 @@ public Art2aResult call() { * (must be greater zero) * @param aRandomSeed Random seed value for random number generator * (must be greater zero) - * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. * @throws IllegalArgumentException Thrown if an argument is illegal * @@ -290,7 +289,7 @@ public Art2aKernel( long aRandomSeed, boolean anIsDataPreprocessing ) throws IllegalArgumentException { - // + // if(!Utils.isDataMatrixValid(aDataMatrix)) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -298,10 +297,20 @@ public Art2aKernel( ); throw new IllegalArgumentException("Art2aKernel.Constructor: aDataMatrix is not valid."); } - if(aMaximumNumberOfEpochs <= 0) { + if(aMaximumNumberOfClusters < 2) { Art2aKernel.LOGGER.log( Level.SEVERE, - "Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." + "Art2aKernel.Constructor: aMaximumNumberOfClusters must be greater 1." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfClusters must be greater 1."); + } + if(aMaximumNumberOfClusters > aDataMatrix.length) { + aMaximumNumberOfClusters = aDataMatrix.length; + } + if(aMaximumNumberOfEpochs <= 0) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); } @@ -358,27 +367,32 @@ public Art2aKernel( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 1.0) and RANDOM_SEED (= 1). - * Note: There is NO data preprocessing. - * + * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: + * Otherwise. * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aKernel( - float[][] aDataMatrix + float[][] aDataMatrix, + int aMaximumNumberOfClusters, + boolean anIsDataPreprocessing ) throws IllegalArgumentException { this( aDataMatrix, - Math.max((int) (aDataMatrix.length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + aMaximumNumberOfClusters, DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT, DEFAULT_RANDOM_SEED, - false + anIsDataPreprocessing ); } @@ -406,7 +420,7 @@ public Art2aKernel( float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { - // + // if(aPreprocessedArt2aData == null) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -414,6 +428,16 @@ public Art2aKernel( ); throw new IllegalArgumentException("Art2aKernel.Constructor: aPreprocessedArt2aData is null."); } + if(aMaximumNumberOfClusters < 2) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.Constructor: aMaximumNumberOfClusters must be greater 1." + ); + throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfClusters must be greater 1."); + } + if(aMaximumNumberOfClusters > aPreprocessedArt2aData.getPreprocessedMatrix().length) { + aMaximumNumberOfClusters = aPreprocessedArt2aData.getPreprocessedMatrix().length; + } if(aMaximumNumberOfEpochs <= 0) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -453,20 +477,23 @@ public Art2aKernel( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aData PreprocessedData object created by static * method Art2aKernel.getPreprocessedArt2aData() + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aKernel( - PreprocessedArt2aData aPreprocessedArt2aData + PreprocessedArt2aData aPreprocessedArt2aData, + int aMaximumNumberOfClusters ) throws IllegalArgumentException { this( aPreprocessedArt2aData, - Math.max((int) (aPreprocessedArt2aData.getPreprocessedMatrix().length * DEFAULT_FRACTION_OF_CLUSTERS), 2), + aMaximumNumberOfClusters, DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, @@ -475,20 +502,22 @@ public Art2aKernel( } // - // + // /** - * Performs ART-2a clustering and returns corresponding - * Art2aResult. + * Performs ART-2a clustering and returns corresponding Art2aResult. * * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param anIsParallelRhoWinnerEvaluation True: Rho winner is evaluated by parallelized calculation, false: Rho + * winner is evaluated by sequential calculation * @return Art2aResult instance * @throws IllegalArgumentException Thrown if argument is illegal * @throws Exception Thrown if exception occurs which should never happen */ public Art2aResult getClusterResult( - float aVigilance + float aVigilance, + boolean anIsParallelRhoWinnerEvaluation ) throws Exception { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -540,6 +569,11 @@ public Art2aResult getClusterResult( // Cluster usage flags. True: Cluster is used, false: Cluster is // empty and can be removed. boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; + // Buffer for Rho values for parallelized Rho winner evaluation + float[] tmpRhoValueBuffer = null; + if (anIsParallelRhoWinnerEvaluation) { + tmpRhoValueBuffer = new float[this.maximumNumberOfClusters]; + } // Initialize cluster indices for data row vectors with -1 to // indicate missing cluster assignment @@ -599,13 +633,24 @@ public Art2aResult getClusterResult( tmpNumberOfDetectedClusters++; } else { // Cluster number is greater than or equal to 1 - Art2aKernel.setRhoWinner( - tmpBufferVector, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, - tmpScalingFactor, - tmpRhoWinner - ); + if (anIsParallelRhoWinnerEvaluation) { + Art2aKernel.setRhoWinnerParallel( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoValueBuffer, + tmpRhoWinner + ); + } else { + Art2aKernel.setRhoWinner( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoWinner + ); + } // Assign to existing cluster or increment clusters if(tmpRhoWinner.getIndexOfCluster() < 0 || tmpRhoWinner.getRhoValue() < aVigilance) { // Increment clusters (if possible) @@ -727,6 +772,7 @@ public Art2aResult getClusterResult( /** * Performs ART-2a clustering for specified vigilance parameters and * returns corresponding Art2aResult objects. + * Note: Parallel Rho winner evaluations is disabled. * * @param aVigilances Vigilance parameters (must each be in interval (0,1)) * @return Art2aResult objects or null if clustering result could @@ -742,7 +788,7 @@ public Art2aResult[] getClusterResults( float[] aVigilances, int aNumberOfConcurrentCalculationThreads ) throws Exception { - // + // if (aVigilances == null || aVigilances.length == 0) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -802,14 +848,14 @@ public Art2aResult[] getClusterResults( } else { Art2aResult[] tmpSequentialResults = new Art2aResult[aVigilances.length]; for (int i = 0; i < aVigilances.length; i++) { - tmpSequentialResults[i] = this.getClusterResult(aVigilances[i]); + tmpSequentialResults[i] = this.getClusterResult(aVigilances[i], false); } return tmpSequentialResults; } } /** - * Nearest (smaller) indices of approximants to the desired number of + * Nearest (smaller) indices of approximates to the desired number of * representatives. * * @param aNumberOfRepresentatives Number of representatives (MUST be @@ -820,7 +866,9 @@ public Art2aResult[] getClusterResults( * (0,1)) * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or * equal to 1) - * @return Nearest (smaller) indices of approximants to the desired number + * @param anIsParallelRhoWinnerEvaluation True: Rho winner is evaluated by parallelized calculation, false: Rho + * winner is evaluated by sequential calculation + * @return Nearest (smaller) indices of approximates to the desired number * of representatives. * @throws IllegalArgumentException Thrown if an argument is illegal * @throws Exception Thrown if exception occurs which should never happen @@ -829,9 +877,10 @@ public int[] getRepresentatives( int aNumberOfRepresentatives, float aVigilanceMin, float aVigilanceMax, - int aNumberOfTrialSteps + int aNumberOfTrialSteps, + boolean anIsParallelRhoWinnerEvaluation ) throws IllegalArgumentException, Exception { - // + // if(aNumberOfRepresentatives < 2) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -870,12 +919,12 @@ public int[] getRepresentatives( // try { - Art2aResult tmpArt2aResult = this.getClusterResult(aVigilanceMin); + Art2aResult tmpArt2aResult = this.getClusterResult(aVigilanceMin, anIsParallelRhoWinnerEvaluation); int[] tmpRepresentativeIndicesOfClusters = tmpArt2aResult.getRepresentativeIndicesOfClusters(); if (tmpArt2aResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { return tmpRepresentativeIndicesOfClusters; } - tmpArt2aResult = this.getClusterResult(aVigilanceMax); + tmpArt2aResult = this.getClusterResult(aVigilanceMax, anIsParallelRhoWinnerEvaluation); if (tmpArt2aResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { return tmpArt2aResult.getRepresentativeIndicesOfClusters(); } @@ -884,7 +933,7 @@ public int[] getRepresentatives( float tmpVigilanceMax = aVigilanceMax; for (int i = 0; i < aNumberOfTrialSteps; i++) { float tmpVigilanceMean = (tmpVigilanceMin + tmpVigilanceMax) / 2.0f; - tmpArt2aResult = this.getClusterResult(tmpVigilanceMean); + tmpArt2aResult = this.getClusterResult(tmpVigilanceMean, anIsParallelRhoWinnerEvaluation); if (tmpArt2aResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { tmpVigilanceMax = tmpVigilanceMean; } else if (tmpArt2aResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { @@ -920,7 +969,9 @@ public int[] getRepresentatives( * CHECKED) * @param aMinimumNumberOfRepresentatives Minimum number of representatives * @param aMaximumNumberOfRepresentatives Maximum number of representatives - * @return Representatives whose mean distance is nearest to the mean + * @param anIsParallelRhoWinnerEvaluation True: Rho winner is evaluated by parallelized calculation, false: Rho + * winner is evaluated by sequential calculation + * @return Representatives whose mean distance is nearest to the mean * distance of all data vectors of specified original data matrix. * @throws IllegalArgumentException Thrown if an argument is illegal * @throws Exception Thrown if exception occurs which should never happen @@ -928,9 +979,10 @@ public int[] getRepresentatives( public int[] getBestRepresentatives( float[][] aDataMatrix, int aMinimumNumberOfRepresentatives, - int aMaximumNumberOfRepresentatives + int aMaximumNumberOfRepresentatives, + boolean anIsParallelRhoWinnerEvaluation ) throws IllegalArgumentException, Exception { - // + // if(aDataMatrix == null || aDataMatrix.length == 0) { Art2aKernel.LOGGER.log( Level.SEVERE, @@ -973,7 +1025,8 @@ public int[] getBestRepresentatives( i, tmpVigilanceMin, tmpVigilanceMax, - tmpNumberOfTrialSteps + tmpNumberOfTrialSteps, + anIsParallelRhoWinnerEvaluation ); float tmpMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpRepresentatives); float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); @@ -997,7 +1050,7 @@ public int[] getBestRepresentatives( } } // - // + // /** * Creates PreprocessedData object with preprocessed ART-2a data for maximum speed * of the clustering process. The PreprocessedData object allocates about the @@ -1076,7 +1129,7 @@ public static PreprocessedArt2aData getPreprocessedArt2aData( } // - // + // /** * Assigns data vectors to clusters * @@ -1146,7 +1199,8 @@ private static int getClusterIndex( int aNumberOfDetectedClusters, float[][] aClusterMatrix ) { - float tmpMaxScalarProduct = Float.MIN_VALUE; + // Note: Scalar product is always greater or equal to 0 + float tmpMaxScalarProduct = -1.0f; int tmpWinnerClusterIndex = -1; for (int i = 0; i < aNumberOfDetectedClusters; i++) { float tmpScalarProduct = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]); @@ -1282,6 +1336,47 @@ private static void setRhoWinner( } aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); } + + /** + * Sets rho winner with the rho value and the cluster index of the winner + * (see code). If the cluster index is negative the first scaled rho value + * is the winner. + * Note: A parallelized stream is used for calculation. + * + * @param aContrastEnhancedUnitVector Contrast enhanced unit vector (IS NOT + * CHANGED) + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aScalingFactor Scaling factor + * @param aRhoValueBuffer Buffer for Rho values + * @param aRhoWinner Rho winner: Is set with the rho value and the cluster + * index of the winner. If the cluster index is negative the first scaled + * rho value is the winner. + */ + private static void setRhoWinnerParallel( + float[] aContrastEnhancedUnitVector, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + float aScalingFactor, + float[] aRhoValueBuffer, + Utils.RhoWinner aRhoWinner + ) { + // Calculate first rho value + float tmpRhoValue = aScalingFactor * Utils.getSumOfComponents(aContrastEnhancedUnitVector); + // Set winner index to negative value + int tmpIndex = -1; + // Calculate other rho values + IntStream.range(0, aNumberOfDetectedClusters).parallel().forEach( + i -> aRhoValueBuffer[i] = Utils.getScalarProduct(aContrastEnhancedUnitVector, aClusterMatrix[i]) + ); + for(int i = 0; i < aNumberOfDetectedClusters; i++) { + if(aRhoValueBuffer[i] > tmpRhoValue) { + tmpRhoValue = aRhoValueBuffer[i]; + tmpIndex = i; + } + } + aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); + } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java index 9b1837d..063e032 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -45,19 +45,19 @@ */ public class Art2aResult { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(Art2aResult.class.getName()); // - // + // /** * Conversion constant from radiant to degree */ private static final float CONVERSION_TO_DEGREE = 180.0f / (float) Math.PI; // - // + // /** * Cluster index of data vector */ @@ -101,7 +101,7 @@ public class Art2aResult { */ private final PreprocessedData preprocessedArt2aData; // - // + // /** * Indexed value */ @@ -125,7 +125,7 @@ public int compareTo(IndexedValue anotherIndexedValue) { } // - // + // /** * Constructor. * Note: No checks are performed. @@ -172,7 +172,7 @@ public Art2aResult( } // - // + // /** * Returns specified cluster vector with index aClusterIndex in * clusterMatrix. @@ -184,7 +184,7 @@ public Art2aResult( public float[] getClusterVector( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, @@ -208,7 +208,7 @@ public float[] getClusterVector( public float[] getScaledClusterVector( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, @@ -233,7 +233,7 @@ public float[] getScaledClusterVector( public int[] getDataVectorIndicesOfCluster( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, @@ -284,7 +284,7 @@ public float getAngleBetweenClusters( int aClusterIndex1, int aClusterIndex2 ) throws IllegalArgumentException { - // + // if(aClusterIndex1 < 0 || aClusterIndex1 >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, @@ -329,7 +329,7 @@ public float getAngleBetweenClusters( public int getClusterSize( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, @@ -378,7 +378,7 @@ public boolean isConverged() { public int getClusterRepresentativeIndex( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, @@ -434,7 +434,7 @@ public int getClusterRepresentativeIndex( public int[] getClusterRepresentativeIndices( int aClusterIndex ) throws IllegalArgumentException { - // + // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( Level.SEVERE, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java index b739c61..92bf2fa 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -38,13 +38,13 @@ */ public class Art2aTask implements Callable { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(Art2aTask.class.getName()); // - // + // /** * ART-2a clustering kernel instance */ @@ -55,7 +55,7 @@ public class Art2aTask implements Callable { private final float vigilance; // - // + // /** * Constructor. * @@ -72,7 +72,7 @@ public class Art2aTask implements Callable { * (must be greater zero) * @param aRandomSeed Random seed value for random number generator * (must be greater zero) - * @param anIsDataPreprocessing True: Data preprocessing is used, false: + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. * @throws IllegalArgumentException Thrown if an argument is illegal */ @@ -87,7 +87,7 @@ public Art2aTask( long aRandomSeed, boolean anIsDataPreprocessing ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( Level.SEVERE, @@ -125,21 +125,26 @@ public Art2aTask( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 1.0) and RANDOM_SEED (= 1). - * Note: There is NO data preprocessing. * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) + * @param anIsDataPreprocessing True: Data preprocessing is performed, false: + * Otherwise. * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aTask( float[][] aDataMatrix, - float aVigilance + float aVigilance, + int aMaximumNumberOfClusters, + boolean anIsDataPreprocessing ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( Level.SEVERE, @@ -153,7 +158,9 @@ public Art2aTask( try { this.art2aClusteringKernel = new Art2aKernel( - aDataMatrix + aDataMatrix, + aMaximumNumberOfClusters, + anIsDataPreprocessing ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( @@ -195,7 +202,7 @@ public Art2aTask( float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( Level.SEVERE, @@ -231,20 +238,23 @@ public Art2aTask( } /** - * Constructor with default values for DEFAULT_FRACTION_OF_CLUSTERS (= 0.2), + * Constructor with default values for * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aData PreprocessedData object created by method * Art2aKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * interval [2, number of data row vectors of aDataMatrix]) * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aTask( PreprocessedArt2aData aPreprocessedArt2aData, - float aVigilance + float aVigilance, + int aMaximumNumberOfClusters ) throws IllegalArgumentException { - // + // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( Level.SEVERE, @@ -258,7 +268,8 @@ public Art2aTask( try { this.art2aClusteringKernel = new Art2aKernel( - aPreprocessedArt2aData + aPreprocessedArt2aData, + aMaximumNumberOfClusters ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( @@ -275,9 +286,10 @@ public Art2aTask( } // - // + // /** * Performs the clustering process. + * Note: Parallel Rho winner evaluations is disabled. * * @return Clustering result or null if clustering process could not be * performed. @@ -285,7 +297,8 @@ public Art2aTask( @Override public Art2aResult call() { try { - return this.art2aClusteringKernel.getClusterResult(this.vigilance); + // Note: Parallel Rho winner evaluations is disabled: Parameter false. + return this.art2aClusteringKernel.getClusterResult(this.vigilance, false); } catch (Exception anException) { Art2aTask.LOGGER.log( Level.SEVERE, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java index 438366c..7aec143 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -35,14 +35,14 @@ */ public class Art2aUtils { - // + // /** * Constructor */ protected Art2aUtils() {} // - // + // /** * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java index fc7e190..38aa939 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aData.java @@ -31,7 +31,7 @@ */ public class PreprocessedArt2aData extends PreprocessedData { - // + // /** * Constructor * diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java index ddfddea..7936e40 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedArt2aEuclidData.java @@ -31,7 +31,7 @@ */ public class PreprocessedArt2aEuclidData extends PreprocessedData { - // + // /** * Constructor * diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java index 2e04bc9..4016241 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java @@ -50,13 +50,13 @@ */ public class PreprocessedData { - // + // /** * Logger of this class */ private static final Logger LOGGER = Logger.getLogger(PreprocessedData.class.getName()); // - // + // /** * Original data matrix with data row vectors */ @@ -88,7 +88,7 @@ public class PreprocessedData { // - // + // /** * Private constructor * Note: No checks are necessary @@ -124,7 +124,7 @@ private PreprocessedData ( this.hasPreprocessedData = aHasPreprocessedData; } // - // + // /** * Constructor * @@ -232,7 +232,7 @@ protected PreprocessedData ( } // - // + // /** * Original data matrix with data row vectors * diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java index 16ff0d3..ee13d44 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java @@ -40,13 +40,13 @@ */ public class Utils { - // + // /** * Value 1.0 */ private static final float ONE = 1.0f; // - // + // /** * Helper record: Minimum and maximum value. *

@@ -67,7 +67,7 @@ protected record MinMaxValue(float minValue, float maxValue) { } //
- // + // /** * Helper class: Rho winner. *

@@ -75,7 +75,7 @@ protected record MinMaxValue(float minValue, float maxValue) { */ protected static class RhoWinner { - // + // /** * Rho value */ @@ -86,14 +86,14 @@ protected static class RhoWinner { private int indexOfCluster; // - // + // /** * Constructor */ protected RhoWinner() {} // - // + // /** * Set rho winner * @@ -136,7 +136,7 @@ protected int getIndexOfCluster() { */ protected static class ClusterRemovalInfo { - // + // /** * True: Cluster is removed, false: Otherwise */ @@ -147,14 +147,14 @@ protected static class ClusterRemovalInfo { private int numberOfDetectedClusters; // - // + // /** * Constructor */ protected ClusterRemovalInfo() {} // - // + // /** * Set cluster removal info * @@ -191,7 +191,7 @@ protected int getNumberOfDetectedClusters() { } // - // + // /** * Constructor */ @@ -200,7 +200,7 @@ protected Utils() {} // TODO: Make tests for public methods - // + // /** * Checks if aDataMatrix is valid. * @@ -246,7 +246,7 @@ public static boolean isDataMatrixValid( public static boolean isNonFiniteComponentRemoval( float[][] aDataMatrix ) { - // + // if(aDataMatrix == null || aDataMatrix.length == 0) { return false; } @@ -299,7 +299,7 @@ public static boolean isNonFiniteComponentRemoval( return tmpHasNonFiniteComponent; } // - // + // /** * (Deep) Copies source matrix to destination matrix. Row vectors of * destination matrix may not have been instantiated. @@ -667,7 +667,7 @@ protected static boolean hasLengthOfZero( protected static boolean hasNonFiniteComponent( float[][] aDataMatrix ) { - // + // if(aDataMatrix == null || aDataMatrix.length == 0) { return false; } diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java index 50d249a..e90517e 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -175,6 +175,63 @@ public void test_Development_CombinedGaussianCouldData() { } } + /** + * Test method for development purposes only + */ + @Test + public void test_Development_CombinedGaussianCouldData_Performance() { + System.out.println("--------------------------------------------------------"); + System.out.println("test_Development_CombinedGaussianCouldData_Performance()"); + System.out.println("--------------------------------------------------------"); + int tmpNumberOfDimensions = 100; + int tmpNumberOfGaussianCloudVectors = 1000; + float tmpStandardDeviation = 0.01f; + Random tmpRandomNumberGenerator = new Random(1L); + float[][] tmpCombinedGaussianCloudDataMatrix = + this.getCombinedGaussianCloudMatrix( + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, + tmpRandomNumberGenerator + ); + + float tmpVigilance = 0.1f; + int tmpMaximumNumberOfClusters = 200; + boolean tmpIsDataPreprocessing = true; + int tmpMaximumNumberOfEpochs = 10; + float tmpConvergenceThreshold = 0.1f; + float tmpLearningParameter = 0.01f; + float tmpOffsetForContrastEnhancement = 1.0f; + long tmpRandomSeed = 1L; + + long tmpStart = System.currentTimeMillis(); + Art2aEuclidKernel tmpArt2aEuclidKernel = + new Art2aEuclidKernel( + tmpCombinedGaussianCloudDataMatrix, + tmpMaximumNumberOfClusters, + tmpMaximumNumberOfEpochs, + tmpConvergenceThreshold, + tmpLearningParameter, + tmpOffsetForContrastEnhancement, + tmpRandomSeed, + tmpIsDataPreprocessing + ); + Art2aEuclidResult tmpArt2aEuclidResult = null; + try { + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + } catch (Exception anException) { + Assertions.assertTrue(false); + } + long tmpEnd = System.currentTimeMillis(); + + System.out.println(" Number of data vectors = " + String.valueOf(tmpNumberOfDimensions * tmpNumberOfGaussianCloudVectors)); + System.out.println(" Elapsed time in ms = " + String.valueOf(tmpEnd - tmpStart)); + System.out.println(" Number of detected clusters = " + String.valueOf(tmpArt2aEuclidResult.getNumberOfDetectedClusters())); + System.out.println(" Number of epochs = " + String.valueOf(tmpArt2aEuclidResult.getNumberOfEpochs())); + System.out.println(" Is converged? = " + String.valueOf(tmpArt2aEuclidResult.isConverged())); + System.out.println(" Is cluster overflow? = " + String.valueOf(tmpArt2aEuclidResult.isClusterOverflow())); + } + /** * Test method for development purposes only */ diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 80d4d4e..e07e77f 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -59,6 +59,7 @@ public void test_Development_IrisFlowerData() { int tmpMaximumNumberOfClusters = 150; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerEvaluation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -81,9 +82,9 @@ public void test_Development_IrisFlowerData() { Assertions.assertNotNull(tmpArt2aKernel); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } Assertions.assertNotNull(tmpArt2aResult); int tmpNumberOfDetectedClusters = tmpArt2aResult.getNumberOfDetectedClusters(); @@ -126,6 +127,7 @@ public void test_Development_CombinedGaussianCouldData() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 1000; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerEvaluation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -146,9 +148,9 @@ public void test_Development_CombinedGaussianCouldData() { ); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } long tmpEnd = System.currentTimeMillis(); @@ -194,8 +196,9 @@ public void test_Development_CombinedGaussianCouldData_Performance() { ); float tmpVigilance = 0.1f; - int tmpMaximumNumberOfClusters = 200; + int tmpMaximumNumberOfClusters = 2000; boolean tmpIsDataPreprocessing = true; + boolean tmpIsParallelRhoWinnerEvaluation = false; int tmpMaximumNumberOfEpochs = 10; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -216,17 +219,18 @@ public void test_Development_CombinedGaussianCouldData_Performance() { ); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } long tmpEnd = System.currentTimeMillis(); System.out.println(" Number of data vectors = " + String.valueOf(tmpNumberOfDimensions * tmpNumberOfGaussianCloudVectors)); System.out.println(" Elapsed time in ms = " + String.valueOf(tmpEnd - tmpStart)); - int tmpNumberOfDetectedClusters = tmpArt2aResult.getNumberOfDetectedClusters(); System.out.println(" Number of detected clusters = " + String.valueOf(tmpArt2aResult.getNumberOfDetectedClusters())); System.out.println(" Number of epochs = " + String.valueOf(tmpArt2aResult.getNumberOfEpochs())); + System.out.println(" Is converged? = " + String.valueOf(tmpArt2aResult.isConverged())); + System.out.println(" Is cluster overflow? = " + String.valueOf(tmpArt2aResult.isClusterOverflow())); } /** @@ -245,6 +249,7 @@ public void test_Development_GetRepresentatives() { float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerEvaluation = false; float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -263,13 +268,19 @@ public void test_Development_GetRepresentatives() { ); try { - int[] tmpBestRepresentatives = tmpArt2aKernel.getBestRepresentatives(tmpIrisFlowerDataMatrix, 2, tmpIrisFlowerDataMatrix.length); + int[] tmpBestRepresentatives = + tmpArt2aKernel.getBestRepresentatives( + tmpIrisFlowerDataMatrix, + 2, + tmpIrisFlowerDataMatrix.length, + tmpIsParallelRhoWinnerEvaluation + ); Arrays.sort(tmpBestRepresentatives); System.out.println( String.valueOf(tmpBestRepresentatives.length) + " best representatives = " + this.getStringFromIntArray(tmpBestRepresentatives) ); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } int[] tmpAllIndices = new int[150]; @@ -287,7 +298,8 @@ public void test_Development_GetRepresentatives() { tmpNumberOfRepresentatives, tmpVigilanceMin, tmpVigilanceMax, - tmpNumberOfTrialSteps + tmpNumberOfTrialSteps, + tmpIsParallelRhoWinnerEvaluation ); if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { Arrays.sort(tmpRepresentatives); @@ -301,7 +313,7 @@ public void test_Development_GetRepresentatives() { ); } } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } } } @@ -322,6 +334,7 @@ public void test_GetRepresentatives() { float tmpOffsetForContrastEnhancement = 0.5f; long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerEvaluation = false; float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -345,11 +358,12 @@ public void test_GetRepresentatives() { tmpNumberOfRepresentatives, tmpVigilanceMin, tmpVigilanceMax, - tmpNumberOfTrialSteps + tmpNumberOfTrialSteps, + tmpIsParallelRhoWinnerEvaluation ); Assertions.assertEquals(tmpRepresentatives.length, tmpNumberOfRepresentatives); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } } @@ -376,6 +390,7 @@ public void test_PerfectClustering() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 100; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerEvaluation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -395,7 +410,7 @@ public void test_PerfectClustering() { ); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { Assertions.assertTrue(false); } @@ -443,7 +458,8 @@ public void test_Preprocessing() { for (float tmpVigilance : tmpVigilances) { // No preprocessing boolean tmpIsDataPreprocessing = false; - Art2aKernel tmpArt2aKernelWithoutPreprocessing = + boolean tmpIsParallelRhoWinnerEvaluation = false; + Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, @@ -456,9 +472,9 @@ public void test_Preprocessing() { ); Art2aResult tmpArt2aResultWithoutPreprocessing = null; try { - tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Preprocessing @@ -476,20 +492,14 @@ public void test_Preprocessing() { ); Art2aResult tmpArt2aResultWithPreprocessing = null; try { - tmpArt2aResultWithPreprocessing = tmpArt2aKernelWithPreprocessing.getClusterResult(tmpVigilance); + tmpArt2aResultWithPreprocessing = tmpArt2aKernelWithPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Assertions.assert that results without and with preprocessing are identical - Assertions.assertTrue( - tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters() == - tmpArt2aResultWithPreprocessing.getNumberOfDetectedClusters() - ); - Assertions.assertTrue( - tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs() == - tmpArt2aResultWithPreprocessing.getNumberOfEpochs() - ); + Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aResultWithPreprocessing.getNumberOfDetectedClusters()); + Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aResultWithPreprocessing.getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { @@ -500,10 +510,7 @@ public void test_Preprocessing() { } for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { - Assertions.assertTrue( - tmpArt2aResultWithoutPreprocessing.getAngleBetweenClusters(i, j) == - tmpArt2aResultWithPreprocessing.getAngleBetweenClusters(i, j) - ); + Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getAngleBetweenClusters(i, j), tmpArt2aResultWithPreprocessing.getAngleBetweenClusters(i, j)); } } } @@ -529,7 +536,8 @@ public void test_Art2aData() { for (float tmpVigilance : tmpVigilances) { // No preprocessing boolean tmpIsDataPreprocessing = false; - Art2aKernel tmpArt2aKernelWithoutPreprocessing = + boolean tmpIsParallelRhoWinnerEvaluation = false; + Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, @@ -542,9 +550,9 @@ public void test_Art2aData() { ); Art2aResult tmpArt2aResultWithoutPreprocessing = null; try { - tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Preprocessed Art2aData @@ -560,7 +568,7 @@ public void test_Art2aData() { ); Art2aResult tmpArt2aResultWithArt2aData = null; try { - tmpArt2aResultWithArt2aData = tmpArt2aKernelWithArt2aData.getClusterResult(tmpVigilance); + tmpArt2aResultWithArt2aData = tmpArt2aKernelWithArt2aData.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { Assertions.assertTrue(false); } @@ -617,7 +625,8 @@ public void test_ParallelClustering() { int tmpIndex = 0; for (float tmpVigilance : tmpVigilances) { boolean tmpIsDataPreprocessing = false; - Art2aKernel tmpArt2aKernelWithoutPreprocessing = + boolean tmpIsParallelRhoWinnerEvaluation = false; + Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, @@ -629,9 +638,9 @@ public void test_ParallelClustering() { tmpIsDataPreprocessing ); try { - tmpSequentialResults[tmpIndex++] = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + tmpSequentialResults[tmpIndex++] = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } } From 5edf3977c911183ec5faf5c24a7caffb57818b85 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Tue, 11 Feb 2025 14:58:48 +0100 Subject: [PATCH 12/18] Bug removal and minor refactoring --- .../clustering/art2a/Art2aEuclidKernel.java | 15 --------------- .../cheminf/clustering/art2a/Art2aKernel.java | 15 --------------- 2 files changed, 30 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index 9809a98..63d14ae 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -198,12 +198,6 @@ public class Art2aEuclidKernel { */ private static class HelperTask implements Callable { - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(HelperTask.class.getName()); - // // /** * Art2aEuclidKernel @@ -240,15 +234,6 @@ public Art2aEuclidResult call() { try { return this.art2aKernel.getClusterResult(this.vigilance); } catch (Exception anException) { - HelperTask.LOGGER.log( - Level.SEVERE, - "SingleTask.call: Can not calculate a cluster result." - ); - HelperTask.LOGGER.log( - Level.SEVERE, - anException.toString(), - anException - ); return null; } } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index dc671b6..4a6e61a 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -197,12 +197,6 @@ public class Art2aKernel { */ private static class HelperTask implements Callable { - // - /** - * Logger of this class - */ - private static final Logger LOGGER = Logger.getLogger(HelperTask.class.getName()); - // // /** * Art2aKernel @@ -241,15 +235,6 @@ public Art2aResult call() { // Note: Parallel Rho winner evaluations is disabled: Parameter false. return this.art2aKernel.getClusterResult(this.vigilance, false); } catch (Exception anException) { - HelperTask.LOGGER.log( - Level.SEVERE, - "SingleTask.call: Can not calculate a cluster result." - ); - HelperTask.LOGGER.log( - Level.SEVERE, - anException.toString(), - anException - ); return null; } } From af6a7e702ffee370523a79d097be519789ec4bc4 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Wed, 12 Feb 2025 21:53:01 +0100 Subject: [PATCH 13/18] Parallelization optimized (use of common thread pool for Rho winner calculation) --- .../cheminf/clustering/art2a/Art2aKernel.java | 234 ++++++++---------- .../cheminf/clustering/art2a/Art2aTask.java | 2 +- .../cheminf/clustering/art2a/Art2aTest.java | 67 +++-- 3 files changed, 128 insertions(+), 175 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 4a6e61a..7c22f57 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -27,18 +27,12 @@ package de.unijena.cheminf.clustering.art2a; import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; import java.util.Random; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; +import java.util.concurrent.ForkJoinPool; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.IntStream; -import static java.util.concurrent.Executors.newFixedThreadPool; - /** * ART-2a algorithm implementation for unsupervised, open categorical * clustering. @@ -187,61 +181,6 @@ public class Art2aKernel { */ private final PreprocessedData preprocessedData; // - // - /** - * Helper callable for a single getClusterResult() calculation task of - * Art2aKernel with a distinct vigilance parameter. - * Note: Parallel Rho winner evaluations is disabled. - *

- * Note: No checks are performed. - */ - private static class HelperTask implements Callable { - - // - /** - * Art2aKernel - */ - private final Art2aKernel art2aKernel; - /** - * Vigilance parameter - */ - private final float vigilance; - // - - // - /** - * Constructor - */ - protected HelperTask( - Art2aKernel anArt2aKernel, - float aVigilance - ) { - this.art2aKernel = anArt2aKernel; - this.vigilance = aVigilance; - } - // - - // - /** - * Performs single getClusterResult() calculation task. - * Note: Parallel Rho winner evaluations is disabled. - * - * @return Art2aResult or null if getClusterResult() calculation task - * could not be performed. - */ - @Override - public Art2aResult call() { - try { - // Note: Parallel Rho winner evaluations is disabled: Parameter false. - return this.art2aKernel.getClusterResult(this.vigilance, false); - } catch (Exception anException) { - return null; - } - } - // - - } - //
// /** @@ -492,15 +431,15 @@ public Art2aKernel( * Performs ART-2a clustering and returns corresponding Art2aResult. * * @param aVigilance Vigilance parameter (must be in interval (0,1)) - * @param anIsParallelRhoWinnerEvaluation True: Rho winner is evaluated by parallelized calculation, false: Rho - * winner is evaluated by sequential calculation + * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation + * is parallelized, false: Rho winner calculation is sequential. * @return Art2aResult instance * @throws IllegalArgumentException Thrown if argument is illegal * @throws Exception Thrown if exception occurs which should never happen */ public Art2aResult getClusterResult( float aVigilance, - boolean anIsParallelRhoWinnerEvaluation + boolean anIsParallelRhoWinnerCalculation ) throws Exception { // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { @@ -511,7 +450,7 @@ public Art2aResult getClusterResult( throw new IllegalArgumentException("Art2aKernel.getClusterResult: aVigilance must be in interval (0,1)."); } // - + try { Random tmpRandomNumberGenerator = new Random(this.randomSeed); boolean tmpIsClusterOverflow = false; @@ -556,7 +495,7 @@ public Art2aResult getClusterResult( boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; // Buffer for Rho values for parallelized Rho winner evaluation float[] tmpRhoValueBuffer = null; - if (anIsParallelRhoWinnerEvaluation) { + if (anIsParallelRhoWinnerCalculation) { tmpRhoValueBuffer = new float[this.maximumNumberOfClusters]; } @@ -580,9 +519,10 @@ public Art2aResult getClusterResult( Utils.RhoWinner tmpRhoWinner = new Utils.RhoWinner(); Utils.ClusterRemovalInfo tmpClusterRemovalInfo = new Utils.ClusterRemovalInfo(); boolean tmpIsConverged = false; + while(!tmpIsConverged && tmpCurrentNumberOfEpochs < this.maximumNumberOfEpochs) { tmpCurrentNumberOfEpochs++; - + // Get random sequence of indices for data row vectors Utils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); @@ -618,17 +558,17 @@ public Art2aResult getClusterResult( tmpNumberOfDetectedClusters++; } else { // Cluster number is greater than or equal to 1 - if (anIsParallelRhoWinnerEvaluation) { - Art2aKernel.setRhoWinnerParallel( - tmpBufferVector, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, - tmpScalingFactor, - tmpRhoValueBuffer, - tmpRhoWinner - ); + if (anIsParallelRhoWinnerCalculation) { + Art2aKernel.setRhoWinnerParallel( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoValueBuffer, + tmpRhoWinner + ); } else { - Art2aKernel.setRhoWinner( + Art2aKernel.setRhoWinnerSequential( tmpBufferVector, tmpClusterMatrix, tmpNumberOfDetectedClusters, @@ -755,90 +695,112 @@ public Art2aResult getClusterResult( } /** - * Performs ART-2a clustering for specified vigilance parameters and + * Performs ART-2a clustering for specified vigilance parameters and * returns corresponding Art2aResult objects. - * Note: Parallel Rho winner evaluations is disabled. + * Note: Parallel Rho winner evaluation is disabled. * * @param aVigilances Vigilance parameters (must each be in interval (0,1)) - * @return Art2aResult objects or null if clustering result could - * not be calculated. - * @param aNumberOfConcurrentCalculationThreads Number of concurrent - * calculation threads for the different vigilance parameters to be - * calculated concurrently (in parallel). If zero, then the different + * @param aNumberOfConcurrentCalculationThreads Number of concurrent + * calculation threads for the different vigilance parameters to be + * calculated concurrently (in parallel). If zero, then the different * vigilance parameters are calculated one after another (sequentially) + * @return Art2aResult objects or null if clustering result could + * not be calculated. * @throws IllegalArgumentException Thrown if argument is illegal - * @throws Exception Thrown if exception occurs which should never happen */ public Art2aResult[] getClusterResults( - float[] aVigilances, - int aNumberOfConcurrentCalculationThreads - ) throws Exception { + float[] aVigilances, + int aNumberOfConcurrentCalculationThreads + ) throws IllegalArgumentException { // if (aVigilances == null || aVigilances.length == 0) { Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: aVigilances is null or has length 0." + Level.SEVERE, + "Art2aKernel.getClusterResults: aVigilances is null or has length 0." ); throw new IllegalArgumentException("Art2aKernel.getClusterResults: aVigilances is null or has length 0."); } for (float tmpVigilance : aVigilances) { if(tmpVigilance <= 0.0f || tmpVigilance >= 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: Vigilance parameter must be in interval (0,1)." + Level.SEVERE, + "Art2aKernel.getClusterResults: Vigilance parameter must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aKernel.getClusterResults: Vigilance parameter must be in interval (0,1)."); } } if (aNumberOfConcurrentCalculationThreads < 0) { Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0." + Level.SEVERE, + "Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0." ); throw new IllegalArgumentException("Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0."); } // if (aNumberOfConcurrentCalculationThreads > 0) { - LinkedList tmpSingleTaskList = new LinkedList<>(); - for (float tmpVigilance : aVigilances) { - tmpSingleTaskList.add(new HelperTask(this, tmpVigilance)); - } - ExecutorService tmpExecutorService = newFixedThreadPool(aNumberOfConcurrentCalculationThreads); - List> tmpFutureList = null; + ForkJoinPool tmpForkJoinPool = null; try { - tmpFutureList = tmpExecutorService.invokeAll(tmpSingleTaskList); - } catch (InterruptedException anInterruptedException) { + tmpForkJoinPool = new ForkJoinPool(aNumberOfConcurrentCalculationThreads); + Art2aResult[] tmpParallelResults = new Art2aResult[aVigilances.length]; + tmpForkJoinPool.submit( + () -> IntStream.range(0, aVigilances.length).parallel().forEach( + i -> + { + try { + // Note: Parallel Rho winner calculation is disabled: Parameter false. + tmpParallelResults[i] = this.getClusterResult(aVigilances[i], false); + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: An exception occurred in custom fork-join pool: This should never happen." + ); + tmpParallelResults[i] = null; + } + } + ) + ).invoke(); + boolean tmpIsSuccessful = true; + for (int i = 0; i < aVigilances.length; i++) { + if (tmpParallelResults[i] == null) { + tmpIsSuccessful = false; + break; + } + } + if (tmpIsSuccessful) { + return tmpParallelResults; + } else { + return null; + } + } catch (Exception anException) { Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: Interrupted exception during concurrent calculation: This should never happen." + Level.SEVERE, + "Art2aKernel.getClusterResults: An exception occurred: This should never happen." ); - throw anInterruptedException; - } - tmpExecutorService.shutdown(); - Art2aResult[] tmpParallelResults = new Art2aResult[aVigilances.length]; - int tmpIndex = 0; - for (Future tmpFuture : tmpFutureList) { - try { - tmpParallelResults[tmpIndex++] = tmpFuture.get(); - } catch (Exception anException) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: Exception in tmpFuture.get()." - ); - throw anException; + return null; + } finally { + if (tmpForkJoinPool != null) { + tmpForkJoinPool.shutdown(); } } - return tmpParallelResults; } else { - Art2aResult[] tmpSequentialResults = new Art2aResult[aVigilances.length]; - for (int i = 0; i < aVigilances.length; i++) { - tmpSequentialResults[i] = this.getClusterResult(aVigilances[i], false); + try { + Art2aResult[] tmpSequentialResults = new Art2aResult[aVigilances.length]; + for (int i = 0; i < aVigilances.length; i++) { + // Note: Parallel Rho winner evaluations is disabled: Parameter 0. + tmpSequentialResults[i] = this.getClusterResult(aVigilances[i], false); + } + return tmpSequentialResults; + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: An exception occurred: This should never happen." + ); + return null; } - return tmpSequentialResults; } } - + /** * Nearest (smaller) indices of approximates to the desired number of * representatives. @@ -851,8 +813,8 @@ public Art2aResult[] getClusterResults( * (0,1)) * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or * equal to 1) - * @param anIsParallelRhoWinnerEvaluation True: Rho winner is evaluated by parallelized calculation, false: Rho - * winner is evaluated by sequential calculation + * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation + * is parallelized, false: Rho winner calculation is sequential. * @return Nearest (smaller) indices of approximates to the desired number * of representatives. * @throws IllegalArgumentException Thrown if an argument is illegal @@ -863,7 +825,7 @@ public int[] getRepresentatives( float aVigilanceMin, float aVigilanceMax, int aNumberOfTrialSteps, - boolean anIsParallelRhoWinnerEvaluation + boolean anIsParallelRhoWinnerCalculation ) throws IllegalArgumentException, Exception { // if(aNumberOfRepresentatives < 2) { @@ -904,12 +866,12 @@ public int[] getRepresentatives( // try { - Art2aResult tmpArt2aResult = this.getClusterResult(aVigilanceMin, anIsParallelRhoWinnerEvaluation); + Art2aResult tmpArt2aResult = this.getClusterResult(aVigilanceMin, anIsParallelRhoWinnerCalculation); int[] tmpRepresentativeIndicesOfClusters = tmpArt2aResult.getRepresentativeIndicesOfClusters(); if (tmpArt2aResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { return tmpRepresentativeIndicesOfClusters; } - tmpArt2aResult = this.getClusterResult(aVigilanceMax, anIsParallelRhoWinnerEvaluation); + tmpArt2aResult = this.getClusterResult(aVigilanceMax, anIsParallelRhoWinnerCalculation); if (tmpArt2aResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { return tmpArt2aResult.getRepresentativeIndicesOfClusters(); } @@ -918,7 +880,7 @@ public int[] getRepresentatives( float tmpVigilanceMax = aVigilanceMax; for (int i = 0; i < aNumberOfTrialSteps; i++) { float tmpVigilanceMean = (tmpVigilanceMin + tmpVigilanceMax) / 2.0f; - tmpArt2aResult = this.getClusterResult(tmpVigilanceMean, anIsParallelRhoWinnerEvaluation); + tmpArt2aResult = this.getClusterResult(tmpVigilanceMean, anIsParallelRhoWinnerCalculation); if (tmpArt2aResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { tmpVigilanceMax = tmpVigilanceMean; } else if (tmpArt2aResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { @@ -954,8 +916,8 @@ public int[] getRepresentatives( * CHECKED) * @param aMinimumNumberOfRepresentatives Minimum number of representatives * @param aMaximumNumberOfRepresentatives Maximum number of representatives - * @param anIsParallelRhoWinnerEvaluation True: Rho winner is evaluated by parallelized calculation, false: Rho - * winner is evaluated by sequential calculation + * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation + * is parallelized, false: Rho winner calculation is sequential. * @return Representatives whose mean distance is nearest to the mean * distance of all data vectors of specified original data matrix. * @throws IllegalArgumentException Thrown if an argument is illegal @@ -965,7 +927,7 @@ public int[] getBestRepresentatives( float[][] aDataMatrix, int aMinimumNumberOfRepresentatives, int aMaximumNumberOfRepresentatives, - boolean anIsParallelRhoWinnerEvaluation + boolean anIsParallelRhoWinnerCalculation ) throws IllegalArgumentException, Exception { // if(aDataMatrix == null || aDataMatrix.length == 0) { @@ -1011,7 +973,7 @@ public int[] getBestRepresentatives( tmpVigilanceMin, tmpVigilanceMax, tmpNumberOfTrialSteps, - anIsParallelRhoWinnerEvaluation + anIsParallelRhoWinnerCalculation ); float tmpMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpRepresentatives); float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); @@ -1300,7 +1262,7 @@ private static void modifyWinnerCluster( * index of the winner. If the cluster index is negative the first scaled * rho value is the winner. */ - private static void setRhoWinner( + private static void setRhoWinnerSequential( float[] aContrastEnhancedUnitVector, float[][] aClusterMatrix, int aNumberOfDetectedClusters, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java index 92bf2fa..9805557 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -289,7 +289,7 @@ public Art2aTask( // /** * Performs the clustering process. - * Note: Parallel Rho winner evaluations is disabled. + * Note: Parallel Rho winner evaluation is disabled. * * @return Clustering result or null if clustering process could not be * performed. diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index e07e77f..5853f8c 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -59,7 +59,7 @@ public void test_Development_IrisFlowerData() { int tmpMaximumNumberOfClusters = 150; boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -82,7 +82,7 @@ public void test_Development_IrisFlowerData() { Assertions.assertNotNull(tmpArt2aKernel); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -127,7 +127,7 @@ public void test_Development_CombinedGaussianCouldData() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 1000; boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -148,7 +148,7 @@ public void test_Development_CombinedGaussianCouldData() { ); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -196,9 +196,9 @@ public void test_Development_CombinedGaussianCouldData_Performance() { ); float tmpVigilance = 0.1f; - int tmpMaximumNumberOfClusters = 2000; + int tmpMaximumNumberOfClusters = 500; boolean tmpIsDataPreprocessing = true; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = true; int tmpMaximumNumberOfEpochs = 10; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -219,7 +219,7 @@ public void test_Development_CombinedGaussianCouldData_Performance() { ); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -249,7 +249,7 @@ public void test_Development_GetRepresentatives() { float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -273,7 +273,7 @@ public void test_Development_GetRepresentatives() { tmpIrisFlowerDataMatrix, 2, tmpIrisFlowerDataMatrix.length, - tmpIsParallelRhoWinnerEvaluation + tmpIsParallelRhoWinnerCalculation ); Arrays.sort(tmpBestRepresentatives); System.out.println( @@ -299,7 +299,7 @@ public void test_Development_GetRepresentatives() { tmpVigilanceMin, tmpVigilanceMax, tmpNumberOfTrialSteps, - tmpIsParallelRhoWinnerEvaluation + tmpIsParallelRhoWinnerCalculation ); if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { Arrays.sort(tmpRepresentatives); @@ -334,7 +334,7 @@ public void test_GetRepresentatives() { float tmpOffsetForContrastEnhancement = 0.5f; long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -359,7 +359,7 @@ public void test_GetRepresentatives() { tmpVigilanceMin, tmpVigilanceMax, tmpNumberOfTrialSteps, - tmpIsParallelRhoWinnerEvaluation + tmpIsParallelRhoWinnerCalculation ); Assertions.assertEquals(tmpRepresentatives.length, tmpNumberOfRepresentatives); } catch (Exception anException) { @@ -390,7 +390,7 @@ public void test_PerfectClustering() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 100; boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -410,7 +410,7 @@ public void test_PerfectClustering() { ); Art2aResult tmpArt2aResult = null; try { - tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.assertTrue(false); } @@ -458,7 +458,7 @@ public void test_Preprocessing() { for (float tmpVigilance : tmpVigilances) { // No preprocessing boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( tmpIrisFlowerDataMatrix, @@ -472,7 +472,7 @@ public void test_Preprocessing() { ); Art2aResult tmpArt2aResultWithoutPreprocessing = null; try { - tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -492,7 +492,7 @@ public void test_Preprocessing() { ); Art2aResult tmpArt2aResultWithPreprocessing = null; try { - tmpArt2aResultWithPreprocessing = tmpArt2aKernelWithPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResultWithPreprocessing = tmpArt2aKernelWithPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -536,7 +536,7 @@ public void test_Art2aData() { for (float tmpVigilance : tmpVigilances) { // No preprocessing boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( tmpIrisFlowerDataMatrix, @@ -550,7 +550,7 @@ public void test_Art2aData() { ); Art2aResult tmpArt2aResultWithoutPreprocessing = null; try { - tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResultWithoutPreprocessing = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -568,7 +568,7 @@ public void test_Art2aData() { ); Art2aResult tmpArt2aResultWithArt2aData = null; try { - tmpArt2aResultWithArt2aData = tmpArt2aKernelWithArt2aData.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpArt2aResultWithArt2aData = tmpArt2aKernelWithArt2aData.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.assertTrue(false); } @@ -625,7 +625,7 @@ public void test_ParallelClustering() { int tmpIndex = 0; for (float tmpVigilance : tmpVigilances) { boolean tmpIsDataPreprocessing = false; - boolean tmpIsParallelRhoWinnerEvaluation = false; + boolean tmpIsParallelRhoWinnerCalculation = false; Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( tmpIrisFlowerDataMatrix, @@ -638,7 +638,7 @@ public void test_ParallelClustering() { tmpIsDataPreprocessing ); try { - tmpSequentialResults[tmpIndex++] = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerEvaluation); + tmpSequentialResults[tmpIndex++] = tmpArt2aKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { Assertions.fail(); } @@ -715,9 +715,9 @@ public void test_ParallelClustering() { * Art2aKernel.getClusterResults() leads to identical results. */ @Test - public void test_ParallelClusteringWithGetGlusterResults() { + public void test_ParallelClusteringWithGetClusterResults() { System.out.println("----------------------------------------------"); - System.out.println("test_ParallelClusteringWithGetGlusterResults()"); + System.out.println("test_ParallelClusteringWithGetClusterResults()"); System.out.println("----------------------------------------------"); float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; @@ -746,7 +746,7 @@ public void test_ParallelClusteringWithGetGlusterResults() { try { tmpSequentialResults = tmpArt2aKernel.getClusterResults(tmpVigilances, 0); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Concurrent (parallel) clustering @@ -755,21 +755,15 @@ public void test_ParallelClusteringWithGetGlusterResults() { try { tmpParallelResults = tmpArt2aKernel.getClusterResults(tmpVigilances, tmpNumberOfParallelCalculationThreads); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Assertions.assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aData are identical for (int i = 0; i < tmpVigilances.length; i++) { - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfDetectedClusters() == - tmpParallelResults[i].getNumberOfDetectedClusters() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfEpochs() == - tmpParallelResults[i].getNumberOfEpochs() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfEpochs(), tmpParallelResults[i].getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { @@ -781,10 +775,7 @@ public void test_ParallelClusteringWithGetGlusterResults() { for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { - Assertions.assertTrue( - tmpSequentialResults[i].getAngleBetweenClusters(j, k) == - tmpParallelResults[i].getAngleBetweenClusters(j, k) - ); + Assertions.assertEquals(tmpSequentialResults[i].getAngleBetweenClusters(j, k), tmpParallelResults[i].getAngleBetweenClusters(j, k)); } } } From 5a44083bd8e0aa2f4c05df990de33a1ffe993b6e Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Thu, 13 Feb 2025 17:24:31 +0100 Subject: [PATCH 14/18] Parallelization improved --- .../cheminf/clustering/art2a/Art2aKernel.java | 55 ++++++------------- .../cheminf/clustering/art2a/Art2aTest.java | 5 +- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 7c22f57..745559a 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -28,7 +28,6 @@ import java.util.Arrays; import java.util.Random; -import java.util.concurrent.ForkJoinPool; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.IntStream; @@ -700,17 +699,14 @@ public Art2aResult getClusterResult( * Note: Parallel Rho winner evaluation is disabled. * * @param aVigilances Vigilance parameters (must each be in interval (0,1)) - * @param aNumberOfConcurrentCalculationThreads Number of concurrent - * calculation threads for the different vigilance parameters to be - * calculated concurrently (in parallel). If zero, then the different - * vigilance parameters are calculated one after another (sequentially) + * @param anIsParallelCalculation True: Calculations are parallelized, false: Calculations are sequential (one after another) * @return Art2aResult objects or null if clustering result could * not be calculated. * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aResult[] getClusterResults( float[] aVigilances, - int aNumberOfConcurrentCalculationThreads + boolean anIsParallelCalculation ) throws IllegalArgumentException { // if (aVigilances == null || aVigilances.length == 0) { @@ -729,38 +725,27 @@ public Art2aResult[] getClusterResults( throw new IllegalArgumentException("Art2aKernel.getClusterResults: Vigilance parameter must be in interval (0,1)."); } } - if (aNumberOfConcurrentCalculationThreads < 0) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0." - ); - throw new IllegalArgumentException("Art2aKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0."); - } // - if (aNumberOfConcurrentCalculationThreads > 0) { - ForkJoinPool tmpForkJoinPool = null; + if (anIsParallelCalculation) { try { - tmpForkJoinPool = new ForkJoinPool(aNumberOfConcurrentCalculationThreads); Art2aResult[] tmpParallelResults = new Art2aResult[aVigilances.length]; - tmpForkJoinPool.submit( - () -> IntStream.range(0, aVigilances.length).parallel().forEach( - i -> - { - try { - // Note: Parallel Rho winner calculation is disabled: Parameter false. - tmpParallelResults[i] = this.getClusterResult(aVigilances[i], false); - } catch (Exception anException) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getClusterResults: An exception occurred in custom fork-join pool: This should never happen." - ); - tmpParallelResults[i] = null; - } + // Advise by Oracle: Parallel streams should use the common fork-join pool. + IntStream.range(0, aVigilances.length).parallel().forEach( + i -> + { + try { + // Note: Parallel Rho winner calculation is disabled: Parameter false. + tmpParallelResults[i] = this.getClusterResult(aVigilances[i], false); + } catch (Exception anException) { + Art2aKernel.LOGGER.log( + Level.SEVERE, + "Art2aKernel.getClusterResults: An exception occurred in fork-join pool: This should never happen." + ); + tmpParallelResults[i] = null; } - ) - ).invoke(); - boolean tmpIsSuccessful = true; + } + ); boolean tmpIsSuccessful = true; for (int i = 0; i < aVigilances.length; i++) { if (tmpParallelResults[i] == null) { tmpIsSuccessful = false; @@ -778,10 +763,6 @@ public Art2aResult[] getClusterResults( "Art2aKernel.getClusterResults: An exception occurred: This should never happen." ); return null; - } finally { - if (tmpForkJoinPool != null) { - tmpForkJoinPool.shutdown(); - } } } else { try { diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 5853f8c..098e167 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -744,16 +744,15 @@ public void test_ParallelClusteringWithGetClusterResults() { // Sequential clustering one after another Art2aResult[] tmpSequentialResults = null; try { - tmpSequentialResults = tmpArt2aKernel.getClusterResults(tmpVigilances, 0); + tmpSequentialResults = tmpArt2aKernel.getClusterResults(tmpVigilances, false); } catch (Exception anException) { Assertions.fail(); } // Concurrent (parallel) clustering - int tmpNumberOfParallelCalculationThreads = 2; Art2aResult[] tmpParallelResults = null; try { - tmpParallelResults = tmpArt2aKernel.getClusterResults(tmpVigilances, tmpNumberOfParallelCalculationThreads); + tmpParallelResults = tmpArt2aKernel.getClusterResults(tmpVigilances, true); } catch (Exception anException) { Assertions.fail(); } From 8c05f6992e8b9777b8fad3e3eab16e875349cd22 Mon Sep 17 00:00:00 2001 From: Achim Zielesny Date: Sun, 23 Feb 2025 16:14:57 +0100 Subject: [PATCH 15/18] Implementation finalized, tests are still insufficient. --- .../clustering/art2a/Art2aEuclidKernel.java | 361 +++++++----------- .../clustering/art2a/Art2aEuclidTask.java | 4 +- .../cheminf/clustering/art2a/Art2aKernel.java | 131 +------ .../cheminf/clustering/art2a/Art2aResult.java | 3 +- .../clustering/art2a/Art2aEuclidTest.java | 153 +++----- .../cheminf/clustering/art2a/Art2aTest.java | 60 +-- 6 files changed, 239 insertions(+), 473 deletions(-) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index 63d14ae..2bf8006 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -27,16 +27,10 @@ package de.unijena.cheminf.clustering.art2a; import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; import java.util.Random; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; - -import static java.util.concurrent.Executors.newFixedThreadPool; +import java.util.stream.IntStream; /** * ART-2a-Euclid algorithm implementation for unsupervised, open categorical @@ -189,58 +183,6 @@ public class Art2aEuclidKernel { */ private final PreprocessedData preprocessedData; // - // - /** - * Helper callable for a single getClusterResult() calculation task of - * Art2aEuclidKernel with a distinct vigilance parameter. - *

- * Note: No checks are performed. - */ - private static class HelperTask implements Callable { - - // - /** - * Art2aEuclidKernel - */ - private final Art2aEuclidKernel art2aKernel; - /** - * Vigilance parameter - */ - private final float vigilance; - // - - // - /** - * Constructor - */ - protected HelperTask( - Art2aEuclidKernel anArt2aEuclidKernel, - float aVigilance - ) { - this.art2aKernel = anArt2aEuclidKernel; - this.vigilance = aVigilance; - } - // - - // - /** - * Performs single getClusterResult() calculation task. - * - * @return Art2aEuclidResult or null if getClusterResult() calculation task - could not be performed. - */ - @Override - public Art2aEuclidResult call() { - try { - return this.art2aKernel.getClusterResult(this.vigilance); - } catch (Exception anException) { - return null; - } - } - // - - } - //
// /** @@ -488,17 +430,22 @@ public Art2aEuclidKernel( // /** - * Performs ART-2a-Euclid clustering and returns corresponding - * Art2aEuclidResult. + * Performs ART-2a-Euclid clustering and returns corresponding Art2aEuclidResult. + * Note: Parallelized Rho winner calculation is faster if many detected clusters, sequential Rho winner + * calculation is faster for a small number of formed clusters. The crossover between both must be evaluated + * experimentally. * * @param aVigilance Vigilance parameter (must be in interval (0,1)) + * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation + * is parallelized, false: Rho winner calculation is sequential. * @return Art2aEuclidResult instance * @throws IllegalArgumentException Thrown if argument is illegal * @throws Exception Thrown if exception occurs which should never happen */ public Art2aEuclidResult getClusterResult( - float aVigilance - ) throws Exception { + float aVigilance, + boolean anIsParallelRhoWinnerCalculation + ) throws IllegalArgumentException, Exception { // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidKernel.LOGGER.log( @@ -554,6 +501,11 @@ public Art2aEuclidResult getClusterResult( // Cluster usage flags. True: Cluster is used, false: Cluster is // empty and can be removed. boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; + // Buffer for Rho values for parallelized Rho winner evaluation + float[] tmpRhoValueBuffer = null; + if (anIsParallelRhoWinnerCalculation) { + tmpRhoValueBuffer = new float[this.maximumNumberOfClusters]; + } // Initialize cluster indices for data row vectors with -1 to // indicate missing cluster assignment @@ -613,13 +565,24 @@ public Art2aEuclidResult getClusterResult( tmpNumberOfDetectedClusters++; } else { // Cluster number is greater than or equal to 1 - Art2aEuclidKernel.setRhoWinner( - tmpBufferVector, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, - tmpScalingFactor, - tmpRhoWinner - ); + if (anIsParallelRhoWinnerCalculation) { + Art2aEuclidKernel.setRhoWinnerParallel( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoValueBuffer, + tmpRhoWinner + ); + } else { + Art2aEuclidKernel.setRhoWinnerSequential( + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoWinner + ); + } // Assign to existing cluster or increment clusters if(tmpRhoWinner.getIndexOfCluster() < 0 || tmpRhoWinner.getRhoValue() > tmpRhoStar) { // Increment clusters (if possible) @@ -740,23 +703,21 @@ public Art2aEuclidResult getClusterResult( } /** - * Performs ART-2a-Euclid clustering for specified vigilance parameters and - * returns corresponding Art2aEuclidResult objects. + * Performs ART-2a-Euclid clustering for specified vigilance parameters and returns corresponding Art2aEuclidResult + * objects. + * Note: Parallelized Rho winner evaluation is disabled. * * @param aVigilances Vigilance parameters (must each be in interval (0,1)) - * @return Art2aEuclidResult objects or null if clustering result could + * @param anIsParallelCalculation True: Calculations are parallelized, false: Calculations are sequential (one + * after another) + * @return Art2aEuclidResult objects or null if clustering result could * not be calculated. - * @param aNumberOfConcurrentCalculationThreads Number of concurrent - * calculation threads for the different vigilance parameters to be - * calculated concurrently (in parallel). If zero, then the different - * vigilance parameters are calculated one after another (sequentially) * @throws IllegalArgumentException Thrown if argument is illegal - * @throws Exception Thrown if exception occurs which should never happen */ public Art2aEuclidResult[] getClusterResults( float[] aVigilances, - int aNumberOfConcurrentCalculationThreads - ) throws Exception { + boolean anIsParallelCalculation + ) throws IllegalArgumentException { // if (aVigilances == null || aVigilances.length == 0) { Art2aEuclidKernel.LOGGER.log( @@ -774,52 +735,60 @@ public Art2aEuclidResult[] getClusterResults( throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: Vigilance parameter must be in interval (0,1)."); } } - if (aNumberOfConcurrentCalculationThreads < 0) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0." - ); - throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: aNumberOfConcurrentCalculationThreads must be greater or equal to 0."); - } // - if (aNumberOfConcurrentCalculationThreads > 0) { - LinkedList tmpSingleTaskList = new LinkedList<>(); - for (float tmpVigilance : aVigilances) { - tmpSingleTaskList.add(new HelperTask(this, tmpVigilance)); - } - ExecutorService tmpExecutorService = newFixedThreadPool(aNumberOfConcurrentCalculationThreads); - List> tmpFutureList = null; + if (anIsParallelCalculation) { try { - tmpFutureList = tmpExecutorService.invokeAll(tmpSingleTaskList); - } catch (InterruptedException anInterruptedException) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getClusterResults: Interrupted exception during concurrent calculation: This should never happen." + Art2aEuclidResult[] tmpParallelResults = new Art2aEuclidResult[aVigilances.length]; + IntStream.range(0, aVigilances.length).parallel().forEach( + i -> + { + try { + // Note: Parallel Rho winner calculation is disabled: Parameter false. + tmpParallelResults[i] = this.getClusterResult(aVigilances[i], false); + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: An exception occurred in common fork-join pool: This should never happen." + ); + tmpParallelResults[i] = null; + } + } ); - throw anInterruptedException; - } - tmpExecutorService.shutdown(); - Art2aEuclidResult[] tmpParallelResults = new Art2aEuclidResult[aVigilances.length]; - int tmpIndex = 0; - for (Future tmpFuture : tmpFutureList) { - try { - tmpParallelResults[tmpIndex++] = tmpFuture.get(); - } catch (Exception anException) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getClusterResults: Exception in tmpFuture.get()." - ); - throw anException; + boolean tmpIsSuccessful = true; + for (int i = 0; i < aVigilances.length; i++) { + if (tmpParallelResults[i] == null) { + tmpIsSuccessful = false; + break; + } + } + if (tmpIsSuccessful) { + return tmpParallelResults; + } else { + return null; } + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: An exception occurred: This should never happen." + ); + return null; } - return tmpParallelResults; } else { - Art2aEuclidResult[] tmpSequentialResults = new Art2aEuclidResult[aVigilances.length]; - for (int i = 0; i < aVigilances.length; i++) { - tmpSequentialResults[i] = this.getClusterResult(aVigilances[i]); + try { + Art2aEuclidResult[] tmpSequentialResults = new Art2aEuclidResult[aVigilances.length]; + for (int i = 0; i < aVigilances.length; i++) { + // Note: Parallel Rho winner evaluations is disabled: Parameter false. + tmpSequentialResults[i] = this.getClusterResult(aVigilances[i], false); + } + return tmpSequentialResults; + } catch (Exception anException) { + Art2aEuclidKernel.LOGGER.log( + Level.SEVERE, + "Art2aEuclidKernel.getClusterResults: An exception occurred: This should never happen." + ); + return null; } - return tmpSequentialResults; } } @@ -830,12 +799,14 @@ public Art2aEuclidResult[] getClusterResults( * @param aNumberOfRepresentatives Number of representatives (MUST be * greater or equal to 2) * @param aVigilanceMin Minimal vigilance parameter (must be in interval - * (0,1)) - * @param aVigilanceMax Maximal vigilance parameter (must be in interval - * (0,1)) - * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or - * equal to 1) - * @return Nearest (smaller) indices of approximants to the desired number + * (0,1), a good default value is 0.0001f) + * @param aVigilanceMax Maximal vigilance parameter (must be in interval + * (0,1), a good default value is 0.9999f) + * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or + * equal to 1, a good default value is 32) + * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation + * is parallelized, false: Rho winner calculation is sequential. + * @return Nearest (smaller) indices of approximants to the desired number * of representatives. * @throws IllegalArgumentException Thrown if an argument is illegal * @throws Exception Thrown if exception occurs which should never happen @@ -844,7 +815,8 @@ public int[] getRepresentatives( int aNumberOfRepresentatives, float aVigilanceMin, float aVigilanceMax, - int aNumberOfTrialSteps + int aNumberOfTrialSteps, + boolean anIsParallelRhoWinnerCalculation ) throws IllegalArgumentException, Exception { // if(aNumberOfRepresentatives < 2) { @@ -885,12 +857,12 @@ public int[] getRepresentatives( // try { - Art2aEuclidResult tmpArt2aEuclidResult = this.getClusterResult(aVigilanceMin); + Art2aEuclidResult tmpArt2aEuclidResult = this.getClusterResult(aVigilanceMin, anIsParallelRhoWinnerCalculation); int[] tmpRepresentativeIndicesOfClusters = tmpArt2aEuclidResult.getRepresentativeIndicesOfClusters(); if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { return tmpRepresentativeIndicesOfClusters; } - tmpArt2aEuclidResult = this.getClusterResult(aVigilanceMax); + tmpArt2aEuclidResult = this.getClusterResult(aVigilanceMax, anIsParallelRhoWinnerCalculation); if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { return tmpArt2aEuclidResult.getRepresentativeIndicesOfClusters(); } @@ -899,7 +871,7 @@ public int[] getRepresentatives( float tmpVigilanceMax = aVigilanceMax; for (int i = 0; i < aNumberOfTrialSteps; i++) { float tmpVigilanceMean = (tmpVigilanceMin + tmpVigilanceMax) / 2.0f; - tmpArt2aEuclidResult = this.getClusterResult(tmpVigilanceMean); + tmpArt2aEuclidResult = this.getClusterResult(tmpVigilanceMean, anIsParallelRhoWinnerCalculation); if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() > aNumberOfRepresentatives) { tmpVigilanceMax = tmpVigilanceMean; } else if (tmpArt2aEuclidResult.getNumberOfDetectedClusters() < aNumberOfRepresentatives) { @@ -923,94 +895,6 @@ public int[] getRepresentatives( throw anException; } } - - /** - * Note: This is a purely experimental nonsense method. - * - * Returns representatives whose mean distance is nearest to the mean - * distance of all data vectors of specified original data matrix. - * Note: This is a O(N^2) operation, N: Number of data vectors. - * - * @param aDataMatrix Original data matrix (IS NOT CHANGED and NOT properly - * CHECKED) - * @param aMinimumNumberOfRepresentatives Minimum number of representatives - * @param aMaximumNumberOfRepresentatives Maximum number of representatives - * @return Representatives whose mean distance is nearest to the mean - * distance of all data vectors of specified original data matrix. - * @throws IllegalArgumentException Thrown if an argument is illegal - * @throws Exception Thrown if exception occurs which should never happen - */ - public int[] getBestRepresentatives( - float[][] aDataMatrix, - int aMinimumNumberOfRepresentatives, - int aMaximumNumberOfRepresentatives - ) throws IllegalArgumentException, Exception { - // - if(aDataMatrix == null || aDataMatrix.length == 0) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getBestRepresentatives: aDataMatrix is null/has length zero." - ); - throw new IllegalArgumentException("Art2aEuclidKernel.getBestRepresentatives: aDataMatrix is null/has length zero."); - } - if(aMinimumNumberOfRepresentatives < 2 || aMinimumNumberOfRepresentatives > aDataMatrix.length - 1) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid." - ); - throw new IllegalArgumentException("Art2aEuclidKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid."); - } - if(aMaximumNumberOfRepresentatives <= aMinimumNumberOfRepresentatives || aMaximumNumberOfRepresentatives > aDataMatrix.length) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid." - ); - throw new IllegalArgumentException("Art2aEuclidKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid."); - } - // - - try { - int[] tmpAllIndices = new int[aDataMatrix.length]; - for (int i = 0; i < tmpAllIndices.length; i++) { - tmpAllIndices[i] = i; - } - float tmpBaseMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpAllIndices); - - float tmpVigilanceMin = 0.0001f; - float tmpVigilanceMax = 0.9999f; - int tmpNumberOfTrialSteps = 32; - - float tmpMinimalDifference = Float.MAX_VALUE; - int[] tmpBestRepresentatives = null; - for (int i = aMinimumNumberOfRepresentatives; i < aMaximumNumberOfRepresentatives; i++) { - int[] tmpRepresentatives = - this.getRepresentatives( - i, - tmpVigilanceMin, - tmpVigilanceMax, - tmpNumberOfTrialSteps - ); - float tmpMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpRepresentatives); - float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); - if (tmpDifference < tmpMinimalDifference) { - tmpMinimalDifference = tmpDifference; - tmpBestRepresentatives = tmpRepresentatives; - } - } - return tmpBestRepresentatives; - } catch (Exception anException) { - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - "Art2aEuclidKernel.getBestRepresentatives: An exception occurred: This should never happen!" - ); - Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - anException.toString(), - anException - ); - throw anException; - } - } // // /** @@ -1263,8 +1147,7 @@ private static void modifyWinnerCluster( * (see code). If the cluster index is negative the first scaled rho value * is the winner. * - * @param aContrastEnhancedVector Contrast enhanced unit vector (IS NOT - * CHANGED) + * @param aContrastEnhancedVector Contrast enhanced unit vector (IS NOT CHANGED) * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) * @param aNumberOfDetectedClusters Number of detected clusters * @param aScalingFactor Scaling factor @@ -1272,7 +1155,7 @@ private static void modifyWinnerCluster( * index of the winner. If the cluster index is negative the first scaled * rho value is the winner. */ - private static void setRhoWinner( + private static void setRhoWinnerSequential( float[] aContrastEnhancedVector, float[][] aClusterMatrix, int aNumberOfDetectedClusters, @@ -1293,6 +1176,46 @@ private static void setRhoWinner( } aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); } + + /** + * Sets rho winner with the rho value and the cluster index of the winner + * (see code). If the cluster index is negative the first scaled rho value + * is the winner. + * Note: A parallelized stream is used for calculation. + * + * @param aContrastEnhancedVector Contrast enhanced vector (IS NOT CHANGED) + * @param aClusterMatrix Cluster matrix (IS NOT CHANGED) + * @param aNumberOfDetectedClusters Number of detected clusters + * @param aScalingFactor Scaling factor + * @param aRhoValueBuffer Buffer for Rho values + * @param aRhoWinner Rho winner: Is set with the rho value and the cluster + * index of the winner. If the cluster index is negative the first scaled + * rho value is the winner. + */ + private static void setRhoWinnerParallel( + float[] aContrastEnhancedVector, + float[][] aClusterMatrix, + int aNumberOfDetectedClusters, + float aScalingFactor, + float[] aRhoValueBuffer, + Utils.RhoWinner aRhoWinner + ) { + // Calculate first rho value + float tmpRhoValue = Utils.getSumOfSquaredDifferences(aContrastEnhancedVector, aScalingFactor); + // Set winner index to negative value + int tmpIndex = -1; + // Calculate other rho values + IntStream.range(0, aNumberOfDetectedClusters).parallel().forEach( + i -> aRhoValueBuffer[i] = Utils.getSquaredDistance(aContrastEnhancedVector, aClusterMatrix[i]) + ); + for(int i = 0; i < aNumberOfDetectedClusters; i++) { + if(aRhoValueBuffer[i] < tmpRhoValue) { + tmpRhoValue = aRhoValueBuffer[i]; + tmpIndex = i; + } + } + aRhoWinner.setRhoWinner(tmpRhoValue, tmpIndex); + } // } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java index a4ef058..36aca58 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -290,6 +290,7 @@ public Art2aEuclidTask( // /** * Performs the clustering process. + * Note: Parallel Rho winner evaluation is disabled. * * @return Clustering result or null if clustering process could not be * performed. @@ -297,7 +298,8 @@ public Art2aEuclidTask( @Override public Art2aEuclidResult call() { try { - return this.art2aClusteringKernel.getClusterResult(this.vigilance); + // Note: Parallel Rho winner evaluations is disabled: Parameter false. + return this.art2aClusteringKernel.getClusterResult(this.vigilance, false); } catch (Exception anException) { Art2aEuclidTask.LOGGER.log( Level.SEVERE, diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index 745559a..dc0a19e 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -428,6 +428,9 @@ public Art2aKernel( // /** * Performs ART-2a clustering and returns corresponding Art2aResult. + * Note: Parallelized Rho winner calculation is faster if many detected clusters, sequential Rho winner + * calculation is faster for a small number of formed clusters. The crossover between both must be evaluated + * experimentally. * * @param aVigilance Vigilance parameter (must be in interval (0,1)) * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation @@ -439,7 +442,7 @@ public Art2aKernel( public Art2aResult getClusterResult( float aVigilance, boolean anIsParallelRhoWinnerCalculation - ) throws Exception { + ) throws IllegalArgumentException, Exception { // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aKernel.LOGGER.log( @@ -568,11 +571,11 @@ public Art2aResult getClusterResult( ); } else { Art2aKernel.setRhoWinnerSequential( - tmpBufferVector, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, - tmpScalingFactor, - tmpRhoWinner + tmpBufferVector, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpScalingFactor, + tmpRhoWinner ); } // Assign to existing cluster or increment clusters @@ -694,14 +697,13 @@ public Art2aResult getClusterResult( } /** - * Performs ART-2a clustering for specified vigilance parameters and - * returns corresponding Art2aResult objects. - * Note: Parallel Rho winner evaluation is disabled. + * Performs ART-2a clustering for specified vigilance parameters and returns corresponding Art2aResult objects. + * Note: Parallelized Rho winner evaluation is disabled. * * @param aVigilances Vigilance parameters (must each be in interval (0,1)) - * @param anIsParallelCalculation True: Calculations are parallelized, false: Calculations are sequential (one after another) - * @return Art2aResult objects or null if clustering result could - * not be calculated. + * @param anIsParallelCalculation True: Calculations are parallelized, false: Calculations are sequential (one + * after another) + * @return Art2aResult objects or null if clustering result could not be calculated. * @throws IllegalArgumentException Thrown if argument is illegal */ public Art2aResult[] getClusterResults( @@ -740,12 +742,13 @@ public Art2aResult[] getClusterResults( } catch (Exception anException) { Art2aKernel.LOGGER.log( Level.SEVERE, - "Art2aKernel.getClusterResults: An exception occurred in fork-join pool: This should never happen." + "Art2aKernel.getClusterResults: An exception occurred in common fork-join pool: This should never happen." ); tmpParallelResults[i] = null; } } - ); boolean tmpIsSuccessful = true; + ); + boolean tmpIsSuccessful = true; for (int i = 0; i < aVigilances.length; i++) { if (tmpParallelResults[i] == null) { tmpIsSuccessful = false; @@ -768,7 +771,7 @@ public Art2aResult[] getClusterResults( try { Art2aResult[] tmpSequentialResults = new Art2aResult[aVigilances.length]; for (int i = 0; i < aVigilances.length; i++) { - // Note: Parallel Rho winner evaluations is disabled: Parameter 0. + // Note: Parallel Rho winner evaluations is disabled: Parameter false. tmpSequentialResults[i] = this.getClusterResult(aVigilances[i], false); } return tmpSequentialResults; @@ -789,11 +792,11 @@ public Art2aResult[] getClusterResults( * @param aNumberOfRepresentatives Number of representatives (MUST be * greater or equal to 2) * @param aVigilanceMin Minimal vigilance parameter (must be in interval - * (0,1)) + * (0,1), a good default value is 0.0001f) * @param aVigilanceMax Maximal vigilance parameter (must be in interval - * (0,1)) + * (0,1), a good default value is 0.9999f) * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or - * equal to 1) + * equal to 1, a good default value is 32) * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation * is parallelized, false: Rho winner calculation is sequential. * @return Nearest (smaller) indices of approximates to the desired number @@ -885,98 +888,6 @@ public int[] getRepresentatives( throw anException; } } - - /** - * Note: This is a purely experimental nonsense method. - * - * Returns representatives whose mean distance is nearest to the mean - * distance of all data vectors of specified original data matrix. - * Note: This is a O(N^2) operation, N: Number of data vectors. - * - * @param aDataMatrix Original data matrix (IS NOT CHANGED and NOT properly - * CHECKED) - * @param aMinimumNumberOfRepresentatives Minimum number of representatives - * @param aMaximumNumberOfRepresentatives Maximum number of representatives - * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation - * is parallelized, false: Rho winner calculation is sequential. - * @return Representatives whose mean distance is nearest to the mean - * distance of all data vectors of specified original data matrix. - * @throws IllegalArgumentException Thrown if an argument is illegal - * @throws Exception Thrown if exception occurs which should never happen - */ - public int[] getBestRepresentatives( - float[][] aDataMatrix, - int aMinimumNumberOfRepresentatives, - int aMaximumNumberOfRepresentatives, - boolean anIsParallelRhoWinnerCalculation - ) throws IllegalArgumentException, Exception { - // - if(aDataMatrix == null || aDataMatrix.length == 0) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getBestRepresentatives: aDataMatrix is null/has length zero." - ); - throw new IllegalArgumentException("Art2aKernel.getBestRepresentatives: aDataMatrix is null/has length zero."); - } - if(aMinimumNumberOfRepresentatives < 2 || aMinimumNumberOfRepresentatives > aDataMatrix.length - 1) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid." - ); - throw new IllegalArgumentException("Art2aKernel.getBestRepresentatives: aMinimumNumberOfRepresentatives is invalid."); - } - if(aMaximumNumberOfRepresentatives <= aMinimumNumberOfRepresentatives || aMaximumNumberOfRepresentatives > aDataMatrix.length) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid." - ); - throw new IllegalArgumentException("Art2aKernel.getBestRepresentatives: aMaximumNumberOfRepresentatives is invalid."); - } - // - - try { - int[] tmpAllIndices = new int[aDataMatrix.length]; - for (int i = 0; i < tmpAllIndices.length; i++) { - tmpAllIndices[i] = i; - } - float tmpBaseMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpAllIndices); - - float tmpVigilanceMin = 0.0001f; - float tmpVigilanceMax = 0.9999f; - int tmpNumberOfTrialSteps = 32; - - float tmpMinimalDifference = Float.MAX_VALUE; - int[] tmpBestRepresentatives = null; - for (int i = aMinimumNumberOfRepresentatives; i < aMaximumNumberOfRepresentatives; i++) { - int[] tmpRepresentatives = - this.getRepresentatives( - i, - tmpVigilanceMin, - tmpVigilanceMax, - tmpNumberOfTrialSteps, - anIsParallelRhoWinnerCalculation - ); - float tmpMeanDistance = Utils.getMeanDistance(aDataMatrix, tmpRepresentatives); - float tmpDifference = Math.abs(tmpMeanDistance - tmpBaseMeanDistance); - if (tmpDifference < tmpMinimalDifference) { - tmpMinimalDifference = tmpDifference; - tmpBestRepresentatives = tmpRepresentatives; - } - } - return tmpBestRepresentatives; - } catch (Exception anException) { - Art2aKernel.LOGGER.log( - Level.SEVERE, - "Art2aKernel.getBestRepresentatives: An exception occurred: This should never happen!" - ); - Art2aKernel.LOGGER.log( - Level.SEVERE, - anException.toString(), - anException - ); - throw anException; - } - } // // /** diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java index 063e032..a827838 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -36,7 +36,7 @@ * Result of an ART-2a clustering process. *

* Note: Art2aResult is a read-only class, i.e. thread-safe. In addition, there - * are NO internal calculated values cached, i.e. each method call performs + * are NO internally calculated values cached, i.e. each method call performs * a full calculation procedure. An Art2aResult object may be distributed to * several concurrent (parallelized) evaluation tasks without any mutual * interference problems. @@ -101,6 +101,7 @@ public class Art2aResult { */ private final PreprocessedData preprocessedArt2aData; //
+ // /** * Indexed value diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java index e90517e..55e5b50 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -58,6 +58,7 @@ public void test_Development_IrisFlowerData() { float[] tmpVigilances = new float[] {0.1f}; boolean tmpIsClusterAnalysis = true; int tmpMaximumNumberOfClusters = 150; + boolean tmpIsParallelRhoWinnerCalculation = false; boolean tmpIsDataPreprocessing = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.1f; @@ -81,9 +82,9 @@ public void test_Development_IrisFlowerData() { Assertions.assertNotNull(tmpArt2aEuclidKernel); Art2aEuclidResult tmpArt2aEuclidResult = null; try { - tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } Assertions.assertNotNull(tmpArt2aEuclidResult); int tmpNumberOfDetectedClusters = tmpArt2aEuclidResult.getNumberOfDetectedClusters(); @@ -128,6 +129,7 @@ public void test_Development_CombinedGaussianCouldData() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 1000; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.1f; float tmpLearningParameter = 0.01f; @@ -148,9 +150,9 @@ public void test_Development_CombinedGaussianCouldData() { ); Art2aEuclidResult tmpArt2aEuclidResult = null; try { - tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } long tmpEnd = System.currentTimeMillis(); @@ -198,6 +200,7 @@ public void test_Development_CombinedGaussianCouldData_Performance() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 200; boolean tmpIsDataPreprocessing = true; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 10; float tmpConvergenceThreshold = 0.1f; float tmpLearningParameter = 0.01f; @@ -218,9 +221,9 @@ public void test_Development_CombinedGaussianCouldData_Performance() { ); Art2aEuclidResult tmpArt2aEuclidResult = null; try { - tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } long tmpEnd = System.currentTimeMillis(); @@ -248,6 +251,7 @@ public void test_Development_GetRepresentatives() { float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerCalculation = false; float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; @@ -265,16 +269,6 @@ public void test_Development_GetRepresentatives() { tmpIsDataPreprocessing ); - try { - int[] tmpBestRepresentatives = tmpArt2aEuclidKernel.getBestRepresentatives(tmpIrisFlowerDataMatrix, 2, tmpIrisFlowerDataMatrix.length); - Arrays.sort(tmpBestRepresentatives); - System.out.println( - String.valueOf(tmpBestRepresentatives.length) + " best representatives = " + this.getStringFromIntArray(tmpBestRepresentatives) - ); - } catch (Exception anException) { - Assertions.assertTrue(false); - } - int[] tmpAllIndices = new int[150]; for (int i = 0; i < 150; i++) { tmpAllIndices[i] = i; @@ -290,7 +284,8 @@ public void test_Development_GetRepresentatives() { tmpNumberOfRepresentatives, tmpVigilanceMin, tmpVigilanceMax, - tmpNumberOfTrialSteps + tmpNumberOfTrialSteps, + tmpIsParallelRhoWinnerCalculation ); if (tmpNumberOfRepresentatives == tmpRepresentatives.length) { Arrays.sort(tmpRepresentatives); @@ -304,7 +299,7 @@ public void test_Development_GetRepresentatives() { ); } } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } } } @@ -325,6 +320,7 @@ public void test_GetRepresentatives() { float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpNumberOfRepresentatives = 10; float tmpVigilanceMin = 0.0001f; @@ -348,7 +344,8 @@ public void test_GetRepresentatives() { tmpNumberOfRepresentatives, tmpVigilanceMin, tmpVigilanceMax, - tmpNumberOfTrialSteps + tmpNumberOfTrialSteps, + tmpIsParallelRhoWinnerCalculation ); System.out.println( String.valueOf(tmpNumberOfRepresentatives) + @@ -376,9 +373,9 @@ public void test_GetRepresentatives() { ); } } - Assertions.assertEquals(tmpRepresentatives.length, tmpNumberOfRepresentatives); + Assertions.assertEquals(tmpNumberOfRepresentatives, tmpRepresentatives.length); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } } @@ -405,6 +402,7 @@ public void test_PerfectClustering() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 100; boolean tmpIsDataPreprocessing = false; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 100; float tmpConvergenceThreshold = 0.1f; float tmpLearningParameter = 0.01f; @@ -424,15 +422,15 @@ public void test_PerfectClustering() { ); Art2aEuclidResult tmpArt2aEuclidResult = null; try { - tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance); + tmpArt2aEuclidResult = tmpArt2aEuclidKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } - Assertions.assertEquals(tmpArt2aEuclidResult.getNumberOfDetectedClusters(), tmpNumberOfDimensions); + Assertions.assertEquals(tmpNumberOfDimensions, tmpArt2aEuclidResult.getNumberOfDetectedClusters()); Assertions.assertTrue(tmpArt2aEuclidResult.getNumberOfEpochs() < tmpMaximumNumberOfEpochs); for (int i = 0; i < tmpArt2aEuclidResult.getNumberOfDetectedClusters(); i++) { - Assertions.assertEquals(tmpArt2aEuclidResult.getClusterSize(i), tmpNumberOfGaussianCloudVectors); + Assertions.assertEquals(tmpNumberOfGaussianCloudVectors, tmpArt2aEuclidResult.getClusterSize(i)); int[] tmpDataVectorIndicesOfCluster = tmpArt2aEuclidResult.getDataVectorIndicesOfCluster(i); int[] tmpClusterRepresentativeIndices = tmpArt2aEuclidResult.getClusterRepresentativeIndices(i); Assertions.assertEquals(tmpArt2aEuclidResult.getClusterRepresentativeIndex(i), tmpClusterRepresentativeIndices[0]); @@ -467,7 +465,8 @@ public void test_Preprocessing() { for (float tmpVigilance : tmpVigilances) { // No preprocessing boolean tmpIsDataPreprocessing = false; - Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = + boolean tmpIsParallelRhoWinnerCalculation = false; + Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = new Art2aEuclidKernel( tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, @@ -480,9 +479,9 @@ public void test_Preprocessing() { ); Art2aEuclidResult tmpArt2aEuclidResultWithoutPreprocessing = null; try { - tmpArt2aEuclidResultWithoutPreprocessing = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + tmpArt2aEuclidResultWithoutPreprocessing = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Preprocessing @@ -500,20 +499,14 @@ public void test_Preprocessing() { ); Art2aEuclidResult tmpArt2aEuclidResultWithPreprocessing = null; try { - tmpArt2aEuclidResultWithPreprocessing = tmpArt2aEuclidKernelWithPreprocessing.getClusterResult(tmpVigilance); + tmpArt2aEuclidResultWithPreprocessing = tmpArt2aEuclidKernelWithPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Assert that results without and with preprocessing are identical - Assertions.assertTrue( - tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters() == - tmpArt2aEuclidResultWithPreprocessing.getNumberOfDetectedClusters() - ); - Assertions.assertTrue( - tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs() == - tmpArt2aEuclidResultWithPreprocessing.getNumberOfEpochs() - ); + Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aEuclidResultWithPreprocessing.getNumberOfDetectedClusters()); + Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aEuclidResultWithPreprocessing.getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { @@ -524,18 +517,14 @@ public void test_Preprocessing() { } for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { - Assertions.assertTrue( - tmpArt2aEuclidResultWithoutPreprocessing.getDistanceBetweenClusters(i, j) == - tmpArt2aEuclidResultWithPreprocessing.getDistanceBetweenClusters(i, j) - ); + Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getDistanceBetweenClusters(i, j), tmpArt2aEuclidResultWithPreprocessing.getDistanceBetweenClusters(i, j)); } } } } /** - * Test that generated Art2aEuclidData object leads to identical clustering - results. + * Test that generated Art2aEuclidData object leads to identical clustering results. */ @Test public void test_Art2aEuclidData() { @@ -553,7 +542,8 @@ public void test_Art2aEuclidData() { for (float tmpVigilance : tmpVigilances) { // No preprocessing boolean tmpIsDataPreprocessing = false; - Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = + boolean tmpIsParallelRhoWinnerCalculation = false; + Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = new Art2aEuclidKernel( tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, @@ -566,9 +556,9 @@ public void test_Art2aEuclidData() { ); Art2aEuclidResult tmpArt2aEuclidResultWithoutPreprocessing = null; try { - tmpArt2aEuclidResultWithoutPreprocessing = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + tmpArt2aEuclidResultWithoutPreprocessing = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Preprocessed Art2aEuclidData @@ -584,21 +574,15 @@ public void test_Art2aEuclidData() { ); Art2aEuclidResult tmpArt2aEuclidResultWithArt2aEuclidData = null; try { - tmpArt2aEuclidResultWithArt2aEuclidData = tmpArt2aEuclidKernelWithArt2aEuclidData.getClusterResult(tmpVigilance); + tmpArt2aEuclidResultWithArt2aEuclidData = tmpArt2aEuclidKernelWithArt2aEuclidData.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Assert that results without preprocessing and preprocessed // Art2aEuclidData are identical - Assertions.assertTrue( - tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters() == - tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfDetectedClusters() - ); - Assertions.assertTrue( - tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs() == - tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfEpochs() - ); + Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfDetectedClusters()); + Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { @@ -609,10 +593,7 @@ public void test_Art2aEuclidData() { } for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { - Assertions.assertTrue( - tmpArt2aEuclidResultWithoutPreprocessing.getDistanceBetweenClusters(i, j) == - tmpArt2aEuclidResultWithArt2aEuclidData.getDistanceBetweenClusters(i, j) - ); + Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getDistanceBetweenClusters(i, j), tmpArt2aEuclidResultWithArt2aEuclidData.getDistanceBetweenClusters(i, j)); } } } @@ -641,7 +622,8 @@ public void test_ParallelClustering() { int tmpIndex = 0; for (float tmpVigilance : tmpVigilances) { boolean tmpIsDataPreprocessing = false; - Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = + boolean tmpIsParallelRhoWinnerCalculation = false; + Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = new Art2aEuclidKernel( tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, @@ -653,9 +635,9 @@ public void test_ParallelClustering() { tmpIsDataPreprocessing ); try { - tmpSequentialResults[tmpIndex++] = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance); + tmpSequentialResults[tmpIndex++] = tmpArt2aEuclidKernelWithoutPreprocessing.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } } @@ -695,15 +677,9 @@ public void test_ParallelClustering() { // Assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aEuclidData are identical for (int i = 0; i < tmpVigilances.length; i++) { - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfDetectedClusters() == - tmpParallelResults[i].getNumberOfDetectedClusters() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfEpochs() == - tmpParallelResults[i].getNumberOfEpochs() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfEpochs(), tmpParallelResults[i].getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { @@ -715,18 +691,15 @@ public void test_ParallelClustering() { for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { - Assertions.assertTrue( - tmpSequentialResults[i].getDistanceBetweenClusters(j, k) == - tmpParallelResults[i].getDistanceBetweenClusters(j, k) - ); + Assertions.assertEquals(tmpSequentialResults[i].getDistanceBetweenClusters(j, k), tmpParallelResults[i].getDistanceBetweenClusters(j, k)); } } } } /** - * Tests that sequential and parallelized clustering with - Art2aEuclidKernel.getClusterResults() leads to identical results. + * Tests that sequential and parallelized clustering with Art2aEuclidKernel.getClusterResults() leads to identical + * results. */ @Test public void test_ParallelClusteringWithGetGlusterResults() { @@ -758,32 +731,25 @@ public void test_ParallelClusteringWithGetGlusterResults() { // Sequential clustering one after another Art2aEuclidResult[] tmpSequentialResults = null; try { - tmpSequentialResults = tmpArt2aEuclidKernel.getClusterResults(tmpVigilances, 0); + tmpSequentialResults = tmpArt2aEuclidKernel.getClusterResults(tmpVigilances, false); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Concurrent (parallel) clustering - int tmpNumberOfParallelCalculationThreads = 2; Art2aEuclidResult[] tmpParallelResults = null; try { - tmpParallelResults = tmpArt2aEuclidKernel.getClusterResults(tmpVigilances, tmpNumberOfParallelCalculationThreads); + tmpParallelResults = tmpArt2aEuclidKernel.getClusterResults(tmpVigilances, true); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aEuclidData are identical for (int i = 0; i < tmpVigilances.length; i++) { - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfDetectedClusters() == - tmpParallelResults[i].getNumberOfDetectedClusters() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfEpochs() == - tmpParallelResults[i].getNumberOfEpochs() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfEpochs(), tmpParallelResults[i].getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { @@ -795,10 +761,7 @@ public void test_ParallelClusteringWithGetGlusterResults() { for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { - Assertions.assertTrue( - tmpSequentialResults[i].getDistanceBetweenClusters(j, k) == - tmpParallelResults[i].getDistanceBetweenClusters(j, k) - ); + Assertions.assertEquals(tmpSequentialResults[i].getDistanceBetweenClusters(j, k), tmpParallelResults[i].getDistanceBetweenClusters(j, k)); } } } diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 098e167..956ce8d 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -198,7 +198,7 @@ public void test_Development_CombinedGaussianCouldData_Performance() { float tmpVigilance = 0.1f; int tmpMaximumNumberOfClusters = 500; boolean tmpIsDataPreprocessing = true; - boolean tmpIsParallelRhoWinnerCalculation = true; + boolean tmpIsParallelRhoWinnerCalculation = false; int tmpMaximumNumberOfEpochs = 10; float tmpConvergenceThreshold = 0.99f; float tmpLearningParameter = 0.01f; @@ -267,22 +267,6 @@ public void test_Development_GetRepresentatives() { tmpIsDataPreprocessing ); - try { - int[] tmpBestRepresentatives = - tmpArt2aKernel.getBestRepresentatives( - tmpIrisFlowerDataMatrix, - 2, - tmpIrisFlowerDataMatrix.length, - tmpIsParallelRhoWinnerCalculation - ); - Arrays.sort(tmpBestRepresentatives); - System.out.println( - String.valueOf(tmpBestRepresentatives.length) + " best representatives = " + this.getStringFromIntArray(tmpBestRepresentatives) - ); - } catch (Exception anException) { - Assertions.fail(); - } - int[] tmpAllIndices = new int[150]; for (int i = 0; i < 150; i++) { tmpAllIndices[i] = i; @@ -361,7 +345,7 @@ public void test_GetRepresentatives() { tmpNumberOfTrialSteps, tmpIsParallelRhoWinnerCalculation ); - Assertions.assertEquals(tmpRepresentatives.length, tmpNumberOfRepresentatives); + Assertions.assertEquals(tmpNumberOfRepresentatives, tmpRepresentatives.length); } catch (Exception anException) { Assertions.fail(); } @@ -412,13 +396,13 @@ public void test_PerfectClustering() { try { tmpArt2aResult = tmpArt2aKernel.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } - Assertions.assertEquals(tmpArt2aResult.getNumberOfDetectedClusters(), tmpNumberOfDimensions); + Assertions.assertEquals(tmpNumberOfDimensions, tmpArt2aResult.getNumberOfDetectedClusters()); Assertions.assertTrue(tmpArt2aResult.getNumberOfEpochs() < tmpMaximumNumberOfEpochs); for (int i = 0; i < tmpArt2aResult.getNumberOfDetectedClusters(); i++) { - Assertions.assertEquals(tmpArt2aResult.getClusterSize(i), tmpNumberOfGaussianCloudVectors); + Assertions.assertEquals(tmpNumberOfGaussianCloudVectors, tmpArt2aResult.getClusterSize(i)); int[] tmpDataVectorIndicesOfCluster = tmpArt2aResult.getDataVectorIndicesOfCluster(i); int[] tmpClusterRepresentativeIndices = tmpArt2aResult.getClusterRepresentativeIndices(i); Assertions.assertEquals(tmpArt2aResult.getClusterRepresentativeIndex(i), tmpClusterRepresentativeIndices[0]); @@ -428,7 +412,7 @@ public void test_PerfectClustering() { } for (int i = 0; i < tmpArt2aResult.getNumberOfDetectedClusters(); i++) { for (int j = i + 1; j < tmpArt2aResult.getNumberOfDetectedClusters(); j++) { - Assertions.assertEquals(tmpArt2aResult.getAngleBetweenClusters(i, j), 90.0, 0.001); + Assertions.assertEquals(90.0, tmpArt2aResult.getAngleBetweenClusters(i, j), 0.001); } } Assertions.assertFalse(tmpArt2aResult.isClusterOverflow()); @@ -570,19 +554,13 @@ public void test_Art2aData() { try { tmpArt2aResultWithArt2aData = tmpArt2aKernelWithArt2aData.getClusterResult(tmpVigilance, tmpIsParallelRhoWinnerCalculation); } catch (Exception anException) { - Assertions.assertTrue(false); + Assertions.fail(); } // Assertions.assert that results without preprocessing and preprocessed // Art2aData are identical - Assertions.assertTrue( - tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters() == - tmpArt2aResultWithArt2aData.getNumberOfDetectedClusters() - ); - Assertions.assertTrue( - tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs() == - tmpArt2aResultWithArt2aData.getNumberOfEpochs() - ); + Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aResultWithArt2aData.getNumberOfDetectedClusters()); + Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aResultWithArt2aData.getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { @@ -593,10 +571,7 @@ public void test_Art2aData() { } for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { for (int j = i + 1; j < tmpNumberOfDetectedClusters; j++) { - Assertions.assertTrue( - tmpArt2aResultWithoutPreprocessing.getAngleBetweenClusters(i, j) == - tmpArt2aResultWithArt2aData.getAngleBetweenClusters(i, j) - ); + Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getAngleBetweenClusters(i, j), tmpArt2aResultWithArt2aData.getAngleBetweenClusters(i, j)); } } } @@ -681,15 +656,9 @@ public void test_ParallelClustering() { // Assertions.assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aData are identical for (int i = 0; i < tmpVigilances.length; i++) { - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfDetectedClusters() == - tmpParallelResults[i].getNumberOfDetectedClusters() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); - Assertions.assertTrue( - tmpSequentialResults[i].getNumberOfEpochs() == - tmpParallelResults[i].getNumberOfEpochs() - ); + Assertions.assertEquals(tmpSequentialResults[i].getNumberOfEpochs(), tmpParallelResults[i].getNumberOfEpochs()); int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { @@ -701,10 +670,7 @@ public void test_ParallelClustering() { for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { for (int k = j + 1; k < tmpNumberOfDetectedClusters; k++) { - Assertions.assertTrue( - tmpSequentialResults[i].getAngleBetweenClusters(j, k) == - tmpParallelResults[i].getAngleBetweenClusters(j, k) - ); + Assertions.assertEquals(tmpSequentialResults[i].getAngleBetweenClusters(j, k), tmpParallelResults[i].getAngleBetweenClusters(j, k)); } } } From 42b814e9a023e1859e9e4cb7e71b519ee27f8581 Mon Sep 17 00:00:00 2001 From: Jonas Schaub <44881147+JonasSchaub@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:04:06 +0100 Subject: [PATCH 16/18] applies spotless, now building --- License-header/License-header.txt | 7 +- .../clustering/art2a/Art2aEuclidKernel.java | 314 +++++++++--------- .../clustering/art2a/Art2aEuclidResult.java | 134 ++++---- .../clustering/art2a/Art2aEuclidTask.java | 98 +++--- .../clustering/art2a/Art2aEuclidUtils.java | 20 +- .../cheminf/clustering/art2a/Art2aKernel.java | 292 ++++++++-------- .../cheminf/clustering/art2a/Art2aResult.java | 126 +++---- .../cheminf/clustering/art2a/Art2aTask.java | 94 +++--- .../cheminf/clustering/art2a/Art2aUtils.java | 4 +- .../clustering/art2a/PreprocessedData.java | 96 +++--- .../clustering/art2a/Art2aEuclidTest.java | 290 ++++++++-------- .../cheminf/clustering/art2a/Art2aTest.java | 286 ++++++++-------- 12 files changed, 881 insertions(+), 880 deletions(-) diff --git a/License-header/License-header.txt b/License-header/License-header.txt index fad48a6..f90947c 100644 --- a/License-header/License-header.txt +++ b/License-header/License-header.txt @@ -1,8 +1,9 @@ /* - * ART2a Clustering for Java - * Copyright (C) $today.year Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at + * * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java index 2bf8006..17e1937 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidKernel.java @@ -1,8 +1,8 @@ /* - * ART-2a-Euclid Clustering for Java + * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -33,89 +33,89 @@ import java.util.stream.IntStream; /** - * ART-2a-Euclid algorithm implementation for unsupervised, open categorical + * ART-2a-Euclid algorithm implementation for unsupervised, open categorical * clustering. *

- * Literature: G.A. Carpenter, S. Grossberg and D.B. Rosen, Neural Networks 4 - * (1991) 493-504; D. Wienke, Neural Resonance and Adaptation - Towards - * Nature’s Principles in Artificial Pattern Recognition, in L. Buydens and - * W. Melssen (Eds.), Chemometrics: Exploring and Exploiting Chemical + * Literature: G.A. Carpenter, S. Grossberg and D.B. Rosen, Neural Networks 4 + * (1991) 493-504; D. Wienke, Neural Resonance and Adaptation - Towards + * Nature’s Principles in Artificial Pattern Recognition, in L. Buydens and + * W. Melssen (Eds.), Chemometrics: Exploring and Exploiting Chemical * Information, Catholic University Nijmegen, 1994. *

- * Use Art2aEuclidKernel for sequential clustering instances and Art2aEuclidTask - * for clustering instances to be executed concurrently (parallelized). See - * hints for ART-2a-Euclid clustering with minimal additional memory allocation - * or maximum speed below. + * Use Art2aEuclidKernel for sequential clustering instances and Art2aEuclidTask + * for clustering instances to be executed concurrently (parallelized). See + * hints for ART-2a-Euclid clustering with minimal additional memory allocation + * or maximum speed below. *

* Note: For clustering of the SAME data with DIFFERENT vigilance parameters use - * method getClusterResults() where the mode of calculation may be specified to + * method getClusterResults() where the mode of calculation may be specified to * be sequential or concurrent (parallelized). *

* All numerical calculations are performed in single (float) precision. *

- * Note, that aDataMatrix may contain data vectors with all components being - * equal to zero (or some constant minimal value). These data vectors are - * removed from the clustering process and their indices are returned by method + * Note, that aDataMatrix may contain data vectors with all components being + * equal to zero (or some constant minimal value). These data vectors are + * removed from the clustering process and their indices are returned by method * getZeroLengthDataVectorIndices() of an Art2aEuclidResult object. *

* ART-2a-Euclid clustering with minimal memory allocation: - * If a data matrix with N data row vectors is used to construct a clustering - * instance without preprocessing (parameter isDataPreprocessing is set to - * false), minimal additional memory is allocated. The data matrix itself is not - * changed. The additional allocated memory can be controlled by the + * If a data matrix with N data row vectors is used to construct a clustering + * instance without preprocessing (parameter isDataPreprocessing is set to + * false), minimal additional memory is allocated. The data matrix itself is not + * changed. The additional allocated memory can be controlled by the * maximumNumberOfClusters parameter and estimated to be about - * (additional memory of ART-2a-Euclid instance) = - * (2 x maximumNumberOfClusters / N) x (memory of data matrix), - * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the - * number of data row vectors will lead to roughly 2 MByte of additionally - * allocated memory. Note, that memory for cluster vectors is only allocated if - * needed, e.g. if specified parameter maximumNumberOfClusters allows 150 - * clusters but only 27 are needed, then only memory for these 27 cluster - * vectors is allocated. The minimal memory allocation comes at the expense of - * clustering speed since preprocessing steps have to be executed repeatedly. - * This also decreases the performance of some methods of the Art2aEuclidResult + * (additional memory of ART-2a-Euclid instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 2 MByte of additionally + * allocated memory. Note, that memory for cluster vectors is only allocated if + * needed, e.g. if specified parameter maximumNumberOfClusters allows 150 + * clusters but only 27 are needed, then only memory for these 27 cluster + * vectors is allocated. The minimal memory allocation comes at the expense of + * clustering speed since preprocessing steps have to be executed repeatedly. + * This also decreases the performance of some methods of the Art2aEuclidResult * object generated by the clustering process, e.g. getClusterRepresentatives(). *

* ART-2a-Euclid clustering with maximum speed: - * If parameter isDataPreprocessing is set to true, preprocessing steps are - * calculated in advance for maximum clustering speed (as well as maximum speed - * of the Art2aResult methods). This requires an additional memory allocation + * If parameter isDataPreprocessing is set to true, preprocessing steps are + * calculated in advance for maximum clustering speed (as well as maximum speed + * of the Art2aResult methods). This requires an additional memory allocation * for the preprocessed data for an ART-2a-Euclid clustering instance: - * (additional memory of ART-2a instance) = - * (1 + 2 x maximumNumberOfClusters / N) x (memory of data matrix), - * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the - * number of data row vectors will lead to roughly 12 MByte of additionally + * (additional memory of ART-2a instance) = + * (1 + 2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 12 MByte of additionally * allocated memory. *

- * CAUTION: Construction of several ART-2a-Euclid clustering instances with the - * SAME data matrix PLUS preprocessing is NOT advised due to the significant - * memory consumption of each instance. In this case, the data matrix should be + * CAUTION: Construction of several ART-2a-Euclid clustering instances with the + * SAME data matrix PLUS preprocessing is NOT advised due to the significant + * memory consumption of each instance. In this case, the data matrix should be * checked with static method Art2aKernel.isDataMatrixValid() (where possible NaN * values can be removed with Utils.isNonFiniteComponentRemoval()) and then a priori - * converted into a preprocessed Art2aEuclidData object with static method - * Art2aEuclidKernel.getArt2aEuclidData(). The generated Art2aData object does - * NOT change or refer to the data matrix so that the data matrix memory could - * be released after conversion (by setting the data matrix object to null). - * The generated Art2aEuclidData object has additionally allocated about the - * same memory as the original data matrix, e.g., a 10 MByte data matrix is - * converted into a roughly 10 MByte Art2aData object. But this single - * Art2aEuclidData object can now be used to construct several ART-2a-Euclid - * clustering instances (Art2aEuclidKernel instances or Art2aEuclidTask - * instances for concurrent (parallelized) execution) where each of these - * ART-2a-Euclid clustering instances (and their generated Art2aEuclidResult - * object methods) performs with maximum speed and allocates only the minimal - * additional memory of - * (additional memory of ART-2a instance) = - * (2 x maximumNumberOfClusters / N) x (memory of data matrix), - * e.g., for 9 constructed ART-2a-Euclid clustering instances for concurrent - * execution only 18 MBytes of additional memory are allocated in total. - * Compare this total additional allocated memory of only 10 + 18 = 28 MByte - * for an Art2aEuclidData object plus 9 ART-2a-Euclid clustering instances with - * the alternative 9 x 12 = 108 MByte of memory for 9 ART-2a-Euclid clustering - * instances constructed with the same data matrix plus independent - * preprocessing in each instance! (Just for completeness: For a minimal memory - * realization of these 9 ART-2a-Euclid clustering instances, each instance can - * be constructed with the same data matrix WITHOUT preprocessing, which would + * converted into a preprocessed Art2aEuclidData object with static method + * Art2aEuclidKernel.getArt2aEuclidData(). The generated Art2aData object does + * NOT change or refer to the data matrix so that the data matrix memory could + * be released after conversion (by setting the data matrix object to null). + * The generated Art2aEuclidData object has additionally allocated about the + * same memory as the original data matrix, e.g., a 10 MByte data matrix is + * converted into a roughly 10 MByte Art2aData object. But this single + * Art2aEuclidData object can now be used to construct several ART-2a-Euclid + * clustering instances (Art2aEuclidKernel instances or Art2aEuclidTask + * instances for concurrent (parallelized) execution) where each of these + * ART-2a-Euclid clustering instances (and their generated Art2aEuclidResult + * object methods) performs with maximum speed and allocates only the minimal + * additional memory of + * (additional memory of ART-2a instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., for 9 constructed ART-2a-Euclid clustering instances for concurrent + * execution only 18 MBytes of additional memory are allocated in total. + * Compare this total additional allocated memory of only 10 + 18 = 28 MByte + * for an Art2aEuclidData object plus 9 ART-2a-Euclid clustering instances with + * the alternative 9 x 12 = 108 MByte of memory for 9 ART-2a-Euclid clustering + * instances constructed with the same data matrix plus independent + * preprocessing in each instance! (Just for completeness: For a minimal memory + * realization of these 9 ART-2a-Euclid clustering instances, each instance can + * be constructed with the same data matrix WITHOUT preprocessing, which would * require only 18 MBytes of additional allocated memory in total.) * * @author Achim Zielesny @@ -152,7 +152,7 @@ public class Art2aEuclidKernel { */ private static final float DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT = 1.0f; /** - * Default value of the convergence threshold for cluster centroid + * Default value of the convergence threshold for cluster centroid * distance */ private static final float DEFAULT_CONVERGENCE_THRESHOLD = 0.1f; @@ -189,16 +189,16 @@ public class Art2aEuclidKernel { * Constructor. * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * distance (must be greater zero) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. @@ -206,10 +206,10 @@ public class Art2aEuclidKernel { * */ public Art2aEuclidKernel( - float[][] aDataMatrix, + float[][] aDataMatrix, int aMaximumNumberOfClusters, - int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, float aLearningParameter, float anOffsetForContrastEnhancement, long aRandomSeed, @@ -218,7 +218,7 @@ public Art2aEuclidKernel( // if(!Utils.isDataMatrixValid(aDataMatrix)) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aDataMatrix is not valid." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aDataMatrix is not valid."); @@ -235,35 +235,35 @@ public Art2aEuclidKernel( } if(aMaximumNumberOfEpochs <= 0) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); } if(aConvergenceThreshold <= 0.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero."); } if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)."); } if(anOffsetForContrastEnhancement <= 0.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: anOffsetForContrastEnhancement must be greater zero." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: anOffsetForContrastEnhancement must be greater zero."); } if(aRandomSeed <= 0L) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aRandomSeed must be greater 0." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aRandomSeed must be greater/equal 0."); @@ -279,7 +279,7 @@ public Art2aEuclidKernel( } else { this.preprocessedData = new PreprocessedData( - aDataMatrix, + aDataMatrix, Utils.getMinMaxComponents(aDataMatrix), anOffsetForContrastEnhancement ); @@ -294,8 +294,8 @@ public Art2aEuclidKernel( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), - * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 0.5) and RANDOM_SEED (= 1). * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) @@ -313,43 +313,43 @@ public Art2aEuclidKernel( this( aDataMatrix, aMaximumNumberOfClusters, - DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, - DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT, DEFAULT_RANDOM_SEED, anIsDataPreprocessing ); } - + /** * Constructor. * * @param aPreprocessedArt2aEuclidData PreprocessedData object * created by method Art2aEuclidKernel.getPreprocessedArt2aEuclidData() - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * distance (must be greater zero) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidKernel( PreprocessedArt2aEuclidData aPreprocessedArt2aEuclidData, int aMaximumNumberOfClusters, - int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { // if(aPreprocessedArt2aEuclidData == null) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData is null." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aPreprocessedArt2aEuclidData is null."); @@ -366,28 +366,28 @@ public Art2aEuclidKernel( } if(aMaximumNumberOfEpochs <= 0) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); } if(aConvergenceThreshold <= 0.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aConvergenceThreshold must be greater zero."); } if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aLearningParameter must be in interval (0,1)."); } if(aRandomSeed <= 0L) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.Constructor: aRandomSeed must be greater 0." ); throw new IllegalArgumentException("Art2aEuclidKernel.Constructor: aRandomSeed must be greater/equal 0."); @@ -404,7 +404,7 @@ public Art2aEuclidKernel( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aEuclidData PreprocessedData object created by method @@ -420,8 +420,8 @@ public Art2aEuclidKernel( this( aPreprocessedArt2aEuclidData, aMaximumNumberOfClusters, - DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, - DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, DEFAULT_RANDOM_SEED ); @@ -449,21 +449,21 @@ public Art2aEuclidResult getClusterResult( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getClusterResult: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResult: aVigilance must be in interval (0,1)."); } // - + try { Random tmpRandomNumberGenerator = new Random(this.randomSeed); boolean tmpIsClusterOverflow = false; - + float[][] tmpDataMatrix = null; float[][] tmpContrastEnhancedMatrix = null; - // Flags array that indicates if data row vectors have a length - // of zero (i.e. where all components are equal to zero). True: + // Flags array that indicates if data row vectors have a length + // of zero (i.e. where all components are equal to zero). True: // Data row vector has a length of zero, false: Otherwise. boolean[] tmpDataVectorZeroLengthFlags = null; int tmpNumberOfComponents = -1; @@ -481,24 +481,24 @@ public Art2aEuclidResult getClusterResult( tmpNumberOfComponents = tmpDataMatrix[0].length; } Utils.MinMaxValue[] tmpMinMaxComponents = this.preprocessedData.getMinMaxComponentsOfDataMatrix(); - + // Set tmpRhoStar float tmpRhoStar = tmpNumberOfComponents * (ONE - aVigilance); // Definitions - float tmpThresholdForContrastEnhancement = + float tmpThresholdForContrastEnhancement = Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, this.preprocessedData.getOffsetForContrastEnhancement() ); // Scaling factor alpha float tmpScalingFactor = tmpThresholdForContrastEnhancement; - - // Initialize cluster matrix and that for previous epoch (old) with + + // Initialize cluster matrix and that for previous epoch (old) with // all row vectors being null float[][] tmpClusterMatrix = new float[this.maximumNumberOfClusters][]; float[][] tmpClusterMatrixOld = new float[this.maximumNumberOfClusters][]; - // Cluster usage flags. True: Cluster is used, false: Cluster is + // Cluster usage flags. True: Cluster is used, false: Cluster is // empty and can be removed. boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; // Buffer for Rho values for parallelized Rho winner evaluation @@ -507,7 +507,7 @@ public Art2aEuclidResult getClusterResult( tmpRhoValueBuffer = new float[this.maximumNumberOfClusters]; } - // Initialize cluster indices for data row vectors with -1 to + // Initialize cluster indices for data row vectors with -1 to // indicate missing cluster assignment int[] tmpClusterIndexOfDataVector = new int[tmpNumberOfDataVectors]; Utils.fillVector(tmpClusterIndexOfDataVector, -1); @@ -520,7 +520,7 @@ public Art2aEuclidResult getClusterResult( // Initialize buffer vector for vector operations float[] tmpBufferVector = new float[tmpNumberOfComponents]; - + // Main clustering loop int tmpCurrentNumberOfEpochs = 0; int tmpNumberOfDetectedClusters = 0; @@ -529,14 +529,14 @@ public Art2aEuclidResult getClusterResult( boolean tmpIsConverged = false; while(!tmpIsConverged && tmpCurrentNumberOfEpochs < this.maximumNumberOfEpochs) { tmpCurrentNumberOfEpochs++; - + // Get random sequence of indices for data row vectors Utils.shuffleIndices(tmpRandomIndices, tmpRandomNumberGenerator); Arrays.fill(tmpClusterUsageFlags, false); for(int i = 0; i < tmpNumberOfDataVectors; i++) { int tmpRandomIndex = tmpRandomIndices[i]; - + if (tmpDataVectorZeroLengthFlags[tmpRandomIndex]) { // Shifted data row vector has length of zero: Ignore! continue; @@ -545,7 +545,7 @@ public Art2aEuclidResult getClusterResult( if (this.preprocessedData.hasPreprocessedData()) { Utils.copyVector(tmpContrastEnhancedMatrix[tmpRandomIndex], tmpBufferVector); } else { - tmpDataVectorZeroLengthFlags[tmpRandomIndex] = + tmpDataVectorZeroLengthFlags[tmpRandomIndex] = Art2aEuclidUtils.setContrastEnhancedVector( tmpDataMatrix[tmpRandomIndex], tmpBufferVector, @@ -610,11 +610,11 @@ public Art2aEuclidResult getClusterResult( } } } - + Utils.removeEmptyClusters( - tmpClusterUsageFlags, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, tmpClusterRemovalInfo ); if (tmpClusterRemovalInfo.isClusterRemoved()) { @@ -623,9 +623,9 @@ public Art2aEuclidResult getClusterResult( } else { tmpIsConverged = Art2aEuclidKernel.isConverged( - tmpNumberOfDetectedClusters, - tmpCurrentNumberOfEpochs, - tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpCurrentNumberOfEpochs, + tmpClusterMatrix, tmpClusterMatrixOld, this.maximumNumberOfEpochs, this.convergenceThreshold @@ -647,14 +647,14 @@ public Art2aEuclidResult getClusterResult( ); // Remove possible empty clusters Utils.removeEmptyClusters( - tmpClusterUsageFlags, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, tmpClusterRemovalInfo ); tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); } - // Check if clusters were removed in last epoch and assure non-empty + // Check if clusters were removed in last epoch and assure non-empty // clusters in the cluster matrix while (tmpClusterRemovalInfo.isClusterRemoved()) { // Empty clusters are removed: Assign data vectors again @@ -669,9 +669,9 @@ public Art2aEuclidResult getClusterResult( tmpClusterUsageFlags ); Utils.removeEmptyClusters( - tmpClusterUsageFlags, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, tmpClusterRemovalInfo ); tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); @@ -680,7 +680,7 @@ public Art2aEuclidResult getClusterResult( aVigilance, tmpThresholdForContrastEnhancement, tmpCurrentNumberOfEpochs, - tmpNumberOfDetectedClusters, + tmpNumberOfDetectedClusters, tmpClusterIndexOfDataVector, tmpClusterMatrix, tmpDataVectorZeroLengthFlags, @@ -690,12 +690,12 @@ public Art2aEuclidResult getClusterResult( ); } catch (Exception anException) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getClusterResult: An exception occurred: This should never happen!" ); Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - anException.toString(), + Level.SEVERE, + anException.toString(), anException ); throw new Exception("Art2aEuclidKernel.getClusterResult: An exception occurred: This should never happen!"); @@ -721,7 +721,7 @@ public Art2aEuclidResult[] getClusterResults( // if (aVigilances == null || aVigilances.length == 0) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getClusterResults: aVigilances is null or has length 0." ); throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: aVigilances is null or has length 0."); @@ -729,7 +729,7 @@ public Art2aEuclidResult[] getClusterResults( for (float tmpVigilance : aVigilances) { if(tmpVigilance <= 0.0f || tmpVigilance >= 1.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getClusterResults: Vigilance parameter must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidKernel.getClusterResults: Vigilance parameter must be in interval (0,1)."); @@ -791,14 +791,14 @@ public Art2aEuclidResult[] getClusterResults( } } } - + /** - * Nearest (smaller) indices of approximants to the desired number of + * Nearest (smaller) indices of approximants to the desired number of * representatives. - * - * @param aNumberOfRepresentatives Number of representatives (MUST be + * + * @param aNumberOfRepresentatives Number of representatives (MUST be * greater or equal to 2) - * @param aVigilanceMin Minimal vigilance parameter (must be in interval + * @param aVigilanceMin Minimal vigilance parameter (must be in interval * (0,1), a good default value is 0.0001f) * @param aVigilanceMax Maximal vigilance parameter (must be in interval * (0,1), a good default value is 0.9999f) @@ -821,35 +821,35 @@ public int[] getRepresentatives( // if(aNumberOfRepresentatives < 2) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2." ); throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2."); } if(aVigilanceMin <= 0.0f || aVigilanceMin >= 1.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)."); } if(aVigilanceMax <= 0.0f || aVigilanceMax >= 1.0f) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)."); } if(aVigilanceMin >= aVigilanceMax) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax." ); throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax."); } if(aNumberOfTrialSteps < 1) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1." ); throw new IllegalArgumentException("Art2aEuclidKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1."); @@ -884,12 +884,12 @@ public int[] getRepresentatives( return tmpRepresentativeIndicesOfClusters; } catch (Exception anException) { Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidKernel.getRepresentatives: An exception occurred: This should never happen!" ); Art2aEuclidKernel.LOGGER.log( - Level.SEVERE, - anException.toString(), + Level.SEVERE, + anException.toString(), anException ); throw anException; @@ -905,12 +905,12 @@ public int[] getRepresentatives( * Note: There a no checks! Check aDataMatrix in advance with method * Utils.isDataMatrixValid(). *
- * Note: aDataMatrix could be set to null after this operation to release + * Note: aDataMatrix could be set to null after this operation to release * its memory. - * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Utils.isDataMatrixValid() in advance) - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) @@ -920,24 +920,24 @@ public static PreprocessedArt2aEuclidData getPreprocessedArt2aEuclidData( float anOffsetForContrastEnhancement ) { int tmpNumberOfComponents = aDataMatrix[0].length; - float tmpThresholdForContrastEnhancement = + float tmpThresholdForContrastEnhancement = Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, anOffsetForContrastEnhancement ); - - // Initialize flags array for scaled data row vectors which have a + + // Initialize flags array for scaled data row vectors which have a // length of zero (i.e. where all components are equal to zero) boolean[] tmpDataVectorZeroLengthFlags = new boolean[aDataMatrix.length]; Utils.fillVector(tmpDataVectorZeroLengthFlags, false); float[][] tmpContrastEnhancedMatrix = new float[aDataMatrix.length][]; - + Utils.MinMaxValue[] tmpMinMaxComponents = Utils.getMinMaxComponents(aDataMatrix); for(int i = 0; i < aDataMatrix.length; i++) { float[] tmpContrastEnhancedVector = new float[tmpNumberOfComponents]; - tmpDataVectorZeroLengthFlags[i] = + tmpDataVectorZeroLengthFlags[i] = Art2aEuclidUtils.setContrastEnhancedVector( aDataMatrix[i], tmpContrastEnhancedVector, @@ -957,13 +957,13 @@ public static PreprocessedArt2aEuclidData getPreprocessedArt2aEuclidData( /** * Creates PreprocessedData object with preprocessed ART-2a-Euclid data for maximum * speed of the clustering process. The PreprocessedData object allocates - * about twice the memory of aDataMatrix. A default value of 1.0 is used + * about twice the memory of aDataMatrix. A default value of 1.0 is used * for the offset for contrast enhancement. *
- * Note: aDataMatrix could be set to null after this operation to release + * Note: aDataMatrix could be set to null after this operation to release * its memory. - * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Utils.isDataMatrixValid() in advance) * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java index 9d09071..134e9f3 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidResult.java @@ -1,8 +1,8 @@ /* - * ART-2a-Euclid Clustering for Java + * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,10 +34,10 @@ /** * Result of an ART-2a-Euclid clustering process. *

- * Note: Art2aEuclidResult is a read-only class, i.e. thread-safe. In addition, - * there are NO internal calculated values cached, i.e. each method call - * performs a full calculation procedure. An Art2aEuclidResult object may be - * distributed to several concurrent (parallelized) evaluation tasks without + * Note: Art2aEuclidResult is a read-only class, i.e. thread-safe. In addition, + * there are NO internal calculated values cached, i.e. each method call + * performs a full calculation procedure. An Art2aEuclidResult object may be + * distributed to several concurrent (parallelized) evaluation tasks without * any mutual interference problems. * * @author Betuel Sevindik, Achim Zielesny @@ -76,8 +76,8 @@ public class Art2aEuclidResult { */ private final float[][] clusterMatrix; /** - * Array with flags. True: Scaled data vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this + * Array with flags. True: Scaled data vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this * case), false: Otherwise */ private final boolean[] dataVectorZeroLengthFlags; @@ -99,18 +99,18 @@ public class Art2aEuclidResult { * Indexed value */ private record IndexedValue ( - int index, + int index, float value ) implements Comparable { - + /** * Constructor - * + * * @param index Index * @param value Value */ public IndexedValue {} - + @Override public int compareTo(IndexedValue anotherIndexedValue) { return Float.compare(value, anotherIndexedValue.value()); @@ -122,20 +122,20 @@ public int compareTo(IndexedValue anotherIndexedValue) { /** * Constructor. * Note: No checks are performed. - * + * * @param aVigilance Vigilance parameter in interval (0,1) - * @param aThresholdForContrastEnhancement Threshold for contrast + * @param aThresholdForContrastEnhancement Threshold for contrast * enhancement * @param aNumberOfEpochs Number of epochs used for clustering * @param aNumberOfDetectedClusters Number of detected clusters * @param aClusterIndexOfDataVector Cluster index of data vector * @param aClusterMatrix Cluster matrix - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this * case), false: Otherwise. - * @param anIsClusterOverflow True: Cluster overflow occurred, false: + * @param anIsClusterOverflow True: Cluster overflow occurred, false: * Otherwise * @param anIsConverged True: Clustering process converged, false: Otherwise * @param aPreprocessedArt2aEuclidData PreprocessedData instance @@ -144,7 +144,7 @@ public Art2aEuclidResult( float aVigilance, float aThresholdForContrastEnhancement, int aNumberOfEpochs, - int aNumberOfDetectedClusters, + int aNumberOfDetectedClusters, int[] aClusterIndexOfDataVector, float[][] aClusterMatrix, boolean[] aDataVectorZeroLengthFlags, @@ -167,9 +167,9 @@ public Art2aEuclidResult( // /** - * Returns specified cluster vector with index aClusterIndex in + * Returns specified cluster vector with index aClusterIndex in * clusterMatrix. - * + * * @param aClusterIndex Index of cluster vector in clusterMatrix * @return Specified cluster vector * @throws IllegalArgumentException Thrown if argument is illegal. @@ -180,7 +180,7 @@ public float[] getClusterVector( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getClusterVector: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aEuclidResult.getClusterVector: aClusterIndex is illegal."); @@ -188,12 +188,12 @@ public float[] getClusterVector( // return this.clusterMatrix[aClusterIndex]; } - + /** - * Returns specified cluster vector with index aClusterIndex in + * Returns specified cluster vector with index aClusterIndex in * cluster matrix with components being scaled to interval [0,1]. * Note: Cluster matrix is NOT changed. - * + * * @param aClusterIndex Index of cluster vector in cluster matrix * @return Specified scaled cluster vector * @throws IllegalArgumentException Thrown if argument is illegal. @@ -204,7 +204,7 @@ public float[] getScaledClusterVector( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getClusterVector: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aEuclidResult.getClusterVector: aClusterIndex is illegal."); @@ -212,12 +212,12 @@ public float[] getScaledClusterVector( // return Utils.getScaledVector(this.clusterMatrix[aClusterIndex]); } - + /** * Returns indices of data vectors in original data matrix that belong to * the specified cluster with index aClusterIndex. * Note: The returned indices are cached for successive fast usage. - * + * * @param aClusterIndex Index of cluster in cluster matrix * @return Indices of data vectors in original data matrix that belong to * the specified cluster with index aClusterIndex. @@ -229,13 +229,13 @@ public int[] getDataVectorIndicesOfCluster( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal." ); throw new IllegalArgumentException("rt2aClusteringResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal."); } // - + LinkedList tmpIndexListOfCluster = new LinkedList<>(); for (int i = 0; i < this.clusterIndexOfDataVector.length; i++) { if (this.clusterIndexOfDataVector[i] == aClusterIndex) { @@ -246,11 +246,11 @@ public int[] getDataVectorIndicesOfCluster( } /** - * Returns all indices of (scaled) data vectors that have a length of + * Returns all indices of (scaled) data vectors that have a length of * zero. The indices refer to the original data matrix. * Note: The returned indices are cached for successive fast usage. - * - * @return All indices of (scaled) data vectors that have a length of + * + * @return All indices of (scaled) data vectors that have a length of * zero. The indices refer to the original data matrix. */ public int[] getZeroLengthDataVectorIndices() { @@ -262,61 +262,61 @@ public int[] getZeroLengthDataVectorIndices() { } return tmpIndexList.stream().mapToInt(Integer::intValue).toArray(); } - + /** * Return distance between specified clusters with aClusterIndex1 and * aClusterIndex2. - * + * * @param aClusterIndex1 Index of cluster 1 in cluster matrix * @param aClusterIndex2 Index of cluster 2 in cluster matrix - * @return Distance between specified clusters with aClusterIndex1 and + * @return Distance between specified clusters with aClusterIndex1 and * aClusterIndex2. * @throws IllegalArgumentException Thrown if an argument is illegal. */ public float getDistanceBetweenClusters( - int aClusterIndex1, + int aClusterIndex1, int aClusterIndex2 ) throws IllegalArgumentException { // if(aClusterIndex1 < 0 || aClusterIndex1 >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex1 is illegal." ); throw new IllegalArgumentException("Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex1 is illegal."); } if(aClusterIndex2 < 0 || aClusterIndex2 >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex2 is illegal." ); throw new IllegalArgumentException("Art2aEuclidResult.getDistanceBetweenClusters: aClusterIndex2 is illegal."); } // - + if (aClusterIndex1 == aClusterIndex2) { return 0.0f; } else { - return + return (float) Math.sqrt( Utils.getSquaredDistance( - this.clusterMatrix[aClusterIndex1], + this.clusterMatrix[aClusterIndex1], this.clusterMatrix[aClusterIndex2] ) ); } } - + /** - * Returns size of the specified cluster with index aClusterIndex, i.e. the - * number of data vectors of original data matrix that belong to the + * Returns size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the * cluster. * Note: The internally evaluated indices of data vectors that belong to the * specified cluster are cached for successive fast usage. - * + * * @param aClusterIndex Index of cluster in cluster matrix - * @return Size of the specified cluster with index aClusterIndex, i.e. the - * number of data vectors of original data matrix that belong to the + * @return Size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the * cluster. * @throws IllegalArgumentException Thrown if argument is illegal. */ @@ -326,7 +326,7 @@ public int getClusterSize( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getClusterSize: aClusterIndex is illegal." ); throw new IllegalArgumentException("rt2aClusteringResult.getClusterSize: aClusterIndex is illegal."); @@ -344,7 +344,7 @@ public int getClusterSize( /** * Returns if cluster overflow occurred. - * + * * @return True: Cluster overflow occurred, false: Otherwise */ public boolean isClusterOverflow() { @@ -353,17 +353,17 @@ public boolean isClusterOverflow() { /** * Returns if clustering process converged. - * + * * @return True: Clustering process converged, false: Otherwise */ public boolean isConverged() { return this.isConverged; } - + /** * Calculates index of representative data vector which is closest to the * specified cluster vector with index aClusterIndex. - * + * * @param aClusterIndex Index of cluster vector in cluster matrix * @return Index of representative data vector which is closest to the * specified cluster vector with index aClusterIndex @@ -375,7 +375,7 @@ public int getClusterRepresentativeIndex( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getClusterRepresentativeIndex: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aEuclidResult.getClusterRepresentativeIndex: aClusterIndex is illegal."); @@ -412,17 +412,17 @@ public int getClusterRepresentativeIndex( } } return tmpBestIndex; - } + } /** * Calculates array of indices of sorted representative data vectors of the - * specified cluster with index aClusterIndex. The data vector with index 0 - * is closest to the cluster vector, the one with index 1 is the second + * specified cluster with index aClusterIndex. The data vector with index 0 + * is closest to the cluster vector, the one with index 1 is the second * closest etc. - * + * * @param aClusterIndex Index of cluster vector in cluster matrix * @return Array of indices of sorted representative data vectors of the - * specified cluster + * specified cluster * @throws IllegalArgumentException Thrown if argument is illegal */ public int[] getClusterRepresentativeIndices( @@ -431,7 +431,7 @@ public int[] getClusterRepresentativeIndices( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aEuclidResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidResult.getClusterRepresentativeIndices: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aEuclidResult.getClusterRepresentativeIndices: aClusterIndex is illegal."); @@ -473,7 +473,7 @@ public int[] getClusterRepresentativeIndices( /** * Returns data vector indices which are closest to their cluster vectors. - * + * * @return Data vector indices which are closest to their cluster vectors */ public int[] getRepresentativeIndicesOfClusters() { @@ -483,10 +483,10 @@ public int[] getRepresentativeIndicesOfClusters() { } return tmpRepresentativeIndicesOfClusters; } - + /** * Vigilance parameter - * + * * @return Vigilance parameter */ public float getVigilance() { @@ -495,16 +495,16 @@ public float getVigilance() { /** * Number of epochs - * + * * @return Number of epochs */ public int getNumberOfEpochs() { return this.numberOfEpochs; } - + /** * Number of detected clusters - * + * * @return Number of detected clusters */ public int getNumberOfDetectedClusters() { diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java index 36aca58..b27928a 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTask.java @@ -1,8 +1,8 @@ /* - * ART-2a-Euclid Clustering for Java + * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -31,8 +31,8 @@ import java.util.logging.Logger; /** - * Callable that wraps an Art2aEuclidKernel instance where the call() method - * returns an Art2aEuclidResult object. See Art2aEuclidKernel for further + * Callable that wraps an Art2aEuclidKernel instance where the call() method + * returns an Art2aEuclidResult object. See Art2aEuclidKernel for further * details. * * @author Betuel Sevindik, Achim Zielesny @@ -62,27 +62,27 @@ public class Art2aEuclidTask implements Callable { * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) * @param aVigilance Vigilance parameter (must be in interval (0,1)) - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * similarity (must be in interval (0,1)) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aEuclidTask( - float[][] aDataMatrix, + float[][] aDataMatrix, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + float aConvergenceThreshold, float aLearningParameter, float anOffsetForContrastEnhancement, long aRandomSeed, @@ -91,7 +91,7 @@ public Art2aEuclidTask( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); @@ -100,12 +100,12 @@ public Art2aEuclidTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aEuclidKernel( - aDataMatrix, + aDataMatrix, aMaximumNumberOfClusters, - aMaximumNumberOfEpochs, - aConvergenceThreshold, + aMaximumNumberOfEpochs, + aConvergenceThreshold, aLearningParameter, anOffsetForContrastEnhancement, aRandomSeed, @@ -113,12 +113,12 @@ public Art2aEuclidTask( ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." ); Art2aEuclidTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; @@ -127,8 +127,8 @@ public Art2aEuclidTask( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), - * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 0.5) and RANDOM_SEED (= 1). * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) @@ -148,7 +148,7 @@ public Art2aEuclidTask( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); @@ -157,7 +157,7 @@ public Art2aEuclidTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aEuclidKernel( aDataMatrix, aMaximumNumberOfClusters, @@ -165,32 +165,32 @@ public Art2aEuclidTask( ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." ); Art2aEuclidTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; } } - + /** * Constructor. * * @param aPreprocessedArt2aEuclidData PreprocessedData data object created by method * Art2aEuclidKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * similarity (must be in interval (0,1)) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ @@ -199,14 +199,14 @@ public Art2aEuclidTask( float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + float aConvergenceThreshold, float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); @@ -215,23 +215,23 @@ public Art2aEuclidTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aEuclidKernel( aPreprocessedArt2aEuclidData, aMaximumNumberOfClusters, - aMaximumNumberOfEpochs, - aConvergenceThreshold, + aMaximumNumberOfEpochs, + aConvergenceThreshold, aLearningParameter, aRandomSeed ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." ); Art2aEuclidTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; @@ -240,7 +240,7 @@ public Art2aEuclidTask( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.1), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aEuclidData PreprocessedData object created by method @@ -258,7 +258,7 @@ public Art2aEuclidTask( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aEuclidTask.Constructor: aVigilance must be in interval (0,1)."); @@ -267,19 +267,19 @@ public Art2aEuclidTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aEuclidKernel( aPreprocessedArt2aEuclidData, aMaximumNumberOfClusters ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.Constructor: Can not instantiate Art2aEuclidKernel object." ); Art2aEuclidTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; @@ -292,7 +292,7 @@ public Art2aEuclidTask( * Performs the clustering process. * Note: Parallel Rho winner evaluation is disabled. * - * @return Clustering result or null if clustering process could not be + * @return Clustering result or null if clustering process could not be * performed. */ @Override @@ -302,12 +302,12 @@ public Art2aEuclidResult call() { return this.art2aClusteringKernel.getClusterResult(this.vigilance, false); } catch (Exception anException) { Art2aEuclidTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aEuclidTask.call: Can not calculate a cluster result." ); Art2aEuclidTask.LOGGER.log( - Level.SEVERE, - anException.toString(), + Level.SEVERE, + anException.toString(), anException ); return null; diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java index a641dd1..756f6dd 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidUtils.java @@ -1,8 +1,8 @@ /* - * ART-2a-Euclid Clustering for Java + * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -30,7 +30,7 @@ * Library of static, thread-safe (stateless) utility methods for ART-2a-Euclid clustering. *

* Note: No checks are performed. - * + * * @author Achim Zielesny */ public class Art2aEuclidUtils { @@ -41,19 +41,19 @@ public class Art2aEuclidUtils { */ protected Art2aEuclidUtils() {} //
- + // /** - * Transforms original data vector into corresponding contrast enhanced + * Transforms original data vector into corresponding contrast enhanced * unit vector (see code). * Note: No checks are performed. - * + * * @param aDataVector Data vector (IS NOT CHANGED) - * @param aBufferVector Buffer vector for contrast enhanced unit vector - * derived from data vector (MUST ALREADY BE INSTANTIATED and is set within + * @param aBufferVector Buffer vector for contrast enhanced unit vector + * derived from data vector (MUST ALREADY BE INSTANTIATED and is set within * the method) * @param aMinMaxComponents Min-max components of original data matrix - * @param aThresholdForContrastEnhancement Threshold for contrast + * @param aThresholdForContrastEnhancement Threshold for contrast * enhancement * @return True: Scaled data vector has a length of zero, false: Otherwise */ @@ -79,5 +79,5 @@ protected static boolean setContrastEnhancedVector( } } // - + } diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java index dc0a19e..1c7ad28 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aKernel.java @@ -2,7 +2,7 @@ * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -33,86 +33,86 @@ import java.util.stream.IntStream; /** - * ART-2a algorithm implementation for unsupervised, open categorical + * ART-2a algorithm implementation for unsupervised, open categorical * clustering. *

- * Literature: G.A. Carpenter, S. Grossberg and D.B. Rosen, Neural Networks 4 - * (1991) 493-504; D. Wienke, Y. Xie, P. K. Hopke, Chemometrics and Intelligent + * Literature: G.A. Carpenter, S. Grossberg and D.B. Rosen, Neural Networks 4 + * (1991) 493-504; D. Wienke, Y. Xie, P. K. Hopke, Chemometrics and Intelligent * Laboratory Systems 24 (1994) 367-387 *

- * Use Art2aKernel for sequential clustering instances and Art2aTask for - * clustering instances to be executed concurrently (parallelized). See hints - * for ART-2a clustering with minimal additional memory allocation or maximum - * speed below. + * Use Art2aKernel for sequential clustering instances and Art2aTask for + * clustering instances to be executed concurrently (parallelized). See hints + * for ART-2a clustering with minimal additional memory allocation or maximum + * speed below. *

* Note: For clustering of the SAME data with DIFFERENT vigilance parameters use - * method getClusterResults() where the mode of calculation may be specified to + * method getClusterResults() where the mode of calculation may be specified to * be sequential or concurrent (parallelized). *

* All numerical calculations are performed in single (float) precision. *

- * Note, that aDataMatrix may contain data vectors with all components being - * equal to zero (or some constant minimal value). These data vectors are - * removed from the clustering process and their indices are returned by method + * Note, that aDataMatrix may contain data vectors with all components being + * equal to zero (or some constant minimal value). These data vectors are + * removed from the clustering process and their indices are returned by method * getZeroLengthDataVectorIndices() of an Art2aResult object. *

* ART-2a clustering with minimal memory allocation: - * If a data matrix with N data row vectors is used to construct a clustering - * instance without preprocessing (parameter isDataPreprocessing is set to - * false), minimal additional memory is allocated. The data matrix itself is not - * changed. The additional allocated memory can be controlled by the + * If a data matrix with N data row vectors is used to construct a clustering + * instance without preprocessing (parameter isDataPreprocessing is set to + * false), minimal additional memory is allocated. The data matrix itself is not + * changed. The additional allocated memory can be controlled by the * maximumNumberOfClusters parameter and estimated to be about - * (additional memory of ART-2a instance) = - * (2 x maximumNumberOfClusters / N) x (memory of data matrix), - * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the - * number of data row vectors will lead to roughly 2 MByte of additionally - * allocated memory. Note, that memory for cluster vectors is only allocated if - * needed, e.g. if specified parameter maximumNumberOfClusters allows 150 - * clusters but only 27 are needed, then only memory for these 27 cluster - * vectors is allocated. The minimal memory allocation comes at the expense of - * clustering speed since preprocessing steps have to be executed repeatedly. - * This also decreases the performance of some methods of the Art2aResult object + * (additional memory of ART-2a instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 2 MByte of additionally + * allocated memory. Note, that memory for cluster vectors is only allocated if + * needed, e.g. if specified parameter maximumNumberOfClusters allows 150 + * clusters but only 27 are needed, then only memory for these 27 cluster + * vectors is allocated. The minimal memory allocation comes at the expense of + * clustering speed since preprocessing steps have to be executed repeatedly. + * This also decreases the performance of some methods of the Art2aResult object * generated by the clustering process, e.g. getClusterRepresentatives(). *

* ART-2a clustering with maximum speed: - * If parameter isDataPreprocessing is set to true, preprocessing steps are - * calculated in advance for maximum clustering speed (as well as maximum speed - * of the Art2aResult methods). This requires an additional memory allocation + * If parameter isDataPreprocessing is set to true, preprocessing steps are + * calculated in advance for maximum clustering speed (as well as maximum speed + * of the Art2aResult methods). This requires an additional memory allocation * for the preprocessed data for an ART-2a clustering instance: - * (additional memory of ART-2a instance) = - * (1 + 2 x maximumNumberOfClusters / N) x (memory of data matrix), - * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the - * number of data row vectors will lead to roughly 12 MByte of additionally + * (additional memory of ART-2a instance) = + * (1 + 2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., a 10 MByte data matrix with a maximum number of clusters of 10% of the + * number of data row vectors will lead to roughly 12 MByte of additionally * allocated memory. *

- * CAUTION: Construction of several ART-2a clustering instances with the SAME - * data matrix PLUS preprocessing is NOT advised due to the significant memory - * consumption of each instance. In this case, the data matrix should be + * CAUTION: Construction of several ART-2a clustering instances with the SAME + * data matrix PLUS preprocessing is NOT advised due to the significant memory + * consumption of each instance. In this case, the data matrix should be * checked with static method Utils.isDataMatrixValid() (where possible NaN * values can be removed with Utils.isNonFiniteComponentRemoval()) and then a priori - * converted into a preprocessed Art2aData object with static method - * Art2aKernel.getArt2aData(). The generated Art2aData object does NOT change - * or refer to the data matrix so that the data matrix memory could be released - * after conversion (by setting the data matrix object to null). The generated - * Art2aData object has additionally allocated about the same memory as the - * original data matrix, e.g., a 10 MByte data matrix is converted into a - * roughly 10 MByte Art2aData object. But this single Art2aData object can now - * be used to construct several ART-2a clustering instances (Art2aKernel - * instance or Art2aTask instances for concurrent (parallelized) execution) - * where each of these ART-2a clustering instances (and their generated - * Art2aResult object methods) performs with maximum speed and allocates only - * the minimal additional memory of - * (additional memory of ART-2a instance) = - * (2 x maximumNumberOfClusters / N) x (memory of data matrix), - * e.g., for 9 constructed ART-2a clustering instances for concurrent execution - * only 18 MBytes of additional memory are allocated in total. Compare this - * total additional allocated memory of only 10 + 18 = 28 MByte for an - * Art2aData object plus 9 ART-2a clustering instances with the alternative - * 9 x 12 = 108 MByte of memory for 9 ART-2a clustering instances constructed - * with the same data matrix plus independent preprocessing in each instance! - * (Just for completeness: For a minimal memory realization of these 9 ART-2a - * clustering instances, each instance can be constructed with the same data - * matrix WITHOUT preprocessing, which would require only 18 MBytes of + * converted into a preprocessed Art2aData object with static method + * Art2aKernel.getArt2aData(). The generated Art2aData object does NOT change + * or refer to the data matrix so that the data matrix memory could be released + * after conversion (by setting the data matrix object to null). The generated + * Art2aData object has additionally allocated about the same memory as the + * original data matrix, e.g., a 10 MByte data matrix is converted into a + * roughly 10 MByte Art2aData object. But this single Art2aData object can now + * be used to construct several ART-2a clustering instances (Art2aKernel + * instance or Art2aTask instances for concurrent (parallelized) execution) + * where each of these ART-2a clustering instances (and their generated + * Art2aResult object methods) performs with maximum speed and allocates only + * the minimal additional memory of + * (additional memory of ART-2a instance) = + * (2 x maximumNumberOfClusters / N) x (memory of data matrix), + * e.g., for 9 constructed ART-2a clustering instances for concurrent execution + * only 18 MBytes of additional memory are allocated in total. Compare this + * total additional allocated memory of only 10 + 18 = 28 MByte for an + * Art2aData object plus 9 ART-2a clustering instances with the alternative + * 9 x 12 = 108 MByte of memory for 9 ART-2a clustering instances constructed + * with the same data matrix plus independent preprocessing in each instance! + * (Just for completeness: For a minimal memory realization of these 9 ART-2a + * clustering instances, each instance can be constructed with the same data + * matrix WITHOUT preprocessing, which would require only 18 MBytes of * additional allocated memory in total.) * * @author Betuel Sevindik, Achim Zielesny @@ -149,7 +149,7 @@ public class Art2aKernel { */ private static final float DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT = 1.0f; /** - * Default value of the convergence threshold for cluster centroid + * Default value of the convergence threshold for cluster centroid * similarity */ private static final float DEFAULT_CONVERGENCE_THRESHOLD = 0.99f; @@ -186,16 +186,16 @@ public class Art2aKernel { * Constructor. * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * similarity (must be in interval (0,1)) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. @@ -203,10 +203,10 @@ public class Art2aKernel { * */ public Art2aKernel( - float[][] aDataMatrix, + float[][] aDataMatrix, int aMaximumNumberOfClusters, - int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, float aLearningParameter, float anOffsetForContrastEnhancement, long aRandomSeed, @@ -215,14 +215,14 @@ public Art2aKernel( // if(!Utils.isDataMatrixValid(aDataMatrix)) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aDataMatrix is not valid." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aDataMatrix is not valid."); } if(aMaximumNumberOfClusters < 2) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aMaximumNumberOfClusters must be greater 1." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfClusters must be greater 1."); @@ -239,28 +239,28 @@ public Art2aKernel( } if(aConvergenceThreshold <= 0.0f || aConvergenceThreshold > 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]."); } if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)."); } if(anOffsetForContrastEnhancement <= 0.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: anOffsetForContrastEnhancement must be greater zero." ); throw new IllegalArgumentException("Art2aKernel.Constructor: anOffsetForContrastEnhancement must be greater zero."); } if(aRandomSeed <= 0L) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aRandomSeed must be greater 0." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aRandomSeed must be greater/equal 0."); @@ -276,7 +276,7 @@ public Art2aKernel( } else { this.preprocessedData = new PreprocessedData( - aDataMatrix, + aDataMatrix, Utils.getMinMaxComponents(aDataMatrix), anOffsetForContrastEnhancement ); @@ -291,8 +291,8 @@ public Art2aKernel( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), - * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 1.0) and RANDOM_SEED (= 1). * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) @@ -310,43 +310,43 @@ public Art2aKernel( this( aDataMatrix, aMaximumNumberOfClusters, - DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, - DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT, DEFAULT_RANDOM_SEED, anIsDataPreprocessing ); } - + /** * Constructor. * * @param aPreprocessedArt2aData PreprocessedData object created by static * method Art2aKernel.getPreprocessedArt2aData() - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * similarity (must be in interval (0,1)) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aKernel( PreprocessedArt2aData aPreprocessedArt2aData, int aMaximumNumberOfClusters, - int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + int aMaximumNumberOfEpochs, + float aConvergenceThreshold, float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { // if(aPreprocessedArt2aData == null) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aPreprocessedArt2aData is null." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aPreprocessedArt2aData is null."); @@ -363,28 +363,28 @@ public Art2aKernel( } if(aMaximumNumberOfEpochs <= 0) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aMaximumNumberOfEpochs must be greater zero."); } if(aConvergenceThreshold <= 0.0f || aConvergenceThreshold > 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aConvergenceThreshold must be in interval (0,1]."); } if(aLearningParameter <= 0.0f || aLearningParameter >= 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aLearningParameter must be in interval (0,1)."); } if(aRandomSeed <= 0L) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.Constructor: aRandomSeed must be greater 0." ); throw new IllegalArgumentException("Art2aKernel.Constructor: aRandomSeed must be greater/equal 0."); @@ -401,7 +401,7 @@ public Art2aKernel( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), + * MAXIMUM_NUMBER_OF_EPOCHS (= 10), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aData PreprocessedData object created by static @@ -417,8 +417,8 @@ public Art2aKernel( this( aPreprocessedArt2aData, aMaximumNumberOfClusters, - DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, - DEFAULT_CONVERGENCE_THRESHOLD, + DEFAULT_MAXIMUM_NUMBER_OF_EPOCHS, + DEFAULT_CONVERGENCE_THRESHOLD, DEFAULT_LEARNING_PARAMETER, DEFAULT_RANDOM_SEED ); @@ -446,7 +446,7 @@ public Art2aResult getClusterResult( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getClusterResult: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aKernel.getClusterResult: aVigilance must be in interval (0,1)."); @@ -456,11 +456,11 @@ public Art2aResult getClusterResult( try { Random tmpRandomNumberGenerator = new Random(this.randomSeed); boolean tmpIsClusterOverflow = false; - + float[][] tmpDataMatrix = null; float[][] tmpContrastEnhancedUnitMatrix = null; - // Flags array that indicates if data row vectors have a length - // of zero (i.e. where all components are equal to zero). True: + // Flags array that indicates if data row vectors have a length + // of zero (i.e. where all components are equal to zero). True: // Data row vector has a length of zero, false: Otherwise. boolean[] tmpDataVectorZeroLengthFlags = null; int tmpNumberOfComponents = -1; @@ -480,19 +480,19 @@ public Art2aResult getClusterResult( Utils.MinMaxValue[] tmpMinMaxComponents = this.preprocessedData.getMinMaxComponentsOfDataMatrix(); // Definitions - float tmpThresholdForContrastEnhancement = + float tmpThresholdForContrastEnhancement = Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, this.preprocessedData.getOffsetForContrastEnhancement() ); // Scaling factor alpha float tmpScalingFactor = tmpThresholdForContrastEnhancement; - - // Initialize cluster matrix and that for previous epoch (old) with + + // Initialize cluster matrix and that for previous epoch (old) with // all row vectors being null float[][] tmpClusterMatrix = new float[this.maximumNumberOfClusters][]; float[][] tmpClusterMatrixOld = new float[this.maximumNumberOfClusters][]; - // Cluster usage flags. True: Cluster is used, false: Cluster is + // Cluster usage flags. True: Cluster is used, false: Cluster is // empty and can be removed. boolean[] tmpClusterUsageFlags = new boolean[this.maximumNumberOfClusters]; // Buffer for Rho values for parallelized Rho winner evaluation @@ -501,7 +501,7 @@ public Art2aResult getClusterResult( tmpRhoValueBuffer = new float[this.maximumNumberOfClusters]; } - // Initialize cluster indices for data row vectors with -1 to + // Initialize cluster indices for data row vectors with -1 to // indicate missing cluster assignment int[] tmpClusterIndexOfDataVector = new int[tmpNumberOfDataVectors]; Utils.fillVector(tmpClusterIndexOfDataVector, -1); @@ -514,7 +514,7 @@ public Art2aResult getClusterResult( // Initialize buffer vector for vector operations float[] tmpBufferVector = new float[tmpNumberOfComponents]; - + // Main clustering loop int tmpCurrentNumberOfEpochs = 0; int tmpNumberOfDetectedClusters = 0; @@ -531,7 +531,7 @@ public Art2aResult getClusterResult( Arrays.fill(tmpClusterUsageFlags, false); for(int i = 0; i < tmpNumberOfDataVectors; i++) { int tmpRandomIndex = tmpRandomIndices[i]; - + if (tmpDataVectorZeroLengthFlags[tmpRandomIndex]) { // Shifted data row vector has length of zero: Ignore! continue; @@ -606,9 +606,9 @@ public Art2aResult getClusterResult( } } Utils.removeEmptyClusters( - tmpClusterUsageFlags, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, tmpClusterRemovalInfo ); if (tmpClusterRemovalInfo.isClusterRemoved()) { @@ -617,9 +617,9 @@ public Art2aResult getClusterResult( } else { tmpIsConverged = Art2aKernel.isConverged( - tmpNumberOfDetectedClusters, - tmpCurrentNumberOfEpochs, - tmpClusterMatrix, + tmpNumberOfDetectedClusters, + tmpCurrentNumberOfEpochs, + tmpClusterMatrix, tmpClusterMatrixOld, this.maximumNumberOfEpochs, this.convergenceThreshold @@ -641,14 +641,14 @@ public Art2aResult getClusterResult( ); // Remove possible empty clusters Utils.removeEmptyClusters( - tmpClusterUsageFlags, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, tmpClusterRemovalInfo ); tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); } - // Check if clusters were removed in last epoch and assure non-empty + // Check if clusters were removed in last epoch and assure non-empty // clusters in the cluster matrix while (tmpClusterRemovalInfo.isClusterRemoved()) { // Empty clusters are removed: Assign data vectors again @@ -663,9 +663,9 @@ public Art2aResult getClusterResult( tmpClusterUsageFlags ); Utils.removeEmptyClusters( - tmpClusterUsageFlags, - tmpClusterMatrix, - tmpNumberOfDetectedClusters, + tmpClusterUsageFlags, + tmpClusterMatrix, + tmpNumberOfDetectedClusters, tmpClusterRemovalInfo ); tmpNumberOfDetectedClusters = tmpClusterRemovalInfo.getNumberOfDetectedClusters(); @@ -674,7 +674,7 @@ public Art2aResult getClusterResult( aVigilance, tmpThresholdForContrastEnhancement, tmpCurrentNumberOfEpochs, - tmpNumberOfDetectedClusters, + tmpNumberOfDetectedClusters, tmpClusterIndexOfDataVector, tmpClusterMatrix, tmpDataVectorZeroLengthFlags, @@ -684,12 +684,12 @@ public Art2aResult getClusterResult( ); } catch (Exception anException) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getClusterResult: An exception occurred: This should never happen!" ); Art2aKernel.LOGGER.log( - Level.SEVERE, - anException.toString(), + Level.SEVERE, + anException.toString(), anException ); throw new Exception("Art2aKernel.getClusterResult: An exception occurred: This should never happen!"); @@ -788,14 +788,14 @@ public Art2aResult[] getClusterResults( /** * Nearest (smaller) indices of approximates to the desired number of * representatives. - * - * @param aNumberOfRepresentatives Number of representatives (MUST be + * + * @param aNumberOfRepresentatives Number of representatives (MUST be * greater or equal to 2) - * @param aVigilanceMin Minimal vigilance parameter (must be in interval + * @param aVigilanceMin Minimal vigilance parameter (must be in interval * (0,1), a good default value is 0.0001f) - * @param aVigilanceMax Maximal vigilance parameter (must be in interval + * @param aVigilanceMax Maximal vigilance parameter (must be in interval * (0,1), a good default value is 0.9999f) - * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or + * @param aNumberOfTrialSteps Number of trial steps (MUST be greater or * equal to 1, a good default value is 32) * @param anIsParallelRhoWinnerCalculation True: Rho winner calculation * is parallelized, false: Rho winner calculation is sequential. @@ -814,35 +814,35 @@ public int[] getRepresentatives( // if(aNumberOfRepresentatives < 2) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2." ); throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aNumberOfRepresentatives must be greater/equal 2."); } if(aVigilanceMin <= 0.0f || aVigilanceMin >= 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aVigilanceMin must be in interval (0,1)."); } if(aVigilanceMax <= 0.0f || aVigilanceMax >= 1.0f) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aVigilanceMax must be in interval (0,1)."); } if(aVigilanceMin >= aVigilanceMax) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax." ); throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aVigilanceMin must be smaller than aVigilanceMax."); } if(aNumberOfTrialSteps < 1) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1." ); throw new IllegalArgumentException("Art2aKernel.getRepresentatives: aNumberOfTrialSteps must be greater/equal 1."); @@ -877,12 +877,12 @@ public int[] getRepresentatives( return tmpRepresentativeIndicesOfClusters; } catch (Exception anException) { Art2aKernel.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aKernel.getRepresentatives: An exception occurred: This should never happen!" ); Art2aKernel.LOGGER.log( - Level.SEVERE, - anException.toString(), + Level.SEVERE, + anException.toString(), anException ); throw anException; @@ -898,12 +898,12 @@ public int[] getRepresentatives( * Note: There a no checks! Check aDataMatrix in advance with method * Utils.isDataMatrixValid(). *
- * Note: aDataMatrix could be set to null after this operation to release + * Note: aDataMatrix could be set to null after this operation to release * its memory. - * - * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Utils.isDataMatrixValid() in advance) - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) @@ -913,19 +913,19 @@ public static PreprocessedArt2aData getPreprocessedArt2aData( float anOffsetForContrastEnhancement ) { int tmpNumberOfComponents = aDataMatrix[0].length; - float tmpThresholdForContrastEnhancement = + float tmpThresholdForContrastEnhancement = Utils.getThresholdForContrastEnhancement( tmpNumberOfComponents, anOffsetForContrastEnhancement ); - - // Initialize flags array for scaled data row vectors which have a + + // Initialize flags array for scaled data row vectors which have a // length of zero (i.e. where all components are equal to zero) boolean[] tmpDataVectorZeroLengthFlags = new boolean[aDataMatrix.length]; Utils.fillVector(tmpDataVectorZeroLengthFlags, false); float[][] tmpContrastEnhancedUnitMatrix = new float[aDataMatrix.length][]; - + Utils.MinMaxValue[] tmpMinMaxComponents = Utils.getMinMaxComponents(aDataMatrix); for(int i = 0; i < aDataMatrix.length; i++) { @@ -950,13 +950,13 @@ public static PreprocessedArt2aData getPreprocessedArt2aData( /** * Creates PreprocessedData object with preprocessed ART-2a data for maximum speed * of the clustering process. The PreprocessedData object allocates about twice - * the memory of aDataMatrix. A default value of 1.0 is used for the offset + * the memory of aDataMatrix. A default value of 1.0 is used for the offset * for contrast enhancement. *
- * Note: aDataMatrix could be set to null after this operation to release + * Note: aDataMatrix could be set to null after this operation to release * its memory. - * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check + * @param aDataMatrix Data matrix (IS NOT CHANGED and MUST BE VALID: Check * with Utils.isDataMatrixValid() in advance) * @return PreprocessedData object for maximum clustering speed but with * additionally allocated memory (about the same memory as aDataMatrix) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java index a827838..421eb33 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aResult.java @@ -2,7 +2,7 @@ * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -37,8 +37,8 @@ *

* Note: Art2aResult is a read-only class, i.e. thread-safe. In addition, there * are NO internally calculated values cached, i.e. each method call performs - * a full calculation procedure. An Art2aResult object may be distributed to - * several concurrent (parallelized) evaluation tasks without any mutual + * a full calculation procedure. An Art2aResult object may be distributed to + * several concurrent (parallelized) evaluation tasks without any mutual * interference problems. * * @author Betuel Sevindik, Achim Zielesny @@ -83,8 +83,8 @@ public class Art2aResult { */ private final float[][] clusterMatrix; /** - * Array with flags. True: Scaled data vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this + * Array with flags. True: Scaled data vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this * case), false: Otherwise */ private final boolean[] dataVectorZeroLengthFlags; @@ -107,18 +107,18 @@ public class Art2aResult { * Indexed value */ private record IndexedValue ( - int index, + int index, float value ) implements Comparable { - + /** * Constructor - * + * * @param index Index * @param value Value */ public IndexedValue {} - + @Override public int compareTo(IndexedValue anotherIndexedValue) { return Float.compare(value, anotherIndexedValue.value()); @@ -130,20 +130,20 @@ public int compareTo(IndexedValue anotherIndexedValue) { /** * Constructor. * Note: No checks are performed. - * + * * @param aVigilance Vigilance parameter in interval (0,1) - * @param aThresholdForContrastEnhancement Threshold for contrast + * @param aThresholdForContrastEnhancement Threshold for contrast * enhancement * @param aNumberOfEpochs Number of epochs used for clustering * @param aNumberOfDetectedClusters Number of detected clusters * @param aClusterIndexOfDataVector Cluster index of data vector * @param aClusterMatrix Cluster matrix - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this * case), false: Otherwise. - * @param anIsClusterOverflow True: Cluster overflow occurred, false: + * @param anIsClusterOverflow True: Cluster overflow occurred, false: * Otherwise * @param anIsConverged True: Clustering process converged, false: Otherwise * @param aPreprocessedArt2aData PreprocessedData instance @@ -152,7 +152,7 @@ public Art2aResult( float aVigilance, float aThresholdForContrastEnhancement, int aNumberOfEpochs, - int aNumberOfDetectedClusters, + int aNumberOfDetectedClusters, int[] aClusterIndexOfDataVector, float[][] aClusterMatrix, boolean[] aDataVectorZeroLengthFlags, @@ -175,9 +175,9 @@ public Art2aResult( // /** - * Returns specified cluster vector with index aClusterIndex in + * Returns specified cluster vector with index aClusterIndex in * clusterMatrix. - * + * * @param aClusterIndex Index of cluster vector in clusterMatrix * @return Specified cluster vector * @throws IllegalArgumentException Thrown if argument is illegal. @@ -188,7 +188,7 @@ public float[] getClusterVector( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getClusterVector: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aResult.getClusterVector: aClusterIndex is illegal."); @@ -196,12 +196,12 @@ public float[] getClusterVector( // return this.clusterMatrix[aClusterIndex]; } - + /** - * Returns specified cluster vector with index aClusterIndex in + * Returns specified cluster vector with index aClusterIndex in * cluster matrix with components being scaled to interval [0,1]. * Note: Cluster matrix is NOT changed. - * + * * @param aClusterIndex Index of cluster vector in cluster matrix * @return Specified scaled cluster vector * @throws IllegalArgumentException Thrown if argument is illegal. @@ -212,7 +212,7 @@ public float[] getScaledClusterVector( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getClusterVector: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aResult.getClusterVector: aClusterIndex is illegal."); @@ -220,12 +220,12 @@ public float[] getScaledClusterVector( // return Utils.getScaledVector(this.clusterMatrix[aClusterIndex]); } - + /** * Returns indices of data vectors in original data matrix that belong to * the specified cluster with index aClusterIndex. * Note: The returned indices are cached for successive fast usage. - * + * * @param aClusterIndex Index of cluster in cluster matrix * @return Indices of data vectors in original data matrix that belong to * the specified cluster with index aClusterIndex. @@ -237,13 +237,13 @@ public int[] getDataVectorIndicesOfCluster( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal." ); throw new IllegalArgumentException("rt2aClusteringResult.getDataVectorIndicesOfCluster: aClusterIndex is illegal."); } // - + LinkedList tmpIndexListOfCluster = new LinkedList<>(); for (int i = 0; i < this.clusterIndexOfDataVector.length; i++) { if (this.clusterIndexOfDataVector[i] == aClusterIndex) { @@ -254,11 +254,11 @@ public int[] getDataVectorIndicesOfCluster( } /** - * Returns all indices of (scaled) data vectors that have a length of + * Returns all indices of (scaled) data vectors that have a length of * zero. The indices refer to the original data matrix. * Note: The returned indices are cached for successive fast usage. - * - * @return All indices of (scaled) data vectors that have a length of + * + * @return All indices of (scaled) data vectors that have a length of * zero. The indices refer to the original data matrix. */ public int[] getZeroLengthDataVectorIndices() { @@ -270,32 +270,32 @@ public int[] getZeroLengthDataVectorIndices() { } return tmpIndexList.stream().mapToInt(Integer::intValue).toArray(); } - + /** * Return angle in degree between specified clusters with aClusterIndex1 and * aClusterIndex2. - * + * * @param aClusterIndex1 Index of cluster 1 in cluster matrix * @param aClusterIndex2 Index of cluster 2 in cluster matrix - * @return Angle in degree between specified clusters with aClusterIndex1 + * @return Angle in degree between specified clusters with aClusterIndex1 * and aClusterIndex2. * @throws IllegalArgumentException Thrown if an argument is illegal. */ public float getAngleBetweenClusters( - int aClusterIndex1, + int aClusterIndex1, int aClusterIndex2 ) throws IllegalArgumentException { // if(aClusterIndex1 < 0 || aClusterIndex1 >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getAngleBetweenClusters: aClusterIndex1 is illegal." ); throw new IllegalArgumentException("Art2aResult.getAngleBetweenClusters: aClusterIndex1 is illegal."); } if(aClusterIndex2 < 0 || aClusterIndex2 >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getAngleBetweenClusters: aClusterIndex2 is illegal." ); throw new IllegalArgumentException("Art2aResult.getAngleBetweenClusters: aClusterIndex2 is illegal."); @@ -304,26 +304,26 @@ public float getAngleBetweenClusters( if (aClusterIndex1 == aClusterIndex2) { return 0.0f; } else { - return + return (float) Math.acos( Utils.getScalarProduct( - this.clusterMatrix[aClusterIndex1], + this.clusterMatrix[aClusterIndex1], this.clusterMatrix[aClusterIndex2] ) ) * CONVERSION_TO_DEGREE; } } - + /** - * Returns size of the specified cluster with index aClusterIndex, i.e. the - * number of data vectors of original data matrix that belong to the + * Returns size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the * cluster. * Note: The internally evaluated indices of data vectors that belong to the * specified cluster are cached for successive fast usage. - * + * * @param aClusterIndex Index of cluster in cluster matrix - * @return Size of the specified cluster with index aClusterIndex, i.e. the - * number of data vectors of original data matrix that belong to the + * @return Size of the specified cluster with index aClusterIndex, i.e. the + * number of data vectors of original data matrix that belong to the * cluster. * @throws IllegalArgumentException Thrown if argument is illegal. */ @@ -333,7 +333,7 @@ public int getClusterSize( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getClusterSize: aClusterIndex is illegal." ); throw new IllegalArgumentException("rt2aClusteringResult.getClusterSize: aClusterIndex is illegal."); @@ -351,7 +351,7 @@ public int getClusterSize( /** * Returns if cluster overflow occurred. - * + * * @return True: Cluster overflow occurred, false: Otherwise */ public boolean isClusterOverflow() { @@ -360,17 +360,17 @@ public boolean isClusterOverflow() { /** * Returns if clustering process converged. - * + * * @return True: Clustering process converged, false: Otherwise */ public boolean isConverged() { return this.isConverged; } - + /** * Calculates index of representative data vector which is closest to the * specified cluster vector with index aClusterIndex. - * + * * @param aClusterIndex Index of cluster vector in cluster matrix * @return Index of representative data vector which is closest to the * specified cluster vector with index aClusterIndex @@ -382,7 +382,7 @@ public int getClusterRepresentativeIndex( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getClusterRepresentativeIndex: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aResult.getClusterRepresentativeIndex: aClusterIndex is illegal."); @@ -419,17 +419,17 @@ public int getClusterRepresentativeIndex( } } return tmpBestIndex; - } + } /** * Calculates array of indices of sorted representative data vectors of the - * specified cluster with index aClusterIndex. The data vector with index 0 - * is closest to the cluster vector, the one with index 1 is the second + * specified cluster with index aClusterIndex. The data vector with index 0 + * is closest to the cluster vector, the one with index 1 is the second * closest etc. - * + * * @param aClusterIndex Index of cluster vector in cluster matrix * @return Array of indices of sorted representative data vectors of the - * specified cluster + * specified cluster * @throws IllegalArgumentException Thrown if argument is illegal */ public int[] getClusterRepresentativeIndices( @@ -438,7 +438,7 @@ public int[] getClusterRepresentativeIndices( // if(aClusterIndex < 0 || aClusterIndex >= this.numberOfDetectedClusters) { Art2aResult.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aResult.getClusterRepresentativeIndices: aClusterIndex is illegal." ); throw new IllegalArgumentException("Art2aResult.getClusterRepresentativeIndices: aClusterIndex is illegal."); @@ -480,7 +480,7 @@ public int[] getClusterRepresentativeIndices( /** * Returns data vector indices which are closest to their cluster vectors. - * + * * @return Data vector indices which are closest to their cluster vectors */ public int[] getRepresentativeIndicesOfClusters() { @@ -490,10 +490,10 @@ public int[] getRepresentativeIndicesOfClusters() { } return tmpRepresentativeIndicesOfClusters; } - + /** * Vigilance parameter - * + * * @return Vigilance parameter */ public float getVigilance() { @@ -502,16 +502,16 @@ public float getVigilance() { /** * Number of epochs - * + * * @return Number of epochs */ public int getNumberOfEpochs() { return this.numberOfEpochs; } - + /** * Number of detected clusters - * + * * @return Number of detected clusters */ public int getNumberOfDetectedClusters() { diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java index 9805557..5ce8e23 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aTask.java @@ -2,7 +2,7 @@ * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -31,7 +31,7 @@ import java.util.logging.Logger; /** - * Callable that wraps an Art2aKernel instance where the call() method returns + * Callable that wraps an Art2aKernel instance where the call() method returns * an Art2aResult object. See Art2aKernel for further details. * * @author Betuel Sevindik, Achim Zielesny @@ -61,27 +61,27 @@ public class Art2aTask implements Callable { * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) * @param aVigilance Vigilance parameter (must be in interval (0,1)) - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * similarity (must be in interval (0,1)) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @param anIsDataPreprocessing True: Data preprocessing is performed, false: * Otherwise. * @throws IllegalArgumentException Thrown if an argument is illegal */ public Art2aTask( - float[][] aDataMatrix, + float[][] aDataMatrix, float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + float aConvergenceThreshold, float aLearningParameter, float anOffsetForContrastEnhancement, long aRandomSeed, @@ -90,7 +90,7 @@ public Art2aTask( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); @@ -99,12 +99,12 @@ public Art2aTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aKernel( - aDataMatrix, + aDataMatrix, aMaximumNumberOfClusters, - aMaximumNumberOfEpochs, - aConvergenceThreshold, + aMaximumNumberOfEpochs, + aConvergenceThreshold, aLearningParameter, anOffsetForContrastEnhancement, aRandomSeed, @@ -112,12 +112,12 @@ public Art2aTask( ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: Can not instantiate Art2aKernel object." ); Art2aTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; @@ -126,8 +126,8 @@ public Art2aTask( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), - * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * LEARNING_PARAMETER (= 0.01), DEFAULT_OFFSET_FOR_CONTRAST_ENHANCEMENT * (= 1.0) and RANDOM_SEED (= 1). * * @param aDataMatrix Data matrix with data row vectors (IS NOT CHANGED) @@ -147,7 +147,7 @@ public Art2aTask( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); @@ -156,7 +156,7 @@ public Art2aTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aKernel( aDataMatrix, aMaximumNumberOfClusters, @@ -164,32 +164,32 @@ public Art2aTask( ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: Can not instantiate Art2aKernel object." ); Art2aTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; } } - + /** * Constructor. * * @param aPreprocessedArt2aData PreprocessedData object created by method * Art2aKernel.getPreprocessedData() * @param aVigilance Vigilance parameter (must be in interval (0,1)) - * @param aMaximumNumberOfClusters Maximum number of clusters (must be in + * @param aMaximumNumberOfClusters Maximum number of clusters (must be in * interval [2, number of data row vectors of aDataMatrix]) - * @param aMaximumNumberOfEpochs Maximum number of epochs for training + * @param aMaximumNumberOfEpochs Maximum number of epochs for training * (must be greater zero) - * @param aConvergenceThreshold Convergence threshold for cluster centroid + * @param aConvergenceThreshold Convergence threshold for cluster centroid * similarity (must be in interval (0,1)) * @param aLearningParameter Learning parameter (must be in interval (0,1)) - * @param aRandomSeed Random seed value for random number generator + * @param aRandomSeed Random seed value for random number generator * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ @@ -198,14 +198,14 @@ public Art2aTask( float aVigilance, int aMaximumNumberOfClusters, int aMaximumNumberOfEpochs, - float aConvergenceThreshold, + float aConvergenceThreshold, float aLearningParameter, long aRandomSeed ) throws IllegalArgumentException { // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); @@ -214,23 +214,23 @@ public Art2aTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aKernel( aPreprocessedArt2aData, aMaximumNumberOfClusters, - aMaximumNumberOfEpochs, - aConvergenceThreshold, + aMaximumNumberOfEpochs, + aConvergenceThreshold, aLearningParameter, aRandomSeed ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: Can not instantiate Art2aKernel object." ); Art2aTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; @@ -239,7 +239,7 @@ public Art2aTask( /** * Constructor with default values for - * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), + * MAXIMUM_NUMBER_OF_EPOCHS (= 100), CONVERGENCE_THRESHOLD (= 0.99), * LEARNING_PARAMETER (= 0.01) and RANDOM_SEED (= 1). * * @param aPreprocessedArt2aData PreprocessedData object created by method @@ -257,7 +257,7 @@ public Art2aTask( // if(aVigilance <= 0.0f || aVigilance >= 1.0f) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: aVigilance must be in interval (0,1)." ); throw new IllegalArgumentException("Art2aTask.Constructor: aVigilance must be in interval (0,1)."); @@ -266,19 +266,19 @@ public Art2aTask( this.vigilance = aVigilance; try { - this.art2aClusteringKernel = + this.art2aClusteringKernel = new Art2aKernel( aPreprocessedArt2aData, aMaximumNumberOfClusters ); } catch (IllegalArgumentException anIllegalArgumentException) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.Constructor: Can not instantiate Art2aKernel object." ); Art2aTask.LOGGER.log( - Level.SEVERE, - anIllegalArgumentException.toString(), + Level.SEVERE, + anIllegalArgumentException.toString(), anIllegalArgumentException ); throw anIllegalArgumentException; @@ -291,7 +291,7 @@ public Art2aTask( * Performs the clustering process. * Note: Parallel Rho winner evaluation is disabled. * - * @return Clustering result or null if clustering process could not be + * @return Clustering result or null if clustering process could not be * performed. */ @Override @@ -301,12 +301,12 @@ public Art2aResult call() { return this.art2aClusteringKernel.getClusterResult(this.vigilance, false); } catch (Exception anException) { Art2aTask.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "Art2aTask.call: Can not calculate a cluster result." ); Art2aTask.LOGGER.log( - Level.SEVERE, - anException.toString(), + Level.SEVERE, + anException.toString(), anException ); return null; diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java index 7aec143..e14cdda 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Art2aUtils.java @@ -2,7 +2,7 @@ * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -30,7 +30,7 @@ * Library of static, thread-safe (stateless) utility methods for ART-2a clustering. *

* Note: No checks are performed. - * + * * @author Achim Zielesny */ public class Art2aUtils { diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java index 4016241..4dc6b9c 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/PreprocessedData.java @@ -2,7 +2,7 @@ * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -45,7 +45,7 @@ * Note: PreprocessedData is a read-only class, i.e. thread-safe. The same PreprocessedData * object may be distributed to several concurrently working clustering tasks without * any mutual interference problems. - * + * * @author Achim Zielesny */ public class PreprocessedData { @@ -66,14 +66,14 @@ public class PreprocessedData { */ private final float[][] preprocessedMatrix; /** - * Flags array that indicates if scaled data row vectors have a length - * of zero (i.e. where all components are equal to zero, the corresponding + * Flags array that indicates if scaled data row vectors have a length + * of zero (i.e. where all components are equal to zero, the corresponding * preprocessed vector is set to null in this case). True: * Scaled data row vector has a length of zero, false: Otherwise. */ private final boolean[] dataVectorZeroLengthFlags; /** - * Min-max components of original data matrix (see method + * Min-max components of original data matrix (see method * Utils.getMinMaxComponents() for data structure) */ private final Utils.MinMaxValue[] minMaxComponentsOfDataMatrix; @@ -86,37 +86,37 @@ public class PreprocessedData { */ private final boolean hasPreprocessedData; //
- - + + // /** * Private constructor * Note: No checks are necessary - * + * * @param aDataMatrix Original data matrix with data row vectors (MAY BE NULL) * @param aPreprocessedMatrix Preprocessed matrix (MAY BE NULL) - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero - * (corresponding contrast enhanced unit vector is set to null in this + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero + * (corresponding contrast enhanced unit vector is set to null in this * case), false: Otherwise. - * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data * matrix - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @param aHasPreprocessedData True: PreprocessedData object has preprocessed data, * false: Otherwise * @throws IllegalArgumentException Thrown if an argument is illegal */ private PreprocessedData ( - float[][] aDataMatrix, + float[][] aDataMatrix, float[][] aPreprocessedMatrix, boolean[] aDataVectorZeroLengthFlags, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement, boolean aHasPreprocessedData ) { - this.dataMatrix = aDataMatrix; + this.dataMatrix = aDataMatrix; this.preprocessedMatrix = aPreprocessedMatrix; this.dataVectorZeroLengthFlags = aDataVectorZeroLengthFlags; this.minMaxComponentsOfDataMatrix = aMinMaxComponentsOfDataMatrix; @@ -127,17 +127,17 @@ private PreprocessedData ( // /** * Constructor - * - * @param aDataMatrix Original data matrix with data row vectors (NOT + * + * @param aDataMatrix Original data matrix with data row vectors (NOT * allowed to be null) - * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data * matrix - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ protected PreprocessedData ( - float[][] aDataMatrix, + float[][] aDataMatrix, Utils.MinMaxValue[] aMinMaxComponentsOfDataMatrix, float anOffsetForContrastEnhancement ) { @@ -151,21 +151,21 @@ protected PreprocessedData ( ); if (!Utils.isMatrixValid(aDataMatrix)) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: aDataMatrix is invalid." ); throw new IllegalArgumentException("PreprocessedData.Constructor: aDataMatrix is invalid"); } if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aDataMatrix[0].length) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." ); throw new IllegalArgumentException("PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); } if (anOffsetForContrastEnhancement <= 0.0f) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero." ); throw new IllegalArgumentException("PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero."); @@ -174,16 +174,16 @@ protected PreprocessedData ( /** * Constructor - * + * * @param aPreprocessedMatrix Preprocessed matrix (NOT allowed to be null) - * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled - * data row vectors have a length of zero (i.e. where all components are - * equal to zero). True: Scaled data row vector has a length of zero + * @param aDataVectorZeroLengthFlags Flags array that indicates if scaled + * data row vectors have a length of zero (i.e. where all components are + * equal to zero). True: Scaled data row vector has a length of zero * (corresponding preprocessed vector is set to null in this * case), false: Otherwise. - * @param aMinMaxComponentsOfDataMatrix Min-max components of original data + * @param aMinMaxComponentsOfDataMatrix Min-max components of original data * matrix - * @param anOffsetForContrastEnhancement Offset for contrast enhancement + * @param anOffsetForContrastEnhancement Offset for contrast enhancement * (must be greater zero) * @throws IllegalArgumentException Thrown if an argument is illegal */ @@ -203,28 +203,28 @@ protected PreprocessedData ( ); if (!Utils.isMatrixValid(aPreprocessedMatrix)) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: aPreprocessedMatrix is invalid." ); throw new IllegalArgumentException("PreprocessedData.Constructor: aPreprocessedMatrix is invalid."); } if (aDataVectorZeroLengthFlags == null || aDataVectorZeroLengthFlags.length == 0 || aDataVectorZeroLengthFlags.length != aPreprocessedMatrix.length) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: aDataVectorZeroLengthFlags is illegal." ); throw new IllegalArgumentException("PreprocessedData.Constructor: aDataVectorZeroLengthFlags is illegal."); } if (aMinMaxComponentsOfDataMatrix == null || aMinMaxComponentsOfDataMatrix.length != aPreprocessedMatrix[0].length) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid." ); throw new IllegalArgumentException("PreprocessedData.Constructor: aMinMaxComponentsOfDataMatrix is invalid"); } if (anOffsetForContrastEnhancement <= 0.0f) { PreprocessedData.LOGGER.log( - Level.SEVERE, + Level.SEVERE, "PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero." ); throw new IllegalArgumentException("PreprocessedData.Constructor: anOffsetForContrastEnhancement must be greater zero."); @@ -235,8 +235,8 @@ protected PreprocessedData ( // /** * Original data matrix with data row vectors - * - * @return Original data matrix with data row vectors or null if + * + * @return Original data matrix with data row vectors or null if * hasPreprocessedData() returns true */ protected float[][] getDataMatrix() { @@ -245,8 +245,8 @@ protected float[][] getDataMatrix() { /** * Matrix of contrast enhanced unit vectors - * - * @return Matrix of contrast enhanced unit vectors or null if + * + * @return Matrix of contrast enhanced unit vectors or null if * hasPreprocessedData() returns false */ protected float[][] getPreprocessedMatrix() { @@ -254,11 +254,11 @@ protected float[][] getPreprocessedMatrix() { } /** - * Flags array that indicates if scaled data row vectors have a length - * of zero (i.e. where all components are equal to zero, the corresponding - * contrast enhanced unit vector is set to null in this case). True: + * Flags array that indicates if scaled data row vectors have a length + * of zero (i.e. where all components are equal to zero, the corresponding + * contrast enhanced unit vector is set to null in this case). True: * Scaled data row vector has a length of zero, false: Otherwise. - * + * * @return Array with flags or null if hasPreprocessedData() returns false */ protected boolean[] getDataVectorZeroLengthFlags() { @@ -267,17 +267,17 @@ protected boolean[] getDataVectorZeroLengthFlags() { /** * Min-max components of original data matrix (see method Utils.getMinMaxComponents() for data structure) - * + * * @return Min-max components of original data matrix */ protected Utils.MinMaxValue[] getMinMaxComponentsOfDataMatrix() { return this.minMaxComponentsOfDataMatrix; } - + /** - * Returns if Art2aData object has preprocessed data, i.e. + * Returns if Art2aData object has preprocessed data, i.e. * contrastEnhancedUnitMatrix and dataVectorZeroLengthFlags are defined. - * + * * @return True: Art2aData object has preprocessed data, false: Otherwise */ protected boolean hasPreprocessedData() { @@ -286,7 +286,7 @@ protected boolean hasPreprocessedData() { /** * Returns offset for contrast enhancement - * + * * @return Offset for contrast enhancement */ protected float getOffsetForContrastEnhancement() { @@ -294,4 +294,4 @@ protected float getOffsetForContrastEnhancement() { } // -} \ No newline at end of file +} diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java index 55e5b50..6c06b01 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aEuclidTest.java @@ -1,8 +1,8 @@ /* - * ART-2a-Euclid Clustering for Java + * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -26,6 +26,9 @@ package de.unijena.cheminf.clustering.art2a; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -34,9 +37,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - /** * Test class for ART-2a-Euclid clustering. * @@ -53,7 +53,7 @@ public void test_Development_IrisFlowerData() { System.out.println("test_Development_IrisFlowerData()"); System.out.println("---------------------------------"); float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); - + // float[] tmpVigilances = new float[] {0.01f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 0.99f}; float[] tmpVigilances = new float[] {0.1f}; boolean tmpIsClusterAnalysis = true; @@ -68,9 +68,9 @@ public void test_Development_IrisFlowerData() { for (float tmpVigilance : tmpVigilances) { System.out.println(" Vigilance parameter = " + String.valueOf(tmpVigilance)); - Art2aEuclidKernel tmpArt2aEuclidKernel = + Art2aEuclidKernel tmpArt2aEuclidKernel = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -118,11 +118,11 @@ public void test_Development_CombinedGaussianCouldData() { int tmpNumberOfGaussianCloudVectors = 100; float tmpStandardDeviation = 0.1f; Random tmpRandomNumberGenerator = new Random(1L); - float[][] tmpCombinedGaussianCloudDataMatrix = + float[][] tmpCombinedGaussianCloudDataMatrix = this.getCombinedGaussianCloudMatrix( - tmpNumberOfDimensions, - tmpNumberOfGaussianCloudVectors, - tmpStandardDeviation, + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, tmpRandomNumberGenerator ); @@ -137,9 +137,9 @@ public void test_Development_CombinedGaussianCouldData() { long tmpRandomSeed = 1L; long tmpStart = System.currentTimeMillis(); - Art2aEuclidKernel tmpArt2aEuclidKernel = + Art2aEuclidKernel tmpArt2aEuclidKernel = new Art2aEuclidKernel( - tmpCombinedGaussianCloudDataMatrix, + tmpCombinedGaussianCloudDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -256,10 +256,10 @@ public void test_Development_GetRepresentatives() { float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; int tmpNumberOfTrialSteps = 32; - - Art2aEuclidKernel tmpArt2aEuclidKernel = + + Art2aEuclidKernel tmpArt2aEuclidKernel = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -279,11 +279,11 @@ public void test_Development_GetRepresentatives() { ); for (int tmpNumberOfRepresentatives = 2; tmpNumberOfRepresentatives < tmpIrisFlowerDataMatrix.length; tmpNumberOfRepresentatives++) { try { - int[] tmpRepresentatives = + int[] tmpRepresentatives = tmpArt2aEuclidKernel.getRepresentatives( - tmpNumberOfRepresentatives, - tmpVigilanceMin, - tmpVigilanceMax, + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, tmpNumberOfTrialSteps, tmpIsParallelRhoWinnerCalculation ); @@ -291,10 +291,10 @@ public void test_Development_GetRepresentatives() { Arrays.sort(tmpRepresentatives); float tmpMeanDistance = Utils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); System.out.println( - String.valueOf(tmpNumberOfRepresentatives) + + String.valueOf(tmpNumberOfRepresentatives) + " Representatives (Mean distance = " + - String.valueOf(tmpMeanDistance) + - ") = " + + String.valueOf(tmpMeanDistance) + + ") = " + this.getStringFromIntArray(tmpRepresentatives) ); } @@ -326,10 +326,10 @@ public void test_GetRepresentatives() { float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; int tmpNumberOfTrialSteps = 32; - - Art2aEuclidKernel tmpArt2aEuclidKernel = + + Art2aEuclidKernel tmpArt2aEuclidKernel = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -339,33 +339,33 @@ public void test_GetRepresentatives() { tmpIsDataPreprocessing ); try { - int[] tmpRepresentatives = + int[] tmpRepresentatives = tmpArt2aEuclidKernel.getRepresentatives( - tmpNumberOfRepresentatives, - tmpVigilanceMin, - tmpVigilanceMax, + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, tmpNumberOfTrialSteps, tmpIsParallelRhoWinnerCalculation ); System.out.println( - String.valueOf(tmpNumberOfRepresentatives) + - " wanted Representatives, " + - String.valueOf(tmpRepresentatives.length) + - " generated = " + + String.valueOf(tmpNumberOfRepresentatives) + + " wanted Representatives, " + + String.valueOf(tmpRepresentatives.length) + + " generated = " + this.getStringFromIntArray(tmpRepresentatives) ); for (int i = 0; i < tmpRepresentatives.length; i++) { for (int j = i + 1; j < tmpRepresentatives.length; j++) { System.out.println( - "Distance between representatives " + - String.valueOf(i) + - " and representative " + - String.valueOf(j) + - "= " + + "Distance between representatives " + + String.valueOf(i) + + " and representative " + + String.valueOf(j) + + "= " + String.valueOf( Math.sqrt( Utils.getSquaredDistance( - tmpIrisFlowerDataMatrix[tmpRepresentatives[i]], + tmpIrisFlowerDataMatrix[tmpRepresentatives[i]], tmpIrisFlowerDataMatrix[tmpRepresentatives[j]] ) ) @@ -391,11 +391,11 @@ public void test_PerfectClustering() { int tmpNumberOfGaussianCloudVectors = 1000; float tmpStandardDeviation = 0.01f; Random tmpRandomNumberGenerator = new Random(1L); - float[][] tmpCombinedGaussianCloudDataMatrix = + float[][] tmpCombinedGaussianCloudDataMatrix = this.getCombinedGaussianCloudMatrix( - tmpNumberOfDimensions, - tmpNumberOfGaussianCloudVectors, - tmpStandardDeviation, + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, tmpRandomNumberGenerator ); @@ -409,9 +409,9 @@ public void test_PerfectClustering() { float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; - Art2aEuclidKernel tmpArt2aEuclidKernel = + Art2aEuclidKernel tmpArt2aEuclidKernel = new Art2aEuclidKernel( - tmpCombinedGaussianCloudDataMatrix, + tmpCombinedGaussianCloudDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -443,9 +443,9 @@ public void test_PerfectClustering() { Assertions.assertEquals(tmpArt2aEuclidResult.getClusterRepresentativeIndex(i), tmpArt2aEuclidResult.getClusterRepresentativeIndices(i)[0]); } } - + /** - * Tests that clustering with and without preprocessing has identical + * Tests that clustering with and without preprocessing has identical * results. */ @Test @@ -468,7 +468,7 @@ public void test_Preprocessing() { boolean tmpIsParallelRhoWinnerCalculation = false; Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -486,9 +486,9 @@ public void test_Preprocessing() { // Preprocessing tmpIsDataPreprocessing = true; - Art2aEuclidKernel tmpArt2aEuclidKernelWithPreprocessing = + Art2aEuclidKernel tmpArt2aEuclidKernelWithPreprocessing = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -503,15 +503,15 @@ public void test_Preprocessing() { } catch (Exception anException) { Assertions.fail(); } - + // Assert that results without and with preprocessing are identical Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aEuclidResultWithPreprocessing.getNumberOfDetectedClusters()); Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aEuclidResultWithPreprocessing.getNumberOfEpochs()); - + int tmpNumberOfDetectedClusters = tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { Assertions.assertArrayEquals( - tmpArt2aEuclidResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aEuclidResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), tmpArt2aEuclidResultWithPreprocessing.getDataVectorIndicesOfCluster(i) ); } @@ -545,7 +545,7 @@ public void test_Art2aEuclidData() { boolean tmpIsParallelRhoWinnerCalculation = false; Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -563,7 +563,7 @@ public void test_Art2aEuclidData() { // Preprocessed Art2aEuclidData PreprocessedArt2aEuclidData tmpPreprocessedArt2aEuclidData = Art2aEuclidKernel.getPreprocessedArt2aEuclidData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); - Art2aEuclidKernel tmpArt2aEuclidKernelWithArt2aEuclidData = + Art2aEuclidKernel tmpArt2aEuclidKernelWithArt2aEuclidData = new Art2aEuclidKernel( tmpPreprocessedArt2aEuclidData, tmpMaximumNumberOfClusters, @@ -579,15 +579,15 @@ public void test_Art2aEuclidData() { Assertions.fail(); } - // Assert that results without preprocessing and preprocessed + // Assert that results without preprocessing and preprocessed // Art2aEuclidData are identical Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfDetectedClusters()); Assertions.assertEquals(tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aEuclidResultWithArt2aEuclidData.getNumberOfEpochs()); - + int tmpNumberOfDetectedClusters = tmpArt2aEuclidResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { Assertions.assertArrayEquals( - tmpArt2aEuclidResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aEuclidResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), tmpArt2aEuclidResultWithArt2aEuclidData.getDataVectorIndicesOfCluster(i) ); } @@ -616,7 +616,7 @@ public void test_ParallelClustering() { float tmpLearningParameter = 0.01f; float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; - + // Sequential clustering one after another Art2aEuclidResult[] tmpSequentialResults = new Art2aEuclidResult[tmpVigilances.length]; int tmpIndex = 0; @@ -625,7 +625,7 @@ public void test_ParallelClustering() { boolean tmpIsParallelRhoWinnerCalculation = false; Art2aEuclidKernel tmpArt2aEuclidKernelWithoutPreprocessing = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -647,7 +647,7 @@ public void test_ParallelClustering() { for (float tmpVigilance : tmpVigilances) { tmpArt2aEuclidTaskList.add(new Art2aEuclidTask( tmpPreprocessedArt2aEuclidData, - tmpVigilance, + tmpVigilance, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -673,8 +673,8 @@ public void test_ParallelClustering() { System.out.println("test_ParallelClustering: Exception occurred."); } } - - // Assert that sequential results without preprocessing and concurrent + + // Assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aEuclidData are identical for (int i = 0; i < tmpVigilances.length; i++) { Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); @@ -684,7 +684,7 @@ public void test_ParallelClustering() { int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { Assertions.assertArrayEquals( - tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), tmpParallelResults[i].getDataVectorIndicesOfCluster(j) ); } @@ -694,7 +694,7 @@ public void test_ParallelClustering() { Assertions.assertEquals(tmpSequentialResults[i].getDistanceBetweenClusters(j, k), tmpParallelResults[i].getDistanceBetweenClusters(j, k)); } } - } + } } /** @@ -716,9 +716,9 @@ public void test_ParallelClusteringWithGetGlusterResults() { long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; - Art2aEuclidKernel tmpArt2aEuclidKernel = + Art2aEuclidKernel tmpArt2aEuclidKernel = new Art2aEuclidKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -743,8 +743,8 @@ public void test_ParallelClusteringWithGetGlusterResults() { } catch (Exception anException) { Assertions.fail(); } - - // Assert that sequential results without preprocessing and concurrent + + // Assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aEuclidData are identical for (int i = 0; i < tmpVigilances.length; i++) { Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); @@ -754,7 +754,7 @@ public void test_ParallelClusteringWithGetGlusterResults() { int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { Assertions.assertArrayEquals( - tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), tmpParallelResults[i].getDataVectorIndicesOfCluster(j) ); } @@ -764,14 +764,14 @@ public void test_ParallelClusteringWithGetGlusterResults() { Assertions.assertEquals(tmpSequentialResults[i].getDistanceBetweenClusters(j, k), tmpParallelResults[i].getDistanceBetweenClusters(j, k)); } } - } + } } - + // /** * Returns int array as a string. * Note: No checks are performed. - * + * * @param anIntArray Int array * @return The int array as a string */ @@ -787,18 +787,18 @@ private String getStringFromIntArray( } return tmpStringBuilder.toString(); } - + /** * Compares two arrays. * Note: No checks are performed. - * + * * @param anArray1 Array 1 * @param anArray2 Array 2 - * @return True: Arrays have the same values in the same order, false: + * @return True: Arrays have the same values in the same order, false: * Otherwise */ private boolean compareArrays( - int[] anArray1, + int[] anArray1, int[] anArray2 ) { boolean isEqual = true; @@ -816,7 +816,7 @@ private boolean compareArrays( // /** * Returns Gaussian cloud matrix - * + * * @param aCentroidVector Centroid vector (IS NOT CHANGED) * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors * @param aStandardDeviation Standard deviation of Gaussian distribution @@ -842,7 +842,7 @@ private float[][] getGaussianCloudMatrix( /** * Returns combined Gaussian cloud matrix (see code) - * + * * @param aNumberOfDimensions Number of dimensions * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors * @param aStandardDeviation Standard deviation of Gaussian distribution @@ -861,11 +861,11 @@ private float[][] getCombinedGaussianCloudMatrix( float[] tmpCentroidVector = new float[aNumberOfDimensions]; Arrays.fill(tmpCentroidVector, 0.0f); tmpCentroidVector[i] = 1.0f; - float[][] tmpGaussianCloudMatrix = + float[][] tmpGaussianCloudMatrix = this.getGaussianCloudMatrix( - tmpCentroidVector, - aNumberOfGaussianCloudVectors, - aStandardDeviation, + tmpCentroidVector, + aNumberOfGaussianCloudVectors, + aStandardDeviation, aRandomNumberGenerator ); for (int j = 0; j < tmpGaussianCloudMatrix.length; j++) { @@ -877,19 +877,19 @@ private float[][] getCombinedGaussianCloudMatrix( // // /** - * Returns Iris flower data: Indices 0-49 = Iris setosa, indices 50-99 = + * Returns Iris flower data: Indices 0-49 = Iris setosa, indices 50-99 = * Iris versicolor, indices 100-149 = Iris virginica - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris flower data */ private float[][] getIrisFlowerDataMatrix() { float[][] tmpIrisSetosaData = this.getIrisSetosaDataMatrix(); float[][] tmpIrisVersicolorData = this.getIrisVersicolorDataMatrix(); float[][] tmpIrisVirginicaData = this.getIrisVirginicaDataMatrix(); - float[][] tmpIrisFlowerData = + float[][] tmpIrisFlowerData = new float[tmpIrisSetosaData.length + tmpIrisVersicolorData.length + tmpIrisVirginicaData.length][]; int tmpIndex = 0; for (int i = 0; i < tmpIrisSetosaData.length; i++) { @@ -903,87 +903,87 @@ private float[][] getIrisFlowerDataMatrix() { } return tmpIrisFlowerData; } - + /** * Returns Iris setosa data - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris setosa data */ private float[][] getIrisSetosaDataMatrix() { - return new + return new float[][] { - {49.0f, 30.0f, 14.0f, 2.0f}, {51.0f, 38.0f, 19.0f, 4.0f}, {52.0f, 41.0f, 15.0f, 1.0f}, {54.0f, 34.0f, 15.0f, 4.0f}, - {50.0f, 36.0f, 14.0f, 2.0f}, {57.0f, 44.0f, 15.0f, 4.0f}, {46.0f, 32.0f, 14.0f, 2.0f}, {50.0f, 34.0f, 16.0f, 4.0f}, - {51.0f, 35.0f, 14.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 2.0f}, {50.0f, 34.0f, 15.0f, 2.0f}, {58.0f, 40.0f, 12.0f, 2.0f}, - {43.0f, 30.0f, 11.0f, 1.0f}, {50.0f, 32.0f, 12.0f, 2.0f}, {50.0f, 30.0f, 16.0f, 2.0f}, {48.0f, 34.0f, 19.0f, 2.0f}, - {51.0f, 38.0f, 16.0f, 2.0f}, {48.0f, 30.0f, 14.0f, 3.0f}, {55.0f, 42.0f, 14.0f, 2.0f}, {44.0f, 30.0f, 13.0f, 2.0f}, - {54.0f, 39.0f, 17.0f, 4.0f}, {48.0f, 34.0f, 16.0f, 2.0f}, {51.0f, 35.0f, 14.0f, 3.0f}, {52.0f, 35.0f, 15.0f, 2.0f}, - {51.0f, 37.0f, 15.0f, 4.0f}, {54.0f, 34.0f, 17.0f, 2.0f}, {51.0f, 38.0f, 15.0f, 3.0f}, {57.0f, 38.0f, 17.0f, 3.0f}, - {45.0f, 23.0f, 13.0f, 3.0f}, {48.0f, 30.0f, 14.0f, 1.0f}, {53.0f, 37.0f, 15.0f, 2.0f}, {44.0f, 29.0f, 14.0f, 2.0f}, - {54.0f, 39.0f, 13.0f, 4.0f}, {54.0f, 37.0f, 15.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 1.0f}, {50.0f, 35.0f, 13.0f, 3.0f}, - {51.0f, 34.0f, 15.0f, 2.0f}, {46.0f, 31.0f, 15.0f, 2.0f}, {47.0f, 32.0f, 13.0f, 2.0f}, {47.0f, 32.0f, 16.0f, 2.0f}, - {50.0f, 33.0f, 14.0f, 2.0f}, {50.0f, 35.0f, 16.0f, 6.0f}, {55.0f, 35.0f, 13.0f, 2.0f}, {46.0f, 34.0f, 14.0f, 3.0f}, - {51.0f, 33.0f, 17.0f, 5.0f}, {52.0f, 34.0f, 14.0f, 2.0f}, {49.0f, 36.0f, 14.0f, 1.0f}, {48.0f, 31.0f, 16.0f, 2.0f}, + {49.0f, 30.0f, 14.0f, 2.0f}, {51.0f, 38.0f, 19.0f, 4.0f}, {52.0f, 41.0f, 15.0f, 1.0f}, {54.0f, 34.0f, 15.0f, 4.0f}, + {50.0f, 36.0f, 14.0f, 2.0f}, {57.0f, 44.0f, 15.0f, 4.0f}, {46.0f, 32.0f, 14.0f, 2.0f}, {50.0f, 34.0f, 16.0f, 4.0f}, + {51.0f, 35.0f, 14.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 2.0f}, {50.0f, 34.0f, 15.0f, 2.0f}, {58.0f, 40.0f, 12.0f, 2.0f}, + {43.0f, 30.0f, 11.0f, 1.0f}, {50.0f, 32.0f, 12.0f, 2.0f}, {50.0f, 30.0f, 16.0f, 2.0f}, {48.0f, 34.0f, 19.0f, 2.0f}, + {51.0f, 38.0f, 16.0f, 2.0f}, {48.0f, 30.0f, 14.0f, 3.0f}, {55.0f, 42.0f, 14.0f, 2.0f}, {44.0f, 30.0f, 13.0f, 2.0f}, + {54.0f, 39.0f, 17.0f, 4.0f}, {48.0f, 34.0f, 16.0f, 2.0f}, {51.0f, 35.0f, 14.0f, 3.0f}, {52.0f, 35.0f, 15.0f, 2.0f}, + {51.0f, 37.0f, 15.0f, 4.0f}, {54.0f, 34.0f, 17.0f, 2.0f}, {51.0f, 38.0f, 15.0f, 3.0f}, {57.0f, 38.0f, 17.0f, 3.0f}, + {45.0f, 23.0f, 13.0f, 3.0f}, {48.0f, 30.0f, 14.0f, 1.0f}, {53.0f, 37.0f, 15.0f, 2.0f}, {44.0f, 29.0f, 14.0f, 2.0f}, + {54.0f, 39.0f, 13.0f, 4.0f}, {54.0f, 37.0f, 15.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 1.0f}, {50.0f, 35.0f, 13.0f, 3.0f}, + {51.0f, 34.0f, 15.0f, 2.0f}, {46.0f, 31.0f, 15.0f, 2.0f}, {47.0f, 32.0f, 13.0f, 2.0f}, {47.0f, 32.0f, 16.0f, 2.0f}, + {50.0f, 33.0f, 14.0f, 2.0f}, {50.0f, 35.0f, 16.0f, 6.0f}, {55.0f, 35.0f, 13.0f, 2.0f}, {46.0f, 34.0f, 14.0f, 3.0f}, + {51.0f, 33.0f, 17.0f, 5.0f}, {52.0f, 34.0f, 14.0f, 2.0f}, {49.0f, 36.0f, 14.0f, 1.0f}, {48.0f, 31.0f, 16.0f, 2.0f}, {46.0f, 36.0f, 10.0f, 2.0f}, {44.0f, 32.0f, 13.0f, 2.0f} }; } /** * Returns Iris versicolor data - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris versicolor data */ private float[][] getIrisVersicolorDataMatrix() { - return new + return new float[][] { - {66.0f, 29.0f, 46.0f, 13.0f}, {61.0f, 29.0f, 47.0f, 14.0f}, {60.0f, 34.0f, 45.0f, 16.0f}, {52.0f, 27.0f, 39.0f, 14.0f}, - {49.0f, 24.0f, 33.0f, 10.0f}, {60.0f, 27.0f, 51.0f, 16.0f}, {56.0f, 27.0f, 42.0f, 13.0f}, {61.0f, 30.0f, 46.0f, 14.0f}, - {55.0f, 24.0f, 37.0f, 10.0f}, {57.0f, 30.0f, 42.0f, 12.0f}, {63.0f, 33.0f, 47.0f, 16.0f}, {69.0f, 31.0f, 49.0f, 15.0f}, - {57.0f, 28.0f, 45.0f, 13.0f}, {61.0f, 28.0f, 47.0f, 12.0f}, {64.0f, 29.0f, 43.0f, 13.0f}, {63.0f, 23.0f, 44.0f, 13.0f}, - {60.0f, 22.0f, 40.0f, 10.0f}, {56.0f, 30.0f, 41.0f, 13.0f}, {63.0f, 25.0f, 49.0f, 15.0f}, {50.0f, 20.0f, 35.0f, 10.0f}, - {59.0f, 30.0f, 42.0f, 15.0f}, {55.0f, 25.0f, 40.0f, 13.0f}, {62.0f, 29.0f, 43.0f, 13.0f}, {51.0f, 25.0f, 30.0f, 11.0f}, - {57.0f, 28.0f, 41.0f, 13.0f}, {58.0f, 27.0f, 39.0f, 12.0f}, {56.0f, 29.0f, 36.0f, 13.0f}, {67.0f, 31.0f, 47.0f, 15.0f}, - {67.0f, 31.0f, 44.0f, 14.0f}, {55.0f, 24.0f, 38.0f, 11.0f}, {56.0f, 30.0f, 45.0f, 15.0f}, {61.0f, 28.0f, 40.0f, 13.0f}, - {50.0f, 23.0f, 33.0f, 10.0f}, {55.0f, 26.0f, 44.0f, 12.0f}, {64.0f, 32.0f, 45.0f, 15.0f}, {55.0f, 23.0f, 40.0f, 13.0f}, - {66.0f, 30.0f, 44.0f, 14.0f}, {68.0f, 28.0f, 48.0f, 14.0f}, {58.0f, 27.0f, 41.0f, 10.0f}, {54.0f, 30.0f, 45.0f, 15.0f}, - {56.0f, 25.0f, 39.0f, 11.0f}, {62.0f, 22.0f, 45.0f, 15.0f}, {65.0f, 28.0f, 46.0f, 15.0f}, {58.0f, 26.0f, 40.0f, 12.0f}, - {57.0f, 29.0f, 42.0f, 13.0f}, {59.0f, 32.0f, 48.0f, 18.0f}, {70.0f, 32.0f, 47.0f, 14.0f}, {60.0f, 29.0f, 45.0f, 15.0f}, - {57.0f, 26.0f, 35.0f, 10.0f}, {67.0f, 30.0f, 50.0f, 17.0f} + {66.0f, 29.0f, 46.0f, 13.0f}, {61.0f, 29.0f, 47.0f, 14.0f}, {60.0f, 34.0f, 45.0f, 16.0f}, {52.0f, 27.0f, 39.0f, 14.0f}, + {49.0f, 24.0f, 33.0f, 10.0f}, {60.0f, 27.0f, 51.0f, 16.0f}, {56.0f, 27.0f, 42.0f, 13.0f}, {61.0f, 30.0f, 46.0f, 14.0f}, + {55.0f, 24.0f, 37.0f, 10.0f}, {57.0f, 30.0f, 42.0f, 12.0f}, {63.0f, 33.0f, 47.0f, 16.0f}, {69.0f, 31.0f, 49.0f, 15.0f}, + {57.0f, 28.0f, 45.0f, 13.0f}, {61.0f, 28.0f, 47.0f, 12.0f}, {64.0f, 29.0f, 43.0f, 13.0f}, {63.0f, 23.0f, 44.0f, 13.0f}, + {60.0f, 22.0f, 40.0f, 10.0f}, {56.0f, 30.0f, 41.0f, 13.0f}, {63.0f, 25.0f, 49.0f, 15.0f}, {50.0f, 20.0f, 35.0f, 10.0f}, + {59.0f, 30.0f, 42.0f, 15.0f}, {55.0f, 25.0f, 40.0f, 13.0f}, {62.0f, 29.0f, 43.0f, 13.0f}, {51.0f, 25.0f, 30.0f, 11.0f}, + {57.0f, 28.0f, 41.0f, 13.0f}, {58.0f, 27.0f, 39.0f, 12.0f}, {56.0f, 29.0f, 36.0f, 13.0f}, {67.0f, 31.0f, 47.0f, 15.0f}, + {67.0f, 31.0f, 44.0f, 14.0f}, {55.0f, 24.0f, 38.0f, 11.0f}, {56.0f, 30.0f, 45.0f, 15.0f}, {61.0f, 28.0f, 40.0f, 13.0f}, + {50.0f, 23.0f, 33.0f, 10.0f}, {55.0f, 26.0f, 44.0f, 12.0f}, {64.0f, 32.0f, 45.0f, 15.0f}, {55.0f, 23.0f, 40.0f, 13.0f}, + {66.0f, 30.0f, 44.0f, 14.0f}, {68.0f, 28.0f, 48.0f, 14.0f}, {58.0f, 27.0f, 41.0f, 10.0f}, {54.0f, 30.0f, 45.0f, 15.0f}, + {56.0f, 25.0f, 39.0f, 11.0f}, {62.0f, 22.0f, 45.0f, 15.0f}, {65.0f, 28.0f, 46.0f, 15.0f}, {58.0f, 26.0f, 40.0f, 12.0f}, + {57.0f, 29.0f, 42.0f, 13.0f}, {59.0f, 32.0f, 48.0f, 18.0f}, {70.0f, 32.0f, 47.0f, 14.0f}, {60.0f, 29.0f, 45.0f, 15.0f}, + {57.0f, 26.0f, 35.0f, 10.0f}, {67.0f, 30.0f, 50.0f, 17.0f} }; } /** * Returns Iris virginica data - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris versicolor data */ private float[][] getIrisVirginicaDataMatrix() { - return new + return new float[][] { - {63.0f, 33.0f, 60.0f, 25.0f}, {65.0f, 30.0f, 52.0f, 20.0f}, {58.0f, 28.0f, 51.0f, 24.0f}, {68.0f, 30.0f, 55.0f, 21.0f}, - {67.0f, 31.0f, 56.0f, 24.0f}, {63.0f, 28.0f, 51.0f, 15.0f}, {69.0f, 31.0f, 51.0f, 23.0f}, {64.0f, 27.0f, 53.0f, 19.0f}, - {69.0f, 31.0f, 54.0f, 21.0f}, {72.0f, 36.0f, 61.0f, 25.0f}, {57.0f, 25.0f, 50.0f, 20.0f}, {65.0f, 32.0f, 51.0f, 20.0f}, - {65.0f, 30.0f, 58.0f, 22.0f}, {62.0f, 34.0f, 54.0f, 23.0f}, {64.0f, 28.0f, 56.0f, 21.0f}, {61.0f, 26.0f, 56.0f, 14.0f}, - {64.0f, 28.0f, 56.0f, 22.0f}, {77.0f, 30.0f, 61.0f, 23.0f}, {67.0f, 30.0f, 52.0f, 23.0f}, {62.0f, 28.0f, 48.0f, 18.0f}, - {59.0f, 30.0f, 51.0f, 18.0f}, {63.0f, 25.0f, 50.0f, 19.0f}, {72.0f, 30.0f, 58.0f, 16.0f}, {76.0f, 30.0f, 66.0f, 21.0f}, - {64.0f, 32.0f, 53.0f, 23.0f}, {61.0f, 30.0f, 49.0f, 18.0f}, {79.0f, 38.0f, 64.0f, 20.0f}, {72.0f, 32.0f, 60.0f, 18.0f}, - {63.0f, 27.0f, 49.0f, 18.0f}, {77.0f, 28.0f, 67.0f, 20.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {67.0f, 25.0f, 58.0f, 18.0f}, - {49.0f, 25.0f, 45.0f, 17.0f}, {67.0f, 33.0f, 57.0f, 21.0f}, {77.0f, 38.0f, 67.0f, 22.0f}, {56.0f, 28.0f, 49.0f, 20.0f}, - {65.0f, 30.0f, 55.0f, 18.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {74.0f, 28.0f, 61.0f, 19.0f}, {69.0f, 32.0f, 57.0f, 23.0f}, - {68.0f, 32.0f, 59.0f, 23.0f}, {73.0f, 29.0f, 63.0f, 18.0f}, {71.0f, 30.0f, 59.0f, 21.0f}, {60.0f, 22.0f, 50.0f, 15.0f}, - {77.0f, 26.0f, 69.0f, 23.0f}, {67.0f, 33.0f, 57.0f, 25.0f}, {63.0f, 29.0f, 56.0f, 18.0f}, {60.0f, 30.0f, 48.0f, 18.0f}, + {63.0f, 33.0f, 60.0f, 25.0f}, {65.0f, 30.0f, 52.0f, 20.0f}, {58.0f, 28.0f, 51.0f, 24.0f}, {68.0f, 30.0f, 55.0f, 21.0f}, + {67.0f, 31.0f, 56.0f, 24.0f}, {63.0f, 28.0f, 51.0f, 15.0f}, {69.0f, 31.0f, 51.0f, 23.0f}, {64.0f, 27.0f, 53.0f, 19.0f}, + {69.0f, 31.0f, 54.0f, 21.0f}, {72.0f, 36.0f, 61.0f, 25.0f}, {57.0f, 25.0f, 50.0f, 20.0f}, {65.0f, 32.0f, 51.0f, 20.0f}, + {65.0f, 30.0f, 58.0f, 22.0f}, {62.0f, 34.0f, 54.0f, 23.0f}, {64.0f, 28.0f, 56.0f, 21.0f}, {61.0f, 26.0f, 56.0f, 14.0f}, + {64.0f, 28.0f, 56.0f, 22.0f}, {77.0f, 30.0f, 61.0f, 23.0f}, {67.0f, 30.0f, 52.0f, 23.0f}, {62.0f, 28.0f, 48.0f, 18.0f}, + {59.0f, 30.0f, 51.0f, 18.0f}, {63.0f, 25.0f, 50.0f, 19.0f}, {72.0f, 30.0f, 58.0f, 16.0f}, {76.0f, 30.0f, 66.0f, 21.0f}, + {64.0f, 32.0f, 53.0f, 23.0f}, {61.0f, 30.0f, 49.0f, 18.0f}, {79.0f, 38.0f, 64.0f, 20.0f}, {72.0f, 32.0f, 60.0f, 18.0f}, + {63.0f, 27.0f, 49.0f, 18.0f}, {77.0f, 28.0f, 67.0f, 20.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {67.0f, 25.0f, 58.0f, 18.0f}, + {49.0f, 25.0f, 45.0f, 17.0f}, {67.0f, 33.0f, 57.0f, 21.0f}, {77.0f, 38.0f, 67.0f, 22.0f}, {56.0f, 28.0f, 49.0f, 20.0f}, + {65.0f, 30.0f, 55.0f, 18.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {74.0f, 28.0f, 61.0f, 19.0f}, {69.0f, 32.0f, 57.0f, 23.0f}, + {68.0f, 32.0f, 59.0f, 23.0f}, {73.0f, 29.0f, 63.0f, 18.0f}, {71.0f, 30.0f, 59.0f, 21.0f}, {60.0f, 22.0f, 50.0f, 15.0f}, + {77.0f, 26.0f, 69.0f, 23.0f}, {67.0f, 33.0f, 57.0f, 25.0f}, {63.0f, 29.0f, 56.0f, 18.0f}, {60.0f, 30.0f, 48.0f, 18.0f}, {64.0f, 31.0f, 55.0f, 18.0f}, {63.0f, 34.0f, 56.0f, 24.0f} }; } // - + } diff --git a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java index 956ce8d..7e213b6 100644 --- a/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java +++ b/src/test/java/de/unijena/cheminf/clustering/art2a/Art2aTest.java @@ -2,7 +2,7 @@ * ART-2a Clustering for Java * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at * * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -26,6 +26,9 @@ package de.unijena.cheminf.clustering.art2a; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -34,9 +37,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - /** * Test class for ART-2a clustering. * @@ -53,7 +53,7 @@ public void test_Development_IrisFlowerData() { System.out.println("test_Development_IrisFlowerData()"); System.out.println("---------------------------------"); float[][] tmpIrisFlowerDataMatrix = this.getIrisFlowerDataMatrix(); - + // float[] tmpVigilances = new float[] {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f}; float[] tmpVigilances = new float[] {0.1f}; @@ -68,9 +68,9 @@ public void test_Development_IrisFlowerData() { for (float tmpVigilance : tmpVigilances) { System.out.println(" Vigilance parameter = " + String.valueOf(tmpVigilance)); - Art2aKernel tmpArt2aKernel = + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -116,11 +116,11 @@ public void test_Development_CombinedGaussianCouldData() { int tmpNumberOfGaussianCloudVectors = 100; float tmpStandardDeviation = 0.1f; Random tmpRandomNumberGenerator = new Random(1L); - float[][] tmpCombinedGaussianCloudDataMatrix = + float[][] tmpCombinedGaussianCloudDataMatrix = this.getCombinedGaussianCloudMatrix( - tmpNumberOfDimensions, - tmpNumberOfGaussianCloudVectors, - tmpStandardDeviation, + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, tmpRandomNumberGenerator ); @@ -135,9 +135,9 @@ public void test_Development_CombinedGaussianCouldData() { long tmpRandomSeed = 1L; long tmpStart = System.currentTimeMillis(); - Art2aKernel tmpArt2aKernel = + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpCombinedGaussianCloudDataMatrix, + tmpCombinedGaussianCloudDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -187,11 +187,11 @@ public void test_Development_CombinedGaussianCouldData_Performance() { int tmpNumberOfGaussianCloudVectors = 1000; float tmpStandardDeviation = 0.01f; Random tmpRandomNumberGenerator = new Random(1L); - float[][] tmpCombinedGaussianCloudDataMatrix = + float[][] tmpCombinedGaussianCloudDataMatrix = this.getCombinedGaussianCloudMatrix( - tmpNumberOfDimensions, - tmpNumberOfGaussianCloudVectors, - tmpStandardDeviation, + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, tmpRandomNumberGenerator ); @@ -206,9 +206,9 @@ public void test_Development_CombinedGaussianCouldData_Performance() { long tmpRandomSeed = 1L; long tmpStart = System.currentTimeMillis(); - Art2aKernel tmpArt2aKernel = + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpCombinedGaussianCloudDataMatrix, + tmpCombinedGaussianCloudDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -254,10 +254,10 @@ public void test_Development_GetRepresentatives() { float tmpVigilanceMin = 0.0001f; float tmpVigilanceMax = 0.9999f; int tmpNumberOfTrialSteps = 32; - - Art2aKernel tmpArt2aKernel = + + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -277,11 +277,11 @@ public void test_Development_GetRepresentatives() { ); for (int tmpNumberOfRepresentatives = 2; tmpNumberOfRepresentatives < tmpIrisFlowerDataMatrix.length; tmpNumberOfRepresentatives++) { try { - int[] tmpRepresentatives = + int[] tmpRepresentatives = tmpArt2aKernel.getRepresentatives( - tmpNumberOfRepresentatives, - tmpVigilanceMin, - tmpVigilanceMax, + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, tmpNumberOfTrialSteps, tmpIsParallelRhoWinnerCalculation ); @@ -289,10 +289,10 @@ public void test_Development_GetRepresentatives() { Arrays.sort(tmpRepresentatives); float tmpMeanDistance = Utils.getMeanDistance(tmpIrisFlowerDataMatrix, tmpRepresentatives); System.out.println( - String.valueOf(tmpNumberOfRepresentatives) + + String.valueOf(tmpNumberOfRepresentatives) + " Representatives (Mean distance = " + - String.valueOf(tmpMeanDistance) + - ") = " + + String.valueOf(tmpMeanDistance) + + ") = " + this.getStringFromIntArray(tmpRepresentatives) ); } @@ -301,7 +301,7 @@ public void test_Development_GetRepresentatives() { } } } - + /** * Tests Art2aKernel method getRepresentatives(). */ @@ -324,10 +324,10 @@ public void test_GetRepresentatives() { float tmpVigilanceMax = 0.9999f; int tmpNumberOfRepresentatives = 7; int tmpNumberOfTrialSteps = 32; - - Art2aKernel tmpArt2aKernel = + + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -337,11 +337,11 @@ public void test_GetRepresentatives() { tmpIsDataPreprocessing ); try { - int[] tmpRepresentatives = + int[] tmpRepresentatives = tmpArt2aKernel.getRepresentatives( - tmpNumberOfRepresentatives, - tmpVigilanceMin, - tmpVigilanceMax, + tmpNumberOfRepresentatives, + tmpVigilanceMin, + tmpVigilanceMax, tmpNumberOfTrialSteps, tmpIsParallelRhoWinnerCalculation ); @@ -363,11 +363,11 @@ public void test_PerfectClustering() { int tmpNumberOfGaussianCloudVectors = 1000; float tmpStandardDeviation = 0.01f; Random tmpRandomNumberGenerator = new Random(1L); - float[][] tmpCombinedGaussianCloudDataMatrix = + float[][] tmpCombinedGaussianCloudDataMatrix = this.getCombinedGaussianCloudMatrix( - tmpNumberOfDimensions, - tmpNumberOfGaussianCloudVectors, - tmpStandardDeviation, + tmpNumberOfDimensions, + tmpNumberOfGaussianCloudVectors, + tmpStandardDeviation, tmpRandomNumberGenerator ); @@ -381,9 +381,9 @@ public void test_PerfectClustering() { float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; - Art2aKernel tmpArt2aKernel = + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpCombinedGaussianCloudDataMatrix, + tmpCombinedGaussianCloudDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -420,9 +420,9 @@ public void test_PerfectClustering() { Assertions.assertEquals(tmpArt2aResult.getClusterRepresentativeIndex(i), tmpArt2aResult.getClusterRepresentativeIndices(i)[0]); } } - + /** - * Tests that clustering with and without preprocessing has identical + * Tests that clustering with and without preprocessing has identical * results. */ @Test @@ -445,7 +445,7 @@ public void test_Preprocessing() { boolean tmpIsParallelRhoWinnerCalculation = false; Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -463,9 +463,9 @@ public void test_Preprocessing() { // Preprocessing tmpIsDataPreprocessing = true; - Art2aKernel tmpArt2aKernelWithPreprocessing = + Art2aKernel tmpArt2aKernelWithPreprocessing = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -480,15 +480,15 @@ public void test_Preprocessing() { } catch (Exception anException) { Assertions.fail(); } - + // Assertions.assert that results without and with preprocessing are identical Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aResultWithPreprocessing.getNumberOfDetectedClusters()); Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aResultWithPreprocessing.getNumberOfEpochs()); - + int tmpNumberOfDetectedClusters = tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { Assertions.assertArrayEquals( - tmpArt2aResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), tmpArt2aResultWithPreprocessing.getDataVectorIndicesOfCluster(i) ); } @@ -501,7 +501,7 @@ public void test_Preprocessing() { } /** - * Test that generated Art2aData object leads to identical clustering + * Test that generated Art2aData object leads to identical clustering * results. */ @Test @@ -523,7 +523,7 @@ public void test_Art2aData() { boolean tmpIsParallelRhoWinnerCalculation = false; Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -541,7 +541,7 @@ public void test_Art2aData() { // Preprocessed Art2aData PreprocessedArt2aData tmpPreprocessedArt2aData = Art2aKernel.getPreprocessedArt2aData(tmpIrisFlowerDataMatrix, tmpOffsetForContrastEnhancement); - Art2aKernel tmpArt2aKernelWithArt2aData = + Art2aKernel tmpArt2aKernelWithArt2aData = new Art2aKernel( tmpPreprocessedArt2aData, tmpMaximumNumberOfClusters, @@ -557,15 +557,15 @@ public void test_Art2aData() { Assertions.fail(); } - // Assertions.assert that results without preprocessing and preprocessed + // Assertions.assert that results without preprocessing and preprocessed // Art2aData are identical Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(), tmpArt2aResultWithArt2aData.getNumberOfDetectedClusters()); Assertions.assertEquals(tmpArt2aResultWithoutPreprocessing.getNumberOfEpochs(), tmpArt2aResultWithArt2aData.getNumberOfEpochs()); - + int tmpNumberOfDetectedClusters = tmpArt2aResultWithoutPreprocessing.getNumberOfDetectedClusters(); for (int i = 0; i < tmpNumberOfDetectedClusters; i++) { Assertions.assertArrayEquals( - tmpArt2aResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), + tmpArt2aResultWithoutPreprocessing.getDataVectorIndicesOfCluster(i), tmpArt2aResultWithArt2aData.getDataVectorIndicesOfCluster(i) ); } @@ -594,7 +594,7 @@ public void test_ParallelClustering() { float tmpLearningParameter = 0.01f; float tmpOffsetForContrastEnhancement = 1.0f; long tmpRandomSeed = 1L; - + // Sequential clustering one after another Art2aResult[] tmpSequentialResults = new Art2aResult[tmpVigilances.length]; int tmpIndex = 0; @@ -603,7 +603,7 @@ public void test_ParallelClustering() { boolean tmpIsParallelRhoWinnerCalculation = false; Art2aKernel tmpArt2aKernelWithoutPreprocessing = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -626,7 +626,7 @@ public void test_ParallelClustering() { tmpArt2aTaskList.add( new Art2aTask( tmpPreprocessedArt2aData, - tmpVigilance, + tmpVigilance, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -652,8 +652,8 @@ public void test_ParallelClustering() { System.out.println("test_ParallelClustering: Exception occurred."); } } - - // Assertions.assert that sequential results without preprocessing and concurrent + + // Assertions.assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aData are identical for (int i = 0; i < tmpVigilances.length; i++) { Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); @@ -663,7 +663,7 @@ public void test_ParallelClustering() { int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { Assertions.assertArrayEquals( - tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), tmpParallelResults[i].getDataVectorIndicesOfCluster(j) ); } @@ -673,11 +673,11 @@ public void test_ParallelClustering() { Assertions.assertEquals(tmpSequentialResults[i].getAngleBetweenClusters(j, k), tmpParallelResults[i].getAngleBetweenClusters(j, k)); } } - } + } } /** - * Tests that sequential and parallelized clustering with + * Tests that sequential and parallelized clustering with * Art2aKernel.getClusterResults() leads to identical results. */ @Test @@ -695,9 +695,9 @@ public void test_ParallelClusteringWithGetClusterResults() { long tmpRandomSeed = 1L; boolean tmpIsDataPreprocessing = false; - Art2aKernel tmpArt2aKernel = + Art2aKernel tmpArt2aKernel = new Art2aKernel( - tmpIrisFlowerDataMatrix, + tmpIrisFlowerDataMatrix, tmpMaximumNumberOfClusters, tmpMaximumNumberOfEpochs, tmpConvergenceThreshold, @@ -722,8 +722,8 @@ public void test_ParallelClusteringWithGetClusterResults() { } catch (Exception anException) { Assertions.fail(); } - - // Assertions.assert that sequential results without preprocessing and concurrent + + // Assertions.assert that sequential results without preprocessing and concurrent // results with preprocessed Art2aData are identical for (int i = 0; i < tmpVigilances.length; i++) { Assertions.assertEquals(tmpSequentialResults[i].getNumberOfDetectedClusters(), tmpParallelResults[i].getNumberOfDetectedClusters()); @@ -733,7 +733,7 @@ public void test_ParallelClusteringWithGetClusterResults() { int tmpNumberOfDetectedClusters = tmpSequentialResults[i].getNumberOfDetectedClusters(); for (int j = 0; j < tmpNumberOfDetectedClusters; j++) { Assertions.assertArrayEquals( - tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), + tmpSequentialResults[i].getDataVectorIndicesOfCluster(j), tmpParallelResults[i].getDataVectorIndicesOfCluster(j) ); } @@ -743,14 +743,14 @@ public void test_ParallelClusteringWithGetClusterResults() { Assertions.assertEquals(tmpSequentialResults[i].getAngleBetweenClusters(j, k), tmpParallelResults[i].getAngleBetweenClusters(j, k)); } } - } + } } - + // /** * Returns int array as a string. * Note: No checks are performed. - * + * * @param anIntArray Int array * @return The int array as a string */ @@ -766,18 +766,18 @@ private String getStringFromIntArray( } return tmpStringBuilder.toString(); } - + /** * Compares two arrays. * Note: No checks are performed. - * + * * @param anArray1 Array 1 * @param anArray2 Array 2 - * @return True: Arrays have the same values in the same order, false: + * @return True: Arrays have the same values in the same order, false: * Otherwise */ private boolean compareArrays( - int[] anArray1, + int[] anArray1, int[] anArray2 ) { boolean isEqual = true; @@ -795,7 +795,7 @@ private boolean compareArrays( // /** * Returns Gaussian cloud matrix - * + * * @param aCentroidVector Centroid vector (IS NOT CHANGED) * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors * @param aStandardDeviation Standard deviation of Gaussian distribution @@ -821,7 +821,7 @@ private float[][] getGaussianCloudMatrix( /** * Returns combined Gaussian cloud matrix (see code) - * + * * @param aNumberOfDimensions Number of dimensions * @param aNumberOfGaussianCloudVectors Number of Gaussian cloud vectors * @param aStandardDeviation Standard deviation of Gaussian distribution @@ -840,11 +840,11 @@ private float[][] getCombinedGaussianCloudMatrix( float[] tmpCentroidVector = new float[aNumberOfDimensions]; Arrays.fill(tmpCentroidVector, 0.0f); tmpCentroidVector[i] = 1.0f; - float[][] tmpGaussianCloudMatrix = + float[][] tmpGaussianCloudMatrix = this.getGaussianCloudMatrix( - tmpCentroidVector, - aNumberOfGaussianCloudVectors, - aStandardDeviation, + tmpCentroidVector, + aNumberOfGaussianCloudVectors, + aStandardDeviation, aRandomNumberGenerator ); for (int j = 0; j < tmpGaussianCloudMatrix.length; j++) { @@ -856,19 +856,19 @@ private float[][] getCombinedGaussianCloudMatrix( // // /** - * Returns Iris flower data: Indices 0-49 = Iris setosa, indices 50-99 = + * Returns Iris flower data: Indices 0-49 = Iris setosa, indices 50-99 = * Iris versicolor, indices 100-149 = Iris virginica - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris flower data */ private float[][] getIrisFlowerDataMatrix() { float[][] tmpIrisSetosaData = this.getIrisSetosaDataMatrix(); float[][] tmpIrisVersicolorData = this.getIrisVersicolorDataMatrix(); float[][] tmpIrisVirginicaData = this.getIrisVirginicaDataMatrix(); - float[][] tmpIrisFlowerData = + float[][] tmpIrisFlowerData = new float[tmpIrisSetosaData.length + tmpIrisVersicolorData.length + tmpIrisVirginicaData.length][]; int tmpIndex = 0; for (int i = 0; i < tmpIrisSetosaData.length; i++) { @@ -882,87 +882,87 @@ private float[][] getIrisFlowerDataMatrix() { } return tmpIrisFlowerData; } - + /** * Returns Iris setosa data - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris setosa data */ private float[][] getIrisSetosaDataMatrix() { - return new + return new float[][] { - {49.0f, 30.0f, 14.0f, 2.0f}, {51.0f, 38.0f, 19.0f, 4.0f}, {52.0f, 41.0f, 15.0f, 1.0f}, {54.0f, 34.0f, 15.0f, 4.0f}, - {50.0f, 36.0f, 14.0f, 2.0f}, {57.0f, 44.0f, 15.0f, 4.0f}, {46.0f, 32.0f, 14.0f, 2.0f}, {50.0f, 34.0f, 16.0f, 4.0f}, - {51.0f, 35.0f, 14.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 2.0f}, {50.0f, 34.0f, 15.0f, 2.0f}, {58.0f, 40.0f, 12.0f, 2.0f}, - {43.0f, 30.0f, 11.0f, 1.0f}, {50.0f, 32.0f, 12.0f, 2.0f}, {50.0f, 30.0f, 16.0f, 2.0f}, {48.0f, 34.0f, 19.0f, 2.0f}, - {51.0f, 38.0f, 16.0f, 2.0f}, {48.0f, 30.0f, 14.0f, 3.0f}, {55.0f, 42.0f, 14.0f, 2.0f}, {44.0f, 30.0f, 13.0f, 2.0f}, - {54.0f, 39.0f, 17.0f, 4.0f}, {48.0f, 34.0f, 16.0f, 2.0f}, {51.0f, 35.0f, 14.0f, 3.0f}, {52.0f, 35.0f, 15.0f, 2.0f}, - {51.0f, 37.0f, 15.0f, 4.0f}, {54.0f, 34.0f, 17.0f, 2.0f}, {51.0f, 38.0f, 15.0f, 3.0f}, {57.0f, 38.0f, 17.0f, 3.0f}, - {45.0f, 23.0f, 13.0f, 3.0f}, {48.0f, 30.0f, 14.0f, 1.0f}, {53.0f, 37.0f, 15.0f, 2.0f}, {44.0f, 29.0f, 14.0f, 2.0f}, - {54.0f, 39.0f, 13.0f, 4.0f}, {54.0f, 37.0f, 15.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 1.0f}, {50.0f, 35.0f, 13.0f, 3.0f}, - {51.0f, 34.0f, 15.0f, 2.0f}, {46.0f, 31.0f, 15.0f, 2.0f}, {47.0f, 32.0f, 13.0f, 2.0f}, {47.0f, 32.0f, 16.0f, 2.0f}, - {50.0f, 33.0f, 14.0f, 2.0f}, {50.0f, 35.0f, 16.0f, 6.0f}, {55.0f, 35.0f, 13.0f, 2.0f}, {46.0f, 34.0f, 14.0f, 3.0f}, - {51.0f, 33.0f, 17.0f, 5.0f}, {52.0f, 34.0f, 14.0f, 2.0f}, {49.0f, 36.0f, 14.0f, 1.0f}, {48.0f, 31.0f, 16.0f, 2.0f}, + {49.0f, 30.0f, 14.0f, 2.0f}, {51.0f, 38.0f, 19.0f, 4.0f}, {52.0f, 41.0f, 15.0f, 1.0f}, {54.0f, 34.0f, 15.0f, 4.0f}, + {50.0f, 36.0f, 14.0f, 2.0f}, {57.0f, 44.0f, 15.0f, 4.0f}, {46.0f, 32.0f, 14.0f, 2.0f}, {50.0f, 34.0f, 16.0f, 4.0f}, + {51.0f, 35.0f, 14.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 2.0f}, {50.0f, 34.0f, 15.0f, 2.0f}, {58.0f, 40.0f, 12.0f, 2.0f}, + {43.0f, 30.0f, 11.0f, 1.0f}, {50.0f, 32.0f, 12.0f, 2.0f}, {50.0f, 30.0f, 16.0f, 2.0f}, {48.0f, 34.0f, 19.0f, 2.0f}, + {51.0f, 38.0f, 16.0f, 2.0f}, {48.0f, 30.0f, 14.0f, 3.0f}, {55.0f, 42.0f, 14.0f, 2.0f}, {44.0f, 30.0f, 13.0f, 2.0f}, + {54.0f, 39.0f, 17.0f, 4.0f}, {48.0f, 34.0f, 16.0f, 2.0f}, {51.0f, 35.0f, 14.0f, 3.0f}, {52.0f, 35.0f, 15.0f, 2.0f}, + {51.0f, 37.0f, 15.0f, 4.0f}, {54.0f, 34.0f, 17.0f, 2.0f}, {51.0f, 38.0f, 15.0f, 3.0f}, {57.0f, 38.0f, 17.0f, 3.0f}, + {45.0f, 23.0f, 13.0f, 3.0f}, {48.0f, 30.0f, 14.0f, 1.0f}, {53.0f, 37.0f, 15.0f, 2.0f}, {44.0f, 29.0f, 14.0f, 2.0f}, + {54.0f, 39.0f, 13.0f, 4.0f}, {54.0f, 37.0f, 15.0f, 2.0f}, {49.0f, 31.0f, 15.0f, 1.0f}, {50.0f, 35.0f, 13.0f, 3.0f}, + {51.0f, 34.0f, 15.0f, 2.0f}, {46.0f, 31.0f, 15.0f, 2.0f}, {47.0f, 32.0f, 13.0f, 2.0f}, {47.0f, 32.0f, 16.0f, 2.0f}, + {50.0f, 33.0f, 14.0f, 2.0f}, {50.0f, 35.0f, 16.0f, 6.0f}, {55.0f, 35.0f, 13.0f, 2.0f}, {46.0f, 34.0f, 14.0f, 3.0f}, + {51.0f, 33.0f, 17.0f, 5.0f}, {52.0f, 34.0f, 14.0f, 2.0f}, {49.0f, 36.0f, 14.0f, 1.0f}, {48.0f, 31.0f, 16.0f, 2.0f}, {46.0f, 36.0f, 10.0f, 2.0f}, {44.0f, 32.0f, 13.0f, 2.0f} }; } /** * Returns Iris versicolor data - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris versicolor data */ private float[][] getIrisVersicolorDataMatrix() { - return new + return new float[][] { - {66.0f, 29.0f, 46.0f, 13.0f}, {61.0f, 29.0f, 47.0f, 14.0f}, {60.0f, 34.0f, 45.0f, 16.0f}, {52.0f, 27.0f, 39.0f, 14.0f}, - {49.0f, 24.0f, 33.0f, 10.0f}, {60.0f, 27.0f, 51.0f, 16.0f}, {56.0f, 27.0f, 42.0f, 13.0f}, {61.0f, 30.0f, 46.0f, 14.0f}, - {55.0f, 24.0f, 37.0f, 10.0f}, {57.0f, 30.0f, 42.0f, 12.0f}, {63.0f, 33.0f, 47.0f, 16.0f}, {69.0f, 31.0f, 49.0f, 15.0f}, - {57.0f, 28.0f, 45.0f, 13.0f}, {61.0f, 28.0f, 47.0f, 12.0f}, {64.0f, 29.0f, 43.0f, 13.0f}, {63.0f, 23.0f, 44.0f, 13.0f}, - {60.0f, 22.0f, 40.0f, 10.0f}, {56.0f, 30.0f, 41.0f, 13.0f}, {63.0f, 25.0f, 49.0f, 15.0f}, {50.0f, 20.0f, 35.0f, 10.0f}, - {59.0f, 30.0f, 42.0f, 15.0f}, {55.0f, 25.0f, 40.0f, 13.0f}, {62.0f, 29.0f, 43.0f, 13.0f}, {51.0f, 25.0f, 30.0f, 11.0f}, - {57.0f, 28.0f, 41.0f, 13.0f}, {58.0f, 27.0f, 39.0f, 12.0f}, {56.0f, 29.0f, 36.0f, 13.0f}, {67.0f, 31.0f, 47.0f, 15.0f}, - {67.0f, 31.0f, 44.0f, 14.0f}, {55.0f, 24.0f, 38.0f, 11.0f}, {56.0f, 30.0f, 45.0f, 15.0f}, {61.0f, 28.0f, 40.0f, 13.0f}, - {50.0f, 23.0f, 33.0f, 10.0f}, {55.0f, 26.0f, 44.0f, 12.0f}, {64.0f, 32.0f, 45.0f, 15.0f}, {55.0f, 23.0f, 40.0f, 13.0f}, - {66.0f, 30.0f, 44.0f, 14.0f}, {68.0f, 28.0f, 48.0f, 14.0f}, {58.0f, 27.0f, 41.0f, 10.0f}, {54.0f, 30.0f, 45.0f, 15.0f}, - {56.0f, 25.0f, 39.0f, 11.0f}, {62.0f, 22.0f, 45.0f, 15.0f}, {65.0f, 28.0f, 46.0f, 15.0f}, {58.0f, 26.0f, 40.0f, 12.0f}, - {57.0f, 29.0f, 42.0f, 13.0f}, {59.0f, 32.0f, 48.0f, 18.0f}, {70.0f, 32.0f, 47.0f, 14.0f}, {60.0f, 29.0f, 45.0f, 15.0f}, - {57.0f, 26.0f, 35.0f, 10.0f}, {67.0f, 30.0f, 50.0f, 17.0f} + {66.0f, 29.0f, 46.0f, 13.0f}, {61.0f, 29.0f, 47.0f, 14.0f}, {60.0f, 34.0f, 45.0f, 16.0f}, {52.0f, 27.0f, 39.0f, 14.0f}, + {49.0f, 24.0f, 33.0f, 10.0f}, {60.0f, 27.0f, 51.0f, 16.0f}, {56.0f, 27.0f, 42.0f, 13.0f}, {61.0f, 30.0f, 46.0f, 14.0f}, + {55.0f, 24.0f, 37.0f, 10.0f}, {57.0f, 30.0f, 42.0f, 12.0f}, {63.0f, 33.0f, 47.0f, 16.0f}, {69.0f, 31.0f, 49.0f, 15.0f}, + {57.0f, 28.0f, 45.0f, 13.0f}, {61.0f, 28.0f, 47.0f, 12.0f}, {64.0f, 29.0f, 43.0f, 13.0f}, {63.0f, 23.0f, 44.0f, 13.0f}, + {60.0f, 22.0f, 40.0f, 10.0f}, {56.0f, 30.0f, 41.0f, 13.0f}, {63.0f, 25.0f, 49.0f, 15.0f}, {50.0f, 20.0f, 35.0f, 10.0f}, + {59.0f, 30.0f, 42.0f, 15.0f}, {55.0f, 25.0f, 40.0f, 13.0f}, {62.0f, 29.0f, 43.0f, 13.0f}, {51.0f, 25.0f, 30.0f, 11.0f}, + {57.0f, 28.0f, 41.0f, 13.0f}, {58.0f, 27.0f, 39.0f, 12.0f}, {56.0f, 29.0f, 36.0f, 13.0f}, {67.0f, 31.0f, 47.0f, 15.0f}, + {67.0f, 31.0f, 44.0f, 14.0f}, {55.0f, 24.0f, 38.0f, 11.0f}, {56.0f, 30.0f, 45.0f, 15.0f}, {61.0f, 28.0f, 40.0f, 13.0f}, + {50.0f, 23.0f, 33.0f, 10.0f}, {55.0f, 26.0f, 44.0f, 12.0f}, {64.0f, 32.0f, 45.0f, 15.0f}, {55.0f, 23.0f, 40.0f, 13.0f}, + {66.0f, 30.0f, 44.0f, 14.0f}, {68.0f, 28.0f, 48.0f, 14.0f}, {58.0f, 27.0f, 41.0f, 10.0f}, {54.0f, 30.0f, 45.0f, 15.0f}, + {56.0f, 25.0f, 39.0f, 11.0f}, {62.0f, 22.0f, 45.0f, 15.0f}, {65.0f, 28.0f, 46.0f, 15.0f}, {58.0f, 26.0f, 40.0f, 12.0f}, + {57.0f, 29.0f, 42.0f, 13.0f}, {59.0f, 32.0f, 48.0f, 18.0f}, {70.0f, 32.0f, 47.0f, 14.0f}, {60.0f, 29.0f, 45.0f, 15.0f}, + {57.0f, 26.0f, 35.0f, 10.0f}, {67.0f, 30.0f, 50.0f, 17.0f} }; } /** * Returns Iris virginica data - * - * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic - * Problems, Annals of Eugenics 7, 179-188, 1936. - * + * + * Literature: R. A. Fisher, The Use of Multiple Measurements in Taxonomic + * Problems, Annals of Eugenics 7, 179-188, 1936. + * * @return Iris versicolor data */ private float[][] getIrisVirginicaDataMatrix() { - return new + return new float[][] { - {63.0f, 33.0f, 60.0f, 25.0f}, {65.0f, 30.0f, 52.0f, 20.0f}, {58.0f, 28.0f, 51.0f, 24.0f}, {68.0f, 30.0f, 55.0f, 21.0f}, - {67.0f, 31.0f, 56.0f, 24.0f}, {63.0f, 28.0f, 51.0f, 15.0f}, {69.0f, 31.0f, 51.0f, 23.0f}, {64.0f, 27.0f, 53.0f, 19.0f}, - {69.0f, 31.0f, 54.0f, 21.0f}, {72.0f, 36.0f, 61.0f, 25.0f}, {57.0f, 25.0f, 50.0f, 20.0f}, {65.0f, 32.0f, 51.0f, 20.0f}, - {65.0f, 30.0f, 58.0f, 22.0f}, {62.0f, 34.0f, 54.0f, 23.0f}, {64.0f, 28.0f, 56.0f, 21.0f}, {61.0f, 26.0f, 56.0f, 14.0f}, - {64.0f, 28.0f, 56.0f, 22.0f}, {77.0f, 30.0f, 61.0f, 23.0f}, {67.0f, 30.0f, 52.0f, 23.0f}, {62.0f, 28.0f, 48.0f, 18.0f}, - {59.0f, 30.0f, 51.0f, 18.0f}, {63.0f, 25.0f, 50.0f, 19.0f}, {72.0f, 30.0f, 58.0f, 16.0f}, {76.0f, 30.0f, 66.0f, 21.0f}, - {64.0f, 32.0f, 53.0f, 23.0f}, {61.0f, 30.0f, 49.0f, 18.0f}, {79.0f, 38.0f, 64.0f, 20.0f}, {72.0f, 32.0f, 60.0f, 18.0f}, - {63.0f, 27.0f, 49.0f, 18.0f}, {77.0f, 28.0f, 67.0f, 20.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {67.0f, 25.0f, 58.0f, 18.0f}, - {49.0f, 25.0f, 45.0f, 17.0f}, {67.0f, 33.0f, 57.0f, 21.0f}, {77.0f, 38.0f, 67.0f, 22.0f}, {56.0f, 28.0f, 49.0f, 20.0f}, - {65.0f, 30.0f, 55.0f, 18.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {74.0f, 28.0f, 61.0f, 19.0f}, {69.0f, 32.0f, 57.0f, 23.0f}, - {68.0f, 32.0f, 59.0f, 23.0f}, {73.0f, 29.0f, 63.0f, 18.0f}, {71.0f, 30.0f, 59.0f, 21.0f}, {60.0f, 22.0f, 50.0f, 15.0f}, - {77.0f, 26.0f, 69.0f, 23.0f}, {67.0f, 33.0f, 57.0f, 25.0f}, {63.0f, 29.0f, 56.0f, 18.0f}, {60.0f, 30.0f, 48.0f, 18.0f}, + {63.0f, 33.0f, 60.0f, 25.0f}, {65.0f, 30.0f, 52.0f, 20.0f}, {58.0f, 28.0f, 51.0f, 24.0f}, {68.0f, 30.0f, 55.0f, 21.0f}, + {67.0f, 31.0f, 56.0f, 24.0f}, {63.0f, 28.0f, 51.0f, 15.0f}, {69.0f, 31.0f, 51.0f, 23.0f}, {64.0f, 27.0f, 53.0f, 19.0f}, + {69.0f, 31.0f, 54.0f, 21.0f}, {72.0f, 36.0f, 61.0f, 25.0f}, {57.0f, 25.0f, 50.0f, 20.0f}, {65.0f, 32.0f, 51.0f, 20.0f}, + {65.0f, 30.0f, 58.0f, 22.0f}, {62.0f, 34.0f, 54.0f, 23.0f}, {64.0f, 28.0f, 56.0f, 21.0f}, {61.0f, 26.0f, 56.0f, 14.0f}, + {64.0f, 28.0f, 56.0f, 22.0f}, {77.0f, 30.0f, 61.0f, 23.0f}, {67.0f, 30.0f, 52.0f, 23.0f}, {62.0f, 28.0f, 48.0f, 18.0f}, + {59.0f, 30.0f, 51.0f, 18.0f}, {63.0f, 25.0f, 50.0f, 19.0f}, {72.0f, 30.0f, 58.0f, 16.0f}, {76.0f, 30.0f, 66.0f, 21.0f}, + {64.0f, 32.0f, 53.0f, 23.0f}, {61.0f, 30.0f, 49.0f, 18.0f}, {79.0f, 38.0f, 64.0f, 20.0f}, {72.0f, 32.0f, 60.0f, 18.0f}, + {63.0f, 27.0f, 49.0f, 18.0f}, {77.0f, 28.0f, 67.0f, 20.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {67.0f, 25.0f, 58.0f, 18.0f}, + {49.0f, 25.0f, 45.0f, 17.0f}, {67.0f, 33.0f, 57.0f, 21.0f}, {77.0f, 38.0f, 67.0f, 22.0f}, {56.0f, 28.0f, 49.0f, 20.0f}, + {65.0f, 30.0f, 55.0f, 18.0f}, {58.0f, 27.0f, 51.0f, 19.0f}, {74.0f, 28.0f, 61.0f, 19.0f}, {69.0f, 32.0f, 57.0f, 23.0f}, + {68.0f, 32.0f, 59.0f, 23.0f}, {73.0f, 29.0f, 63.0f, 18.0f}, {71.0f, 30.0f, 59.0f, 21.0f}, {60.0f, 22.0f, 50.0f, 15.0f}, + {77.0f, 26.0f, 69.0f, 23.0f}, {67.0f, 33.0f, 57.0f, 25.0f}, {63.0f, 29.0f, 56.0f, 18.0f}, {60.0f, 30.0f, 48.0f, 18.0f}, {64.0f, 31.0f, 55.0f, 18.0f}, {63.0f, 34.0f, 56.0f, 24.0f} }; } // - + } From 57370308ea6309b25be6bc70354da310dd39dd42 Mon Sep 17 00:00:00 2001 From: Jonas Schaub <44881147+JonasSchaub@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:11:21 +0100 Subject: [PATCH 17/18] removes build for Java 11 --- .github/workflows/gradle.yml | 2 +- build.gradle | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ce0f36d..a6e34c0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 11, 17, 21 ] + java: [ 17, 21 ] name: Java ${{ matrix.java }} steps: diff --git a/build.gradle b/build.gradle index 330224c..9fb2a24 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,9 @@ /* - * ART2a Clustering for Java - * Copyright (C) 2024 Betuel Sevindik, Felix Baensch, Jonas Schaub, Christoph Steinbeck, and Achim Zielesny + * ART-2a Clustering for Java + * Copyright (C) 2025 Jonas Schaub, Betuel Sevindik, Achim Zielesny * - * Source code is available at + * Source code is available at + * * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal From 29cebaa65f2f2c404b1f8a19e273cdb11e6b3181 Mon Sep 17 00:00:00 2001 From: Jonas Schaub <44881147+JonasSchaub@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:12:10 +0100 Subject: [PATCH 18/18] adds comment to Sonar complaint --- src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java index ee13d44..e6fb46b 100644 --- a/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java +++ b/src/main/java/de/unijena/cheminf/clustering/art2a/Utils.java @@ -463,6 +463,7 @@ protected static float getMeanDistance( tmpSum += (float) Math.sqrt(Utils.getSquaredDistance(aMatrix[anIndicesOfRowVectors[i]], aMatrix[anIndicesOfRowVectors[j]])); } } + //note: (n*(n-1)) is always even, so integer division can be used and is also preferred because it is correct return tmpSum / (float) (anIndicesOfRowVectors.length * (anIndicesOfRowVectors.length - 1) / 2); }