Skip to content

Commit 6c3f5a1

Browse files
Merge pull request #139 from mastodon-sc/lineage-motifs-multiple-projects
Compare lineage motifs from multiple projects by importing the motif to compare
2 parents 89e4910 + 75c62df commit 6c3f5a1

32 files changed

Lines changed: 2111 additions & 668 deletions

README.md

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -452,41 +452,53 @@ Tree2
452452

453453
## Lineage Motif Search
454454

455-
* Menu Location: `Plugins > Lineage Analysis > Lineage Motif Search`
456-
* This command is capable of finding lineage motifs that are similar to a motif defined by a selection of the user.
457-
* The linage motif search operates on Mastodon's branch graph.
455+
* Menu Location: `Plugins > Lineage Analysis > Find lineage motifs`
456+
* This command is capable of finding lineage motifs that are similar to a motif defined by either a tracklet selected by
457+
the user or a tracklet imported via a graph ml file.
458+
* The linage motif search can operate on Mastodon's branch graph or on the model graph. While using the model graph is
459+
slightly more accurate, it is recommended to use the branch graph since it is a lot faster.
458460
* Lineage trees are considered similar if they share a similar structure and thus represent a similar cell division
459-
pattern. The structure of a lineage tree is represented by the tree topology.
461+
pattern. The tree topology represents the structure of a lineage tree.
460462
This tree topology consists of the actual branching pattern and the cell lifetimes,
461-
i.e., the time points between two subsequent cell divisions.
462-
* The algorithm iterates over the branch graph
463+
i.e., the time points between two cell divisions.
463464

464465
### Workflow
465466

466-
1. The user selects a motif in the track scheme, which should be searched for in the lineage trees.
467+
1. The user selects a motif
468+
1. By selecting it in the track scheme
469+
2. By importing it from a graph ml file
467470
2. The algorithm iterates over each branch in the branch graph using an offset before the first division, which is the
468471
same as the duration before the first division of the selected motif.
469-
3. For each branch, the algorithm computes the tree edit distance between the selected motif and the branch
472+
3. For each branch, the algorithm computes the tree edit distance between the selected (imported) motif and the branch
470473
tree. For more details on the tree edit distance, see section [Zhang tree edit distance](#zhang-tree-edit-distance).
471-
4. The algorithm creates a new tag set and tags the spots belonging to the `n` most similar branches with a color
474+
4. The algorithm sorts the branches based on their computed distance (ascending order) to the selected (imported) motif.
475+
5. The algorithm creates a new tag set and tags the spots belonging to the `n` most similar branches with a color
472476
that is faded out from the original motif color. `n` is the number of motifs specified by the user. The lower the
473477
computed distance, the more similar the found motif is to the original motif.
474478

475479
### Usage
476480

477-
1. The user selects a motif in the track scheme, which should be searched for in the lineage trees.
478-
1. The motif must have exactly one root node, i.e. the selected spots all must be connected to one root spot.
481+
1. The user selects a motif, which should be searched for in the lineage trees.
482+
1. Directly in the track scheme
483+
2. By importing it from a graph ml file. The file can be chosen to choose the file in the dialog
484+
3. In both cases: the motif must have exactly one root node, i.e., the selected spots all must be connected to one
485+
root spot.
479486
2. Open the dialog. Set the parameters and click on `OK`.
480-
3. Visualize the results in the track scheme and/or the BigDataViewer using `View > Coloring > The generated tag set`.
487+
3. The algorithm computes the most similar motifs in the lineage trees and creates a new tag set with a tag and color
488+
for each found motif.
489+
4. Visualize the results in the track scheme and/or the BigDataViewer using `View > Coloring > The generated tag set`.
481490

482491
### Parameters
483492

484493
* Number of motifs
485494
* The number of motifs to search for in the lineage trees.
486495
* The given motif will always be included in the results.
487-
* Color
488-
* A color that will be used to tag spots that are part of a motif. The actual colors will be faded out versions of
489-
the given color. The more the color is faded, the less similar is the found motif to the original motif.
496+
* Color of the most similar motif
497+
* A color that will be used to tag spots that are part of the most similar motif.
498+
* Color of the least similar motif
499+
* A color that will be used to tag spots that are part of the least similar motif.
500+
* The colors of the motifs in between will be interpolated on a gradient from the color of the most similar motif to
501+
the color of the least similar motif.
490502
* Similarity measure:
491503
1. (default) ![normalized_zhang_distance.gif](doc/clustering/normalized_zhang_distance.gif)<sup>1,2</sup>
492504
2. ![per_branch_zhang_distance.gif](doc/clustering/per_branch_zhang_distance.gif)<sup>1</sup>
@@ -495,6 +507,19 @@ Tree2
495507
* <sup>1</sup>Local cost function: ![local_cost.gif](doc/clustering/local_cost.gif)
496508
* <sup>2</sup>Local cost function with
497509
normalization: ![local_cost_normalized.gif](doc/clustering/local_cost_normalized.gif)
510+
* Run on:
511+
* The graph on which the motif search should be run
512+
1. Branch graph (default): faster, (sightly) less accurate
513+
2. Model graph: much slower, (sightly) more accurate
514+
* Load motif from a file:
515+
* Only available when choosing Find similar motifs based on imported motif
516+
* The file must be a graph ml file containing a single tree with exactly one root node
517+
* Such a file can be exported from any Mastodon project using after selecting a tracklet and
518+
`File > Export > Export selected spots to GraphML (one file)`
519+
* Scaling of the search motif:
520+
* Only available when choosing Find similar motifs based on imported motif
521+
* The imported motif can be scaled in time to account for faster or slower cell cycles in the current project
522+
compared to the project from which the motif had been exported.
498523

499524
### Example
500525

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@
179179
<scope>test</scope>
180180
</dependency>
181181

182+
<!-- Awaitility used in some unit tests -->
183+
<dependency>
184+
<groupId>org.awaitility</groupId>
185+
<artifactId>awaitility</artifactId>
186+
<version>4.3.0</version>
187+
<scope>test</scope>
188+
</dependency>
189+
182190

183191
<!-- hierarchical clustering, tests only -->
184192
<dependency>

src/main/java/org/mastodon/mamut/clustering/config/SimilarityMeasure.java

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,12 @@
1-
/*-
2-
* #%L
3-
* mastodon-deep-lineage
4-
* %%
5-
* Copyright (C) 2022 - 2025 Stefan Hahmann
6-
* %%
7-
* Redistribution and use in source and binary forms, with or without
8-
* modification, are permitted provided that the following conditions are met:
9-
*
10-
* 1. Redistributions of source code must retain the above copyright notice,
11-
* this list of conditions and the following disclaimer.
12-
* 2. Redistributions in binary form must reproduce the above copyright notice,
13-
* this list of conditions and the following disclaimer in the documentation
14-
* and/or other materials provided with the distribution.
15-
*
16-
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17-
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18-
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19-
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20-
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21-
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22-
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23-
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24-
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25-
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26-
* POSSIBILITY OF SUCH DAMAGE.
27-
* #L%
28-
*/
291
package org.mastodon.mamut.clustering.config;
302

31-
import org.apache.commons.lang3.function.TriFunction;
3+
import java.util.NoSuchElementException;
4+
325
import org.mastodon.mamut.clustering.treesimilarity.TreeDistances;
336
import org.mastodon.mamut.clustering.treesimilarity.ZhangUnorderedTreeEditDistance;
347
import org.mastodon.mamut.clustering.treesimilarity.tree.Tree;
35-
36-
import java.util.NoSuchElementException;
37-
import java.util.function.ToDoubleBiFunction;
8+
import org.mastodon.mamut.util.ToDoubleQuadFunction;
9+
import org.mastodon.mamut.util.ToDoubleTriFunction;
3810

3911
public enum SimilarityMeasure implements HasName
4012
{
@@ -56,31 +28,32 @@ public enum SimilarityMeasure implements HasName
5628

5729
private final String name;
5830

59-
private final TriFunction< Tree< Double >, Tree< Double >, ToDoubleBiFunction< Double, Double >, Double > distanceFunction;
31+
private final ToDoubleQuadFunction< Tree< Double >, Tree< Double >, ToDoubleTriFunction< Double, Double, Double >,
32+
Double > distanceFunction;
6033

61-
private final ToDoubleBiFunction< Double, Double > costFunction;
34+
private final ToDoubleTriFunction< Double, Double, Double > costFunctionWithScale;
6235

63-
SimilarityMeasure( final String name,
64-
final TriFunction< Tree< Double >, Tree< Double >, ToDoubleBiFunction< Double, Double >, Double > distanceFunction,
65-
final ToDoubleBiFunction< Double, Double > costFunction )
36+
SimilarityMeasure( final String name, final ToDoubleQuadFunction< Tree< Double >, Tree< Double >,
37+
ToDoubleTriFunction< Double, Double, Double >, Double > distanceFunction,
38+
final ToDoubleTriFunction< Double, Double, Double > costFunctionWithScale )
6639
{
6740
this.name = name;
6841
this.distanceFunction = distanceFunction;
69-
this.costFunction = costFunction;
42+
this.costFunctionWithScale = costFunctionWithScale;
7043
}
7144

72-
public static SimilarityMeasure getByName(final String name)
45+
public static SimilarityMeasure getByName( final String name )
7346
{
74-
for (final SimilarityMeasure measure : values())
75-
if (measure.getName().equals(name))
47+
for ( final SimilarityMeasure measure : values() )
48+
if ( measure.getName().equals( name ) )
7649
return measure;
7750

7851
throw new NoSuchElementException();
7952
}
8053

81-
public double compute( final Tree< Double > tree1, final Tree< Double > tree2 )
54+
public double compute( final Tree< Double > tree1, final Tree< Double > tree2, final Double scale )
8255
{
83-
return distanceFunction.apply( tree1, tree2, costFunction );
56+
return distanceFunction.applyAsDouble( tree1, tree2, costFunctionWithScale, scale );
8457
}
8558

8659
public String getName()

src/main/java/org/mastodon/mamut/clustering/treesimilarity/TreeDistances.java

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030

3131
import org.mastodon.mamut.clustering.treesimilarity.tree.Tree;
3232
import org.mastodon.mamut.clustering.treesimilarity.tree.TreeUtils;
33+
import org.mastodon.mamut.util.ToDoubleTriFunction;
3334

3435
import javax.annotation.Nullable;
35-
import java.util.function.ToDoubleBiFunction;
3636

3737
/**
3838
* Utility class for calculating distances between trees.
@@ -51,14 +51,16 @@ private TreeDistances()
5151
*
5252
* @see <a href="https://gitlab.inria.fr/mosaic/treex/-/blob/master/test/test_analysis/test_zhang_labeled_trees.py?ref_type=heads#L99">treex library</a>
5353
*/
54-
public static final ToDoubleBiFunction< Double, Double > LOCAL_ABSOLUTE_COST_FUNCTION = TreeDistances::localAbsoluteCostFunction;
54+
public static final ToDoubleTriFunction< Double, Double, Double > LOCAL_ABSOLUTE_COST_FUNCTION =
55+
TreeDistances::localAbsoluteCostFunction;
5556

5657
/**
5758
* Cost function as used in Guignard et al. 2020. It returns the normalized absolute difference between two attributes or 1 if one attribute is {@code null}.
5859
*
5960
* @see <a href="https://www.science.org/doi/suppl/10.1126/science.aar5663/suppl_file/aar5663_guignard_sm.pdf">Guignard et al. (2020) Page 38-39</a>
6061
*/
61-
public static final ToDoubleBiFunction< Double, Double > LOCAL_NORMALIZED_COST_FUNCTION = TreeDistances::localNormalizedCostFunction;
62+
public static final ToDoubleTriFunction< Double, Double, Double > LOCAL_NORMALIZED_COST_FUNCTION =
63+
TreeDistances::localNormalizedCostFunction;
6264

6365
/**
6466
* Calculates the normalized Zhang edit distance between two labeled unordered trees.
@@ -73,14 +75,14 @@ private TreeDistances()
7375
* @return The normalized Zhang edit distance between tree1 and tree2.
7476
*/
7577
public static < T > double normalizedDistance( @Nullable final Tree< T > tree1, final @Nullable Tree< T > tree2,
76-
final ToDoubleBiFunction< T, T > costFunction )
78+
final ToDoubleTriFunction< T, T, T > costFunction, final T scale )
7779
{
78-
double denominator = ZhangUnorderedTreeEditDistance.distance( tree1, null, costFunction )
79-
+ ZhangUnorderedTreeEditDistance.distance( null, tree2, costFunction );
80+
double denominator = ZhangUnorderedTreeEditDistance.distance( tree1, null, costFunction, scale )
81+
+ ZhangUnorderedTreeEditDistance.distance( null, tree2, costFunction, scale );
8082
// NB: avoid division by zero. Two empty trees are considered equal and also two trees with zero edit distance are considered equal.
8183
if ( denominator == 0 )
8284
return 0;
83-
return ZhangUnorderedTreeEditDistance.distance( tree1, tree2, costFunction ) / denominator;
85+
return ZhangUnorderedTreeEditDistance.distance( tree1, tree2, costFunction, scale ) / denominator;
8486
}
8587

8688
/**
@@ -96,38 +98,55 @@ public static < T > double normalizedDistance( @Nullable final Tree< T > tree1,
9698
* @return The average Zhang edit distance between tree1 and tree2.
9799
*/
98100
public static < T > double averageDistance( @Nullable final Tree< T > tree1, final @Nullable Tree< T > tree2,
99-
final ToDoubleBiFunction< T, T > costFunction )
101+
final ToDoubleTriFunction< T, T, T > costFunction, final T scale )
100102
{
101103
double denominator = ( double ) TreeUtils.size( tree1 ) + ( double ) TreeUtils.size( tree2 );
102104
// NB: avoid division by zero. Two empty trees are considered equal.
103105
if ( denominator == 0 )
104106
return 0;
105-
return ZhangUnorderedTreeEditDistance.distance( tree1, tree2, costFunction ) / denominator;
107+
return ZhangUnorderedTreeEditDistance.distance( tree1, tree2, costFunction, scale ) / denominator;
106108
}
107109

108110
/**
109111
* @see <a href="https://gitlab.inria.fr/mosaic/treex/-/blob/master/test/test_analysis/test_zhang_labeled_trees.py?ref_type=heads#L99">treex library</a>
110112
*/
111-
private static Double localAbsoluteCostFunction( final Double o1, final Double o2 )
113+
private static Double localAbsoluteCostFunction( final Double o1, final Double o2, final Double scale )
112114
{
113115
if ( o2 == null )
114116
return o1;
115117
else if ( o1 == null )
116-
return o2;
118+
return scale == 1d ? o2 : o2 * scale;
117119
else
118-
return Math.abs( o1 - o2 );
120+
{
121+
if ( scale == 1d )
122+
return Math.abs( o1 - o2 );
123+
else
124+
return Math.abs( o1 - o2 * scale );
125+
}
119126
}
120127

121128
/**
122129
* @see <a href="https://www.science.org/doi/suppl/10.1126/science.aar5663/suppl_file/aar5663_guignard_sm.pdf">Guignard et al. (2020) Page 38</a>
123130
*/
124-
private static Double localNormalizedCostFunction( final Double o1, final Double o2 )
131+
private static Double localNormalizedCostFunction( final Double o1, final Double o2, final Double scale )
125132
{
126133
if ( o1 == null || o2 == null )
127134
return 1d;
128-
else if ( o1.equals( o2 ) ) // NB: avoid non-required division and possible division by zero
129-
return 0d;
135+
136+
if ( scale == 1d )
137+
{
138+
if ( o1.equals( o2 ) )
139+
return 0d;
140+
else
141+
return Math.abs( o1 - o2 ) / ( o1 + o2 );
142+
}
130143
else
131-
return Math.abs( o1 - o2 ) / ( o1 + o2 );
144+
{
145+
double scaleTimesO2 = o2 * scale;
146+
if ( o1.equals( scaleTimesO2 ) )
147+
return 0d;
148+
else
149+
return Math.abs( o1 - scaleTimesO2 ) / ( o1 + scaleTimesO2 );
150+
}
132151
}
133152
}

0 commit comments

Comments
 (0)