From 6087bb8bd9e7b0291beb99af79e35f4fe189cea2 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Wed, 19 Apr 2017 13:21:13 +0100 Subject: [PATCH] JAL-2480 cache min-max score values per sequence and feature type --- src/jalview/datamodel/features/FeatureStore.java | 175 +++++++++++++++----- .../datamodel/features/SequenceFeatures.java | 30 ++++ .../datamodel/features/FeatureStoreTest.java | 90 ++++++++++ .../datamodel/features/SequenceFeaturesTest.java | 62 +++++++ 4 files changed, 314 insertions(+), 43 deletions(-) diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java index 23edade..cd7d055 100644 --- a/src/jalview/datamodel/features/FeatureStore.java +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -127,6 +127,14 @@ public class FeatureStore */ int totalExtent; + float positionalMinScore; + + float positionalMaxScore; + + float nonPositionalMinScore; + + float nonPositionalMaxScore; + /** * Constructor */ @@ -134,6 +142,11 @@ public class FeatureStore { nonNestedFeatures = new ArrayList(); positionalFeatureGroups = new HashSet(); + nonPositionalFeatureGroups = new HashSet(); + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; // we only construct nonPositionalFeatures, contactFeatures // or the NCList if we need to @@ -182,14 +195,33 @@ public class FeatureStore } } - /* - * record the total extent of positional features, to make - * getTotalFeatureLength possible; we count the length of a - * contact feature as 1 - */ if (added) { + /* + * record the total extent of positional features, to make + * getTotalFeatureLength possible; we count the length of a + * contact feature as 1 + */ totalExtent += getFeatureLength(feature); + + /* + * record the minimum and maximum score for positional + * and non-positional features + */ + float score = feature.getScore(); + if (!Float.isNaN(score)) + { + if (feature.isNonPositional()) + { + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + else + { + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); + } + } } return added; @@ -229,7 +261,6 @@ public class FeatureStore if (nonPositionalFeatures == null) { nonPositionalFeatures = new ArrayList(); - nonPositionalFeatureGroups = new HashSet(); } if (nonPositionalFeatures.contains(feature)) { @@ -657,55 +688,87 @@ public class FeatureStore if (removed) { - /* - * rescan (positional or non-positional) features to rebuild the - * set of distinct feature groups present - */ - rebuildFeatureGroups(sf.getFeatureGroup(), removedNonPositional); - - /* - * subtract deleted feature's length from stored total length - * TODO: can start/end have changed since the feature was added? - */ - int extent = getFeatureLength(sf); - totalExtent = Math.max(0, totalExtent - extent); + rescanAfterDelete(); } return removed; } /** - * Check whether the given feature group is still represented, in either - * positional or non-positional features, and if not, remove it from the set - * of feature groups + * Rescan all features to recompute any cached values after an entry has been + * deleted + */ + protected synchronized void rescanAfterDelete() + { + positionalFeatureGroups.clear(); + nonPositionalFeatureGroups.clear(); + totalExtent = 0; + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; + + /* + * scan non-positional features for groups and scores + */ + for (SequenceFeature sf : getNonPositionalFeatures()) + { + nonPositionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + + /* + * scan positional features for groups, scores and extents + */ + for (SequenceFeature sf : getPositionalFeatures()) + { + positionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); + totalExtent += getFeatureLength(sf); + } + } + + /** + * A helper method to return the minimum of two floats, where a non-NaN value + * is treated as 'less than' a NaN value (unlike Math.min which does the + * opposite) * - * @param featureGroup - * @param nonPositional + * @param f1 + * @param f2 */ - protected void rebuildFeatureGroups(String featureGroup, - boolean nonPositional) + protected static float min(float f1, float f2) { - if (nonPositional && nonPositionalFeatures != null) + if (Float.isNaN(f1)) { - boolean found = false; - for (SequenceFeature sf : nonPositionalFeatures) - { - String group = sf.getFeatureGroup(); - if (featureGroup == group - || (featureGroup != null && featureGroup.equals(group))) - { - found = true; - break; - } - } - if (!found) - { - nonPositionalFeatureGroups.remove(featureGroup); - } + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.min(f1, f2); + } + } + + /** + * A helper method to return the maximum of two floats, where a non-NaN value + * is treated as 'greater than' a NaN value (unlike Math.max which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float max(float f1, float f2) + { + if (Float.isNaN(f1)) + { + return Float.isNaN(f2) ? f1 : f2; } - else if (!findFeatureGroup(featureGroup)) + else { - positionalFeatureGroups.remove(featureGroup); + return Float.isNaN(f2) ? f1 : Math.max(f1, f2); } } @@ -845,4 +908,30 @@ public class FeatureStore { return totalExtent; } + + /** + * Answers the minimum score held for positional or non-positional features. + * This may be Float.NaN if there are no features, are none has a non-NaN + * score. + * + * @param positional + * @return + */ + public float getMinimumScore(boolean positional) + { + return positional ? positionalMinScore : nonPositionalMinScore; + } + + /** + * Answers the maximum score held for positional or non-positional features. + * This may be Float.NaN if there are no features, are none has a non-NaN + * score. + * + * @param positional + * @return + */ + public float getMaximumScore(boolean positional) + { + return positional ? positionalMaxScore : nonPositionalMaxScore; + } } diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java index 2f40193..c825761 100644 --- a/src/jalview/datamodel/features/SequenceFeatures.java +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -297,4 +297,34 @@ public class SequenceFeatures implements SequenceFeaturesI } return types; } + + /** + * Answers the minimum score held for positional or non-positional features + * for the specified type. This may be Float.NaN if there are no features, or + * none has a non-NaN score. + * + * @param type + * @param positional + * @return + */ + public float getMinimumScore(String type, boolean positional) + { + return featureStore.containsKey(type) ? featureStore.get(type) + .getMinimumScore(positional) : Float.NaN; + } + + /** + * Answers the maximum score held for positional or non-positional features + * for the specified type. This may be Float.NaN if there are no features, or + * none has a non-NaN score. + * + * @param type + * @param positional + * @return + */ + public float getMaximumScore(String type, boolean positional) + { + return featureStore.containsKey(type) ? featureStore.get(type) + .getMaximumScore(positional) : Float.NaN; + } } diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java index 6e30990..5d3b13f 100644 --- a/test/jalview/datamodel/features/FeatureStoreTest.java +++ b/test/jalview/datamodel/features/FeatureStoreTest.java @@ -597,4 +597,94 @@ public class FeatureStoreTest 14, 28, 1f, "AGroup"); assertEquals(FeatureStore.getFeatureLength(sf3), 1); } + + @Test(groups = "Functional") + public void testMin() + { + assertEquals(FeatureStore.min(Float.NaN, Float.NaN), Float.NaN); + assertEquals(FeatureStore.min(Float.NaN, 2f), 2f); + assertEquals(FeatureStore.min(-2f, Float.NaN), -2f); + assertEquals(FeatureStore.min(2f, -3f), -3f); + } + + @Test(groups = "Functional") + public void testMax() + { + assertEquals(FeatureStore.max(Float.NaN, Float.NaN), Float.NaN); + assertEquals(FeatureStore.max(Float.NaN, 2f), 2f); + assertEquals(FeatureStore.max(-2f, Float.NaN), -2f); + assertEquals(FeatureStore.max(2f, -3f), 2f); + } + + @Test(groups = "Functional") + public void testGetMinimumScore_getMaximumScore() + { + FeatureStore fs = new FeatureStore(); + assertEquals(fs.getMinimumScore(true), Float.NaN); // positional + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); // non-positional + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // add features with no score + SequenceFeature sf1 = new SequenceFeature("type", "desc", 0, 0, + Float.NaN, "group"); + fs.addFeature(sf1); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 10, 20, + Float.NaN, "group"); + fs.addFeature(sf2); + assertEquals(fs.getMinimumScore(true), Float.NaN); + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // add positional features with score + SequenceFeature sf3 = new SequenceFeature("type", "desc", 10, 20, 1f, + "group"); + fs.addFeature(sf3); + SequenceFeature sf4 = new SequenceFeature("type", "desc", 12, 16, 4f, + "group"); + fs.addFeature(sf4); + assertEquals(fs.getMinimumScore(true), 1f); + assertEquals(fs.getMaximumScore(true), 4f); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // add non-positional features with score + SequenceFeature sf5 = new SequenceFeature("type", "desc", 0, 0, 11f, + "group"); + fs.addFeature(sf5); + SequenceFeature sf6 = new SequenceFeature("type", "desc", 0, 0, -7f, + "group"); + fs.addFeature(sf6); + assertEquals(fs.getMinimumScore(true), 1f); + assertEquals(fs.getMaximumScore(true), 4f); + assertEquals(fs.getMinimumScore(false), -7f); + assertEquals(fs.getMaximumScore(false), 11f); + + // delete one positional and one non-positional + // min-max should be recomputed + assertTrue(fs.delete(sf6)); + assertTrue(fs.delete(sf3)); + assertEquals(fs.getMinimumScore(true), 4f); + assertEquals(fs.getMaximumScore(true), 4f); + assertEquals(fs.getMinimumScore(false), 11f); + assertEquals(fs.getMaximumScore(false), 11f); + + // delete remaining features with score + assertTrue(fs.delete(sf4)); + assertTrue(fs.delete(sf5)); + assertEquals(fs.getMinimumScore(true), Float.NaN); + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + + // delete all features + assertTrue(fs.delete(sf1)); + assertTrue(fs.delete(sf2)); + assertTrue(fs.isEmpty()); + assertEquals(fs.getMinimumScore(true), Float.NaN); + assertEquals(fs.getMaximumScore(true), Float.NaN); + assertEquals(fs.getMinimumScore(false), Float.NaN); + assertEquals(fs.getMaximumScore(false), Float.NaN); + } } diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java index 31703cd..eaf20b0 100644 --- a/test/jalview/datamodel/features/SequenceFeaturesTest.java +++ b/test/jalview/datamodel/features/SequenceFeaturesTest.java @@ -768,4 +768,66 @@ public class SequenceFeaturesTest assertTrue(store.delete(sf1)); assertEquals(store.getTotalFeatureLength(), 0); } + + @Test + public void testGetMinimumScore_getMaximumScore() + { + SequenceFeatures sf = new SequenceFeatures(); + SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 0, 0, + Float.NaN, "group"); // non-positional, no score + sf.add(sf1); + SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 10, 20, + Float.NaN, "group"); // positional, no score + sf.add(sf2); + SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 10, 20, 1f, + "group"); + sf.add(sf3); + SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 12, 16, 4f, + "group"); + sf.add(sf4); + SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 0, 0, 11f, + "group"); + sf.add(sf5); + SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, -7f, + "group"); + sf.add(sf6); + + assertEquals(sf.getMinimumScore("nosuchtype", true), Float.NaN); + assertEquals(sf.getMinimumScore("nosuchtype", false), Float.NaN); + assertEquals(sf.getMaximumScore("nosuchtype", true), Float.NaN); + assertEquals(sf.getMaximumScore("nosuchtype", false), Float.NaN); + + // positional features min-max: + assertEquals(sf.getMinimumScore("Metal", true), 1f); + assertEquals(sf.getMaximumScore("Metal", true), 4f); + assertEquals(sf.getMinimumScore("Cath", true), Float.NaN); + assertEquals(sf.getMaximumScore("Cath", true), Float.NaN); + + // non-positional features min-max: + assertEquals(sf.getMinimumScore("Cath", false), -7f); + assertEquals(sf.getMaximumScore("Cath", false), 11f); + assertEquals(sf.getMinimumScore("Metal", false), Float.NaN); + assertEquals(sf.getMaximumScore("Metal", false), Float.NaN); + + // delete features; min-max should get recomputed + sf.delete(sf6); + assertEquals(sf.getMinimumScore("Cath", false), 11f); + assertEquals(sf.getMaximumScore("Cath", false), 11f); + sf.delete(sf4); + assertEquals(sf.getMinimumScore("Metal", true), 1f); + assertEquals(sf.getMaximumScore("Metal", true), 1f); + sf.delete(sf5); + assertEquals(sf.getMinimumScore("Cath", false), Float.NaN); + assertEquals(sf.getMaximumScore("Cath", false), Float.NaN); + sf.delete(sf3); + assertEquals(sf.getMinimumScore("Metal", true), Float.NaN); + assertEquals(sf.getMaximumScore("Metal", true), Float.NaN); + sf.delete(sf1); + sf.delete(sf2); + assertFalse(sf.hasFeatures()); + assertEquals(sf.getMinimumScore("Cath", false), Float.NaN); + assertEquals(sf.getMaximumScore("Cath", false), Float.NaN); + assertEquals(sf.getMinimumScore("Metal", true), Float.NaN); + assertEquals(sf.getMaximumScore("Metal", true), Float.NaN); + } } -- 1.7.10.2