You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@uima.apache.org by re...@apache.org on 2020/10/21 07:19:24 UTC

[uima-uimaj] branch feature/UIMA-6283-Annotation-relation-predicates created (now 5c75217)

This is an automated email from the ASF dual-hosted git repository.

rec pushed a change to branch feature/UIMA-6283-Annotation-relation-predicates
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git.


      at 5c75217  [UIMA-6283] Annotation relation predicates

This branch includes the following new commits:

     new 5c75217  [UIMA-6283] Annotation relation predicates

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[uima-uimaj] 01/01: [UIMA-6283] Annotation relation predicates

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rec pushed a commit to branch feature/UIMA-6283-Annotation-relation-predicates
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git

commit 5c75217429a757cf6ff667b242fd6daac2348873
Author: Richard Eckart de Castilho <re...@apache.org>
AuthorDate: Wed Oct 21 09:19:12 2020 +0200

    [UIMA-6283] Annotation relation predicates
    
    - Added AnnotationPredicates class
    - Added more unit tests to SelectFsTest (good part currently failing)
---
 .../apache/uima/cas/text/AnnotationPredicates.java | 215 +++++++++++
 .../org/apache/uima/cas/impl/SelectFsAssert.java   | 132 +++++++
 .../org/apache/uima/cas/impl/SelectFsTest.java     | 148 +++++++-
 .../uima/cas/text/AnnotationPredicatesTest.java    | 407 +++++++++++++++++++++
 4 files changed, 900 insertions(+), 2 deletions(-)

diff --git a/uimaj-core/src/main/java/org/apache/uima/cas/text/AnnotationPredicates.java b/uimaj-core/src/main/java/org/apache/uima/cas/text/AnnotationPredicates.java
new file mode 100644
index 0000000..ae630e5
--- /dev/null
+++ b/uimaj-core/src/main/java/org/apache/uima/cas/text/AnnotationPredicates.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.cas.text;
+
+public final class AnnotationPredicates {
+  private AnnotationPredicates() {
+    // No instances
+  }
+  
+  public static boolean coveredBy(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin <= aXBegin && aXEnd <= aYEnd && (aXBegin == aYBegin || aXBegin != aYEnd);
+  }
+
+  public static boolean coveredBy(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    return aYBegin <= xBegin && (xBegin == aYBegin || xBegin != aYEnd) && aX.getEnd() <= aYEnd;
+  }
+
+  /**
+   * Y is starting before or at the same position as A and ends after or at the same position as X.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is covered by Y.
+   */
+  public static boolean coveredBy(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    int yBegin = aY.getBegin();
+    int yEnd = aY.getEnd();
+    return yBegin <= xBegin && (xBegin == yBegin || xBegin != yEnd) && aX.getEnd() <= yEnd;
+  }
+
+  public static boolean covers(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin <= aYBegin && aYEnd <= aXEnd && (aYBegin == aXBegin || aYBegin != aXEnd);
+  }
+
+  public static boolean covers(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    int xEnd = aX.getEnd();
+    return xBegin <= aYBegin && aYEnd <= xEnd && (aYBegin == xBegin || aYBegin != xEnd);
+  }
+
+  /**
+   * X is starting before or at the same position as Y and ends after or at the same position as Y.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is covering Y.
+   */
+  public static boolean covers(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    int xEnd = aX.getEnd();
+    int yBegin = aY.getBegin();
+    return xBegin <= yBegin && (yBegin == xBegin || yBegin != xEnd) && aY.getEnd() <= xEnd;
+  }
+
+  public static boolean colocated(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin == aYBegin && aXEnd == aYEnd;
+  }
+
+  public static boolean colocated(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aX.getBegin() == aYBegin && aX.getEnd() == aYEnd;
+  }
+
+  /**
+   * X starts and ends at the same position as Y.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is at the same location as Y.
+   */
+  public static boolean colocated(AnnotationFS aX, AnnotationFS aY) {
+    return aX.getBegin() == aY.getBegin() && aX.getEnd() == aY.getEnd();
+  }
+
+  public static boolean overlaps(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin == aXBegin || (aXBegin < aYEnd && aYBegin < aXEnd);
+  }
+
+  public static boolean overlaps(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    return aYBegin == xBegin || (xBegin < aYEnd && aYBegin < aX.getEnd());
+  }
+
+  /**
+   * The intersection of the spans X and Y is non-empty. If either X or Y have a zero-width, then
+   * the intersection is considered to be non-empty if the begin of X is either within Y or the same
+   * as the begin of Y - and vice versa.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X overlaps with Y in any way.
+   */
+  public static boolean overlaps(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    int yBegin = aY.getBegin();
+    return yBegin == xBegin || (xBegin < aY.getEnd() && yBegin < aX.getEnd());
+  }
+
+  public static boolean overlapsLeft(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin < aYBegin && aYBegin < aXEnd && aXEnd < aYEnd;
+  }
+
+  public static boolean overlapsLeft(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xEnd = aX.getEnd();
+    return aYBegin < xEnd && xEnd < aYEnd && aX.getBegin() < aYBegin;
+  }
+
+  /**
+   * X is starting before or at the same position as Y and ends before Y ends.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X overlaps Y on the left.
+   */
+  public static boolean overlapsLeft(AnnotationFS aX, AnnotationFS aY) {
+    int xEnd = aX.getEnd();
+    int yBegin = aY.getBegin();
+    return yBegin < xEnd && xEnd < aY.getEnd() && aX.getBegin() < yBegin;
+  }
+
+  public static boolean overlapsRight(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin < aXBegin && aXBegin < aYEnd && aYEnd < aXEnd;
+  }
+
+  public static boolean overlapsRight(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    return aYBegin < xBegin && xBegin < aYEnd && aYEnd < aX.getEnd();
+  }
+
+  /**
+   * X is starting after Y starts and ends after or at the same position as Y.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X overlaps Y on the right.
+   */
+  public static boolean overlapsRight(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    int yEnd = aY.getEnd();
+    return xBegin < yEnd && aY.getBegin() < xBegin && yEnd < aX.getEnd();
+  }
+
+  public static boolean rightOf(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin >= aYEnd && aXBegin != aYBegin;
+  }
+
+  public static boolean rightOf(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    return xBegin >= aYEnd && xBegin != aYBegin;
+  }
+
+  /**
+   * X starts at or after the position that Y ends.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is right of Y.
+   */
+  public static boolean rightOf(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    return xBegin >= aY.getEnd() && xBegin != aY.getBegin();
+  }
+
+  public static boolean leftOf(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin >= aXEnd && aXBegin != aYBegin;
+  }
+
+  public static boolean leftOf(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aYBegin >= aX.getEnd() && aX.getBegin() != aYBegin;
+  }
+
+  /**
+   * X ends before or at the position that Y starts.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X left of Y.
+   */
+  public static boolean leftOf(AnnotationFS aX, AnnotationFS aY) {
+    return aY.getBegin() >= aX.getEnd() && aX.getBegin() != aY.getBegin();
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java b/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java
new file mode 100644
index 0000000..1f6cdd3
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.cas.impl;
+
+import static java.util.Arrays.asList;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.TEST_CASES;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition;
+import org.apache.uima.cas.text.AnnotationPredicatesTest.TestCase;
+import org.apache.uima.jcas.tcas.Annotation;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+import org.assertj.core.api.AutoCloseableSoftAssertions;
+
+public class SelectFsAssert {
+  public static void assertSelectFS(RelativePosition aCondition, RelativeAnnotationPredicate aPredicate)
+      throws Exception {
+    CAS cas = CasCreationUtils.createCas();
+    Type type = cas.getAnnotationType();
+
+    try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) {
+      for (TestCase testCase : TEST_CASES) {
+        cas.reset();
+
+        // Create annotations
+        Annotation x = (Annotation) cas.createAnnotation(type, 0, 0);
+        Annotation y = (Annotation) cas.createAnnotation(type, 0, 0);
+
+        // Position the annotations according to the test data
+        testCase.getTest().apply((beginA, endA, beginB, endB) -> {
+          x.setBegin(beginA);
+          x.setEnd(endA);
+          y.setBegin(beginB);
+          y.setEnd(endB);
+          cas.addFsToIndexes(x);
+          cas.addFsToIndexes(y);
+          return true;
+        });
+
+        softly.assertThat(aPredicate.apply(cas, type, x, y)).as(testCase.getDescription())
+            .isEqualTo(testCase.getValidPositions().contains(aCondition));
+      }
+    }
+  }
+
+  public static void assertSelectionIsEqualOnRandomData(TypeByContextSelector aExpected, TypeByContextSelector aActual)
+      throws Exception {
+    final int ITERATIONS = 50;
+
+    String type1Name = "test.Type1";
+    String type2Name = "test.Type2";
+
+    TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
+    tsd.addType(type1Name, "", CAS.TYPE_NAME_ANNOTATION);
+    tsd.addType(type2Name, "", CAS.TYPE_NAME_ANNOTATION);
+
+    CAS randomCas = CasCreationUtils.createCas(tsd, null, null, null);
+    Type type1 = randomCas.getTypeSystem().getType(type1Name);
+    Type type2 = randomCas.getTypeSystem().getType(type2Name);
+
+    for (int i = 0; i < ITERATIONS; i++) {
+      System.out.printf("Iteration %d%n", i);
+
+      initRandomCas(randomCas, 3 * i, type1, type2);
+
+      for (Annotation context : randomCas.<Annotation>select(type1)) {
+        List<AnnotationFS> expected = aExpected.select(randomCas, type2, context);
+        List<AnnotationFS> actual = aActual.select(randomCas, type2, context);
+
+        assertThat(actual)
+            .as("Selected [%s] with context [%s]@[%d..%d]", type2Name, type1Name, context.getBegin(), context.getEnd())
+            .containsExactlyElementsOf(expected);
+      }
+    }
+  }
+
+  private static void initRandomCas(CAS aCas, int aSize, Type... aTypes) {
+    Random rnd = new Random();
+
+    List<Type> types = new ArrayList<>(asList(aTypes));
+
+    // Shuffle the types
+    for (int n = 0; n < 10; n++) {
+      Type t = types.remove(rnd.nextInt(types.size()));
+      types.add(t);
+    }
+
+    // Randomly generate annotations
+    for (int n = 0; n < aSize; n++) {
+      for (Type t : types) {
+        int begin = rnd.nextInt(100);
+        int end = begin + rnd.nextInt(30);
+        aCas.addFsToIndexes(aCas.createAnnotation(t, begin, end));
+      }
+    }
+  }
+
+  @FunctionalInterface
+  public static interface RelativeAnnotationPredicate {
+    boolean apply(CAS cas, Type type, Annotation x, Annotation y);
+  }
+
+  @FunctionalInterface
+  public static interface TypeByContextSelector {
+    List<AnnotationFS> select(CAS aCas, Type aType, Annotation aContext);
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsTest.java b/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsTest.java
index b2089a9..c7b004e 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsTest.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsTest.java
@@ -16,23 +16,37 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.uima.cas.impl;
 
+import static java.util.Arrays.asList;
 import static java.util.stream.Collectors.toList;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.TEST_CASES;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.COLOCATED;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.COVERED_BY;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.COVERING;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.LEFT_OF;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.RIGHT_OF;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.apache.uima.cas.impl.SelectFsAssert.*;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Random;
 import java.util.stream.Collectors;
 
 import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
 import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.cas.text.AnnotationPredicates;
+import org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition;
+import org.apache.uima.cas.text.AnnotationPredicatesTest.TestCase;
 import org.apache.uima.jcas.JCas;
 import org.apache.uima.jcas.tcas.Annotation;
 import org.apache.uima.resource.metadata.TypeSystemDescription;
@@ -40,13 +54,17 @@ import org.apache.uima.resource.metadata.impl.TypePriorities_impl;
 import org.apache.uima.test.junit_extension.JUnitExtension;
 import org.apache.uima.util.CasCreationUtils;
 import org.apache.uima.util.XMLInputSource;
+import org.assertj.core.api.AutoCloseableSoftAssertions;
 import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
 import org.junit.Test;
+import org.junit.runners.MethodSorters;
 
 import x.y.z.Sentence;
 import x.y.z.Token;
 
-public class SelectFsTest  {
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SelectFsTest {
 
   private static TypeSystemDescription typeSystemDescription;
   
@@ -299,4 +317,130 @@ public class SelectFsTest  {
 
     assertThat(result).containsExactly(a1, a2, a3);
   }
+  
+  @Test
+  public void thatSelectAtWorksOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> cas.getAnnotationIndex(type).select()
+            .filter(candidate -> AnnotationPredicates.colocated(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> cas.<Annotation>select(type)
+                .at(context)
+                .map(a -> (AnnotationFS) a)
+                .collect(toList()));
+  }
+
+  @Test
+  public void thatSelectCoveringWorksOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> cas.getAnnotationIndex(type).select()
+            .filter(candidate -> AnnotationPredicates.covers(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> cas.<Annotation>select(type)
+                .covering(context)
+                .map(a -> (AnnotationFS) a)
+                .collect(toList()));
+  }
+
+  @Test
+  public void thatSelectCoveredByWorksOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> cas.getAnnotationIndex(type).select()
+            .filter(candidate -> AnnotationPredicates.coveredBy(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> cas.<Annotation>select(type)
+                .coveredBy(context)
+                .map(a -> (AnnotationFS) a)
+                .collect(toList()));
+  }
+  
+  @Test
+  public void thatSelectCoveredByZeroSizeAtEndOfContextIsNotIncluded()
+  {
+    Annotation a1 = cas.createAnnotation(cas.getCasType(Sentence.class), 0, 1);
+    Annotation a2 = cas.createAnnotation(cas.getCasType(Token.class), 1, 1);
+    
+    asList(a1, a2).forEach(cas::addFsToIndexes);
+    
+    List<Annotation> selection = cas.<Annotation>select(cas.getCasType(Token.class))
+        .coveredBy(a1)
+        .asList();
+    
+    assertThat(selection)
+            .isEmpty();
+  }
+
+//  @Test
+//  public void thatSingleCaseIAmCurrentlyDebugging()
+//  {
+//    Annotation a1 = cas.createAnnotation(cas.getCasType(Sentence.class), 13, 31);
+//    Annotation a2 = cas.createAnnotation(cas.getCasType(Token.class), 10, 31);
+//    
+//    asList(a1, a2).forEach(cas::addFsToIndexes);
+//    
+//    List<Annotation> selection = cas.<Annotation>select(cas.getCasType(Sentence.class))
+//        .covering(a2)
+//        .asList();
+//    
+//    assertThat(selection)
+//            .containsExactly(a1);
+//  }
+
+  @Test
+  public void thatSelectCoveringBeroSizeAtEndOfContextIsNotIncluded()
+  {
+    Annotation a1 = cas.createAnnotation(cas.getCasType(Sentence.class), 6, 27);
+    Annotation a2 = cas.createAnnotation(cas.getCasType(Sentence.class), 11, 13);
+    Annotation a3 = cas.createAnnotation(cas.getCasType(Token.class), 13, 13);
+    
+    asList(a1, a2, a3).forEach(cas::addFsToIndexes);
+    
+    List<Annotation> selection = cas.<Annotation>select(cas.getCasType(Sentence.class))
+        .covering(a3)
+        .asList();
+    
+    assertThat(selection)
+            .containsExactly(a1);
+  }
+
+  @Test
+  public void thatSelectFsBehaviorAlignsWithLeftOfPredicate() throws Exception {
+    // In order to find annotations that X is left of, we select the following annotations
+    assertSelectFS(LEFT_OF,
+            (cas, type, x, y) -> cas.select(type).following(x).asList().contains(y));
+  }
+
+  @Test
+  public void thatSelectFsBehaviorAlignsWithRightOfPredicate() throws Exception {
+    // In order to find annotations that X is right of, we select the preceding annotations
+    assertSelectFS(RIGHT_OF,
+            (cas, type, x, y) -> cas.select(type).preceding(x).asList().contains(y));
+  }
+
+  @Test
+  public void thatSelectFsBehaviorAlignsWithCoveredByPredicate() throws Exception {
+    // X covered by Y means that Y is covering X, so we need to select the covering annotations
+    // below.
+    assertSelectFS(COVERED_BY,
+            (cas, type, x, y) -> cas.select(type).covering(x).asList().contains(y));
+  }
+
+  @Test
+  public void thatSelectFsBehaviorAlignsWithCoveringPredicate() throws Exception {
+    // X covering Y means that Y is covered by Y, so we need to select the covered by annotations
+    // below.
+    assertSelectFS(COVERING,
+            (cas, type, x, y) -> cas.select(type).coveredBy(x).asList().contains(y));
+  }
+
+  @Test
+  public void thatSelectFsBehaviorAlignsWithColocatedPredicate() throws Exception {
+    // X covering Y means that Y is covered by Y, so we need to select the covered by annotations
+    // below.
+    assertSelectFS(COLOCATED,
+            (cas, type, x, y) -> cas.select(type).at(x).asList().contains(y));
+  }
 }
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/text/AnnotationPredicatesTest.java b/uimaj-core/src/test/java/org/apache/uima/cas/text/AnnotationPredicatesTest.java
new file mode 100644
index 0000000..2a7dd10
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/text/AnnotationPredicatesTest.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.cas.text;
+
+import static java.lang.Integer.MAX_VALUE;
+import static java.util.Arrays.asList;
+import static org.apache.uima.cas.text.AnnotationPredicates.colocated;
+import static org.apache.uima.cas.text.AnnotationPredicates.coveredBy;
+import static org.apache.uima.cas.text.AnnotationPredicates.covers;
+import static org.apache.uima.cas.text.AnnotationPredicates.leftOf;
+import static org.apache.uima.cas.text.AnnotationPredicates.overlaps;
+import static org.apache.uima.cas.text.AnnotationPredicates.overlapsLeft;
+import static org.apache.uima.cas.text.AnnotationPredicates.overlapsRight;
+import static org.apache.uima.cas.text.AnnotationPredicates.rightOf;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.COLOCATED;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.COVERED_BY;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.COVERING;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.LEFT_OF;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.OVERLAPPING;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.OVERLAPPING_LEFT;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.OVERLAPPING_RIGHT;
+import static org.apache.uima.cas.text.AnnotationPredicatesTest.RelativePosition.RIGHT_OF;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.util.CasCreationUtils;
+import org.assertj.core.api.AutoCloseableSoftAssertions;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class AnnotationPredicatesTest {
+    
+    public static enum RelativePosition {
+      COLOCATED,
+      OVERLAPPING,
+      OVERLAPPING_LEFT,
+      OVERLAPPING_RIGHT,
+      COVERING,
+      COVERED_BY,
+      LEFT_OF,
+      RIGHT_OF
+    }
+    
+    // Used as fixed references for the annotation relation cases.
+    private static final int BEGIN = 10;
+    private static final int END = 20;
+    private static final int Z_POS = 10;
+
+    public static final List<TestCase> TEST_CASES = asList(
+        new TestCase("1) Y begins and ends after X (### [---])", 
+                p -> p.apply(BEGIN, END, END + 1, MAX_VALUE),
+                asList(LEFT_OF)),
+        new TestCase("2) Y begins at X's end and ends after X (###[---])", 
+                p -> p.apply(BEGIN, END, END, MAX_VALUE),
+                asList(LEFT_OF)),
+        new TestCase("3) Y begins within and ends after X (##[#--])", 
+                p -> p.apply(BEGIN, END, END - 1 , MAX_VALUE),
+                asList(OVERLAPPING, OVERLAPPING_LEFT)),
+        new TestCase("4) Y begins and ends at X's boundries ([###])", 
+                p -> p.apply(BEGIN, END, BEGIN, END),
+                asList(OVERLAPPING, COLOCATED, COVERED_BY, COVERING)),
+        new TestCase("5) Y begins and ends within X (#[#]#)", 
+                p -> p.apply(BEGIN, END, BEGIN + 1, END - 1),
+                asList(OVERLAPPING, COVERING)),
+        new TestCase("6) Y begins at and ends before X's boundries ([##]#)", 
+                p -> p.apply(BEGIN, END, BEGIN, END - 1),
+                asList(OVERLAPPING, COVERING)),
+        new TestCase("7) Y begins after and ends at X's boundries (#[##])", 
+                p -> p.apply(BEGIN, END, BEGIN + 1, END),
+                asList(OVERLAPPING, COVERING)),
+        new TestCase("8) Y begins before and ends after X's boundries ([-###-])", 
+                p -> p.apply(BEGIN, END, BEGIN - 1, END + 1),
+                asList(OVERLAPPING, COVERED_BY)),
+        new TestCase("9) X starts where Y begins and ends within Y ([##-])", 
+                p -> p.apply(BEGIN, END, BEGIN, END + 1),
+                asList(OVERLAPPING, COVERED_BY)),
+        new TestCase("10) X starts within Y and ends where Y ends ([-##])", 
+                p -> p.apply(BEGIN, END, BEGIN - 1, END),
+                asList(OVERLAPPING, COVERED_BY)),
+        new TestCase("11) Y begins before and ends within X ([--#]##)", 
+                p -> p.apply(BEGIN, END, 0, BEGIN + 1),
+                asList(OVERLAPPING, OVERLAPPING_RIGHT)),
+        new TestCase("12) Y begins before and ends where X begins ([---]###)", 
+                p -> p.apply(BEGIN, END, 0, BEGIN),
+                asList(RIGHT_OF)),
+        new TestCase("13) Y begins and ends before X begins ([---] ###)", 
+                p -> p.apply(BEGIN, END, 0, BEGIN - 1),
+                asList(RIGHT_OF)),
+        new TestCase("Z1) Zero-width X before Y start (# [---])", 
+                p -> p.apply(Z_POS, Z_POS, Z_POS + 10, Z_POS + 20),
+                asList(LEFT_OF)),
+        new TestCase("Z2) Zero-width Y after X's end (### |)", 
+                p -> p.apply(BEGIN, END, END + 1, END + 1),
+                asList(LEFT_OF)),
+        new TestCase("Z3) Zero-width X at Y's start (#---])", 
+                p -> p.apply(Z_POS, Z_POS, Z_POS, Z_POS + 10),
+                asList(OVERLAPPING, COVERED_BY)),
+        new TestCase("Z4) Zero-width X at Y's end ([---#)", 
+                p -> p.apply(Z_POS, Z_POS, Z_POS-10, Z_POS),
+                asList(RIGHT_OF)),
+        new TestCase("Z5) Zero-width Y where X begins (|###)", 
+                p -> p.apply(BEGIN, END, BEGIN, BEGIN),
+                asList(OVERLAPPING, COVERING)),
+        new TestCase("Z6) Zero-width Y within X (#|#)", 
+                p -> p.apply(BEGIN, END, BEGIN + 1, BEGIN + 1),
+                asList(OVERLAPPING, COVERING)),
+        new TestCase("Z7) Zero-width Y at X's end (###|)", 
+                p -> p.apply(BEGIN, END, END, END),
+                asList(LEFT_OF)),
+        new TestCase("Z8) Zero-width X with Y (-|-)", 
+                p -> p.apply(Z_POS, Z_POS, Z_POS - 5, Z_POS + 5),
+                asList(OVERLAPPING, COVERED_BY)),
+        new TestCase("Z9) Zero-width X after Y's end ([---] #)", 
+                p -> p.apply(Z_POS, Z_POS, Z_POS - 10, Z_POS - 5),
+                asList(RIGHT_OF)),
+        new TestCase("Z10) Zero-width Y before X begins (| ###)", 
+                p -> p.apply(BEGIN, END, BEGIN - 1, BEGIN - 1),
+                asList(RIGHT_OF)),
+        new TestCase("Z11) Zero-width X matches zero-width Y start/end (#)", 
+                p -> p.apply(Z_POS, Z_POS, Z_POS, Z_POS),
+                asList(OVERLAPPING, COVERED_BY, COVERING, COLOCATED)));
+  
+    @Test
+    public void thatCoveringWithIntervalsWorks() throws Exception {
+      assertPosition(COVERING, AnnotationPredicates::covers);
+    }
+
+    @Test
+    public void thatCoveringWithAnnotationAndIntervalWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();;
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(COVERING, (beginA, endA, beginB,
+              endB) -> covers(cas.createAnnotation(type, beginA, endA), beginB, endB));
+    }
+
+    @Test
+    public void thatCoveringWithAnnotationsWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();;
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(COVERING,
+              (beginA, endA, beginB, endB) -> covers(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+    }
+    
+    @Test
+    public void thatCoveredByWithIntervalsWorks() throws Exception {
+      assertPosition(COVERED_BY, AnnotationPredicates::coveredBy);
+    }
+
+    @Test
+    public void thatCoveredByWithAnnotationAndIntervalWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(COVERED_BY, (beginA, endA, beginB,
+              endB) -> coveredBy(cas.createAnnotation(type, beginA, endA), beginB, endB));
+    }
+
+    @Test
+    public void thatCoveredByWithAnnotationsWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(COVERED_BY,
+              (beginA, endA, beginB, endB) -> coveredBy(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+    }
+
+    @Test
+    public void thatColocatedWithIntervalsWorks() throws Exception {
+      assertPosition(COLOCATED, AnnotationPredicates::colocated);
+      
+      // It must also work if we switch the of the spans
+      assertPosition(COLOCATED,
+              (beginA, endA, beginB, endB) -> colocated(beginB, endB, beginA, endA));
+    }
+
+    @Test
+    public void thatColocatedWithAnnotationAndIntervalWorks() throws Exception
+    {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(COLOCATED, (beginA, endA, beginB,
+              endB) -> colocated(cas.createAnnotation(type, beginA, endA), beginB, endB));
+      
+      // It must also work if we switch the of the spans
+      assertPosition(COLOCATED, (beginA, endA, beginB,
+              endB) -> colocated(cas.createAnnotation(type, beginB, endB), beginA, endA));
+    }
+
+    @Test
+    public void thatColocatedWithAnnotationsWorks() throws Exception
+    {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(COLOCATED,
+              (beginA, endA, beginB, endB) -> colocated(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+      
+      // It must also work if we switch the of the spans
+      assertPosition(COLOCATED,
+              (beginA, endA, beginB, endB) -> colocated(cas.createAnnotation(type, beginB, endB),
+                      cas.createAnnotation(type, beginA, endA)));
+    }
+    
+    @Test
+    public void thatOverlapsLeftWithIntervalsWorks() throws Exception {
+      assertPosition(OVERLAPPING_LEFT, AnnotationPredicates::overlapsLeft);
+    }
+
+    @Test
+    public void thatOverlapsLeftWithAnnotationAndIntervalWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(OVERLAPPING_LEFT, (beginA, endA, beginB,
+              endB) -> overlapsLeft(cas.createAnnotation(type, beginA, endA), beginB, endB));
+    }
+
+    @Test
+    public void thatOverlapsLeftWithAnnotationsWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(OVERLAPPING_LEFT,
+              (beginA, endA, beginB, endB) -> overlapsLeft(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+    }
+
+    @Test
+    public void thatOverlapsRightWithIntervalsWorks() throws Exception {
+      assertPosition(OVERLAPPING_RIGHT, AnnotationPredicates::overlapsRight);
+    }
+
+    @Test
+    public void thatOverlapsRightWithAnnotationAndIntervalWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(OVERLAPPING_RIGHT, (beginA, endA, beginB,
+              endB) -> overlapsRight(cas.createAnnotation(type, beginA, endA), beginB, endB));
+    }
+
+    @Test
+    public void thatOverlapsRightWithAnnotationsWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(OVERLAPPING_RIGHT,
+              (beginA, endA, beginB, endB) -> overlapsRight(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+    }
+    
+    @Test
+    public void thatOverlapsWithIntervalsWorks() throws Exception {
+      assertPosition(OVERLAPPING, AnnotationPredicates::overlaps);
+
+      // It must also work if we switch the of the spans
+      assertPosition(OVERLAPPING,
+              (beginA, endA, beginB, endB) -> overlaps(beginB, endB, beginA, endA));
+    }
+
+    @Test
+    public void thatOverlapsWithAnnotationAndIntervalWorks() throws Exception
+    {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(OVERLAPPING, (beginA, endA, beginB,
+              endB) -> overlaps(cas.createAnnotation(type, beginA, endA), beginB, endB));
+      
+      // It must also work if we switch the of the spans
+      assertPosition(OVERLAPPING, (beginA, endA, beginB,
+              endB) -> overlaps(cas.createAnnotation(type, beginB, endB), beginA, endA));
+    }
+
+    @Test
+    public void thatOverlapsWithAnnotationsWorks() throws Exception
+    {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(OVERLAPPING,
+              (beginA, endA, beginB, endB) -> overlaps(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+      
+      // It must also work if we switch the of the spans
+      assertPosition(OVERLAPPING,
+              (beginA, endA, beginB, endB) -> overlaps(cas.createAnnotation(type, beginB, endB),
+                      cas.createAnnotation(type, beginA, endA)));
+    }
+    
+    @Test
+    public void thatLeftOfWithIntervalsWorks() throws Exception {
+      assertPosition(LEFT_OF, AnnotationPredicates::leftOf);
+    }
+
+    @Test
+    public void thatLeftOfWithAnnotationAndIntervalWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(LEFT_OF, (beginA, endA, beginB,
+              endB) -> leftOf(cas.createAnnotation(type, beginA, endA), beginB, endB));
+    }
+
+    @Test
+    public void thatLeftOfWithAnnotationsWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+      
+      assertPosition(LEFT_OF,
+              (beginA, endA, beginB, endB) -> leftOf(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+    }
+
+    @Test
+    public void thatRightOfWithIntervalsWorks() throws Exception {
+      assertPosition(RIGHT_OF, AnnotationPredicates::rightOf);
+    }
+
+    @Test
+    public void thatRightOfWithAnnotationAndIntervalWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+
+      assertPosition(RIGHT_OF, (beginA, endA, beginB,
+              endB) -> rightOf(cas.createAnnotation(type, beginA, endA), beginB, endB));
+    }
+
+    @Test
+    public void thatRightOfWithAnnotationsWorks() throws Exception {
+      CAS cas = CasCreationUtils.createCas();
+      Type type = cas.getAnnotationType();
+
+      assertPosition(RIGHT_OF,
+              (beginA, endA, beginB, endB) -> rightOf(cas.createAnnotation(type, beginA, endA),
+                      cas.createAnnotation(type, beginB, endB)));
+    }
+    
+    public void assertPosition(RelativePosition aCondition, RelativePositionPredicate aPredicate)
+            throws Exception {
+        try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) {
+          for (TestCase testCase : TEST_CASES) {
+            softly.assertThat(testCase.getTest().apply(aPredicate))
+              .as(testCase.getDescription())
+              .isEqualTo(testCase.getValidPositions().contains(aCondition));
+          }
+        }
+    }
+    
+    @FunctionalInterface
+    public static interface RelativePositionPredicate {
+      boolean apply(int beginA, int endA, int beginB, int endB);
+    }
+
+    public static class TestCase {
+      private final String description;
+
+      private final Function<RelativePositionPredicate, Boolean> predicate;
+      
+      private final List<RelativePosition> validPositions;
+
+      public TestCase(String aDescription, Function<RelativePositionPredicate, Boolean> aPredicate, List<RelativePosition> aValidPositions) {
+        description = aDescription;
+        predicate = aPredicate;
+        validPositions = aValidPositions;
+      }
+
+      public String getDescription() {
+        return description;
+      }
+
+      public Function<RelativePositionPredicate, Boolean> getTest() {
+        return predicate;
+      }
+      
+      public List<RelativePosition> getValidPositions() {
+        return validPositions;
+      }
+    }
+  }
\ No newline at end of file