diff --git a/README.md b/README.md index e4c303387..0d7538187 100644 --- a/README.md +++ b/README.md @@ -304,29 +304,33 @@ Tree2 ## Dimensionality reduction For visualizing high-dimensional data, e.g. in two dimensions, potentially getting more insights into your data, you can -reduce the dimensionality of the measurements, using this algorithm: +reduce the dimensionality of the measurements, using these algorithms: -* [Uniform Manifold Approximation Projection (UMAP)](https://arxiv.org/abs/1802.03426) -* [UMAP Python implementation](https://umap-learn.readthedocs.io/en/latest/) +* UMAP + * [Uniform Manifold Approximation Projection (UMAP)](https://arxiv.org/abs/1802.03426) + * [UMAP Python implementation](https://umap-learn.readthedocs.io/en/latest/) +* t-SNE + * [t-distributed Stochastic Neighbor Embedding (t-SNE)](https://lvdmaaten.github.io/tsne/) + * [t-SNE Python implementation](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html) ### Usage -* Menu Location: `Plugins > Compute Feature > Dimensionality reduction > UMAP` +* Menu Location: `Plugins > Compute Feature > Dimensionality reduction` Select the graph type whose features should be dimensionality reduced, either the Model Graph with Features for Spots and Links or the Branch Graph with Features on BranchSpots and BranchLinks. Next, select the feature + feature projections that should be dimensionality reduced. Prefer to select features, which describe the phenotype (e.g. size, shape, velocity, number of neighbors, etc.). -Only select positional features (e.g. centroid, coordinates, timeframe, etc.) if the position of cells within +Only select positional features (e.g. centroid, coordinates, timeframe, etc.), if the position of cells within the image are descriptive for the phenotype. If you are unsure, you can select all features and then remove the positional features later. ### Description -The UMAP algorithm reduces the dimensionality of the selected features and adds the reduced features to the table. -In order to do so, the UMAP algorithm uses the data matrix from the spot or branch spot table, where each row represents -a spot or branch spot and each column represents a feature. The link and branch link features can be included in the -algorithm. +The available algorithms reduce the dimensionality of the selected features and adds the results as a new feature to the +table. In order to do so, the selected algorithm uses the data matrix from the spot or branch spot table, where each row +represents a spot or branch spot and each column represents a feature. The link and branch link features can be included +in the algorithm. If they are selected, the algorithm will use the link feature value of its incoming edge or the average of all values of all incoming edges, if there is more than one incoming edge. @@ -338,12 +342,17 @@ By default, all measurements are selected in the box. ### Parameters +#### Common Parameters + * Standardize: Whether to standardize the data before reducing the dimensionality. Standardization is recommended when the data has different scales / units. Further reading: [Standardization](https://scikit-learn.org/stable/modules/preprocessing.html#standardization-or-mean-removal-and-variance-scaling). * Number of dimensions: The number of reduced dimensions to use. The default is 2, but 3 is also common. Further reading: [Number of Dimensions](https://umap-learn.readthedocs.io/en/latest/parameters.html#n-components). + +#### UMAP Parameters + * Number of neighbors: The size of the local neighborhood (in terms of number of neighboring sample points) used for manifold approximation. Larger values result in more global views of the manifold, while smaller values result in more local data being @@ -354,7 +363,18 @@ By default, all measurements are selected in the box. representation. This parameter controls how tightly UMAP is allowed to pack points together. Further reading: [Minimum Distance](https://umap-learn.readthedocs.io/en/latest/parameters.html#min-dist). -When you are done with the selection, click on `Compute UMAP`. +#### t-SNE Parameters + +* Perplexity: The perplexity is related to the number of nearest neighbors that are used in other manifold learning + algorithms. Larger datasets usually require a larger perplexity. The recommended range is between 5 and 50. + Further + reading: [Perplexity](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html#sklearn.manifold.TSNE). +* Maximum number of iterations: The maximum number of iterations for the optimization. The default is 1000. More + iterations will give more accurate results, but will also take longer to compute. + Further + reading: [Maximum Number of Iterations](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html#sklearn.manifold.TSNE). + +When you are done with the selection, click on `Compute`. The resulting values will be added as additional columns to the selected table. ![umap_table.png](doc/dimensionalityreduction/umap_table.png) diff --git a/doc/dimensionalityreduction/umap_dialog.png b/doc/dimensionalityreduction/umap_dialog.png index 5a47c390b..966e1df84 100644 Binary files a/doc/dimensionalityreduction/umap_dialog.png and b/doc/dimensionalityreduction/umap_dialog.png differ diff --git a/pom.xml b/pom.xml index de1b5ebe9..57254af65 100644 --- a/pom.xml +++ b/pom.xml @@ -122,6 +122,13 @@ 1.0 + + + com.github.lejon.T-SNE-Java + tsne + v2.6.4 + + org.apache.commons @@ -213,7 +220,7 @@ scijava.public - https://maven.scijava.org/content/groups/public + https://maven.scijava.org/content/repositories/public/ diff --git a/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/BranchOutputSerializerTools.java b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/BranchOutputSerializerTools.java new file mode 100644 index 000000000..ec32445f8 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/BranchOutputSerializerTools.java @@ -0,0 +1,58 @@ +package org.mastodon.mamut.feature.branch.dimensionalityreduction; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.mastodon.io.FileIdToObjectMap; +import org.mastodon.io.ObjectToFileIdMap; +import org.mastodon.io.properties.DoublePropertyMapSerializer; +import org.mastodon.mamut.feature.branch.BranchFeatureSerializer; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeature; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.model.branch.BranchSpot; +import org.mastodon.mamut.model.branch.ModelBranchGraph; +import org.mastodon.properties.DoublePropertyMap; + +public abstract class BranchOutputSerializerTools +{ + + private BranchOutputSerializerTools() + { + // prevent instantiation + } + + public static void serialize( final AbstractOutputFeature< BranchSpot > feature, final ObjectToFileIdMap< Spot > idmap, + final ObjectOutputStream oos, final ModelBranchGraph branchGraph, final ModelGraph graph ) throws IOException + { + oos.writeInt( feature.getOutputMaps().size() ); + for ( DoublePropertyMap< BranchSpot > outputMap : feature.getOutputMaps() ) + { + final DoublePropertyMap< Spot > spotMap = BranchFeatureSerializer.branchSpotMapToMap( outputMap, branchGraph, graph ); + final DoublePropertyMapSerializer< Spot > propertyMapSerializer = new DoublePropertyMapSerializer<>( spotMap ); + propertyMapSerializer.writePropertyMap( idmap, oos ); + } + } + + public static < T extends AbstractOutputFeature< BranchSpot > > T deserialize( final FileIdToObjectMap< Spot > idmap, + final ObjectInputStream ois, final ModelBranchGraph branchGraph, final ModelGraph graph, + final Function< List< DoublePropertyMap< BranchSpot > >, T > featureCreator ) throws ClassNotFoundException, IOException + { + int numDimensions = ois.readInt(); + List< DoublePropertyMap< BranchSpot > > outputMaps = new ArrayList<>( numDimensions ); + for ( int i = 0; i < numDimensions; i++ ) + { + final DoublePropertyMap< Spot > spotMap = new DoublePropertyMap<>( graph.vertices(), Double.NaN ); + DoublePropertyMapSerializer< Spot > serializer = new DoublePropertyMapSerializer<>( spotMap ); + serializer.readPropertyMap( idmap, ois ); + + DoublePropertyMap< BranchSpot > output = BranchFeatureSerializer.mapToBranchSpotMap( spotMap, branchGraph ); + outputMaps.add( output ); + } + return featureCreator.apply( outputMaps ); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeature.java b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeature.java new file mode 100644 index 000000000..3bffc13e3 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeature.java @@ -0,0 +1,85 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.branch.dimensionalityreduction.tsne; + +import java.util.List; + +import org.mastodon.feature.Feature; +import org.mastodon.feature.FeatureProjectionKey; +import org.mastodon.feature.FeatureProjectionSpec; +import org.mastodon.feature.FeatureSpec; +import org.mastodon.feature.Multiplicity; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeature; +import org.mastodon.mamut.model.branch.BranchSpot; +import org.mastodon.properties.DoublePropertyMap; +import org.scijava.plugin.Plugin; + +/** + * Represents a t-SNE feature for BranchSpots in the Mastodon project. + *
+ * This feature is used to store the t-SNE outputs for BranchSpots. + *
+ * The t-SNE outputs are stored in a list of {@link DoublePropertyMap}s. The size of the list is equal to the number of dimensions of the t-SNE output. + */ +public class BranchTSneFeature extends AbstractTSneFeature< BranchSpot > +{ + public static final String KEY = "Branch t-SNE outputs"; + + private final BranchSpotTSneFeatureSpec adaptedSpec; + + public static final BranchSpotTSneFeatureSpec GENERIC_SPEC = new BranchSpotTSneFeatureSpec(); + + public BranchTSneFeature( final List< DoublePropertyMap< BranchSpot > > outputMaps ) + { + super( outputMaps ); + FeatureProjectionSpec[] projectionSpecs = + projectionMap.keySet().stream().map( FeatureProjectionKey::getSpec ).toArray( FeatureProjectionSpec[]::new ); + this.adaptedSpec = new BranchSpotTSneFeatureSpec( projectionSpecs ); + } + + @Plugin( type = FeatureSpec.class ) + public static class BranchSpotTSneFeatureSpec extends FeatureSpec< BranchTSneFeature, BranchSpot > + { + public BranchSpotTSneFeatureSpec() + { + super( KEY, HELP_STRING, BranchTSneFeature.class, BranchSpot.class, Multiplicity.SINGLE ); + } + + public BranchSpotTSneFeatureSpec( final FeatureProjectionSpec... projectionSpecs ) + { + super( KEY, HELP_STRING, BranchTSneFeature.class, BranchSpot.class, Multiplicity.SINGLE, projectionSpecs ); + } + } + + @Override + public FeatureSpec< ? extends Feature< BranchSpot >, BranchSpot > getSpec() + { + return adaptedSpec; + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureComputer.java b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureComputer.java new file mode 100644 index 000000000..f167f20cb --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureComputer.java @@ -0,0 +1,76 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.branch.dimensionalityreduction.tsne; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.mastodon.RefPool; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeature; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeatureComputer; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.branch.BranchLink; +import org.mastodon.mamut.model.branch.BranchSpot; +import org.mastodon.mamut.model.branch.ModelBranchGraph; +import org.mastodon.properties.DoublePropertyMap; +import org.scijava.Context; + +public class BranchTSneFeatureComputer extends AbstractTSneFeatureComputer< BranchSpot, BranchLink, ModelBranchGraph > +{ + + public BranchTSneFeatureComputer( final Model model, final Context context ) + { + super( model, context ); + } + + @Override + protected AbstractTSneFeature< BranchSpot > createFeatureInstance( final List< DoublePropertyMap< BranchSpot > > umapOutputMaps ) + { + return new BranchTSneFeature( umapOutputMaps ); + } + + @Override + protected RefPool< BranchSpot > getRefPool() + { + return model.getBranchGraph().vertices().getRefPool(); + } + + @Override + protected ReentrantReadWriteLock getLock( final ModelBranchGraph branchGraph ) + { + return branchGraph.getLock(); + } + + @Override + protected Collection< BranchSpot > getVertices() + { + return model.getBranchGraph().vertices(); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureSerializer.java b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureSerializer.java new file mode 100644 index 000000000..8ffae45a1 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureSerializer.java @@ -0,0 +1,72 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.branch.dimensionalityreduction.tsne; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.mastodon.feature.FeatureSpec; +import org.mastodon.feature.io.FeatureSerializer; +import org.mastodon.io.FileIdToObjectMap; +import org.mastodon.io.ObjectToFileIdMap; +import org.mastodon.mamut.feature.branch.BranchFeatureSerializer; +import org.mastodon.mamut.feature.branch.dimensionalityreduction.BranchOutputSerializerTools; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.model.branch.BranchSpot; +import org.mastodon.mamut.model.branch.ModelBranchGraph; +import org.scijava.plugin.Plugin; + +/** + * De-/serializes {@link BranchTSneFeature} + */ +@Plugin( type = FeatureSerializer.class ) +public class BranchTSneFeatureSerializer implements BranchFeatureSerializer< BranchTSneFeature, BranchSpot, Spot > +{ + @Override + public FeatureSpec< BranchTSneFeature, BranchSpot > getFeatureSpec() + { + return BranchTSneFeature.GENERIC_SPEC; + } + + @Override + public void serialize( final BranchTSneFeature feature, final ObjectToFileIdMap< Spot > idmap, final ObjectOutputStream oos, + final ModelBranchGraph branchGraph, final ModelGraph graph ) throws IOException + { + BranchOutputSerializerTools.serialize( feature, idmap, oos, branchGraph, graph ); + } + + @Override + public BranchTSneFeature deserialize( final FileIdToObjectMap< Spot > idmap, final ObjectInputStream ois, + final ModelBranchGraph branchGraph, final ModelGraph graph ) throws ClassNotFoundException, IOException + { + return BranchOutputSerializerTools.deserialize( idmap, ois, branchGraph, graph, BranchTSneFeature::new ); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureSerializer.java b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureSerializer.java index bb4324852..15789140c 100644 --- a/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureSerializer.java +++ b/src/main/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureSerializer.java @@ -28,31 +28,27 @@ */ package org.mastodon.mamut.feature.branch.dimensionalityreduction.umap; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + import org.mastodon.feature.FeatureSpec; import org.mastodon.feature.io.FeatureSerializer; import org.mastodon.io.FileIdToObjectMap; import org.mastodon.io.ObjectToFileIdMap; -import org.mastodon.io.properties.DoublePropertyMapSerializer; import org.mastodon.mamut.feature.branch.BranchFeatureSerializer; +import org.mastodon.mamut.feature.branch.dimensionalityreduction.BranchOutputSerializerTools; import org.mastodon.mamut.model.ModelGraph; import org.mastodon.mamut.model.Spot; import org.mastodon.mamut.model.branch.BranchSpot; import org.mastodon.mamut.model.branch.ModelBranchGraph; -import org.mastodon.properties.DoublePropertyMap; import org.scijava.plugin.Plugin; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.List; - /** * De-/serializes {@link BranchUmapFeature} */ @Plugin( type = FeatureSerializer.class ) public class BranchUmapFeatureSerializer implements BranchFeatureSerializer< BranchUmapFeature, BranchSpot, Spot > - { @Override public FeatureSpec< BranchUmapFeature, BranchSpot > getFeatureSpec() @@ -64,31 +60,13 @@ public FeatureSpec< BranchUmapFeature, BranchSpot > getFeatureSpec() public void serialize( final BranchUmapFeature feature, final ObjectToFileIdMap< Spot > idmap, final ObjectOutputStream oos, final ModelBranchGraph branchGraph, final ModelGraph graph ) throws IOException { - oos.writeInt( feature.getUmapOutputMaps().size() ); - for ( DoublePropertyMap< BranchSpot > umapOutput : feature.getUmapOutputMaps() ) - { - final DoublePropertyMap< Spot > spotMap = - BranchFeatureSerializer.branchSpotMapToMap( umapOutput, branchGraph, graph ); - final DoublePropertyMapSerializer< Spot > propertyMapSerializer = new DoublePropertyMapSerializer<>( spotMap ); - propertyMapSerializer.writePropertyMap( idmap, oos ); - } + BranchOutputSerializerTools.serialize( feature, idmap, oos, branchGraph, graph ); } @Override public BranchUmapFeature deserialize( final FileIdToObjectMap< Spot > idmap, final ObjectInputStream ois, final ModelBranchGraph branchGraph, final ModelGraph graph ) throws ClassNotFoundException, IOException { - int numDimensions = ois.readInt(); - List< DoublePropertyMap< BranchSpot > > umapOutputs = new ArrayList<>( numDimensions ); - for ( int i = 0; i < numDimensions; i++ ) - { - final DoublePropertyMap< Spot > spotMap = new DoublePropertyMap<>( graph.vertices(), Double.NaN ); - DoublePropertyMapSerializer< Spot > serializer = new DoublePropertyMapSerializer<>( spotMap ); - serializer.readPropertyMap( idmap, ois ); - - DoublePropertyMap< BranchSpot > umapOutput = BranchFeatureSerializer.mapToBranchSpotMap( spotMap, branchGraph ); - umapOutputs.add( umapOutput ); - } - return new BranchUmapFeature( umapOutputs ); + return BranchOutputSerializerTools.deserialize( idmap, ois, branchGraph, graph, BranchUmapFeature::new ); } } diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/AbstractOutputFeature.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/AbstractOutputFeature.java new file mode 100644 index 000000000..dc84c4aa5 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/AbstractOutputFeature.java @@ -0,0 +1,111 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import static org.mastodon.feature.FeatureProjectionKey.key; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.mastodon.feature.Dimension; +import org.mastodon.feature.Feature; +import org.mastodon.feature.FeatureProjection; +import org.mastodon.feature.FeatureProjectionKey; +import org.mastodon.feature.FeatureProjectionSpec; +import org.mastodon.feature.FeatureProjections; +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.ValueIsSetEvaluator; +import org.mastodon.properties.DoublePropertyMap; + +/** + * This generic feature is used to store the UMAP outputs. + *
+ * The UMAP outputs are stored in a list of {@link DoublePropertyMap}s. The size of the list is equal to the number of dimensions of the UMAP output. + */ +public abstract class AbstractOutputFeature< V extends Vertex< ? > > implements Feature< V >, ValueIsSetEvaluator< V > +{ + private final List< DoublePropertyMap< V > > outputMaps; + + protected final Map< FeatureProjectionKey, FeatureProjection< V > > projectionMap; + + protected AbstractOutputFeature( final List< DoublePropertyMap< V > > outputMaps ) + { + this.outputMaps = outputMaps; + this.projectionMap = new LinkedHashMap<>( 2 ); + for ( int i = 0; i < outputMaps.size(); i++ ) + { + FeatureProjectionSpec projectionSpec = new FeatureProjectionSpec( getProjectionName( i ), Dimension.NONE ); + final FeatureProjectionKey key = key( projectionSpec ); + projectionMap.put( key, FeatureProjections.project( key, getOutputMaps().get( i ), Dimension.NONE_UNITS ) ); + } + } + + public String getProjectionName( final int outputDimension ) + { + return String.format( getProjectionNameTemplate(), outputDimension + 1 ); + } + + public List< DoublePropertyMap< V > > getOutputMaps() + { + return outputMaps; + } + + protected abstract String getProjectionNameTemplate(); + + @Override + public void invalidate( V vertex ) + { + getOutputMaps().forEach( map -> map.remove( vertex ) ); + } + + @Override + public boolean valueIsSet( final V vertex ) + { + for ( final DoublePropertyMap< V > map : getOutputMaps() ) + if ( !map.isSet( vertex ) ) + return false; + return true; + } + + @Override + public FeatureProjection< V > project( final FeatureProjectionKey key ) + { + return projectionMap.get( key ); + } + + @Override + public Set< FeatureProjection< V > > projections() + { + return new LinkedHashSet<>( projectionMap.values() ); + } + +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/AbstractOutputFeatureComputer.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/AbstractOutputFeatureComputer.java new file mode 100644 index 000000000..674bf6373 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/AbstractOutputFeatureComputer.java @@ -0,0 +1,237 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.mastodon.RefPool; +import org.mastodon.collection.RefIntMap; +import org.mastodon.collection.ref.RefIntHashMap; +import org.mastodon.graph.Edge; +import org.mastodon.graph.ReadOnlyGraph; +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.AbstractSerialFeatureComputer; +import org.mastodon.mamut.feature.ValueIsSetEvaluator; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; +import org.mastodon.mamut.feature.dimensionalityreduction.util.StandardScaler; +import org.mastodon.mamut.model.Model; +import org.mastodon.properties.DoublePropertyMap; +import org.scijava.Context; +import org.scijava.app.StatusService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract class for computing features that result from dimensionality reduction in the Mastodon project. + *
+ * This provides the base implementation for computing UMAP features on vertices in a read-only graph. + * It handles the setup, execution, and caching of UMAP computations. + *
+ * This class connects the UMAP library to the Mastodon project by providing the necessary data and settings. + * It ensures that only valid data rows (i.e. rows where the selected feature projections do not have values, such as {@link Double#NaN} or {@link Double#POSITIVE_INFINITY}) are used for UMAP computations. + * + * @param the type of vertex + * @param the type of read-only graph + */ +public abstract class AbstractOutputFeatureComputer< V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, E > > + extends AbstractSerialFeatureComputer< V > +{ + + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private final StatusService statusService; + + private List< InputDimension< V > > inputDimensions; + + protected CommonSettings settings; + + private AbstractOutputFeature< V > feature; + + private final RefIntMap< V > vertexToRowIndexMap; + + private static final int NO_ENTRY = -1; + + protected AbstractOutputFeatureComputer( final Model model, final Context context ) + { + this.model = model; + this.statusService = context.getService( StatusService.class ); + this.vertexToRowIndexMap = new RefIntHashMap<>( getRefPool(), NO_ENTRY ); + } + + /** + * Computes the feature with the given settings and input dimensions and declares it in the feature model. + *
+ * During computation, the given graph is locked for reading. + * The feature values are computed for each vertex in the graph, excluding vertices with invalid data rows + * (i.e. rows where the selected feature projections do not have values, such as {@link Double#NaN} or {@link Double#POSITIVE_INFINITY}). + * + * @param settings the UMAP settings + * @param inputDimensions the input dimensions + * @param graph the read-only graph + */ + protected void computeFeature( final CommonSettings settings, final List< InputDimension< V > > inputDimensions, final G graph ) + { + logger.info( "Computing Feature with these common settings: {}", settings ); + this.settings = settings; + logger.info( "Computing with {} input dimensions.", inputDimensions.size() ); + for ( InputDimension< V > inputDimension : inputDimensions ) + logger.info( "Input dimension: {}", inputDimension ); + this.inputDimensions = inputDimensions; + this.forceComputeAll = new AtomicBoolean( true ); + long start = System.currentTimeMillis(); + ReentrantReadWriteLock.ReadLock lock = getLock( graph ).readLock(); + lock.lock(); + try + { + run(); + } + finally + { + lock.unlock(); + } + logger.info( "Finished computing output in {} ms", System.currentTimeMillis() - start ); + model.getFeatureModel().declareFeature( feature ); + } + + @Override + protected void compute( final V vertex ) + { + int rowIndex = vertexToRowIndexMap.get( vertex ); + if ( rowIndex == NO_ENTRY ) + return; + for ( int i = 0; i < settings.getNumberOfOutputDimensions(); i++ ) + { + DoublePropertyMap< V > outputMap = feature.getOutputMaps().get( i ); + double[][] result = getResult(); + outputMap.set( vertex, result[ rowIndex ][ i ] ); + } + } + + @Override + public void createOutput() + { + if ( feature == null ) + feature = initFeature( settings.getNumberOfOutputDimensions() ); + compute(); + } + + @Override + protected void notifyProgress( final int finished, final int total ) + { + statusService.showStatus( finished, total, "Computing UmapFeature" ); + } + + @Override + protected ValueIsSetEvaluator< V > getEvaluator() + { + return feature; + } + + @Override + protected void reset() + { + if ( feature == null ) + return; + feature.getOutputMaps().forEach( DoublePropertyMap::beforeClearPool ); + } + + private void compute() + { + vertexToRowIndexMap.clear(); + List< double[] > data = extractValidDataRowsAndCacheIndexes(); + double[][] dataMatrix = data.toArray( new double[ 0 ][ 0 ] ); + if ( dataMatrix.length == 0 ) + { + logger.error( + "No valid data rows found, i.e. in each existing data row there is at least one non-finite value, such as Not a Number or Infinity." ); + throw new IllegalArgumentException( + "No valid data rows found, i.e. in each existing data row there is at least one non-finite value, such as Not a Number or Infinity." ); + } + if ( settings.isStandardizeFeatures() ) + { + logger.debug( "Standardizing features with {} rows and {} columns.", dataMatrix.length, inputDimensions.size() ); + StandardScaler.standardizeColumns( dataMatrix ); + logger.debug( "Finished standardizing features" ); + } + computeAlgorithm( dataMatrix ); + } + + private List< double[] > extractValidDataRowsAndCacheIndexes() + { + List< double[] > data = new ArrayList<>(); + int index = 0; + for ( V vertex : getVertices() ) + { + double[] row = new double[ inputDimensions.size() ]; + boolean finiteRow = true; + for ( int i = 0; i < inputDimensions.size(); i++ ) + { + InputDimension< V > inputDimension = inputDimensions.get( i ); + double value = inputDimension.getValue( vertex ); + if ( Double.isNaN( value ) ) + { + finiteRow = false; + break; + } + row[ i ] = value; + } + if ( !finiteRow ) + continue; + data.add( row ); + vertexToRowIndexMap.put( vertex, index ); + index++; + } + return data; + } + + private AbstractOutputFeature< V > initFeature( int numOutputDimensions ) + { + List< DoublePropertyMap< V > > umapOutputMaps; + umapOutputMaps = new ArrayList<>( numOutputDimensions ); + for ( int i = 0; i < numOutputDimensions; i++ ) + { + umapOutputMaps.add( new DoublePropertyMap<>( getRefPool(), Double.NaN ) ); + } + return createFeatureInstance( umapOutputMaps ); + } + + protected abstract double[][] getResult(); + + protected abstract void computeAlgorithm( double[][] dataMatrix ); + + protected abstract AbstractOutputFeature< V > createFeatureInstance( final List< DoublePropertyMap< V > > umapOutputMaps ); + + protected abstract RefPool< V > getRefPool(); + + protected abstract ReentrantReadWriteLock getLock( final G graph ); +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/CommonSettings.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/CommonSettings.java new file mode 100644 index 000000000..831cbc94b --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/CommonSettings.java @@ -0,0 +1,134 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import java.lang.invoke.MethodHandles; + +import org.scijava.prefs.PrefService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Settings for the Dimensionality reduction + *
+ * Encapsulates the common settings for the dimensionality reduction, such as: + *
    + *
  • whether to standardize features
  • + *
  • the number of output dimensions
  • + *
+ */ +public class CommonSettings +{ + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + public static final int DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS = 2; + + public static final boolean DEFAULT_STANDARDIZE_FEATURES = true; + + private static final String NUMBER_OF_DIMENSIONS_SETTING = "NumberOfDimensions"; + + private static final String STANDARDIZE_FEATURES_SETTING = "StandardizeFeatures"; + + private int numberOfOutputDimensions; + + private boolean standardizeFeatures; + + /** + * Constructor with default values. + * Default values are: + *
    + *
  • number of dimensions: {@value DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS}
  • + *
  • standardize features: {@value DEFAULT_STANDARDIZE_FEATURES}
  • + *
+ */ + public CommonSettings() + { + this( DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS, DEFAULT_STANDARDIZE_FEATURES ); + } + + /** + * Constructor with number of neighbors. + * + * @param numberOfOutputDimensions the number of neighbors to consider for relative movement. + */ + public CommonSettings( final int numberOfOutputDimensions, final boolean standardizeFeatures ) + { + this.numberOfOutputDimensions = numberOfOutputDimensions; + this.standardizeFeatures = standardizeFeatures; + } + + public int getNumberOfOutputDimensions() + { + return numberOfOutputDimensions; + } + + public boolean isStandardizeFeatures() + { + return standardizeFeatures; + } + + public void setNumberOfOutputDimensions( final int numberOfOutputDimensions ) + { + this.numberOfOutputDimensions = numberOfOutputDimensions; + } + + public void setStandardizeFeatures( final boolean standardizeFeatures ) + { + this.standardizeFeatures = standardizeFeatures; + } + + static CommonSettings loadSettingsFromPreferences( final PrefService prefs ) + { + boolean standardize = prefs == null || prefs.getBoolean( CommonSettings.class, STANDARDIZE_FEATURES_SETTING, + CommonSettings.DEFAULT_STANDARDIZE_FEATURES ); + int dimensions = prefs == null ? CommonSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS + : prefs.getInt( CommonSettings.class, NUMBER_OF_DIMENSIONS_SETTING, + CommonSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS ); + return new CommonSettings( dimensions, standardize ); + } + + /** + * Saves the dimensionality reduction settings to the user preferences. + */ + void saveSettingsToPreferences( final PrefService prefs ) + { + logger.debug( "Save Dimensionality Reduction settings." ); + if ( prefs == null ) + return; + prefs.put( CommonSettings.class, STANDARDIZE_FEATURES_SETTING, isStandardizeFeatures() ); + prefs.put( CommonSettings.class, NUMBER_OF_DIMENSIONS_SETTING, getNumberOfOutputDimensions() ); + } + + @Override + public String toString() + { + return "DimensionalityReductionSettings{" + "numberOfOutputDimensions=" + numberOfOutputDimensions + ", standardizeFeatures=" + + standardizeFeatures + '}'; + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionAlgorithm.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionAlgorithm.java new file mode 100644 index 000000000..652f3493c --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionAlgorithm.java @@ -0,0 +1,28 @@ +package org.mastodon.mamut.feature.dimensionalityreduction; + +public enum DimensionalityReductionAlgorithm +{ + UMAP( "UMAP", "Uniform Manifold Approximation and Projection for Dimension Reduction." ), + TSNE( "t-SNE", "t-distributed Stochastic Neighbor Embedding." ); + + private final String name; + + private final String description; + + DimensionalityReductionAlgorithm( final String name, final String description ) + { + this.name = name; + this.description = description; + } + + public String getDescription() + { + return description; + } + + @Override + public String toString() + { + return name; + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionController.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionController.java new file mode 100644 index 000000000..d08ca2fab --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionController.java @@ -0,0 +1,292 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.function.Supplier; + +import net.imglib2.util.Cast; + +import org.mastodon.graph.Edge; +import org.mastodon.graph.ReadOnlyGraph; +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.branch.dimensionalityreduction.tsne.BranchTSneFeatureComputer; +import org.mastodon.mamut.feature.branch.dimensionalityreduction.umap.BranchUmapFeatureComputer; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.TSneSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeatureComputer; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.feature.AbstractUmapFeatureComputer; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; +import org.mastodon.mamut.feature.spot.dimensionalityreduction.tsne.SpotTSneFeatureComputer; +import org.mastodon.mamut.feature.spot.dimensionalityreduction.umap.SpotUmapFeatureComputer; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.model.branch.BranchLink; +import org.mastodon.mamut.model.branch.BranchSpot; +import org.scijava.Context; +import org.scijava.prefs.PrefService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Controller for the dimensionality reduction. + *
+ * This class provides methods to compute the features (such as UMAP, t-SNE, PCA) for selected input dimensions. + * It also handles the user preferences for the dimensionality reduction parameters. + */ +public class DimensionalityReductionController +{ + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private boolean running; + + private final Context context; + + private final Model model; + + protected final PrefService prefs; + + protected boolean isModelGraph; + + protected DimensionalityReductionAlgorithm algorithm; + + private final CommonSettings commonSettings; + + private final UmapSettings umapSettings; + + private final TSneSettings tSneSettings; + + private static final String IS_MODEL_GRAPH = "IsModelGraph"; + + private static final String DIMENSIONALITY_REDUCTION_ALGORITHM = "DimensionalityReductionAlgorithm"; + + public DimensionalityReductionController( final Model model, final Context context ) + { + this.model = model; + this.prefs = context.getService( PrefService.class ); + this.context = context; + this.commonSettings = CommonSettings.loadSettingsFromPreferences( prefs ); + this.umapSettings = UmapSettings.loadSettingsFromPreferences( prefs ); + this.tSneSettings = TSneSettings.loadSettingsFromPreferences( prefs ); + loadSettingsFromPreferences(); + } + + /** + * Gets the common dimensionality reduction settings. + * @return the the common dimensionality reduction settings + */ + public CommonSettings getCommonSettings() + { + return commonSettings; + } + + /** + * Gets the UMAP specific settings. + * @return the UMAP specific settings + */ + public UmapSettings getUmapSettings() + { + return umapSettings; + } + + /** + * Gets the t-SNE specific settings. + * @return the t-SNE specific settings + */ + public TSneSettings getTSneSettings() + { + return tSneSettings; + } + + /** + * Computes the dimensionality reduction with the selected algorithm for the selected input dimensions. + *
+ * Since the dimensionality reduction computations are computationally expensive, this method prevents multiple executions of itself at the same time. + * @param inputDimensionsSupplier a supplier for the selected input dimensions + */ + public < V extends Vertex< E >, E extends Edge< V > > void + computeFeature( final Supplier< List< InputDimension< V > > > inputDimensionsSupplier ) + { + if ( running ) + { + logger.debug( "Dimensionality computation currently running." ); + return; + } + + try + { + running = true; + if ( inputDimensionsSupplier != null ) + updateFeature( inputDimensionsSupplier.get() ); + } + finally + { + running = false; + } + } + + /** + * Sets the graph type for the input and output feature. + * @param isModelGraph {@code true} if the dimensionality reduction is to be computed for the model graph, {@code false} for the branch graph + */ + public void setModelGraph( final boolean isModelGraph ) + { + this.isModelGraph = isModelGraph; + } + + /** + * Sets the dimensionality reduction algorithm. + * @param algorithm the algorithm + */ + public void setAlgorithm( final DimensionalityReductionAlgorithm algorithm ) + { + this.algorithm = algorithm; + } + + private < V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, E > > void + updateFeature( final List< InputDimension< V > > inputDimensions ) + { + if ( inputDimensions.isEmpty() ) + { + logger.error( "No features selected." ); + throw new IllegalArgumentException( "No features selected." ); + } + if ( commonSettings.getNumberOfOutputDimensions() >= inputDimensions.size() ) + { + logger.error( "Number of output dimensions ({}) must be smaller than the number of input features {}.", + commonSettings.getNumberOfOutputDimensions(), inputDimensions.size() ); + throw new IllegalArgumentException( "Number of output dimensions (" + commonSettings.getNumberOfOutputDimensions() + + ") must be smaller than the number of input features (" + inputDimensions.size() + ")." ); + } + + G graph = getGraph( isModelGraph ); + switch ( algorithm ) + { + case UMAP: + AbstractUmapFeatureComputer< V, E, G > umapFeatureComputer = + isModelGraph ? Cast.unchecked( new SpotUmapFeatureComputer( model, context ) ) + : Cast.unchecked( new BranchUmapFeatureComputer( model, context ) ); + umapFeatureComputer.computeFeature( commonSettings, umapSettings, inputDimensions, graph ); + break; + case TSNE: + AbstractTSneFeatureComputer< V, E, G > tSneFeatureComputer = + isModelGraph ? Cast.unchecked( new SpotTSneFeatureComputer( model, context ) ) + : Cast.unchecked( new BranchTSneFeatureComputer( model, context ) ); + try + { + tSneFeatureComputer.computeFeature( commonSettings, tSneSettings, inputDimensions, graph ); + } + catch ( ArrayIndexOutOfBoundsException e ) + { + logger.error( "Not enough data for t-SNE computation. {}", e.getMessage() ); + throw new ArrayIndexOutOfBoundsException( "Not enough data for t-SNE computation." ); + } + break; + default: + throw new IllegalArgumentException( "Unknown algorithm: " + algorithm ); + } + } + + private < V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, ? > > G getGraph( boolean isSpotGraph ) + { + if ( isSpotGraph ) + return Cast.unchecked( model.getGraph() ); + return Cast.unchecked( model.getBranchGraph() ); + } + + /** + * Gets the vertex and edge type for the dimensionality reduction, i.e. {@link Spot} or {@link BranchSpot}. + * @return the vertex type + */ + public Class< ? extends Vertex< ? > > getVertexType() + { + if ( isModelGraph ) + return Spot.class; + return BranchSpot.class; + } + + /** + * Gets the edge type for the dimensionality reduction, i.e. {@link Link} or {@link BranchLink}. + * @return the edge type + */ + public Class< ? extends Edge< ? > > getEdgeType() + { + if ( isModelGraph ) + return Link.class; + return BranchLink.class; + } + + /** + * Gets from the user preferences, whether the dimensionality reduction is to be computed for the model graph. + * If the preferences are not available, the default value is {@code true}. + * @return {@code true} if the dimensionality reduction is to be computed for the model graph, {@code false} otherwise + */ + public boolean isModelGraphPreferences() + { + return prefs == null || prefs.getBoolean( DimensionalityReductionController.class, IS_MODEL_GRAPH, true ); + } + + /** + * Gets the dimensionality reduction algorithm from the user preferences. + * If the preferences are not available, the default value is {@link DimensionalityReductionAlgorithm#UMAP}. + * @return the dimensionality reduction algorithm + */ + public DimensionalityReductionAlgorithm getAlgorithm() + { + return prefs == null ? DimensionalityReductionAlgorithm.UMAP + : DimensionalityReductionAlgorithm + .valueOf( prefs.get( DimensionalityReductionController.class, DIMENSIONALITY_REDUCTION_ALGORITHM, + DimensionalityReductionAlgorithm.UMAP.name() ) ); + } + + private void loadSettingsFromPreferences() + { + isModelGraph = prefs == null || prefs.getBoolean( DimensionalityReductionController.class, IS_MODEL_GRAPH, true ); + algorithm = DimensionalityReductionAlgorithm.valueOf( prefs == null ? DimensionalityReductionAlgorithm.UMAP.name() + : prefs.get( DimensionalityReductionController.class, DIMENSIONALITY_REDUCTION_ALGORITHM, + DimensionalityReductionAlgorithm.UMAP.name() ) ); + } + + /** + * Saves the dimensionality reduction settings to the user preferences. + */ + public void saveSettingsToPreferences() + { + logger.debug( "Save dimensionality reduction settings." ); + if ( prefs == null ) + return; + prefs.put( DimensionalityReductionController.class, IS_MODEL_GRAPH, isModelGraph ); + prefs.put( DimensionalityReductionController.class, DIMENSIONALITY_REDUCTION_ALGORITHM, algorithm.name() ); + commonSettings.saveSettingsToPreferences( prefs ); + umapSettings.saveSettingsToPreferences( prefs ); + tSneSettings.saveSettingsToPreferences( prefs ); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapPlugin.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionPlugin.java similarity index 75% rename from src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapPlugin.java rename to src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionPlugin.java index eea3cdd3f..57b1420c1 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapPlugin.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionPlugin.java @@ -26,12 +26,12 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap; +package org.mastodon.mamut.feature.dimensionalityreduction; import org.mastodon.app.ui.ViewMenuBuilder; import org.mastodon.mamut.KeyConfigScopes; import org.mastodon.mamut.ProjectModel; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.ui.UmapView; +import org.mastodon.mamut.feature.dimensionalityreduction.ui.DimensionalityReductionView; import org.mastodon.mamut.plugin.MamutPlugin; import org.mastodon.ui.keymap.KeyConfigContexts; import org.scijava.AbstractContextual; @@ -52,11 +52,11 @@ @SuppressWarnings( "unused" ) @Plugin( type = MamutPlugin.class ) -public class UmapPlugin extends AbstractContextual implements MamutPlugin +public class DimensionalityReductionPlugin extends AbstractContextual implements MamutPlugin { - private static final String ACTION_NAME = "UMAP"; + private static final String DIMENSIONALITY_REDUCTION_ACTION_NAME = "Dimensionality reduction"; - private static final String[] SHORT_CUT = { "ctrl alt U" }; + private static final String[] DIMENSIONALITY_REDUCTION_SHORT_CUT = { "ctrl alt D" }; private final AbstractNamedAction action; @@ -67,9 +67,9 @@ public class UmapPlugin extends AbstractContextual implements MamutPlugin private CommandService commandService; @SuppressWarnings( "unused" ) - public UmapPlugin() + public DimensionalityReductionPlugin() { - action = new RunnableAction( ACTION_NAME, this::showUmapDialog ); + action = new RunnableAction( DIMENSIONALITY_REDUCTION_ACTION_NAME, this::showDimensionalityReductionDialog ); } @Override @@ -82,18 +82,18 @@ public void setAppPluginModel( final ProjectModel projectModel ) public List< ViewMenuBuilder.MenuItem > getMenuItems() { return Collections.singletonList( - menu( "Plugins", menu( "Compute feature", menu( "Dimensionality reduction", item( ACTION_NAME ) ) ) ) ); + menu( "Plugins", menu( "Compute feature", item( DIMENSIONALITY_REDUCTION_ACTION_NAME ) ) ) ); } @Override public void installGlobalActions( Actions actions ) { - actions.namedAction( action, SHORT_CUT ); + actions.namedAction( action, DIMENSIONALITY_REDUCTION_SHORT_CUT ); } - private void showUmapDialog() + private void showDimensionalityReductionDialog() { - new UmapView( projectModel.getModel(), getContext() ).setVisible( true ); + new DimensionalityReductionView( projectModel.getModel(), getContext() ).setVisible( true ); } /* @@ -110,7 +110,8 @@ public Descriptions() @Override public void getCommandDescriptions( final CommandDescriptions descriptions ) { - descriptions.add( ACTION_NAME, SHORT_CUT, "Uniform Manifold Approximation and Projection for Dimension Reduction." ); + descriptions.add( DIMENSIONALITY_REDUCTION_ACTION_NAME, DIMENSIONALITY_REDUCTION_SHORT_CUT, + "Dimensionality Reduction of Feature Data using different algorithms, such as UMAP, t-SNE." ); } } } diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneSettings.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneSettings.java new file mode 100644 index 000000000..800558fd8 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneSettings.java @@ -0,0 +1,161 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne; + +import java.lang.invoke.MethodHandles; + +import org.scijava.prefs.PrefService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Settings for the t-SNE feature. + *
+ * Encapsulates the settings for the t-SNE feature, such as: + *
    + *
  • the perplexity to consider in the t-SNE algorithm
  • + *
  • the maximum number of iterations
  • + *
+ */ +public class TSneSettings +{ + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + public static final int DEFAULT_PERPLEXITY = 30; + + public static final int MIN_VALUE_PERPLEXITY = 5; + + public static final int MAX_VALUE_PERPLEXITY = 50; + + public static final int DEFAULT_MAX_ITERATIONS = 1000; + + public static final int MIN_VALUE_MAX_ITERATIONS = 250; + + public static final int MAX_VALUE_MAX_ITERATIONS = 5000; + + public static final int INITIAL_DIMENSIONS = 50; // 50 is the default value in the original t-SNE implementation. It is used to reduce the dimensionality of the input data using PCA before running t-SNE. + + public static final boolean USE_PCA = true; + + public static final double THETA = 0.5d; + + private int perplexity; + + private int maxIterations; + + private static final String PERPLEXITY_SETTING = "Perplexity"; + + private static final String MAX_ITERATIONS_SETTING = "MaxIterations"; + + /** + * Constructor with default values. + * Default values are: + *
    + *
  • perplexity: {@value DEFAULT_PERPLEXITY}
  • + *
  • maximum iterations: {@value DEFAULT_MAX_ITERATIONS}
  • + *
+ */ + public TSneSettings() + { + this( DEFAULT_PERPLEXITY, DEFAULT_MAX_ITERATIONS ); + } + + public TSneSettings( final int perplexity, final int maxIterations ) + { + this.perplexity = perplexity; + this.maxIterations = maxIterations; + } + + public int getPerplexity() + { + return perplexity; + } + + public int getMaxIterations() + { + return maxIterations; + } + + public void setPerplexity( final int perplexity ) + { + this.perplexity = perplexity; + } + + public void setMaxIterations( final int maxIterations ) + { + this.maxIterations = maxIterations; + } + + public static TSneSettings loadSettingsFromPreferences( final PrefService prefs ) + { + int perplexity = prefs == null ? TSneSettings.DEFAULT_PERPLEXITY + : prefs.getInt( TSneSettings.class, PERPLEXITY_SETTING, TSneSettings.DEFAULT_PERPLEXITY ); + int maxIterations = prefs == null ? TSneSettings.DEFAULT_MAX_ITERATIONS + : prefs.getInt( TSneSettings.class, MAX_ITERATIONS_SETTING, TSneSettings.DEFAULT_MAX_ITERATIONS ); + return new TSneSettings( perplexity, maxIterations ); + } + + /** + * Saves the UMAP settings to the user preferences. + */ + public void saveSettingsToPreferences( final PrefService prefs ) + { + logger.debug( "Save t-SNE settings." ); + if ( prefs == null ) + return; + prefs.put( TSneSettings.class, PERPLEXITY_SETTING, getPerplexity() ); + prefs.put( TSneSettings.class, MAX_ITERATIONS_SETTING, getMaxIterations() ); + } + + /** + * Checks if the perplexity is valid for the given number of dataset rows. + * @param rows the number of rows in the dataset + * @return {@code true} if the perplexity is valid, {@code false} otherwise + */ + public boolean isValidPerplexity( final int rows ) + { + return ( double ) rows - 1 >= 3d * perplexity; + } + + /** + * Returns the maximum valid perplexity for the given number of dataset rows. + * @param rows the number of rows in the dataset + * @return the maximum valid perplexity + */ + public int getMaxValidPerplexity( final int rows ) + { + return ( int ) ( ( rows - 1 ) / 3d ); + } + + @Override + public String toString() + { + return "TSneSettings{numberOfNeighbors=" + perplexity + ", minimumDistance=" + maxIterations + '}'; + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeature.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeature.java new file mode 100644 index 000000000..099cacf01 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeature.java @@ -0,0 +1,59 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature; + +import java.util.List; + +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeature; +import org.mastodon.properties.DoublePropertyMap; + +/** + * This generic feature is used to store the t-SNE outputs. + *
+ * The t-SNE outputs are stored in a list of {@link DoublePropertyMap}s. The size of the list is equal to the number of dimensions of the t-SNE output. + */ +public abstract class AbstractTSneFeature< V extends Vertex< ? > > extends AbstractOutputFeature< V > +{ + private static final String PROJECTION_NAME_TEMPLATE = "tSNE%d"; + + protected static final String HELP_STRING = + "Computes the t-SNE according to the selected input dimensions, number of target dimensions, the perplexity value and maximum number of iterations."; + + protected AbstractTSneFeature( final List< DoublePropertyMap< V > > umapOutputMaps ) + { + super( umapOutputMaps ); + } + + @Override + protected String getProjectionNameTemplate() + { + return PROJECTION_NAME_TEMPLATE; + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeatureComputer.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeatureComputer.java new file mode 100644 index 000000000..6a77277ea --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeatureComputer.java @@ -0,0 +1,124 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature; + +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import com.jujutsu.tsne.TSneConfiguration; +import com.jujutsu.tsne.barneshut.BarnesHutTSne; +import com.jujutsu.tsne.barneshut.ParallelBHTsne; +import com.jujutsu.utils.TSneUtils; + +import org.mastodon.RefPool; +import org.mastodon.graph.Edge; +import org.mastodon.graph.ReadOnlyGraph; +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeatureComputer; +import org.mastodon.mamut.feature.dimensionalityreduction.CommonSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.TSneSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; +import org.mastodon.mamut.model.Model; +import org.mastodon.properties.DoublePropertyMap; +import org.scijava.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract class for computing t-SNE features in the Mastodon project. + *
+ * This provides the base implementation for computing t-SNE features on vertices in a read-only graph. + * It handles the setup, execution, and caching of t-SNE computations. + *
+ * This class connects the t-SNE library to the Mastodon project by providing the necessary data and settings. + * It ensures that only valid data rows (i.e. rows where the selected feature projections do not have values, such as {@link Double#NaN} or {@link Double#POSITIVE_INFINITY}) are used for t-SNE computations. + * + * @param the type of vertex + * @param the type of read-only graph + */ +public abstract class AbstractTSneFeatureComputer< V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, E > > + extends AbstractOutputFeatureComputer< V, E, G > +{ + + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private TSneSettings tSneSettings; + + private double[][] tSneResult; + + protected AbstractTSneFeatureComputer( final Model model, final Context context ) + { + super( model, context ); + } + + public void computeFeature( final CommonSettings commonSettings, final TSneSettings tSneSettings, + final List< InputDimension< V > > inputDimensions, final G graph ) + { + this.tSneSettings = tSneSettings; + super.computeFeature( commonSettings, inputDimensions, graph ); + } + + @Override + protected void computeAlgorithm( double[][] dataMatrix ) + { + int rows = dataMatrix.length; + if ( !tSneSettings.isValidPerplexity( rows ) ) + { + logger.error( + "For t-SNE, the number of valid rows in the dataset ({}) requires the perplexity ({}) to not be higher than ({}).", + rows, tSneSettings.getPerplexity(), tSneSettings.getMaxValidPerplexity( rows ) ); + throw new IllegalArgumentException( "For t-SNE, the number of valid rows in the dataset (" + rows + + ") requires the perplexity (" + tSneSettings.getPerplexity() + ") to not be higher than (" + + tSneSettings.getMaxValidPerplexity( rows ) + ")." ); + } + TSneConfiguration tSneConfig = + TSneUtils.buildConfig( dataMatrix, settings.getNumberOfOutputDimensions(), TSneSettings.INITIAL_DIMENSIONS, + tSneSettings.getPerplexity(), + tSneSettings.getMaxIterations(), TSneSettings.USE_PCA, TSneSettings.THETA, false, true ); + + BarnesHutTSne tsne = new ParallelBHTsne(); + logger.info( "Computing t-SNE. Data matrix has {} rows x {} columns.", dataMatrix.length, dataMatrix[ 0 ].length ); + tSneResult = tsne.tsne( tSneConfig ); + logger.info( "Finished computing t-SNE. Results has {} rows x {} columns.", tSneResult.length, + tSneResult.length > 0 ? tSneResult[ 0 ].length : 0 ); + } + + @Override + protected double[][] getResult() + { + return tSneResult; + } + + protected abstract AbstractTSneFeature< V > createFeatureInstance( final List< DoublePropertyMap< V > > umapOutputMaps ); + + protected abstract RefPool< V > getRefPool(); + + protected abstract ReentrantReadWriteLock getLock( final G graph ); +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionView.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionView.java new file mode 100644 index 000000000..9ad578064 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionView.java @@ -0,0 +1,394 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.ui; + +import net.imglib2.util.Cast; +import net.miginfocom.swing.MigLayout; +import org.mastodon.feature.FeatureModel; +import org.mastodon.graph.Edge; +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionAlgorithm; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionController; +import org.mastodon.mamut.feature.dimensionalityreduction.CommonSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.TSneSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapSettings; +import org.mastodon.mamut.model.Model; +import org.scijava.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSpinner; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.WindowConstants; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.lang.invoke.MethodHandles; +import java.util.Objects; + +/** + * This class represents the user interface for performing dimensionality reduction using different algorithms. + * It provides various input fields and buttons to configure and execute the selected algorithm. + * The selected algorithm is executed asynchronously to avoid blocking the user interface. + *
+ */ +public class DimensionalityReductionView extends JFrame +{ + + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + public static final String SPLIT_2 = "split 2"; + + public static final String WMIN_35_WRAP = "wmin 35, wrap"; + + private final JPanel canvas; + + private final JRadioButton modelGraphRadioButton; + + private final JRadioButton branchGraphRadioButton; + + private final JRadioButton umapRadioButton; + + private final JRadioButton tsneRadioButton; + + private final JCheckBox standardizeFeaturesCheckBox; + + private final JSpinner numberOfDimensionsInput; + + private final JSpinner numberOfNeighborsInput; + + private final JSpinner minimumDistanceInput; + + private final JSpinner perplexityInput; + + private final JSpinner maxIterationsInput; + + private final JPanel algorithmSpecificSettingsPanel; + + private InputDimensionsPanel< ?, ? > inputDimensionsPanel; + + private final JLabel feedbackLabel; + + private final JButton computeButton; + + private final ImageIcon loadingIcon; + + private final FeatureModel featureModel; + + private final DimensionalityReductionController controller; + + private static final String PANEL_CONSTRAINTS = "span, growx, pushx, growy, pushy, wrap"; + + public DimensionalityReductionView( final Model model, final Context context ) + { + this.featureModel = model.getFeatureModel(); + this.controller = new DimensionalityReductionController( model, context ); + + canvas = new JPanel( new MigLayout( "insets 0 10 0 10, fill", "", "" ) ); + setTitle( "Dimensionality Reduction" ); + setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE ); + setSize( 600, 600 ); + setLocationRelativeTo( null ); + + modelGraphRadioButton = new JRadioButton( "Model Graph Features" ); + branchGraphRadioButton = new JRadioButton( "Branch Graph Features" ); + + umapRadioButton = new JRadioButton( "UMAP" ); + tsneRadioButton = new JRadioButton( "t-SNE" ); + + // Common settings + standardizeFeaturesCheckBox = new JCheckBox( "Standardize features" ); + numberOfDimensionsInput = new JSpinner(); + // UMAP settings + numberOfNeighborsInput = new JSpinner(); + minimumDistanceInput = new JSpinner(); + // t-SNE settings + perplexityInput = new JSpinner(); + maxIterationsInput = new JSpinner(); + + algorithmSpecificSettingsPanel = new JPanel( new MigLayout( "insets 0 0 0 0, fill", "", "" ) ); + addAlgorithmSpecificSettings( controller.getAlgorithm() ); + inputDimensionsPanel = createInputDimensionsPanel(); + feedbackLabel = new JLabel(); + computeButton = new JButton( "Compute" ); + loadingIcon = new ImageIcon( Objects.requireNonNull( getClass().getResource( "loading.gif" ) ) ); + + initSettings(); + initBehavior(); + initLayout(); + initToolTips(); + } + + private void initSettings() + { + logger.debug( "Initializing dimensionality reduction settings." ); + boolean isModelGraph = controller.isModelGraphPreferences(); + modelGraphRadioButton.setSelected( isModelGraph ); + branchGraphRadioButton.setSelected( !isModelGraph ); + DimensionalityReductionAlgorithm algorithm = controller.getAlgorithm(); + umapRadioButton.setSelected( algorithm == DimensionalityReductionAlgorithm.UMAP ); + tsneRadioButton.setSelected( algorithm == DimensionalityReductionAlgorithm.TSNE ); + CommonSettings settings = controller.getCommonSettings(); + UmapSettings umapSettings = controller.getUmapSettings(); + TSneSettings tSneSettings = controller.getTSneSettings(); + standardizeFeaturesCheckBox.setSelected( settings.isStandardizeFeatures() ); + numberOfDimensionsInput.setModel( getNumberOfDimensionsSpinnerModel() ); + numberOfNeighborsInput.setModel( new SpinnerNumberModel( controller.getUmapSettings().getNumberOfNeighbors(), + UmapSettings.MIN_VALUE_NUMBER_OF_NEIGHBORS, UmapSettings.MAX_VALUE_NUMBER_OF_NEIGHBORS, 1 ) ); + minimumDistanceInput.setModel( new SpinnerNumberModel( umapSettings.getMinimumDistance(), UmapSettings.MIN_VALUE_MINIMUM_DISTANCE, + UmapSettings.MAX_VALUE_MINIMUM_DISTANCE, 0.1d ) ); + perplexityInput.setModel( new SpinnerNumberModel( tSneSettings.getPerplexity(), TSneSettings.MIN_VALUE_PERPLEXITY, + TSneSettings.MAX_VALUE_PERPLEXITY, 1 ) ); + maxIterationsInput.setModel( new SpinnerNumberModel( tSneSettings.getMaxIterations(), TSneSettings.MIN_VALUE_MAX_ITERATIONS, + TSneSettings.MAX_VALUE_MAX_ITERATIONS, 1 ) ); + } + + private void initBehavior() + { + modelGraphRadioButton.addActionListener( e -> updateInputDimensionsPanel() ); + branchGraphRadioButton.addActionListener( e -> updateInputDimensionsPanel() ); + + ButtonGroup graphGroup = new ButtonGroup(); + graphGroup.add( modelGraphRadioButton ); + graphGroup.add( branchGraphRadioButton ); + + umapRadioButton.addActionListener( e -> updateAlgorithmSettings() ); + tsneRadioButton.addActionListener( e -> updateAlgorithmSettings() ); + + ButtonGroup algorithmGroup = new ButtonGroup(); + algorithmGroup.add( umapRadioButton ); + algorithmGroup.add( tsneRadioButton ); + + CommonSettings commonSettings = controller.getCommonSettings(); + UmapSettings umapSettings = controller.getUmapSettings(); + TSneSettings tsneSettings = controller.getTSneSettings(); + standardizeFeaturesCheckBox + .addActionListener( e -> commonSettings.setStandardizeFeatures( standardizeFeaturesCheckBox.isSelected() ) ); + numberOfDimensionsInput + .addChangeListener( e -> commonSettings.setNumberOfOutputDimensions( ( int ) numberOfDimensionsInput.getValue() ) ); + numberOfNeighborsInput.addChangeListener( e -> umapSettings.setNumberOfNeighbors( ( int ) numberOfNeighborsInput.getValue() ) ); + minimumDistanceInput.addChangeListener( e -> umapSettings.setMinimumDistance( ( double ) minimumDistanceInput.getValue() ) ); + perplexityInput.addChangeListener( e -> tsneSettings.setPerplexity( ( int ) perplexityInput.getValue() ) ); + maxIterationsInput.addChangeListener( e -> tsneSettings.setMaxIterations( ( int ) maxIterationsInput.getValue() ) ); + computeButton.addActionListener( e -> SwingUtilities.invokeLater( this::run ) ); + + addWindowListener( new WindowAdapter() + { + @Override + public void windowClosing( WindowEvent e ) + { + onWindowClosing(); + } + } ); + } + + private void initLayout() + { + add( canvas, BorderLayout.CENTER ); + + canvas.add( new JLabel( "Graph type:" ), "split 3" ); + canvas.add( modelGraphRadioButton ); + canvas.add( branchGraphRadioButton, "wrap" ); + canvas.add( new JLabel( "Algorithm:" ), "split 3" ); + canvas.add( umapRadioButton ); + canvas.add( tsneRadioButton, "wrap" ); + canvas.add( standardizeFeaturesCheckBox, "wrap" ); + canvas.add( new JLabel( "Number of dimensions:" ), SPLIT_2 ); + canvas.add( numberOfDimensionsInput, WMIN_35_WRAP ); + canvas.add( algorithmSpecificSettingsPanel, "wrap" ); + canvas.add( inputDimensionsPanel, PANEL_CONSTRAINTS ); + canvas.add( computeButton, "dock south, gapleft 10, gapbottom 10, wmax 150, wrap" ); + canvas.add( feedbackLabel, "dock south, gapleft 10, gapbottom 10, wrap" ); + } + + private void initToolTips() + { + standardizeFeaturesCheckBox.setToolTipText( + "Whether to standardize the data before reducing the dimensionality." + + "
Standardization is recommended when the data has different scales / units." ); + numberOfDimensionsInput + .setToolTipText( "The number of reduced dimensions to use.
The default is 2, but 3 is also common." ); + numberOfNeighborsInput.setToolTipText( + "The size of the local neighborhood (in terms of number of neighboring sample points) used for manifold approximation." + + "
Larger values result in more global views of the manifold, while smaller values result in more local data being preserved." + + "
In general, it should be in the range 2 to 100." ); + minimumDistanceInput.setToolTipText( + "The minimum distance that points are allowed to be apart from each other in the low dimensional representation." + + "
This parameter controls how tightly UMAP is allowed to pack points together." ); + perplexityInput.setToolTipText( + "The perplexity is related to the number of nearest neighbors that is used in other manifold learning algorithms. Larger datasets usually require a larger perplexity.
" + + "Consider selecting a value between 5 and 50. Different values can result in significantly different results.
" + + "The perplexity must be less than the number of samples." ); + maxIterationsInput.setToolTipText( "The maximum number of iterations for the optimization.
" + + "The optimization algorithm will stop when the maximum number of iterations is reached.
" + + "Should be at least 250. More iterations will give more accurate results, but will also take longer to compute" ); + } + + private void updateInputDimensionsPanel() + { + logger.debug( "Updating input dimensions panel." ); + canvas.remove( inputDimensionsPanel ); + controller.setModelGraph( modelGraphRadioButton.isSelected() ); + inputDimensionsPanel = createInputDimensionsPanel(); + canvas.add( inputDimensionsPanel, PANEL_CONSTRAINTS ); + revalidate(); + repaint(); + numberOfDimensionsInput.setModel( getNumberOfDimensionsSpinnerModel() ); + } + + private void updateAlgorithmSettings() + { + algorithmSpecificSettingsPanel.removeAll(); + DimensionalityReductionAlgorithm algorithm; + if ( umapRadioButton.isSelected() ) + algorithm = DimensionalityReductionAlgorithm.UMAP; + else + algorithm = DimensionalityReductionAlgorithm.TSNE; + controller.setAlgorithm( algorithm ); + addAlgorithmSpecificSettings( algorithm ); + revalidate(); + repaint(); + } + + private void addAlgorithmSpecificSettings( final DimensionalityReductionAlgorithm algorithm ) + { + logger.debug( "Adding algorithm specific settings for {}.", algorithm ); + switch ( algorithm ) + { + case UMAP: + algorithmSpecificSettingsPanel.add( new JLabel( "Number of neighbors:" ), SPLIT_2 ); + algorithmSpecificSettingsPanel.add( numberOfNeighborsInput, WMIN_35_WRAP ); + algorithmSpecificSettingsPanel.add( new JLabel( "Minimum distance:" ), SPLIT_2 ); + algorithmSpecificSettingsPanel.add( minimumDistanceInput, "wmin 40, wrap" ); + break; + case TSNE: + algorithmSpecificSettingsPanel.add( new JLabel( "Perplexity:" ), SPLIT_2 ); + algorithmSpecificSettingsPanel.add( perplexityInput, WMIN_35_WRAP ); + algorithmSpecificSettingsPanel.add( new JLabel( "Maximum number of iterations:" ), SPLIT_2 ); + algorithmSpecificSettingsPanel.add( maxIterationsInput, WMIN_35_WRAP ); + break; + default: + break; + } + } + + private < V extends Vertex< E >, E extends Edge< V > > InputDimensionsPanel< V, E > createInputDimensionsPanel() + { + return new InputDimensionsPanel<>( Cast.unchecked( controller.getVertexType() ), + Cast.unchecked( controller.getEdgeType() ), featureModel ); + } + + private SpinnerModel getNumberOfDimensionsSpinnerModel() + { + int maximumNumberOfDimensions = Math.max( 2, inputDimensionsPanel.getNumberOfFeatures() ); + int numberOfDimensions = Math.min( controller.getCommonSettings().getNumberOfOutputDimensions(), maximumNumberOfDimensions ); + return new SpinnerNumberModel( numberOfDimensions, CommonSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS, + maximumNumberOfDimensions, 1 ); + } + + // Method called when the JFrame is closed + private void onWindowClosing() + { + // Perform any cleanup or actions needed before closing the window + logger.debug( "View is closing." ); + controller.saveSettingsToPreferences(); + // Dispose the window + dispose(); + } + + private void run() + { + beforeRun(); + executeAsynchronously(); + } + + private void beforeRun() + { + computeButton.setEnabled( false ); + computeButton.setIcon( loadingIcon ); + feedbackLabel.setText( "Computing ..." ); + feedbackLabel.setForeground( Color.BLACK ); + repaint(); + } + + private void executeAsynchronously() + { + SwingWorker< Void, Void > worker = new SwingWorker< Void, Void >() + { + @Override + protected Void doInBackground() + { + controller.computeFeature( inputDimensionsPanel ); + return null; + } + + @Override + protected void done() + { + try + { + get(); + executionCompleted( "Successfully computed dimensionality reduction.", new Color( 0, 100, 0 ) ); + } + catch ( Exception e ) + { + String message = e.getCause().getMessage(); + logger.error( "Running dimensionality reduction failed. {}", message, e.getCause() ); + executionCompleted( message, Color.RED ); + Thread.currentThread().interrupt(); + } + finally + { + computeButton.setEnabled( true ); + } + } + }; + worker.execute(); + } + + private void executionCompleted( final String message, final Color color ) + { + feedbackLabel.setForeground( color ); + feedbackLabel.setText( message ); + computeButton.setIcon( null ); + repaint(); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapInputDimensionsPanel.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/InputDimensionsPanel.java similarity index 80% rename from src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapInputDimensionsPanel.java rename to src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/InputDimensionsPanel.java index a3502ea36..59c7df039 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapInputDimensionsPanel.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/InputDimensionsPanel.java @@ -26,13 +26,13 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.ui; +package org.mastodon.mamut.feature.dimensionalityreduction.ui; import net.miginfocom.swing.MigLayout; import org.mastodon.feature.FeatureModel; import org.mastodon.graph.Edge; import org.mastodon.graph.Vertex; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.UmapInputDimension; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; import javax.swing.DefaultListModel; import javax.swing.JLabel; @@ -43,12 +43,12 @@ import java.util.List; import java.util.function.Supplier; -class UmapInputDimensionsPanel< V extends Vertex< E >, E extends Edge< V > > extends JPanel - implements Supplier< List< UmapInputDimension< V > > > +public class InputDimensionsPanel< V extends Vertex< E >, E extends Edge< V > > extends JPanel + implements Supplier< List< InputDimension< V > > > { - private final JList< UmapInputDimension< V > > featureList; + private final JList< InputDimension< V > > featureList; - private final DefaultListModel< UmapInputDimension< V > > listModel; + private final DefaultListModel< InputDimension< V > > listModel; private final Class< V > vertexType; @@ -56,7 +56,7 @@ class UmapInputDimensionsPanel< V extends Vertex< E >, E extends Edge< V > > ext private final FeatureModel featureModel; - UmapInputDimensionsPanel( final Class< V > vertexType, final Class< E > edgeType, final FeatureModel featureModel ) + public InputDimensionsPanel( final Class< V > vertexType, final Class< E > edgeType, final FeatureModel featureModel ) { super( new MigLayout( "insets 0 0 0 0, fill", "", "" ) ); this.vertexType = vertexType; @@ -96,19 +96,20 @@ private void selectAllDimensions() private void updateItemList() { listModel.clear(); - List< UmapInputDimension< V > > items = UmapInputDimension.getListFromFeatureModel( featureModel, vertexType, edgeType ); - for ( UmapInputDimension< V > item : items ) + List< InputDimension< V > > items = + InputDimension.getListFromFeatureModel( featureModel, vertexType, edgeType ); + for ( InputDimension< V > item : items ) listModel.addElement( item ); } - int getNumberOfFeatures() + public int getNumberOfFeatures() { featureList.getSelectedValuesList(); return listModel.getSize(); } @Override - public List< UmapInputDimension< V > > get() + public List< InputDimension< V > > get() { return featureList.getSelectedValuesList(); } diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapController.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapController.java deleted file mode 100644 index 9a94abb8e..000000000 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapController.java +++ /dev/null @@ -1,221 +0,0 @@ -/*- - * #%L - * mastodon-deep-lineage - * %% - * Copyright (C) 2022 - 2024 Stefan Hahmann - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap; - -import net.imglib2.util.Cast; -import org.mastodon.graph.Edge; -import org.mastodon.graph.ReadOnlyGraph; -import org.mastodon.graph.Vertex; -import org.mastodon.mamut.feature.branch.dimensionalityreduction.umap.BranchUmapFeatureComputer; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.feature.AbstractUmapFeatureComputer; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.UmapInputDimension; -import org.mastodon.mamut.feature.spot.dimensionalityreduction.umap.SpotUmapFeatureComputer; -import org.mastodon.mamut.model.Link; -import org.mastodon.mamut.model.Model; -import org.mastodon.mamut.model.Spot; -import org.mastodon.mamut.model.branch.BranchLink; -import org.mastodon.mamut.model.branch.BranchSpot; -import org.scijava.Context; -import org.scijava.prefs.PrefService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.function.Supplier; - -/** - * Controller for the UMAP feature computation. - *
- * This class provides methods to compute the UMAP feature for selected input dimensions. - * It also handles the user preferences for the UMAP feature settings. - */ -public class UmapController -{ - private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); - - private boolean running; - - private final Context context; - - private final Model model; - - private final PrefService prefs; - - private boolean isModelGraph; - - private final UmapFeatureSettings settings; - - private static final String IS_MODEL_GRAPH = "IsModelGraph"; - - private static final String NUMBER_OF_DIMENSIONS_SETTING = "NumberOfDimensions"; - - private static final String NUMBER_OF_NEIGHBORS_SETTING = "NumberOfNeighbors"; - - private static final String MINIMUM_DISTANCE_SETTING = "MinimumDistance"; - - private static final String STANDARDIZE_FEATURES_SETTING = "StandardizeFeatures"; - - public UmapController( final Model model, final Context context ) - { - this.model = model; - this.prefs = context.getService( PrefService.class ); - this.context = context; - this.settings = loadSettingsFromPreferences(); - } - - /** - * Gets the UMAP feature settings. - * @return the UMAP feature settings - */ - public UmapFeatureSettings getFeatureSettings() - { - return settings; - } - - /** - * Computes the UMAP feature for the selected input dimensions. - *
- * Since the UMAP computation is computationally expensive, this method prevents multiple executions of itself at the same time. - * @param inputDimensionsSupplier a supplier for the selected input dimensions - */ - public < V extends Vertex< E >, E extends Edge< V > > void - computeFeature( final Supplier< List< UmapInputDimension< V > > > inputDimensionsSupplier ) - { - if ( running ) - { - logger.debug( "UMAP computation currently running." ); - return; - } - - try - { - running = true; - if ( inputDimensionsSupplier != null ) - updateFeature( inputDimensionsSupplier.get() ); - } - finally - { - running = false; - } - } - - /** - * Sets the graph type for the UMAP feature. - * @param isModelGraph {@code true} if the UMAP feature is to be computed for the model graph, {@code false} for the branch graph - */ - public void setModelGraph( final boolean isModelGraph ) - { - this.isModelGraph = isModelGraph; - } - - private < V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, E > > void - updateFeature( final List< UmapInputDimension< V > > inputDimensions ) - { - if ( inputDimensions.isEmpty() ) - throw new IllegalArgumentException( "No features selected." ); - if ( settings.getNumberOfOutputDimensions() >= inputDimensions.size() ) - throw new IllegalArgumentException( "Number of output dimensions (" + settings.getNumberOfOutputDimensions() - + ") must be smaller than the number of input features (" + inputDimensions.size() + ")." ); - G graph = getGraph( isModelGraph ); - AbstractUmapFeatureComputer< V, E, G > umapFeatureComputer = - isModelGraph ? Cast.unchecked( new SpotUmapFeatureComputer( model, context ) ) - : Cast.unchecked( new BranchUmapFeatureComputer( model, context ) ); - umapFeatureComputer.computeFeature( settings, inputDimensions, graph ); - } - - private < V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, ? > > G getGraph( boolean isSpotGraph ) - { - if ( isSpotGraph ) - return Cast.unchecked( model.getGraph() ); - return Cast.unchecked( model.getBranchGraph() ); - } - - /** - * Gets the vertex and edge type for the UMAP feature, i.e. {@link Spot} or {@link BranchSpot}. - * @return the vertex type - */ - public Class< ? extends Vertex< ? > > getVertexType() - { - if ( isModelGraph ) - return Spot.class; - return BranchSpot.class; - } - - /** - * Gets the edge type for the UMAP feature, i.e. {@link Link} or {@link BranchLink}. - * @return the edge type - */ - public Class< ? extends Edge< ? > > getEdgeType() - { - if ( isModelGraph ) - return Link.class; - return BranchLink.class; - } - - /** - * Gets from the user preferences, whether the UMAP feature is to be computed for the model graph. - * If the preferences are not available, the default value is {@code true}. - * @return {@code true} if the UMAP feature is to be computed for the model graph, {@code false} otherwise - */ - public boolean isModelGraphPreferences() - { - return prefs == null || prefs.getBoolean( UmapController.class, IS_MODEL_GRAPH, true ); - } - - private UmapFeatureSettings loadSettingsFromPreferences() - { - isModelGraph = prefs == null || prefs.getBoolean( UmapController.class, IS_MODEL_GRAPH, true ); - boolean standardizeFeatures = prefs == null - || prefs.getBoolean( UmapController.class, STANDARDIZE_FEATURES_SETTING, UmapFeatureSettings.DEFAULT_STANDARDIZE_FEATURES ); - int numberOfDimensions = prefs == null ? UmapFeatureSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS - : prefs.getInt( UmapController.class, NUMBER_OF_DIMENSIONS_SETTING, - UmapFeatureSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS ); - int numberOfNeighbours = prefs == null ? UmapFeatureSettings.DEFAULT_NUMBER_OF_NEIGHBORS - : prefs.getInt( UmapController.class, NUMBER_OF_NEIGHBORS_SETTING, UmapFeatureSettings.DEFAULT_NUMBER_OF_NEIGHBORS ); - double minimumDistance = prefs == null ? UmapFeatureSettings.DEFAULT_MINIMUM_DISTANCE - : prefs.getDouble( UmapController.class, MINIMUM_DISTANCE_SETTING, UmapFeatureSettings.DEFAULT_MINIMUM_DISTANCE ); - return new UmapFeatureSettings( numberOfDimensions, numberOfNeighbours, minimumDistance, standardizeFeatures ); - } - - /** - * Saves the UMAP settings to the user preferences. - */ - public void saveSettingsToPreferences() - { - logger.debug( "Save UMAP settings." ); - if ( prefs == null ) - return; - prefs.put( UmapController.class, IS_MODEL_GRAPH, isModelGraph ); - prefs.put( UmapController.class, STANDARDIZE_FEATURES_SETTING, settings.isStandardizeFeatures() ); - prefs.put( UmapController.class, NUMBER_OF_DIMENSIONS_SETTING, settings.getNumberOfOutputDimensions() ); - prefs.put( UmapController.class, NUMBER_OF_NEIGHBORS_SETTING, settings.getNumberOfNeighbors() ); - prefs.put( UmapController.class, MINIMUM_DISTANCE_SETTING, settings.getMinimumDistance() ); - } -} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapFeatureSettings.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapSettings.java similarity index 59% rename from src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapFeatureSettings.java rename to src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapSettings.java index 11246e0a1..abf76f107 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapFeatureSettings.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapSettings.java @@ -28,6 +28,12 @@ */ package org.mastodon.mamut.feature.dimensionalityreduction.umap; +import java.lang.invoke.MethodHandles; + +import org.scijava.prefs.PrefService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Settings for the UMAP feature. *
@@ -35,60 +41,49 @@ *
    *
  • the number of neighbors to consider in the umap algorithm
  • *
  • the minimum distance between points
  • - *
  • whether to standardize features
  • - *
  • the number of output dimensions
  • *
*/ -public class UmapFeatureSettings +public class UmapSettings { - public static final int DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS = 2; + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); public static final int DEFAULT_NUMBER_OF_NEIGHBORS = 15; + public static final int MIN_VALUE_NUMBER_OF_NEIGHBORS = 1; + + public static final int MAX_VALUE_NUMBER_OF_NEIGHBORS = 100; + public static final double DEFAULT_MINIMUM_DISTANCE = 0.1d; - public static final boolean DEFAULT_STANDARDIZE_FEATURES = true; + public static final double MIN_VALUE_MINIMUM_DISTANCE = 0.1d; - private int numberOfOutputDimensions; + public static final double MAX_VALUE_MINIMUM_DISTANCE = 1d; private int numberOfNeighbors; private double minimumDistance; - private boolean standardizeFeatures; + private static final String NUMBER_OF_NEIGHBORS_SETTING = "NumberOfNeighbors"; + + private static final String MINIMUM_DISTANCE_SETTING = "MinimumDistance"; /** * Constructor with default values. * Default values are: *
    - *
  • number of dimensions: {@value DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS}
  • *
  • number of neighbors: {@value DEFAULT_NUMBER_OF_NEIGHBORS}
  • *
  • minimum distance: {@value DEFAULT_MINIMUM_DISTANCE}
  • - *
  • standardize features: {@value DEFAULT_STANDARDIZE_FEATURES}
  • *
*/ - public UmapFeatureSettings() + public UmapSettings() { - this( DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS, DEFAULT_NUMBER_OF_NEIGHBORS, DEFAULT_MINIMUM_DISTANCE, DEFAULT_STANDARDIZE_FEATURES ); + this( DEFAULT_NUMBER_OF_NEIGHBORS, DEFAULT_MINIMUM_DISTANCE ); } - /** - * Constructor with number of neighbors. - * - * @param numberOfOutputDimensions the number of neighbors to consider for relative movement. - */ - public UmapFeatureSettings( final int numberOfOutputDimensions, final int numberOfNeighbors, final double minimumDistance, - final boolean standardizeFeatures ) + public UmapSettings( final int numberOfNeighbors, final double minimumDistance ) { - this.numberOfOutputDimensions = numberOfOutputDimensions; this.numberOfNeighbors = numberOfNeighbors; this.minimumDistance = minimumDistance; - this.standardizeFeatures = standardizeFeatures; - } - - public int getNumberOfOutputDimensions() - { - return numberOfOutputDimensions; } public int getNumberOfNeighbors() @@ -101,16 +96,6 @@ public double getMinimumDistance() return minimumDistance; } - public boolean isStandardizeFeatures() - { - return standardizeFeatures; - } - - public void setNumberOfOutputDimensions( final int numberOfOutputDimensions ) - { - this.numberOfOutputDimensions = numberOfOutputDimensions; - } - public void setNumberOfNeighbors( final int numberOfNeighbors ) { this.numberOfNeighbors = numberOfNeighbors; @@ -121,15 +106,30 @@ public void setMinimumDistance( final double minimumDistance ) this.minimumDistance = minimumDistance; } - public void setStandardizeFeatures( final boolean standardizeFeatures ) + public static UmapSettings loadSettingsFromPreferences( final PrefService prefs ) + { + int numberOfNeighbours = prefs == null ? UmapSettings.DEFAULT_NUMBER_OF_NEIGHBORS + : prefs.getInt( UmapSettings.class, NUMBER_OF_NEIGHBORS_SETTING, UmapSettings.DEFAULT_NUMBER_OF_NEIGHBORS ); + double minimumDistance = prefs == null ? UmapSettings.DEFAULT_MINIMUM_DISTANCE + : prefs.getDouble( UmapSettings.class, MINIMUM_DISTANCE_SETTING, UmapSettings.DEFAULT_MINIMUM_DISTANCE ); + return new UmapSettings( numberOfNeighbours, minimumDistance ); + } + + /** + * Saves the UMAP settings to the user preferences. + */ + public void saveSettingsToPreferences( final PrefService prefs ) { - this.standardizeFeatures = standardizeFeatures; + logger.debug( "Save UMAP settings." ); + if ( prefs == null ) + return; + prefs.put( UmapSettings.class, NUMBER_OF_NEIGHBORS_SETTING, getNumberOfNeighbors() ); + prefs.put( UmapSettings.class, MINIMUM_DISTANCE_SETTING, getMinimumDistance() ); } @Override public String toString() { - return "UmapFeatureSettings{" + "numberOfOutputDimensions=" + numberOfOutputDimensions + ", numberOfNeighbors=" + numberOfNeighbors - + ", minimumDistance=" + minimumDistance + ", standardizeFeatures=" + standardizeFeatures + '}'; + return "UmapSettings{numberOfNeighbors=" + numberOfNeighbors + ", minimumDistance=" + minimumDistance + '}'; } } diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeature.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeature.java index 58b2109b3..7f358d518 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeature.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeature.java @@ -28,87 +28,32 @@ */ package org.mastodon.mamut.feature.dimensionalityreduction.umap.feature; -import org.mastodon.feature.Dimension; -import org.mastodon.feature.Feature; -import org.mastodon.feature.FeatureProjection; -import org.mastodon.feature.FeatureProjectionKey; -import org.mastodon.feature.FeatureProjectionSpec; -import org.mastodon.feature.FeatureProjections; -import org.mastodon.graph.Vertex; -import org.mastodon.mamut.feature.ValueIsSetEvaluator; -import org.mastodon.properties.DoublePropertyMap; - -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.Set; -import static org.mastodon.feature.FeatureProjectionKey.key; +import org.mastodon.graph.Vertex; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeature; +import org.mastodon.properties.DoublePropertyMap; /** * This generic feature is used to store the UMAP outputs. *
* The UMAP outputs are stored in a list of {@link DoublePropertyMap}s. The size of the list is equal to the number of dimensions of the UMAP output. */ -public abstract class AbstractUmapFeature< V extends Vertex< ? > > implements Feature< V >, ValueIsSetEvaluator< V > +public abstract class AbstractUmapFeature< V extends Vertex< ? > > extends AbstractOutputFeature< V > { private static final String PROJECTION_NAME_TEMPLATE = "UMAP%d"; protected static final String HELP_STRING = - "Computes the umap according to the selected dimensions, the selected number of target dimensions and the minimum distance value."; - - private final List< DoublePropertyMap< V > > umapOutputMaps; - - protected final Map< FeatureProjectionKey, FeatureProjection< V > > projectionMap; + "Computes the UMAP according to the selected input dimensions, number of target dimensions, the minimum distance value and the number of neighbors."; protected AbstractUmapFeature( final List< DoublePropertyMap< V > > umapOutputMaps ) { - this.umapOutputMaps = umapOutputMaps; - this.projectionMap = new LinkedHashMap<>( 2 ); - for ( int i = 0; i < umapOutputMaps.size(); i++ ) - { - FeatureProjectionSpec projectionSpec = new FeatureProjectionSpec( getProjectionName( i ), Dimension.NONE ); - final FeatureProjectionKey key = key( projectionSpec ); - projectionMap.put( key, FeatureProjections.project( key, getUmapOutputMaps().get( i ), Dimension.NONE_UNITS ) ); - } - } - - public String getProjectionName( final int outputDimension ) - { - return String.format( PROJECTION_NAME_TEMPLATE, outputDimension + 1 ); - } - - public List< DoublePropertyMap< V > > getUmapOutputMaps() - { - return umapOutputMaps; + super( umapOutputMaps ); } @Override - public void invalidate( V vertex ) + protected String getProjectionNameTemplate() { - getUmapOutputMaps().forEach( map -> map.remove( vertex ) ); + return PROJECTION_NAME_TEMPLATE; } - - @Override - public boolean valueIsSet( final V vertex ) - { - for ( final DoublePropertyMap< V > map : getUmapOutputMaps() ) - if ( !map.isSet( vertex ) ) - return false; - return true; - } - - @Override - public FeatureProjection< V > project( final FeatureProjectionKey key ) - { - return projectionMap.get( key ); - } - - @Override - public Set< FeatureProjection< V > > projections() - { - return new LinkedHashSet<>( projectionMap.values() ); - } - } diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeatureComputer.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeatureComputer.java index 07a7387aa..2d1ce13c5 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeatureComputer.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/feature/AbstractUmapFeatureComputer.java @@ -28,30 +28,25 @@ */ package org.mastodon.mamut.feature.dimensionalityreduction.umap.feature; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; + import org.mastodon.RefPool; -import org.mastodon.collection.RefIntMap; -import org.mastodon.collection.ref.RefIntHashMap; import org.mastodon.graph.Edge; import org.mastodon.graph.ReadOnlyGraph; import org.mastodon.graph.Vertex; -import org.mastodon.mamut.feature.AbstractSerialFeatureComputer; -import org.mastodon.mamut.feature.ValueIsSetEvaluator; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapFeatureSettings; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.StandardScaler; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.UmapInputDimension; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeatureComputer; +import org.mastodon.mamut.feature.dimensionalityreduction.CommonSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; import org.mastodon.mamut.model.Model; import org.mastodon.properties.DoublePropertyMap; import org.scijava.Context; -import org.scijava.app.StatusService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import tagbio.umap.Umap; -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import tagbio.umap.Umap; /** * Abstract class for computing UMAP features in the Mastodon project. @@ -66,172 +61,46 @@ * @param the type of read-only graph */ public abstract class AbstractUmapFeatureComputer< V extends Vertex< E >, E extends Edge< V >, G extends ReadOnlyGraph< V, E > > - extends AbstractSerialFeatureComputer< V > + extends AbstractOutputFeatureComputer< V, E, G > { private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); - private final StatusService statusService; - - private List< UmapInputDimension< V > > inputDimensions; - - private UmapFeatureSettings settings; - - private AbstractUmapFeature< V > feature; + private UmapSettings umapSettings; private double[][] umapResult; - private final RefIntMap< V > vertexToRowIndexMap; - - private static final int NO_ENTRY = -1; - protected AbstractUmapFeatureComputer( final Model model, final Context context ) { - this.model = model; - this.statusService = context.getService( StatusService.class ); - this.vertexToRowIndexMap = new RefIntHashMap<>( getRefPool(), NO_ENTRY ); + super( model, context ); } - /** - * Computes the UMAP feature with the given settings and input dimensions and declares it in the feature model. - *
- * During computation, the given graph is locked for reading. - * The UMAP feature is computed for each vertex in the graph, excluding vertices with invalid data rows - * (i.e. rows where the selected feature projections do not have values, such as {@link Double#NaN} or {@link Double#POSITIVE_INFINITY}). - * - * @param settings the UMAP settings - * @param inputDimensions the input dimensions - * @param graph the read-only graph - */ - public void computeFeature( final UmapFeatureSettings settings, final List< UmapInputDimension< V > > inputDimensions, - final G graph ) + public void computeFeature( final CommonSettings commonSettings, final UmapSettings umapSettings, + final List< InputDimension< V > > inputDimensions, final G graph ) { - logger.info( "Computing UmapFeature with settings: {}", settings ); - this.settings = settings; - logger.info( "Computing UmapFeatureComputer with {} input dimensions.", inputDimensions.size() ); - for ( UmapInputDimension< V > inputDimension : inputDimensions ) - logger.info( "Input dimension: {}", inputDimension ); - this.inputDimensions = inputDimensions; - this.forceComputeAll = new AtomicBoolean( true ); - long start = System.currentTimeMillis(); - ReentrantReadWriteLock.ReadLock lock = getLock( graph ).readLock(); - lock.lock(); - try - { - run(); - } - finally - { - lock.unlock(); - } - logger.info( "Finished computing UmapFeature in {} ms", System.currentTimeMillis() - start ); - model.getFeatureModel().declareFeature( feature ); + this.umapSettings = umapSettings; + super.computeFeature( commonSettings, inputDimensions, graph ); } @Override - protected void compute( final V vertex ) + protected void computeAlgorithm( double[][] dataMatrix ) { - int rowIndex = vertexToRowIndexMap.get( vertex ); - if ( rowIndex == NO_ENTRY ) - return; - for ( int i = 0; i < settings.getNumberOfOutputDimensions(); i++ ) - { - DoublePropertyMap< V > umapOutput = feature.getUmapOutputMaps().get( i ); - umapOutput.set( vertex, umapResult[ rowIndex ][ i ] ); - } - } - - @Override - public void createOutput() - { - if ( feature == null ) - feature = initFeature( settings.getNumberOfOutputDimensions() ); - computeUmap(); - } - - @Override - protected void notifyProgress( final int finished, final int total ) - { - statusService.showStatus( finished, total, "Computing UmapFeature" ); - } - - @Override - protected ValueIsSetEvaluator< V > getEvaluator() - { - return feature; - } - - @Override - protected void reset() - { - if ( feature == null ) - return; - feature.getUmapOutputMaps().forEach( DoublePropertyMap::beforeClearPool ); - } - - private void computeUmap() - { - vertexToRowIndexMap.clear(); - List< double[] > data = extractValidDataRowsAndCacheIndexes(); - double[][] dataMatrix = data.toArray( new double[ 0 ][ 0 ] ); - if ( dataMatrix.length == 0 ) - throw new IllegalArgumentException( - "No valid data rows found, i.e. in each existing data row there is at least one non-finite value, such as Not a Number or Infinity." ); - if ( settings.isStandardizeFeatures() ) - { - logger.debug( "Standardizing features with {} rows and {} columns.", dataMatrix.length, inputDimensions.size() ); - StandardScaler.standardizeColumns( dataMatrix ); - logger.debug( "Finished standardizing features" ); - } Umap umap = new Umap(); umap.setNumberComponents( settings.getNumberOfOutputDimensions() ); - umap.setNumberNearestNeighbours( settings.getNumberOfNeighbors() ); - umap.setMinDist( ( float ) settings.getMinimumDistance() ); + umap.setNumberNearestNeighbours( umapSettings.getNumberOfNeighbors() ); + umap.setMinDist( ( float ) umapSettings.getMinimumDistance() ); umap.setThreads( 1 ); umap.setSeed( 42 ); - logger.info( "Fitting umap. Data matrix has {} rows x {} columns.", dataMatrix.length, inputDimensions.size() ); + logger.info( "Fitting umap. Data matrix has {} rows x {} columns.", dataMatrix.length, dataMatrix[ 0 ].length ); umapResult = umap.fitTransform( dataMatrix ); logger.info( "Finished fitting umap. Results has {} rows x {} columns.", umapResult.length, umapResult.length > 0 ? umapResult[ 0 ].length : 0 ); } - private List< double[] > extractValidDataRowsAndCacheIndexes() - { - List< double[] > data = new ArrayList<>(); - int index = 0; - for ( V vertex : getVertices() ) - { - double[] row = new double[ inputDimensions.size() ]; - boolean finiteRow = true; - for ( int i = 0; i < inputDimensions.size(); i++ ) - { - UmapInputDimension< V > inputDimension = inputDimensions.get( i ); - double value = inputDimension.getValue( vertex ); - if ( Double.isNaN( value ) ) - { - finiteRow = false; - break; - } - row[ i ] = value; - } - if ( !finiteRow ) - continue; - data.add( row ); - vertexToRowIndexMap.put( vertex, index ); - index++; - } - return data; - } - - private AbstractUmapFeature< V > initFeature( int numOutputDimensions ) + @Override + protected double[][] getResult() { - List< DoublePropertyMap< V > > umapOutputMaps; - umapOutputMaps = new ArrayList<>( numOutputDimensions ); - for ( int i = 0; i < numOutputDimensions; i++ ) - { - umapOutputMaps.add( new DoublePropertyMap<>( getRefPool(), Double.NaN ) ); - } - return createFeatureInstance( umapOutputMaps ); + return umapResult; } protected abstract AbstractUmapFeature< V > createFeatureInstance( final List< DoublePropertyMap< V > > umapOutputMaps ); diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapView.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapView.java deleted file mode 100644 index 6ecd8a30f..000000000 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapView.java +++ /dev/null @@ -1,318 +0,0 @@ -/*- - * #%L - * mastodon-deep-lineage - * %% - * Copyright (C) 2022 - 2024 Stefan Hahmann - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.ui; - -import net.imglib2.util.Cast; -import net.miginfocom.swing.MigLayout; -import org.mastodon.feature.FeatureModel; -import org.mastodon.graph.Edge; -import org.mastodon.graph.Vertex; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapController; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapFeatureSettings; -import org.mastodon.mamut.model.Model; -import org.scijava.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.ButtonGroup; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JSpinner; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; -import javax.swing.WindowConstants; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.lang.invoke.MethodHandles; -import java.util.Objects; - -/** - * This class represents the user interface for performing dimensionality reduction using UMAP. - * It provides various input fields and buttons to configure and execute the UMAP algorithm. - * The UMAP algorithm is executed asynchronously to avoid blocking the user interface. - *
- * The input fields include: - *
    - *
  • Radio buttons to select the type of graph (Spot or Branch)
  • - *
  • Check box to standardize features
  • - *
  • Spinners to set the number of dimensions, number of neighbors, and minimum distance
  • - *
  • Panel to select input dimensions
  • - *
- */ -public class UmapView extends JFrame -{ - - private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); - - private final JPanel canvas; - - private final JRadioButton modelGraphRadioButton; - - private final JRadioButton branchGraphRadioButton; - - private final JCheckBox standardizeFeaturesCheckBox; - - private final JSpinner numberOfDimensionsInput; - - private final JSpinner numberOfNeighborsInput; - - private final JSpinner minimumDistanceInput; - - private UmapInputDimensionsPanel< ?, ? > umapInputDimensionsPanel; - - private final JLabel feedbackLabel; - - private final JButton computeButton; - - private final ImageIcon loadingIcon; - - private final FeatureModel featureModel; - - private final UmapController umapController; - - private static final String UMAP_PANEL_CONSTRANTS = "span, growx, pushx, growy, pushy, wrap"; - - /** - * Constructs a new UmapView with the specified model and context. - * - * @param model The model containing the data to be processed. - * @param context The context in which the UmapView is created. - */ - public UmapView( final Model model, final Context context ) - { - this.featureModel = model.getFeatureModel(); - this.umapController = new UmapController( model, context ); - - canvas = new JPanel( new MigLayout( "insets 0 10 0 10, fill", "", "" ) ); - setTitle( "Dimensionality Reduction using UMAP" ); - setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE ); - setSize( 600, 600 ); - setLocationRelativeTo( null ); - - modelGraphRadioButton = new JRadioButton( "Model Graph Features" ); - branchGraphRadioButton = new JRadioButton( "Branch Graph Features" ); - - standardizeFeaturesCheckBox = new JCheckBox( "Standardize features" ); - numberOfDimensionsInput = new JSpinner(); - numberOfNeighborsInput = new JSpinner(); - minimumDistanceInput = new JSpinner(); - umapInputDimensionsPanel = createUmapInputDimensionsPanel(); - feedbackLabel = new JLabel(); - computeButton = new JButton( "Compute UMAP" ); - loadingIcon = new ImageIcon( Objects.requireNonNull( getClass().getResource( "loading.gif" ) ) ); - - initSettings(); - initBehavior(); - initLayout(); - initToolTips(); - } - - private void initSettings() - { - logger.debug( "Initializing UMAP settings." ); - boolean isModelGraph = umapController.isModelGraphPreferences(); - modelGraphRadioButton.setSelected( isModelGraph ); - branchGraphRadioButton.setSelected( !isModelGraph ); - UmapFeatureSettings settings = umapController.getFeatureSettings(); - standardizeFeaturesCheckBox.setSelected( settings.isStandardizeFeatures() ); - numberOfDimensionsInput.setModel( getNumberOfDimensionsSpinnerModel() ); - numberOfNeighborsInput.setModel( getNumberOfNeighborsSpinnerModel() ); - minimumDistanceInput.setModel( new SpinnerNumberModel( settings.getMinimumDistance(), 0d, 1d, 0.1d ) ); - } - - private void initBehavior() - { - modelGraphRadioButton.addActionListener( e -> updateUmapInputDimensionsPanel() ); - branchGraphRadioButton.addActionListener( e -> updateUmapInputDimensionsPanel() ); - - ButtonGroup group = new ButtonGroup(); - group.add( modelGraphRadioButton ); - group.add( branchGraphRadioButton ); - - UmapFeatureSettings settings = umapController.getFeatureSettings(); - standardizeFeaturesCheckBox.addActionListener( e -> settings.setStandardizeFeatures( standardizeFeaturesCheckBox.isSelected() ) ); - numberOfDimensionsInput - .addChangeListener( e -> settings.setNumberOfOutputDimensions( ( int ) numberOfDimensionsInput.getValue() ) ); - numberOfNeighborsInput.addChangeListener( e -> settings.setNumberOfNeighbors( ( int ) numberOfNeighborsInput.getValue() ) ); - minimumDistanceInput.addChangeListener( e -> settings.setMinimumDistance( ( double ) minimumDistanceInput.getValue() ) ); - computeButton.addActionListener( e -> SwingUtilities.invokeLater( this::run ) ); - - addWindowListener( new WindowAdapter() - { - @Override - public void windowClosing( WindowEvent e ) - { - onWindowClosing(); - } - } ); - } - - private void initLayout() - { - add( canvas, BorderLayout.CENTER ); - - canvas.add( new JLabel( "Graph type:" ), "split 3" ); - canvas.add( modelGraphRadioButton ); - canvas.add( branchGraphRadioButton, "wrap" ); - canvas.add( standardizeFeaturesCheckBox, "wrap" ); - String split2 = "split 2"; - canvas.add( new JLabel( "Number of dimensions:" ), split2 ); - canvas.add( numberOfDimensionsInput, "wmin 35, wrap" ); - canvas.add( new JLabel( "Number of neighbors:" ), split2 ); - canvas.add( numberOfNeighborsInput, "wmin 35, wrap" ); - canvas.add( new JLabel( "Minimum distance:" ), split2 ); - canvas.add( minimumDistanceInput, "wmin 40, wrap" ); - canvas.add( umapInputDimensionsPanel, UMAP_PANEL_CONSTRANTS ); - canvas.add( computeButton, "dock south, gapleft 10, gapbottom 10, wmax 150, wrap" ); - canvas.add( feedbackLabel, "dock south, gapleft 10, gapbottom 10, wrap" ); - } - - private void initToolTips() - { - numberOfDimensionsInput - .setToolTipText( "The number of reduced dimensions to use.
The default is 2, but 3 is also common." ); - numberOfNeighborsInput.setToolTipText( - "The size of the local neighborhood (in terms of number of neighboring sample points) used for manifold approximation." - + "
Larger values result in more global views of the manifold, while smaller values result in more local data being preserved." - + "
In general, it should be in the range 2 to 100." ); - minimumDistanceInput.setToolTipText( - "The minimum distance that points are allowed to be apart from each other in the low dimensional representation." - + "
This parameter controls how tightly UMAP is allowed to pack points together." ); - standardizeFeaturesCheckBox.setToolTipText( - "Whether to standardize the data before reducing the dimensionality." - + "
Standardization is recommended when the data has different scales / units." ); - } - - private void updateUmapInputDimensionsPanel() - { - canvas.remove( umapInputDimensionsPanel ); - umapController.setModelGraph( modelGraphRadioButton.isSelected() ); - umapInputDimensionsPanel = createUmapInputDimensionsPanel(); - canvas.add( umapInputDimensionsPanel, UMAP_PANEL_CONSTRANTS ); - revalidate(); - repaint(); - numberOfDimensionsInput.setModel( getNumberOfDimensionsSpinnerModel() ); - } - - private < V extends Vertex< E >, E extends Edge< V > > UmapInputDimensionsPanel< V, E > createUmapInputDimensionsPanel() - { - return new UmapInputDimensionsPanel<>( Cast.unchecked( umapController.getVertexType() ), - Cast.unchecked( umapController.getEdgeType() ), featureModel ); - } - - private SpinnerModel getNumberOfDimensionsSpinnerModel() - { - int maximumNumberOfDimensions = Math.max( 2, umapInputDimensionsPanel.getNumberOfFeatures() ); - int numberOfDimensions = Math.min( umapController.getFeatureSettings().getNumberOfOutputDimensions(), maximumNumberOfDimensions ); - return new SpinnerNumberModel( numberOfDimensions, UmapFeatureSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS, - maximumNumberOfDimensions, 1 ); - } - - private SpinnerModel getNumberOfNeighborsSpinnerModel() - { - return new SpinnerNumberModel( umapController.getFeatureSettings().getNumberOfNeighbors(), 1, 100, 1 ); - } - - // Method called when the JFrame is closed - private void onWindowClosing() - { - // Perform any cleanup or actions needed before closing the window - logger.debug( "UmapView is closing." ); - umapController.saveSettingsToPreferences(); - // Dispose the window - dispose(); - } - - private void run() - { - beforeRun(); - executeAsynchronously(); - } - - private void beforeRun() - { - computeButton.setEnabled( false ); - computeButton.setIcon( loadingIcon ); - feedbackLabel.setText( "Computing UMAP..." ); - feedbackLabel.setForeground( Color.BLACK ); - repaint(); - } - - private void executeAsynchronously() - { - SwingWorker< Void, Void > worker = new SwingWorker< Void, Void >() - { - @Override - protected Void doInBackground() - { - umapController.computeFeature( umapInputDimensionsPanel ); - return null; - } - - @Override - protected void done() - { - try - { - get(); - executionCompleted( "UMAP sucessfully computed.", new Color( 0, 100, 0 ) ); - } - catch ( Exception e ) - { - String message = e.getCause().getMessage(); - logger.error( "Running umap failed. {}", message, e.getCause() ); - executionCompleted( message, Color.RED ); - Thread.currentThread().interrupt(); - } - finally - { - computeButton.setEnabled( true ); - } - } - }; - worker.execute(); - } - - private void executionCompleted( final String message, final Color color ) - { - feedbackLabel.setForeground( color ); - feedbackLabel.setText( message ); - computeButton.setIcon( null ); - repaint(); - } -} diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/UmapInputDimension.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/util/InputDimension.java similarity index 72% rename from src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/UmapInputDimension.java rename to src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/util/InputDimension.java index 165152624..47587c428 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/UmapInputDimension.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/util/InputDimension.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.util; +package org.mastodon.mamut.feature.dimensionalityreduction.util; import net.imglib2.util.Cast; import net.imglib2.util.Util; @@ -38,25 +38,30 @@ import org.mastodon.graph.Vertex; import org.mastodon.mamut.feature.LinkTargetIdFeature; import org.mastodon.mamut.feature.SpotTrackIDFeature; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.feature.AbstractUmapFeature; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeature; import org.mastodon.mamut.feature.spot.SpotBranchIDFeature; import org.mastodon.util.FeatureUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.ToDoubleFunction; /** - * Represents a UMAP input dimension for a given feature and a projection of that feature. + * Represents an input dimension for dimensionality reduction for a given feature and a projection of that feature. *
* This class encapsulates a feature and a projection of it, providing methods to * retrieve the projection and generate a string representation of them. * * @param the type of vertex */ -public class UmapInputDimension< V extends Vertex< ? > > +public class InputDimension< V extends Vertex< ? > > { + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + private final Feature< ? > feature; private final FeatureProjection< ? > featureProjection; @@ -64,43 +69,44 @@ public class UmapInputDimension< V extends Vertex< ? > > private final ToDoubleFunction< V > vertexValueFunction; /** - * Creates a new UMAP input dimension for the given vertex feature and projection. + * Creates a new input dimension for the given vertex feature and projection. *
* The vertex value function is set to the value of the vertex feature projection for a given vertex. * @param vertexFeature the vertex feature * @param vertexProjection the projection of the vertex feature - * @return a new UMAP input dimension + * @return a new input dimension * @param the type of vertex * @param the type of edge */ - public static < V extends Vertex< E >, E extends Edge< V > > UmapInputDimension< V > + public static < V extends Vertex< E >, E extends Edge< V > > InputDimension< V > fromVertexFeature( final Feature< V > vertexFeature, final FeatureProjection< V > vertexProjection ) { - return new UmapInputDimension<>( vertexFeature, vertexProjection, vertexProjection::value ); + return new InputDimension<>( vertexFeature, vertexProjection, vertexProjection::value ); } /** - * Creates a new UMAP input dimension for the given edge feature and projection. + * Creates a new input dimension for the given edge feature and projection. *
* The vertex value function is set to the average of the edge feature projection values for the incoming edges of a given vertex. * @param edgeFeature the edge feature * @param edgeProjection the projection of the edge feature - * @return a new UMAP input dimension + * @return a new input dimension * @param the type of vertex * @param the type of edge */ - public static < V extends Vertex< E >, E extends Edge< V > > UmapInputDimension< V > fromEdgeFeature( final Feature< E > edgeFeature, + public static < V extends Vertex< E >, E extends Edge< V > > InputDimension< V > fromEdgeFeature( + final Feature< E > edgeFeature, final FeatureProjection< E > edgeProjection ) { - return new UmapInputDimension<>( edgeFeature, edgeProjection, edgeProjectionFunction( edgeProjection ) ); + return new InputDimension<>( edgeFeature, edgeProjection, edgeProjectionFunction( edgeProjection ) ); } /** - * Creates a new UMAP input dimension for the given feature and projection. + * Creates a new input dimension for the given feature and projection. * @param feature the vertex feature * @param projection the projection of the vertex feature */ - private UmapInputDimension( final Feature< ? > feature, final FeatureProjection< ? > projection, + private InputDimension( final Feature< ? > feature, final FeatureProjection< ? > projection, final ToDoubleFunction< V > vertexValueFunction ) { @@ -120,11 +126,11 @@ public String toString() } /** - * Returns the value of the UMAP input dimension for the given vertex. + * Returns the value of the input dimension for the given vertex. *
* The value is determined by the given vertex value function. * @param vertex the vertex - * @return the value of the UMAP input dimension + * @return the value of the input dimension */ public double getValue( final V vertex ) { @@ -132,48 +138,48 @@ public double getValue( final V vertex ) } /** - * Returns a list of UMAP input dimensions for the given feature model, vertex type and edge type. + * Returns a list of input dimensions for the given feature model, vertex type and edge type. *
* This method collects all features of the given vertex type or edge type from the feature model and - * creates a UMAP input dimension for each feature and projection. + * creates a input dimension for each feature and projection. * @param featureModel the feature model * @param vertexType the vertex type, e.g. {@link org.mastodon.mamut.model.Spot} or {{@link org.mastodon.mamut.model.branch.BranchSpot}} * @param edgeType the edge type, e.g. {@link org.mastodon.mamut.model.Link} or {{@link org.mastodon.mamut.model.branch.BranchLink}} - * @return a list of UMAP input dimensions + * @return a list of input dimensions * @param the type of vertex * @param the type of edge */ - public static < V extends Vertex< E >, E extends Edge< V > > List< UmapInputDimension< V > > getListFromFeatureModel( + public static < V extends Vertex< E >, E extends Edge< V > > List< InputDimension< V > > getListFromFeatureModel( final FeatureModel featureModel, final Class< V > vertexType, final Class< E > edgeType ) { - List< UmapInputDimension< V > > umapInputDimensions = getVertexDimensions( featureModel, vertexType ); - umapInputDimensions.addAll( getEdgeDimensions( featureModel, edgeType ) ); - getEdgeDimensions( featureModel, edgeType ); - return umapInputDimensions; + List< InputDimension< V > > inputDimensions = getVertexDimensions( featureModel, vertexType ); + inputDimensions.addAll( getEdgeDimensions( featureModel, edgeType ) ); + return inputDimensions; } - private static < V extends Vertex< E >, E extends Edge< V > > List< UmapInputDimension< V > > + private static < V extends Vertex< E >, E extends Edge< V > > List< InputDimension< V > > getVertexDimensions( final FeatureModel featureModel, final Class< V > vertexType ) { - List< UmapInputDimension< V > > umapInputDimensions = new ArrayList<>(); + List< InputDimension< V > > inputDimensions = new ArrayList<>(); Collection< Feature< V > > vertexFeatures = FeatureUtils.collectFeatureMap( featureModel, vertexType ).values(); Collection< Class< ? extends Feature< V > > > excludedVertexFeatures = new ArrayList<>(); excludedVertexFeatures.add( Cast.unchecked( SpotTrackIDFeature.class ) ); excludedVertexFeatures.add( Cast.unchecked( SpotBranchIDFeature.class ) ); for ( Feature< V > feature : vertexFeatures ) { - if ( excludedVertexFeatures.contains( feature.getClass() ) || feature instanceof AbstractUmapFeature ) + if ( excludedVertexFeatures.contains( feature.getClass() ) || feature instanceof AbstractOutputFeature ) continue; for ( FeatureProjection< V > projection : feature.projections() ) - umapInputDimensions.add( UmapInputDimension.fromVertexFeature( feature, projection ) ); + inputDimensions.add( InputDimension.fromVertexFeature( feature, projection ) ); } - return umapInputDimensions; + logger.debug( "Found {} input dimensions for vertex type '{}'.", inputDimensions.size(), vertexType.getSimpleName() ); + return inputDimensions; } - private static < V extends Vertex< E >, E extends Edge< V > > List< UmapInputDimension< V > > + private static < V extends Vertex< E >, E extends Edge< V > > List< InputDimension< V > > getEdgeDimensions( final FeatureModel featureModel, final Class< E > edgeType ) { - List< UmapInputDimension< V > > umapInputDimensions = new ArrayList<>(); + List< InputDimension< V > > inputDimensions = new ArrayList<>(); Collection< Feature< E > > edgeFeatures = FeatureUtils.collectFeatureMap( featureModel, edgeType ).values(); Collection< Class< ? extends Feature< E > > > excludedEdgeFeatures = new ArrayList<>(); excludedEdgeFeatures.add( Cast.unchecked( LinkTargetIdFeature.class ) ); @@ -182,9 +188,10 @@ public static < V extends Vertex< E >, E extends Edge< V > > List< UmapInputDime if ( excludedEdgeFeatures.contains( feature.getClass() ) ) continue; for ( FeatureProjection< E > projection : feature.projections() ) - umapInputDimensions.add( UmapInputDimension.fromEdgeFeature( feature, projection ) ); + inputDimensions.add( InputDimension.fromEdgeFeature( feature, projection ) ); } - return umapInputDimensions; + logger.debug( "Found {} input dimensions for edge type '{}'.", inputDimensions.size(), edgeType.getSimpleName() ); + return inputDimensions; } private static < V extends Vertex< E >, E extends Edge< V > > ToDoubleFunction< V > edgeProjectionFunction( diff --git a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/StandardScaler.java b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/util/StandardScaler.java similarity index 98% rename from src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/StandardScaler.java rename to src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/util/StandardScaler.java index c11b25388..750f0ead0 100644 --- a/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/StandardScaler.java +++ b/src/main/java/org/mastodon/mamut/feature/dimensionalityreduction/util/StandardScaler.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.util; +package org.mastodon.mamut.feature.dimensionalityreduction.util; import org.apache.commons.math3.stat.StatUtils; diff --git a/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/SpotOutputFeatureSerializerTools.java b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/SpotOutputFeatureSerializerTools.java new file mode 100644 index 000000000..aa81c2e97 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/SpotOutputFeatureSerializerTools.java @@ -0,0 +1,51 @@ +package org.mastodon.mamut.feature.spot.dimensionalityreduction; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.mastodon.collection.RefCollection; +import org.mastodon.io.FileIdToObjectMap; +import org.mastodon.io.ObjectToFileIdMap; +import org.mastodon.io.properties.DoublePropertyMapSerializer; +import org.mastodon.mamut.feature.dimensionalityreduction.AbstractOutputFeature; +import org.mastodon.mamut.model.Spot; +import org.mastodon.properties.DoublePropertyMap; + +public class SpotOutputFeatureSerializerTools +{ + private SpotOutputFeatureSerializerTools() + { + // prevent instantiation + } + + public static void serialize( final AbstractOutputFeature< Spot > feature, final ObjectToFileIdMap< Spot > idMap, + final ObjectOutputStream oos ) throws IOException + { + oos.writeInt( feature.getOutputMaps().size() ); + for ( DoublePropertyMap< Spot > output : feature.getOutputMaps() ) + { + final DoublePropertyMapSerializer< Spot > serializer = new DoublePropertyMapSerializer<>( output ); + serializer.writePropertyMap( idMap, oos ); + } + } + + public static < T extends AbstractOutputFeature< Spot > > T deserialize( final FileIdToObjectMap< Spot > idMap, + final RefCollection< Spot > pool, final ObjectInputStream ois, + final Function< List< DoublePropertyMap< Spot > >, T > featureCreator ) throws IOException, ClassNotFoundException + { + int numDimensions = ois.readInt(); + List< DoublePropertyMap< Spot > > outputMaps = new ArrayList<>( numDimensions ); + for ( int i = 0; i < numDimensions; i++ ) + { + DoublePropertyMap< Spot > umapOutput = new DoublePropertyMap<>( pool, Double.NaN ); + DoublePropertyMapSerializer< Spot > serializer = new DoublePropertyMapSerializer<>( umapOutput ); + serializer.readPropertyMap( idMap, ois ); + outputMaps.add( umapOutput ); + } + return featureCreator.apply( outputMaps ); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeature.java b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeature.java new file mode 100644 index 000000000..5293f76a0 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeature.java @@ -0,0 +1,85 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.spot.dimensionalityreduction.tsne; + +import java.util.List; + +import org.mastodon.feature.Feature; +import org.mastodon.feature.FeatureProjectionKey; +import org.mastodon.feature.FeatureProjectionSpec; +import org.mastodon.feature.FeatureSpec; +import org.mastodon.feature.Multiplicity; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeature; +import org.mastodon.mamut.model.Spot; +import org.mastodon.properties.DoublePropertyMap; +import org.scijava.plugin.Plugin; + +/** + * Represents a t-SNE feature for spots in the Mastodon project. + *
+ * This feature is used to store the t-SNE outputs for spots. + *
+ * The t-SNE outputs are stored in a list of {@link DoublePropertyMap}s. The size of the list is equal to the number of dimensions of the t-SNE output. + */ +public class SpotTSneFeature extends AbstractTSneFeature< Spot > +{ + public static final String KEY = "Spot t-SNE outputs"; + + private final SpotTSneFeatureSpec adaptedSpec; + + public static final SpotTSneFeatureSpec GENERIC_SPEC = new SpotTSneFeatureSpec(); + + public SpotTSneFeature( final List< DoublePropertyMap< Spot > > outputMaps ) + { + super( outputMaps ); + FeatureProjectionSpec[] projectionSpecs = + projectionMap.keySet().stream().map( FeatureProjectionKey::getSpec ).toArray( FeatureProjectionSpec[]::new ); + this.adaptedSpec = new SpotTSneFeatureSpec( projectionSpecs ); + } + + @Plugin( type = FeatureSpec.class ) + public static class SpotTSneFeatureSpec extends FeatureSpec< SpotTSneFeature, Spot > + { + public SpotTSneFeatureSpec() + { + super( KEY, HELP_STRING, SpotTSneFeature.class, Spot.class, Multiplicity.SINGLE ); + } + + public SpotTSneFeatureSpec( final FeatureProjectionSpec... projectionSpecs ) + { + super( KEY, HELP_STRING, SpotTSneFeature.class, Spot.class, Multiplicity.SINGLE, projectionSpecs ); + } + } + + @Override + public FeatureSpec< ? extends Feature< Spot >, Spot > getSpec() + { + return adaptedSpec; + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureComputer.java b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureComputer.java new file mode 100644 index 000000000..aad5cf950 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureComputer.java @@ -0,0 +1,76 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.spot.dimensionalityreduction.tsne; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.mastodon.RefPool; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeature; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature.AbstractTSneFeatureComputer; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; +import org.mastodon.properties.DoublePropertyMap; +import org.scijava.Context; + +public class SpotTSneFeatureComputer extends AbstractTSneFeatureComputer< Spot, Link, ModelGraph > +{ + + public SpotTSneFeatureComputer( final Model model, final Context context ) + { + super( model, context ); + } + + @Override + protected AbstractTSneFeature< Spot > createFeatureInstance( final List< DoublePropertyMap< Spot > > outputMaps ) + { + return new SpotTSneFeature( outputMaps ); + } + + @Override + protected RefPool< Spot > getRefPool() + { + return model.getGraph().vertices().getRefPool(); + } + + @Override + protected ReentrantReadWriteLock getLock( final ModelGraph graph ) + { + return graph.getLock(); + } + + @Override + protected Collection< Spot > getVertices() + { + return model.getGraph().vertices(); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureSerializer.java b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureSerializer.java new file mode 100644 index 000000000..7fc696396 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureSerializer.java @@ -0,0 +1,70 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.spot.dimensionalityreduction.tsne; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.mastodon.collection.RefCollection; +import org.mastodon.feature.FeatureSpec; +import org.mastodon.feature.io.FeatureSerializer; +import org.mastodon.io.FileIdToObjectMap; +import org.mastodon.io.ObjectToFileIdMap; +import org.mastodon.mamut.feature.spot.dimensionalityreduction.SpotOutputFeatureSerializerTools; +import org.mastodon.mamut.model.Spot; +import org.scijava.plugin.Plugin; + +/** + * De-/serializes {@link SpotTSneFeature} + */ +@Plugin( type = FeatureSerializer.class ) +public class SpotTSneFeatureSerializer implements FeatureSerializer< SpotTSneFeature, Spot > +{ + + @Override + public FeatureSpec< SpotTSneFeature, Spot > getFeatureSpec() + { + return SpotTSneFeature.GENERIC_SPEC; + } + + @Override + public void serialize( final SpotTSneFeature feature, final ObjectToFileIdMap< Spot > idMap, final ObjectOutputStream oos ) + throws IOException + { + SpotOutputFeatureSerializerTools.serialize( feature, idMap, oos ); + } + + @Override + public SpotTSneFeature deserialize( final FileIdToObjectMap< Spot > idMap, final RefCollection< Spot > pool, + final ObjectInputStream ois ) throws IOException, ClassNotFoundException + { + return SpotOutputFeatureSerializerTools.deserialize( idMap, pool, ois, SpotTSneFeature::new ); + } +} diff --git a/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureSerializer.java b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureSerializer.java index 834fd3680..e4d2e8462 100644 --- a/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureSerializer.java +++ b/src/main/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureSerializer.java @@ -28,22 +28,19 @@ */ package org.mastodon.mamut.feature.spot.dimensionalityreduction.umap; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + import org.mastodon.collection.RefCollection; import org.mastodon.feature.FeatureSpec; import org.mastodon.feature.io.FeatureSerializer; import org.mastodon.io.FileIdToObjectMap; import org.mastodon.io.ObjectToFileIdMap; -import org.mastodon.io.properties.DoublePropertyMapSerializer; +import org.mastodon.mamut.feature.spot.dimensionalityreduction.SpotOutputFeatureSerializerTools; import org.mastodon.mamut.model.Spot; -import org.mastodon.properties.DoublePropertyMap; import org.scijava.plugin.Plugin; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.List; - /** * De-/serializes {@link SpotUmapFeature} */ @@ -61,27 +58,13 @@ public FeatureSpec< SpotUmapFeature, Spot > getFeatureSpec() public void serialize( final SpotUmapFeature feature, final ObjectToFileIdMap< Spot > idMap, final ObjectOutputStream oos ) throws IOException { - oos.writeInt( feature.getUmapOutputMaps().size() ); - for ( DoublePropertyMap< Spot > umapOutput : feature.getUmapOutputMaps() ) - { - final DoublePropertyMapSerializer< Spot > serializer = new DoublePropertyMapSerializer<>( umapOutput ); - serializer.writePropertyMap( idMap, oos ); - } + SpotOutputFeatureSerializerTools.serialize( feature, idMap, oos ); } @Override public SpotUmapFeature deserialize( final FileIdToObjectMap< Spot > idMap, final RefCollection< Spot > pool, final ObjectInputStream ois ) throws IOException, ClassNotFoundException { - int numDimensions = ois.readInt(); - List< DoublePropertyMap< Spot > > umapOutputs = new ArrayList<>( numDimensions ); - for ( int i = 0; i < numDimensions; i++ ) - { - DoublePropertyMap< Spot > umapOutput = new DoublePropertyMap<>( pool, Double.NaN ); - DoublePropertyMapSerializer< Spot > serializer = new DoublePropertyMapSerializer<>( umapOutput ); - serializer.readPropertyMap( idMap, ois ); - umapOutputs.add( umapOutput ); - } - return new SpotUmapFeature( umapOutputs ); + return SpotOutputFeatureSerializerTools.deserialize( idMap, pool, ois, SpotUmapFeature::new ); } } diff --git a/src/main/resources/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/loading.gif b/src/main/resources/org/mastodon/mamut/feature/dimensionalityreduction/ui/loading.gif similarity index 100% rename from src/main/resources/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/loading.gif rename to src/main/resources/org/mastodon/mamut/feature/dimensionalityreduction/ui/loading.gif diff --git a/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureTest.java b/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureTest.java new file mode 100644 index 000000000..3d5aa223e --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/tsne/BranchTSneFeatureTest.java @@ -0,0 +1,161 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.branch.dimensionalityreduction.tsne; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +import net.imglib2.util.Cast; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mastodon.feature.Dimension; +import org.mastodon.feature.FeatureModel; +import org.mastodon.feature.FeatureProjection; +import org.mastodon.feature.FeatureProjectionSpec; +import org.mastodon.mamut.feature.AbstractFeatureTest; +import org.mastodon.mamut.feature.FeatureComputerTestUtils; +import org.mastodon.mamut.feature.FeatureSerializerTestUtils; +import org.mastodon.mamut.feature.FeatureUtils; +import org.mastodon.mamut.feature.branch.BranchDisplacementDurationFeature; +import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph7; +import org.mastodon.mamut.feature.branch.sinuosity.BranchSinuosityFeature; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionAlgorithm; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionController; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.TSneSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.branch.BranchLink; +import org.mastodon.mamut.model.branch.BranchSpot; +import org.scijava.Context; + +public class BranchTSneFeatureTest extends AbstractFeatureTest< BranchSpot > +{ + private BranchTSneFeature tSneFeature; + + private final ExampleGraph7 graph7 = new ExampleGraph7(); + + private FeatureProjectionSpec spec0; + + private FeatureProjectionSpec spec1; + + @BeforeEach + public void setUp() + { + try (Context context = new Context()) + { + Model model = graph7.getModel(); + FeatureModel featureModel = model.getFeatureModel(); + + // declare some features as input dimensions + BranchDisplacementDurationFeature branchDisplacementDurationFeature = Cast.unchecked( + FeatureComputerTestUtils.getFeature( context, model, BranchDisplacementDurationFeature.SPEC ) ); + featureModel.declareFeature( branchDisplacementDurationFeature ); + BranchSinuosityFeature branchSinuosityFeature = Cast.unchecked( + FeatureComputerTestUtils.getFeature( context, model, BranchSinuosityFeature.BRANCH_SINUOSITY_FEATURE_SPEC ) ); + featureModel.declareFeature( branchSinuosityFeature ); + List< InputDimension< BranchSpot > > inputDimensions = + InputDimension.getListFromFeatureModel( featureModel, BranchSpot.class, BranchLink.class ); + + // set up the controller and compute the feature + Supplier< List< InputDimension< BranchSpot > > > inputDimensionsSupplier = () -> inputDimensions; + DimensionalityReductionController controller = new DimensionalityReductionController( graph7.getModel(), context ); + TSneSettings tSneSettings = controller.getTSneSettings(); + tSneSettings.setPerplexity( 10 ); + controller.setModelGraph( false ); + controller.setAlgorithm( DimensionalityReductionAlgorithm.TSNE ); + controller.computeFeature( inputDimensionsSupplier ); + tSneFeature = FeatureUtils.getFeature( graph7.getModel(), BranchTSneFeature.BranchSpotTSneFeatureSpec.class ); + assertNotNull( tSneFeature ); + spec0 = new FeatureProjectionSpec( tSneFeature.getProjectionName( 0 ), Dimension.NONE ); + spec1 = new FeatureProjectionSpec( tSneFeature.getProjectionName( 1 ), Dimension.NONE ); + + } + } + + @Test + @Override + public void testFeatureComputation() + { + assertNotNull( tSneFeature ); + FeatureProjection< BranchSpot > projection0 = getProjection( tSneFeature, spec0 ); + FeatureProjection< BranchSpot > projection1 = getProjection( tSneFeature, spec1 ); + Iterator< BranchSpot > branchSpotIterator = graph7.getModel().getBranchGraph().vertices().iterator(); + BranchSpot branchSpot = branchSpotIterator.next(); + assertFalse( Double.isNaN( projection0.value( branchSpot ) ) ); + assertNotEquals( 0, projection0.value( branchSpot ) ); + assertFalse( Double.isNaN( projection1.value( branchSpot ) ) ); + assertNotEquals( 0, projection1.value( branchSpot ) ); + } + + @Test + @Override + public void testFeatureSerialization() throws IOException + { + BranchTSneFeature tSneFeatureReloaded; + try (Context context = new Context()) + { + tSneFeatureReloaded = + ( BranchTSneFeature ) FeatureSerializerTestUtils.saveAndReload( context, graph7.getModel(), this.tSneFeature ); + } + assertNotNull( tSneFeatureReloaded ); + Iterator< BranchSpot > branchSpotIterator = graph7.getModel().getBranchGraph().vertices().iterator(); + BranchSpot branchSpot = branchSpotIterator.next(); + // check that the feature has correct values after saving and reloading + assertTrue( FeatureSerializerTestUtils.checkFeatureProjectionEquality( this.tSneFeature, tSneFeatureReloaded, + Collections.singleton( branchSpot ) ) ); + } + + @Test + @Override + public void testFeatureInvalidate() + { + Iterator< BranchSpot > branchSpotIterator = graph7.getModel().getBranchGraph().vertices().iterator(); + BranchSpot branchSpot = branchSpotIterator.next(); + + // test, if features are not NaN before invalidation + assertFalse( Double.isNaN( getProjection( tSneFeature, spec0 ).value( branchSpot ) ) ); + assertFalse( Double.isNaN( getProjection( tSneFeature, spec1 ).value( branchSpot ) ) ); + + // invalidate feature + tSneFeature.invalidate( branchSpot ); + + // test, if features are NaN after invalidation + assertTrue( Double.isNaN( getProjection( tSneFeature, spec0 ).value( branchSpot ) ) ); + assertTrue( Double.isNaN( getProjection( tSneFeature, spec1 ).value( branchSpot ) ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureTest.java b/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureTest.java index cfc02826d..29aaaa5d8 100644 --- a/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureTest.java +++ b/src/test/java/org/mastodon/mamut/feature/branch/dimensionalityreduction/umap/BranchUmapFeatureTest.java @@ -42,9 +42,9 @@ import org.mastodon.mamut.feature.branch.BranchDepthFeature; import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; import org.mastodon.mamut.feature.branch.sinuosity.BranchSinuosityFeature; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapController; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapFeatureSettings; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.UmapInputDimension; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionController; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; import org.mastodon.mamut.model.Model; import org.mastodon.mamut.model.branch.BranchLink; import org.mastodon.mamut.model.branch.BranchSpot; @@ -79,23 +79,23 @@ public void setUp() Model model = graph2.getModel(); FeatureModel featureModel = model.getFeatureModel(); - // declare some features + // declare some features as input dimensions BranchDepthFeature branchDepthFeature = new BranchDepthFeature( new IntPropertyMap<>( model.getBranchGraph().vertices().getRefPool(), -1 ) ); featureModel.declareFeature( branchDepthFeature ); BranchSinuosityFeature branchSinuosityFeature = Cast.unchecked( FeatureComputerTestUtils.getFeature( context, model, BranchSinuosityFeature.BRANCH_SINUOSITY_FEATURE_SPEC ) ); featureModel.declareFeature( branchSinuosityFeature ); - List< UmapInputDimension< BranchSpot > > umapInputDimensions = - UmapInputDimension.getListFromFeatureModel( featureModel, BranchSpot.class, BranchLink.class ); + List< InputDimension< BranchSpot > > inputDimensions = + InputDimension.getListFromFeatureModel( featureModel, BranchSpot.class, BranchLink.class ); // set up the controller and compute the feature - Supplier< List< UmapInputDimension< BranchSpot > > > inputDimensionsSupplier = () -> umapInputDimensions; - UmapController umapController = new UmapController( graph2.getModel(), context ); - UmapFeatureSettings settings = umapController.getFeatureSettings(); - settings.setNumberOfNeighbors( 3 ); - umapController.setModelGraph( false ); - umapController.computeFeature( inputDimensionsSupplier ); + Supplier< List< InputDimension< BranchSpot > > > inputDimensionsSupplier = () -> inputDimensions; + DimensionalityReductionController controller = new DimensionalityReductionController( graph2.getModel(), context ); + UmapSettings umapSettings = controller.getUmapSettings(); + umapSettings.setNumberOfNeighbors( 3 ); + controller.setModelGraph( false ); + controller.computeFeature( inputDimensionsSupplier ); umapFeature = FeatureUtils.getFeature( graph2.getModel(), BranchUmapFeature.BranchSpotUmapFeatureSpec.class ); assertNotNull( umapFeature ); spec0 = new FeatureProjectionSpec( umapFeature.getProjectionName( 0 ), Dimension.NONE ); diff --git a/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph7.java b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph7.java new file mode 100644 index 000000000..716da8adb --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph7.java @@ -0,0 +1,43 @@ +package org.mastodon.mamut.feature.branch.exampleGraph; + +import java.util.Random; + +import org.mastodon.mamut.model.Spot; + +public class ExampleGraph7 extends AbstractExampleGraph +{ + private int counter = 0; + + private final Random random = new Random( 42 ); + + public ExampleGraph7() + { + for ( int i = 0; i < 50; i++ ) + { + addBranchSpot(); + } + } + + private void addBranchSpot() + { + + Spot spot0 = addSpot( 0 ); + counter++; + Spot spot1 = addSpot( 1 ); + counter++; + Spot spot2 = addSpot( 2 ); + counter++; + + addEdge( spot0, spot1 ); + addEdge( spot1, spot2 ); + + getBranchSpot( spot0 ); + } + + private Spot addSpot( int timepoint ) + { + int range = 20; + return addNode( String.valueOf( counter ), timepoint, + new double[] { random.nextInt( range ), random.nextInt( range ), random.nextInt( range ) } ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/CommonSettingsTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/CommonSettingsTest.java new file mode 100644 index 000000000..ff9e83579 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/CommonSettingsTest.java @@ -0,0 +1,44 @@ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CommonSettingsTest +{ + private CommonSettings commonSettings; + + @BeforeEach + void setUp() + { + commonSettings = new CommonSettings(); + } + + @Test + void getNumberOfOutputDimensions() + { + assertEquals( CommonSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS, commonSettings.getNumberOfOutputDimensions() ); + } + + @Test + void isStandardizeFeatures() + { + assertEquals( CommonSettings.DEFAULT_STANDARDIZE_FEATURES, commonSettings.isStandardizeFeatures() ); + } + + @Test + void setNumberOfOutputDimensions() + { + commonSettings.setNumberOfOutputDimensions( 5 ); + assertEquals( 5, commonSettings.getNumberOfOutputDimensions() ); + } + + @Test + void setStandardizeFeatures() + { + commonSettings.setStandardizeFeatures( false ); + assertFalse( commonSettings.isStandardizeFeatures() ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapControllerTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionControllerTest.java similarity index 57% rename from src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapControllerTest.java rename to src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionControllerTest.java index 56137b404..da8fd226c 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapControllerTest.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/DimensionalityReductionControllerTest.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap; +package org.mastodon.mamut.feature.dimensionalityreduction; import net.imglib2.util.Cast; import org.junit.jupiter.api.Test; @@ -34,7 +34,9 @@ import org.mastodon.feature.FeatureModel; import org.mastodon.feature.FeatureProjection; import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.UmapInputDimension; +import org.mastodon.mamut.feature.dimensionalityreduction.tsne.TSneSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; import org.mastodon.mamut.feature.spot.dimensionalityreduction.umap.SpotUmapFeature; import org.mastodon.mamut.model.Link; import org.mastodon.mamut.model.Model; @@ -57,35 +59,57 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class UmapControllerTest +class DimensionalityReductionControllerTest { @Test void testSaveSettingsToPreferences() { try (Context context = new Context()) { + boolean modelGraph = true; int numberOfOutputDimensions = 5; + boolean standardizeFeatures = false; int numberOfNeighbors = 10; double minimumDistance = 0.5; - boolean standardizeFeatures = false; + int perplexity = 50; + int maxIterations = 2000; + DimensionalityReductionAlgorithm algorithm = DimensionalityReductionAlgorithm.TSNE; Model model = new Model(); - UmapController umapController = new UmapController( model, context ); - UmapFeatureSettings umapFeatureSettings = umapController.getFeatureSettings(); - umapFeatureSettings.setNumberOfOutputDimensions( numberOfOutputDimensions ); - umapFeatureSettings.setNumberOfNeighbors( numberOfNeighbors ); - umapFeatureSettings.setMinimumDistance( minimumDistance ); - umapFeatureSettings.setStandardizeFeatures( standardizeFeatures ); - umapController.saveSettingsToPreferences(); - - UmapController umapController2 = new UmapController( model, context ); - UmapFeatureSettings umapFeatureSettings2 = umapController2.getFeatureSettings(); - assertEquals( numberOfOutputDimensions, umapFeatureSettings2.getNumberOfOutputDimensions() ); - assertEquals( numberOfNeighbors, umapFeatureSettings2.getNumberOfNeighbors() ); - assertEquals( minimumDistance, umapFeatureSettings2.getMinimumDistance() ); - assertEquals( standardizeFeatures, umapFeatureSettings2.isStandardizeFeatures() ); - - context.getService( PrefService.class ).clear( UmapController.class ); + DimensionalityReductionController controller = new DimensionalityReductionController( model, context ); + CommonSettings commonSettings = controller.getCommonSettings(); + UmapSettings umapSettings = controller.getUmapSettings(); + TSneSettings tsneSettings = controller.getTSneSettings(); + commonSettings.setNumberOfOutputDimensions( numberOfOutputDimensions ); + umapSettings.setNumberOfNeighbors( numberOfNeighbors ); + umapSettings.setMinimumDistance( minimumDistance ); + tsneSettings.setPerplexity( perplexity ); + tsneSettings.setMaxIterations( maxIterations ); + commonSettings.setStandardizeFeatures( standardizeFeatures ); + controller.setModelGraph( modelGraph ); + controller.setAlgorithm( algorithm ); + controller.saveSettingsToPreferences(); + + DimensionalityReductionController controller2 = new DimensionalityReductionController( model, context ); + CommonSettings commonSettings2 = controller2.getCommonSettings(); + UmapSettings umapSettings2 = controller2.getUmapSettings(); + TSneSettings tsneSettings2 = controller2.getTSneSettings(); + assertEquals( controller.isModelGraph, controller2.isModelGraph ); + assertEquals( controller.getAlgorithm(), controller2.getAlgorithm() ); + + assertEquals( numberOfOutputDimensions, commonSettings2.getNumberOfOutputDimensions() ); + assertEquals( standardizeFeatures, commonSettings2.isStandardizeFeatures() ); + + assertEquals( numberOfNeighbors, umapSettings2.getNumberOfNeighbors() ); + assertEquals( minimumDistance, umapSettings2.getMinimumDistance() ); + + assertEquals( perplexity, tsneSettings2.getPerplexity() ); + assertEquals( maxIterations, tsneSettings2.getMaxIterations() ); + + context.getService( PrefService.class ).clear( DimensionalityReductionController.class ); + context.getService( PrefService.class ).clear( CommonSettings.class ); + context.getService( PrefService.class ).clear( UmapSettings.class ); + context.getService( PrefService.class ).clear( TSneSettings.class ); } } @@ -95,10 +119,10 @@ void testGetVertexType() try (Context context = new Context()) { Model model = new Model(); - UmapController umapController = new UmapController( model, context ); - assertEquals( Spot.class, umapController.getVertexType() ); - umapController.setModelGraph( false ); - assertEquals( BranchSpot.class, umapController.getVertexType() ); + DimensionalityReductionController controller = new DimensionalityReductionController( model, context ); + assertEquals( Spot.class, controller.getVertexType() ); + controller.setModelGraph( false ); + assertEquals( BranchSpot.class, controller.getVertexType() ); } } @@ -108,10 +132,10 @@ void testGetEdgeType() try (Context context = new Context()) { Model model = new Model(); - UmapController umapController = new UmapController( model, context ); - assertEquals( Link.class, umapController.getEdgeType() ); - umapController.setModelGraph( false ); - assertEquals( BranchLink.class, umapController.getEdgeType() ); + DimensionalityReductionController controller = new DimensionalityReductionController( model, context ); + assertEquals( Link.class, controller.getEdgeType() ); + controller.setModelGraph( false ); + assertEquals( BranchLink.class, controller.getEdgeType() ); } } @@ -123,14 +147,16 @@ void testIllegalArgumentExceptions() int numberOfOutputDimensions = 10; Model model = new Model(); - UmapController umapController = new UmapController( model, context ); - UmapFeatureSettings umapFeatureSettings = umapController.getFeatureSettings(); - umapFeatureSettings.setNumberOfOutputDimensions( numberOfOutputDimensions ); - List< UmapInputDimension< Spot > > umapInputDimensions = - UmapInputDimension.getListFromFeatureModel( model.getFeatureModel(), Spot.class, Link.class ); - assertThrows( IllegalArgumentException.class, () -> umapController.computeFeature( () -> umapInputDimensions ) ); - Supplier< List< UmapInputDimension< Spot > > > emptyInputDimensionsSupplier = Collections::emptyList; - assertThrows( IllegalArgumentException.class, () -> umapController.computeFeature( emptyInputDimensionsSupplier ) ); + DimensionalityReductionController controller = new DimensionalityReductionController( model, context ); + CommonSettings commonSettings = controller.getCommonSettings(); + commonSettings.setNumberOfOutputDimensions( numberOfOutputDimensions ); + List< InputDimension< Spot > > inputDimensions = + InputDimension.getListFromFeatureModel( model.getFeatureModel(), Spot.class, Link.class ); + assertThrows( IllegalArgumentException.class, () -> controller.computeFeature( () -> inputDimensions ) ); + commonSettings.setNumberOfOutputDimensions( 11 ); + assertThrows( IllegalArgumentException.class, () -> controller.computeFeature( () -> inputDimensions ) ); + Supplier< List< InputDimension< Spot > > > emptyInputDimensionsSupplier = Collections::emptyList; + assertThrows( IllegalArgumentException.class, () -> controller.computeFeature( emptyInputDimensionsSupplier ) ); } } @@ -144,11 +170,12 @@ void testIllegalData() try (Context context = new Context()) { - UmapController umapController = new UmapController( graph2.getModel(), context ); - UmapFeatureSettings settings = umapController.getFeatureSettings(); + DimensionalityReductionController umapController = new DimensionalityReductionController( graph2.getModel(), context ); + UmapSettings settings = umapController.getUmapSettings(); settings.setNumberOfNeighbors( 5 ); - Supplier< List< UmapInputDimension< Spot > > > inputDimensionsSupplier = - () -> UmapInputDimension.getListFromFeatureModel( graph2.getModel().getFeatureModel(), Spot.class, Link.class ); + Supplier< List< InputDimension< Spot > > > inputDimensionsSupplier = + () -> InputDimension.getListFromFeatureModel( graph2.getModel().getFeatureModel(), Spot.class, + Link.class ); assertThrows( IllegalArgumentException.class, () -> umapController.computeFeature( inputDimensionsSupplier ) ); } } @@ -173,11 +200,12 @@ void testPartiallyIllegalData() try (Context context = new Context()) { - UmapController umapController = new UmapController( graph2.getModel(), context ); - UmapFeatureSettings settings = umapController.getFeatureSettings(); + DimensionalityReductionController umapController = new DimensionalityReductionController( graph2.getModel(), context ); + UmapSettings settings = umapController.getUmapSettings(); settings.setNumberOfNeighbors( 5 ); - Supplier< List< UmapInputDimension< Spot > > > inputDimensionsSupplier = - () -> UmapInputDimension.getListFromFeatureModel( graph2.getModel().getFeatureModel(), Spot.class, Link.class ); + Supplier< List< InputDimension< Spot > > > inputDimensionsSupplier = + () -> InputDimension.getListFromFeatureModel( graph2.getModel().getFeatureModel(), Spot.class, + Link.class ); umapController.computeFeature( inputDimensionsSupplier ); Feature< Spot > spotUmapFeature = Cast.unchecked( featureModel.getFeature( SpotUmapFeature.GENERIC_SPEC ) ); Set< FeatureProjection< Spot > > projections = spotUmapFeature.projections(); @@ -204,10 +232,10 @@ void testMultipleIncomingEdges() try (Context context = new Context()) { - UmapController umapController = new UmapController( model, context ); + DimensionalityReductionController umapController = new DimensionalityReductionController( model, context ); FeatureModel featureModel = model.getFeatureModel(); - Supplier< List< UmapInputDimension< Spot > > > inputDimensionsSupplier = - () -> UmapInputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); + Supplier< List< InputDimension< Spot > > > inputDimensionsSupplier = + () -> InputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); umapController.computeFeature( inputDimensionsSupplier ); Feature< Spot > spotUmapFeature = Cast.unchecked( featureModel.getFeature( SpotUmapFeature.GENERIC_SPEC ) ); Set< FeatureProjection< Spot > > projections = spotUmapFeature.projections(); diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/PlotPoints.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/PlotPoints.java new file mode 100644 index 000000000..dc2ca0fb8 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/PlotPoints.java @@ -0,0 +1,68 @@ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import java.awt.Color; +import java.awt.Graphics; +import java.lang.invoke.MethodHandles; +import java.util.function.Predicate; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PlotPoints extends JPanel +{ + + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private final double[][] points; + + private final double[][] result; + + private final Predicate< double[] > filter; + + public static void plot( final double[][] sampleData, final double[][] umapResult, final Predicate< double[] > filter ) + { + PlotPoints plotPoints = new PlotPoints( sampleData, umapResult, filter ); + JFrame frame = new JFrame( "Dimensionality Reduction Demo. Reduction from 3 dimensions to 2." ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.setSize( 600, 400 ); + frame.add( plotPoints ); + frame.setVisible( true ); + } + + private PlotPoints( final double[][] points, final double[][] result, Predicate< double[] > filter ) + + { + this.points = points; + this.result = result; + this.filter = filter; + } + + @Override + protected void paintComponent( Graphics g ) + { + super.paintComponent( g ); + int offsetX = 200; + int offsetY = 100; + g.drawLine( -10 + offsetX, offsetY, 10 + offsetX, offsetY ); + g.drawLine( offsetX, -10 + offsetY, offsetX, 10 + offsetY ); + for ( int i = 0; i < points.length; i++ ) + { + int x = ( int ) points[ i ][ 0 ]; + int y = ( int ) points[ i ][ 1 ]; + int z = ( int ) points[ i ][ 2 ]; + int resultX = ( int ) result[ i ][ 0 ]; + int resultY = ( int ) result[ i ][ 1 ]; + if ( filter.test( result[ i ] ) ) + g.setColor( Color.RED ); + else + g.setColor( Color.BLUE ); + + logger.debug( "i = {}, x = {}, y = {}, z= {}, dim reduced X = {}, dim reduced Y = {}", i, x, y, z, resultX, resultY ); + g.fillOval( x, y, 5, 5 ); + g.fillRect( resultX + offsetX, resultY + offsetY, 2, 2 ); + } + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/RandomDataTools.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/RandomDataTools.java new file mode 100644 index 000000000..dcd5c56c0 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/RandomDataTools.java @@ -0,0 +1,52 @@ +package org.mastodon.mamut.feature.dimensionalityreduction; + +import java.util.Random; + +public class RandomDataTools +{ + private static final Random random = new Random( 42 ); + + public static double[][] generateSampleData() + { + return generateSampleData( 50, 100 ); + } + + public static double[][] generateSampleData( int numCluster1, int numCluster2 ) + { + double[][] firstPointCloud = generateRandomPointsInSphere( 100, 100, -10, 20, numCluster1 ); + double[][] secondPointCloud = generateRandomPointsInSphere( 250, 250, 10, 50, numCluster2 ); + + return concatenateArrays( firstPointCloud, secondPointCloud ); + } + + private static double[][] concatenateArrays( final double[][] firstPointCloud, final double[][] secondPointCloud ) + { + double[][] concatenated = new double[ firstPointCloud.length + secondPointCloud.length ][ 2 ]; + System.arraycopy( firstPointCloud, 0, concatenated, 0, firstPointCloud.length ); + System.arraycopy( secondPointCloud, 0, concatenated, firstPointCloud.length, secondPointCloud.length ); + return concatenated; + } + + private static double[][] generateRandomPointsInSphere( double centerX, double centerY, double centerZ, double radius, + int numberOfPoints ) + { + double[][] points = new double[ numberOfPoints ][ 3 ]; + + for ( int i = 0; i < numberOfPoints; i++ ) + { + double r = radius * Math.cbrt( random.nextDouble() ); + double theta = 2 * Math.PI * random.nextDouble(); + double phi = Math.acos( 2 * random.nextDouble() - 1 ); + + double x = centerX + r * Math.sin( phi ) * Math.cos( theta ); + double y = centerY + r * Math.sin( phi ) * Math.sin( theta ); + double z = centerZ + r * Math.cos( phi ); + + points[ i ][ 0 ] = x; + points[ i ][ 1 ] = y; + points[ i ][ 2 ] = z; + } + + return points; + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneDemo.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneDemo.java new file mode 100644 index 000000000..799e37a7a --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneDemo.java @@ -0,0 +1,31 @@ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne; + +import com.jujutsu.tsne.TSneConfiguration; +import com.jujutsu.tsne.barneshut.BarnesHutTSne; +import com.jujutsu.tsne.barneshut.ParallelBHTsne; +import com.jujutsu.utils.TSneUtils; + +import org.mastodon.mamut.feature.dimensionalityreduction.PlotPoints; +import org.mastodon.mamut.feature.dimensionalityreduction.RandomDataTools; + +public class TSneDemo +{ + public static void main( final String[] args ) + { + double[][] inputData = RandomDataTools.generateSampleData(); + TSneConfiguration config = setUpTSne( inputData ); + BarnesHutTSne tsne = new ParallelBHTsne(); // according to https://github.com/lejon/T-SNE-Java/ the parallel version is faster at same accuracy + double[][] tsneResult = tsne.tsne( config ); + PlotPoints.plot( inputData, tsneResult, resultValues -> resultValues[ 0 ] > 10 ); + } + + static TSneConfiguration setUpTSne( double[][] inputData ) + { + // Recommendations for t-SNE defaults: https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html + int initialDimensions = 50; // used if PCA is true and dimensions of the input data are greater than this value + double perplexity = 30d; // recommended value is between 5 and 50 + int maxIterations = 1000; // should be at least 250 + + return TSneUtils.buildConfig( inputData, 2, initialDimensions, perplexity, maxIterations, true, 0.5d, false, true ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneSettingsTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneSettingsTest.java new file mode 100644 index 000000000..6ea28aa0a --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneSettingsTest.java @@ -0,0 +1,90 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TSneSettingsTest +{ + private TSneSettings tSneSettings; + + @BeforeEach + void setUp() + { + tSneSettings = new TSneSettings(); + } + + @Test + void getPerplexity() + { + assertEquals( TSneSettings.DEFAULT_PERPLEXITY, tSneSettings.getPerplexity() ); + } + + @Test + void testGetMaxIterations() + { + assertEquals( TSneSettings.DEFAULT_MAX_ITERATIONS, tSneSettings.getMaxIterations() ); + } + + @Test + void testSetPerplexity() + { + tSneSettings.setPerplexity( 10 ); + assertEquals( 10, tSneSettings.getPerplexity() ); + } + + @Test + void testSetMaxIterations() + { + tSneSettings.setMaxIterations( 20 ); + assertEquals( 20, tSneSettings.getMaxIterations() ); + } + + @Test + void testIsValidPerplexity() + { + assertFalse( tSneSettings.isValidPerplexity( 90 ) ); + assertFalse( tSneSettings.isValidPerplexity( 0 ) ); + assertFalse( tSneSettings.isValidPerplexity( -1 ) ); + assertTrue( tSneSettings.isValidPerplexity( 91 ) ); + } + + @Test + void testIsValidMaxIterations() + { + assertEquals( TSneSettings.DEFAULT_PERPLEXITY, tSneSettings.getMaxValidPerplexity( 91 ) ); + assertEquals( 29, tSneSettings.getMaxValidPerplexity( 90 ) ); + assertEquals( 3, tSneSettings.getMaxValidPerplexity( 10 ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneTest.java new file mode 100644 index 000000000..a9736f4b1 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/TSneTest.java @@ -0,0 +1,77 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.invoke.MethodHandles; + +import com.jujutsu.tsne.TSneConfiguration; +import com.jujutsu.tsne.barneshut.BarnesHutTSne; +import com.jujutsu.tsne.barneshut.ParallelBHTsne; +import com.jujutsu.utils.TSneUtils; + +import org.junit.jupiter.api.Test; +import org.mastodon.mamut.feature.dimensionalityreduction.RandomDataTools; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class TSneTest +{ + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + @Test + void test() + { + int numCluster1 = 50; + int numCluster2 = 100; + double[][] inputData = RandomDataTools.generateSampleData( numCluster1, numCluster2 ); + logger.debug( "dimensions rows: {}, columns:{}", inputData.length, inputData[ 0 ].length ); + + // Recommendations for t-SNE defaults: https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html + int initialDimensions = 50; // used if PCA is true and dimensions of the input data are greater than this value + double perplexity = 30d; // recommended value is between 5 and 50 + int maxIterations = 1000; // should be at least 250 + + TSneConfiguration tSneConfig = + TSneUtils.buildConfig( inputData, 2, initialDimensions, perplexity, maxIterations, true, 0.5d, false, true ); + + BarnesHutTSne tsne = new ParallelBHTsne(); + double[][] tsneResult = tsne.tsne( tSneConfig ); + + assertEquals( tsneResult.length, inputData.length ); + assertEquals( 2, tsneResult[ 0 ].length ); + + for ( int i = 0; i < numCluster1; i++ ) + assertTrue( tsneResult[ i ][ 0 ] > 10 ); + for ( int i = numCluster1; i < numCluster1 + numCluster2; i++ ) + assertTrue( tsneResult[ i ][ 0 ] < 10 ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeatureComputerTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeatureComputerTest.java new file mode 100644 index 000000000..2ac801a47 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/tsne/feature/AbstractTSneFeatureComputerTest.java @@ -0,0 +1,37 @@ +package org.mastodon.mamut.feature.dimensionalityreduction.tsne.feature; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.mastodon.feature.FeatureModel; +import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionAlgorithm; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionController; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Spot; +import org.scijava.Context; + +class AbstractTSneFeatureComputerTest +{ + + @Test + void testDataDrivenExceptions() + { + ExampleGraph2 graph2 = new ExampleGraph2(); + try (Context context = new Context()) + { + FeatureModel featureModel = graph2.getModel().getFeatureModel(); + DimensionalityReductionController controller = new DimensionalityReductionController( graph2.getModel(), context ); + controller.setAlgorithm( DimensionalityReductionAlgorithm.TSNE ); + Supplier< List< InputDimension< Spot > > > inputDimensionsSupplier = + () -> InputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); + assertThrows( IllegalArgumentException.class, () -> controller.computeFeature( inputDimensionsSupplier ) ); + controller.getTSneSettings().setPerplexity( 2 ); + assertThrows( ArrayIndexOutOfBoundsException.class, () -> controller.computeFeature( inputDimensionsSupplier ) ); + } + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapViewDemo.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionViewDemo.java similarity index 82% rename from src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapViewDemo.java rename to src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionViewDemo.java index e67319a59..6d3b00f7c 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapViewDemo.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionViewDemo.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.ui; +package org.mastodon.mamut.feature.dimensionalityreduction.ui; import mpicbg.spim.data.SpimDataException; import org.mastodon.mamut.ProjectModel; @@ -34,11 +34,12 @@ import org.mastodon.mamut.io.ProjectLoader; import org.scijava.Context; +import javax.swing.JFrame; import javax.swing.UIManager; import java.io.File; import java.io.IOException; -public class UmapViewDemo +public class DimensionalityReductionViewDemo { public static void main( String[] args ) throws IOException, SpimDataException @@ -55,13 +56,13 @@ public static void main( String[] args ) throws IOException, SpimDataException try (Context context = new Context()) { - File tempFile1 = TestUtils.getTempFileCopy( "src/test/resources/org/mastodon/mamut/classification/model1.mastodon", "model", + File tempFile1 = TestUtils.getTempFileCopy( "src/test/resources/org/mastodon/mamut/clustering/model1.mastodon", "model", ".mastodon" ); ProjectModel projectModel = ProjectLoader.open( tempFile1.getAbsolutePath(), context, false, true ); - UmapView umapView = new UmapView( projectModel.getModel(), context ); - umapView.setVisible( true ); - umapView.setDefaultCloseOperation( UmapView.EXIT_ON_CLOSE ); + DimensionalityReductionView dimensionalityReductionView = new DimensionalityReductionView( projectModel.getModel(), context ); + dimensionalityReductionView.setVisible( true ); + dimensionalityReductionView.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); } } } diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapViewTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionViewTest.java similarity index 90% rename from src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapViewTest.java rename to src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionViewTest.java index 67e5e472f..003d2e997 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/ui/UmapViewTest.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/ui/DimensionalityReductionViewTest.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.ui; +package org.mastodon.mamut.feature.dimensionalityreduction.ui; import org.junit.jupiter.api.Test; import org.mastodon.mamut.model.Model; @@ -34,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -class UmapViewTest +class DimensionalityReductionViewTest { @Test void testUmapView() @@ -42,7 +42,7 @@ void testUmapView() try (Context context = new Context()) { Model model = new Model(); - assertDoesNotThrow( () -> new UmapView( model, context ) ); + assertDoesNotThrow( () -> new DimensionalityReductionView( model, context ) ); } } } diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/SimpleUmapDemo.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/SimpleUmapDemo.java deleted file mode 100644 index 846d8290f..000000000 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/SimpleUmapDemo.java +++ /dev/null @@ -1,146 +0,0 @@ -/*- - * #%L - * mastodon-deep-lineage - * %% - * Copyright (C) 2022 - 2024 Stefan Hahmann - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap; - -import tagbio.umap.Umap; - -import javax.swing.JFrame; -import javax.swing.JPanel; -import java.awt.Color; -import java.awt.Graphics; -import java.util.Random; - -public class SimpleUmapDemo extends JPanel -{ - private static final Random random = new Random( 42 ); - - public static void main( final String[] args ) - { - double[][] sampleData = generateSampleData(); - Umap umap = setUpUmap(); - double[][] umapResult = umap.fitTransform( sampleData ); - plot( sampleData, umapResult ); - } - - private static void plot( final double[][] sampleData, final double[][] umapResult ) - { - PlotPoints plotPoints = new PlotPoints( sampleData, umapResult ); - JFrame frame = new JFrame( "Simple Umap Demo. Reduction from 3 dimensions to 2." ); - frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); - frame.setSize( 600, 400 ); - frame.add( plotPoints ); - frame.setVisible( true ); - } - - static Umap setUpUmap() - { - Umap umap = new Umap(); - umap.setVerbose( true ); - umap.setNumberComponents( 2 ); - umap.setMinDist( 0.1f ); - umap.setNumberNearestNeighbours( 15 ); - umap.setRandom( random ); - return umap; - } - - static double[][] generateSampleData() - { - double[][] firstPointCloud = generateRandomPointsInSphere( 100, 100, -10, 20, 50 ); - double[][] secondPointCloud = generateRandomPointsInSphere( 250, 250, 10, 50, 100 ); - - return concatenateArrays( firstPointCloud, secondPointCloud ); - } - - private static double[][] concatenateArrays( final double[][] firstPointCloud, final double[][] secondPointCloud ) - { - double[][] concatenated = new double[ firstPointCloud.length + secondPointCloud.length ][ 2 ]; - System.arraycopy( firstPointCloud, 0, concatenated, 0, firstPointCloud.length ); - System.arraycopy( secondPointCloud, 0, concatenated, firstPointCloud.length, secondPointCloud.length ); - return concatenated; - } - - private static double[][] generateRandomPointsInSphere( double centerX, double centerY, double centerZ, double radius, - int numberOfPoints ) - { - double[][] points = new double[ numberOfPoints ][ 3 ]; - - for ( int i = 0; i < numberOfPoints; i++ ) - { - double r = radius * Math.cbrt( random.nextDouble() ); - double theta = 2 * Math.PI * random.nextDouble(); - double phi = Math.acos( 2 * random.nextDouble() - 1 ); - - double x = centerX + r * Math.sin( phi ) * Math.cos( theta ); - double y = centerY + r * Math.sin( phi ) * Math.sin( theta ); - double z = centerZ + r * Math.cos( phi ); - - points[ i ][ 0 ] = x; - points[ i ][ 1 ] = y; - points[ i ][ 2 ] = z; - } - - return points; - } - - private static class PlotPoints extends JPanel - { - - private final double[][] points; - - private final double[][] umapResult; - - private PlotPoints( final double[][] points, final double[][] umapResult ) - { - this.points = points; - this.umapResult = umapResult; - } - - @Override - protected void paintComponent( Graphics g ) - { - super.paintComponent( g ); - for ( int i = 0; i < points.length; i++ ) - { - int x = ( int ) points[ i ][ 0 ]; - int y = ( int ) points[ i ][ 1 ]; - int z = ( int ) points[ i ][ 2 ]; - int umapX = ( int ) umapResult[ i ][ 0 ]; - int umapY = ( int ) umapResult[ i ][ 1 ]; - if ( umapX > 0 ) - g.setColor( Color.RED ); - else - g.setColor( Color.BLUE ); - - System.out.println( "i = " + i + ", x = " + x + ", y = " + y + ", z= " + z + ", umapX = " + umapX + ", umapY = " + umapY ); - g.fillOval( x, y, 5, 5 ); - g.fillRect( umapX + 200, umapY + 100, 2, 2 ); - } - } - } -} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapDemo.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapDemo.java new file mode 100644 index 000000000..626b9aa64 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapDemo.java @@ -0,0 +1,61 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.dimensionalityreduction.umap; + +import java.util.Random; + +import javax.swing.JPanel; + +import org.mastodon.mamut.feature.dimensionalityreduction.PlotPoints; +import org.mastodon.mamut.feature.dimensionalityreduction.RandomDataTools; + +import tagbio.umap.Umap; + +public class UmapDemo extends JPanel +{ + + public static void main( final String[] args ) + { + double[][] sampleData = RandomDataTools.generateSampleData(); + Umap umap = setUpUmap(); + double[][] umapResult = umap.fitTransform( sampleData ); + PlotPoints.plot( sampleData, umapResult, resultValues -> resultValues[ 0 ] > 0 ); + } + + static Umap setUpUmap() + { + Umap umap = new Umap(); + umap.setVerbose( true ); + umap.setNumberComponents( 2 ); + umap.setMinDist( 0.1f ); + umap.setNumberNearestNeighbours( 15 ); + umap.setRandom( new Random( 42 ) ); + return umap; + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapFeatureSettingsTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapSettingsTest.java similarity index 55% rename from src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapFeatureSettingsTest.java rename to src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapSettingsTest.java index 0d75c0cae..ebb32f6ae 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapFeatureSettingsTest.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapSettingsTest.java @@ -33,65 +33,39 @@ import static org.junit.jupiter.api.Assertions.*; -class UmapFeatureSettingsTest +class UmapSettingsTest { - private UmapFeatureSettings umapFeatureSettings; + private UmapSettings umapSettings; @BeforeEach void setUp() { - umapFeatureSettings = new UmapFeatureSettings(); + umapSettings = new UmapSettings(); } @Test - void getNumberOfOutputDimensions() + void testGetNumberOfNeighbors() { - assertEquals( UmapFeatureSettings.DEFAULT_NUMBER_OF_OUTPUT_DIMENSIONS, umapFeatureSettings.getNumberOfOutputDimensions() ); + assertEquals( UmapSettings.DEFAULT_NUMBER_OF_NEIGHBORS, umapSettings.getNumberOfNeighbors() ); } @Test - void getNumberOfNeighbors() + void testGetMinimumDistance() { - assertEquals( UmapFeatureSettings.DEFAULT_NUMBER_OF_NEIGHBORS, umapFeatureSettings.getNumberOfNeighbors() ); + assertEquals( UmapSettings.DEFAULT_MINIMUM_DISTANCE, umapSettings.getMinimumDistance() ); } @Test - void getMinimumDistance() + void testSetNumberOfNeighbors() { - assertEquals( UmapFeatureSettings.DEFAULT_MINIMUM_DISTANCE, umapFeatureSettings.getMinimumDistance() ); + umapSettings.setNumberOfNeighbors( 10 ); + assertEquals( 10, umapSettings.getNumberOfNeighbors() ); } @Test - void isStandardizeFeatures() + void testSetMinimumDistance() { - assertEquals( UmapFeatureSettings.DEFAULT_STANDARDIZE_FEATURES, umapFeatureSettings.isStandardizeFeatures() ); - } - - @Test - void setNumberOfOutputDimensions() - { - umapFeatureSettings.setNumberOfOutputDimensions( 5 ); - assertEquals( 5, umapFeatureSettings.getNumberOfOutputDimensions() ); - } - - @Test - void setNumberOfNeighbors() - { - umapFeatureSettings.setNumberOfNeighbors( 10 ); - assertEquals( 10, umapFeatureSettings.getNumberOfNeighbors() ); - } - - @Test - void setMinimumDistance() - { - umapFeatureSettings.setMinimumDistance( 0.5 ); - assertEquals( 0.5, umapFeatureSettings.getMinimumDistance() ); - } - - @Test - void setStandardizeFeatures() - { - umapFeatureSettings.setStandardizeFeatures( false ); - assertFalse( umapFeatureSettings.isStandardizeFeatures() ); + umapSettings.setMinimumDistance( 0.5 ); + assertEquals( 0.5, umapSettings.getMinimumDistance() ); } } diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapTest.java index f046860e8..12f45ce0e 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapTest.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/UmapTest.java @@ -29,6 +29,8 @@ package org.mastodon.mamut.feature.dimensionalityreduction.umap; import org.junit.jupiter.api.Test; +import org.mastodon.mamut.feature.dimensionalityreduction.RandomDataTools; + import tagbio.umap.Umap; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -39,8 +41,8 @@ class UmapTest @Test void test() { - double[][] sampleData = SimpleUmapDemo.generateSampleData(); - Umap umap = SimpleUmapDemo.setUpUmap(); + double[][] sampleData = RandomDataTools.generateSampleData(); + Umap umap = UmapDemo.setUpUmap(); double[][] umapResult = umap.fitTransform( sampleData ); assertEquals( umapResult.length, sampleData.length ); diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/UmapInputDimensionTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/util/InputDimensionTest.java similarity index 80% rename from src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/UmapInputDimensionTest.java rename to src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/util/InputDimensionTest.java index cf3bd2423..3e545761a 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/UmapInputDimensionTest.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/util/InputDimensionTest.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.util; +package org.mastodon.mamut.feature.dimensionalityreduction.util; import org.junit.jupiter.api.Test; import org.mastodon.feature.FeatureModel; @@ -38,7 +38,7 @@ import static org.junit.jupiter.api.Assertions.*; -class UmapInputDimensionTest +class InputDimensionTest { @Test @@ -46,9 +46,9 @@ void getListFromFeatureModel() { Model model = new Model(); FeatureModel featureModel = model.getFeatureModel(); - List< UmapInputDimension< Spot > > umapInputDimensions = - UmapInputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); - assertNotNull( umapInputDimensions ); - assertFalse( umapInputDimensions.isEmpty() ); // NB: we do not test for specific content, as this is defined by the core and may change. + List< InputDimension< Spot > > inputDimensions = + InputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); + assertNotNull( inputDimensions ); + assertFalse( inputDimensions.isEmpty() ); // NB: we do not test for specific content, as this is defined by the core and may change. } } diff --git a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/StandardScalerTest.java b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/util/StandardScalerTest.java similarity index 98% rename from src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/StandardScalerTest.java rename to src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/util/StandardScalerTest.java index ac0163b75..4c5afb083 100644 --- a/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/umap/util/StandardScalerTest.java +++ b/src/test/java/org/mastodon/mamut/feature/dimensionalityreduction/util/StandardScalerTest.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.mastodon.mamut.feature.dimensionalityreduction.umap.util; +package org.mastodon.mamut.feature.dimensionalityreduction.util; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureTest.java b/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureTest.java new file mode 100644 index 000000000..198dfa8d3 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/tsne/SpotTSneFeatureTest.java @@ -0,0 +1,142 @@ +/*- + * #%L + * mastodon-deep-lineage + * %% + * Copyright (C) 2022 - 2024 Stefan Hahmann + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.feature.spot.dimensionalityreduction.tsne; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mastodon.feature.Dimension; +import org.mastodon.feature.FeatureModel; +import org.mastodon.feature.FeatureProjection; +import org.mastodon.feature.FeatureProjectionSpec; +import org.mastodon.mamut.feature.AbstractFeatureTest; +import org.mastodon.mamut.feature.FeatureSerializerTestUtils; +import org.mastodon.mamut.feature.FeatureUtils; +import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph7; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionAlgorithm; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionController; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Spot; +import org.scijava.Context; + +public class SpotTSneFeatureTest extends AbstractFeatureTest< Spot > +{ + private SpotTSneFeature spotTSneFeature; + + private final ExampleGraph7 graph7 = new ExampleGraph7(); + + private FeatureProjectionSpec spec0; + + private FeatureProjectionSpec spec1; + + @BeforeEach + public void setUp() + { + try (Context context = new Context()) + { + FeatureModel featureModel = graph7.getModel().getFeatureModel(); + DimensionalityReductionController controller = new DimensionalityReductionController( graph7.getModel(), context ); + controller.setAlgorithm( DimensionalityReductionAlgorithm.TSNE ); + Supplier< List< InputDimension< Spot > > > inputDimensionsSupplier = + () -> InputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); + controller.computeFeature( inputDimensionsSupplier ); + spotTSneFeature = FeatureUtils.getFeature( graph7.getModel(), SpotTSneFeature.SpotTSneFeatureSpec.class ); + assertNotNull( spotTSneFeature ); + spec0 = new FeatureProjectionSpec( spotTSneFeature.getProjectionName( 0 ), Dimension.NONE ); + spec1 = new FeatureProjectionSpec( spotTSneFeature.getProjectionName( 1 ), Dimension.NONE ); + } + } + + @Test + @Override + public void testFeatureComputation() + { + assertNotNull( spotTSneFeature ); + FeatureProjection< Spot > projection0 = getProjection( spotTSneFeature, spec0 ); + FeatureProjection< Spot > projection1 = getProjection( spotTSneFeature, spec1 ); + Iterator< Spot > spotIterator = graph7.getModel().getGraph().vertices().iterator(); + Spot spot0 = spotIterator.next(); + assertTrue( Double.isNaN( projection0.value( spot0 ) ) ); + assertTrue( Double.isNaN( projection1.value( spot0 ) ) ); + Spot spot1 = spotIterator.next(); + assertFalse( Double.isNaN( projection0.value( spot1 ) ) ); + assertFalse( Double.isNaN( projection1.value( spot1 ) ) ); + assertNotEquals( 0, projection0.value( spot1 ) ); + assertNotEquals( 0, projection1.value( spot1 ) ); + } + + @Test + @Override + public void testFeatureSerialization() throws IOException + { + SpotTSneFeature spotTSneFeatureReloaded; + try (Context context = new Context()) + { + spotTSneFeatureReloaded = + ( SpotTSneFeature ) FeatureSerializerTestUtils.saveAndReload( context, graph7.getModel(), spotTSneFeature ); + } + assertNotNull( spotTSneFeatureReloaded ); + // check that the feature has correct values after saving and reloading + Iterator< Spot > spotIterator = graph7.getModel().getGraph().vertices().iterator(); + spotIterator.next(); + Spot spot1 = spotIterator.next(); + assertTrue( FeatureSerializerTestUtils.checkFeatureProjectionEquality( spotTSneFeature, spotTSneFeatureReloaded, + Collections.singleton( spot1 ) ) ); + } + + @Test + @Override + public void testFeatureInvalidate() + { + // test, if features are not NaN before invalidation + Iterator< Spot > spotIterator = graph7.getModel().getGraph().vertices().iterator(); + spotIterator.next(); + Spot spot1 = spotIterator.next(); + assertFalse( Double.isNaN( getProjection( spotTSneFeature, spec0 ).value( spot1 ) ) ); + assertFalse( Double.isNaN( getProjection( spotTSneFeature, spec1 ).value( spot1 ) ) ); + + // invalidate feature + spotTSneFeature.invalidate( spot1 ); + + // test, if features are NaN after invalidation + assertTrue( Double.isNaN( getProjection( spotTSneFeature, spec0 ).value( spot1 ) ) ); + assertTrue( Double.isNaN( getProjection( spotTSneFeature, spec1 ).value( spot1 ) ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureTest.java b/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureTest.java index 506dafa1d..30c14c2a9 100644 --- a/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureTest.java +++ b/src/test/java/org/mastodon/mamut/feature/spot/dimensionalityreduction/umap/SpotUmapFeatureTest.java @@ -38,9 +38,9 @@ import org.mastodon.mamut.feature.FeatureSerializerTestUtils; import org.mastodon.mamut.feature.FeatureUtils; import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapController; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapFeatureSettings; -import org.mastodon.mamut.feature.dimensionalityreduction.umap.util.UmapInputDimension; +import org.mastodon.mamut.feature.dimensionalityreduction.DimensionalityReductionController; +import org.mastodon.mamut.feature.dimensionalityreduction.umap.UmapSettings; +import org.mastodon.mamut.feature.dimensionalityreduction.util.InputDimension; import org.mastodon.mamut.model.Link; import org.mastodon.mamut.model.Spot; import org.scijava.Context; @@ -71,11 +71,11 @@ public void setUp() try (Context context = new Context()) { FeatureModel featureModel = graph2.getModel().getFeatureModel(); - UmapController umapController = new UmapController( graph2.getModel(), context ); - UmapFeatureSettings settings = umapController.getFeatureSettings(); + DimensionalityReductionController umapController = new DimensionalityReductionController( graph2.getModel(), context ); + UmapSettings settings = umapController.getUmapSettings(); settings.setNumberOfNeighbors( 5 ); - Supplier< List< UmapInputDimension< Spot > > > inputDimensionsSupplier = - () -> UmapInputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); + Supplier< List< InputDimension< Spot > > > inputDimensionsSupplier = + () -> InputDimension.getListFromFeatureModel( featureModel, Spot.class, Link.class ); umapController.computeFeature( inputDimensionsSupplier ); spotUmapFeature = FeatureUtils.getFeature( graph2.getModel(), SpotUmapFeature.SpotUmapFeatureSpec.class ); assertNotNull( spotUmapFeature );