[ 
https://issues.apache.org/jira/browse/OPENNLP-547?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18123178#comment-18123178
 ] 

ASF GitHub Bot commented on OPENNLP-547:
----------------------------------------

rzo1 commented on code in PR #1236:
URL: https://github.com/apache/opennlp/pull/1236#discussion_r4178546967


##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencyGraph.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+import opennlp.tools.util.StringUtil;
+
+/**
+ * An immutable dependency tree over one sentence: for every token, the index 
of its head
+ * and the label of the relation to that head.
+ *
+ * <p>Token indices are zero-based positions in the sentence the graph was 
built for.
+ * Exactly one token carries the head value {@link DependencyArc#ROOT_HEAD}, 
marking it as
+ * the sentence root. Instances are immutable and safe to share between 
threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencyGraph implements Serializable {
+
+  @Serial
+  private static final long serialVersionUID = 5144949884365294061L;
+
+  /** Traversal state of a token whose head chain has not been followed yet. */
+  private static final byte UNVISITED = 0;
+
+  /** Traversal state of a token on the head chain currently being followed. */
+  private static final byte VISITING = 1;
+
+  /** Traversal state of a token whose head chain is known to reach the root. 
*/
+  private static final byte VISITED = 2;
+
+  private final int[] heads;
+  private final String[] relations;
+  private final int root;
+
+  /**
+   * Wraps already validated arrays; instances are created through {@link #of}.
+   *
+   * @param heads The validated head array, owned by the new instance.
+   * @param relations The validated relation array, owned by the new instance.
+   * @param root The index of the single token whose head is {@link 
DependencyArc#ROOT_HEAD}.
+   */
+  private DependencyGraph(int[] heads, String[] relations, int root) {
+    this.heads = heads;
+    this.relations = relations;
+    this.root = root;
+  }
+
+  /**
+   * Creates a {@link DependencyGraph} from parallel head and relation arrays.
+   *
+   * @param heads For each token, the zero-based index of its head token, or
+   *              {@link DependencyArc#ROOT_HEAD} for the sentence root. Must 
not be
+   *              {@code null} or empty, every value must be a valid token 
index or
+   *              {@link DependencyArc#ROOT_HEAD}, no token may head itself, 
and exactly
+   *              one token must be the root.
+   * @param relations For each token, the label of the relation to its head. 
Must not be
+   *                  {@code null}, must have the same length as {@code 
heads}, and no
+   *                  entry may be {@code null} or blank.
+   * @return A validated {@link DependencyGraph}. Never {@code null}.
+   * @throws IllegalArgumentException Thrown if any of the above constraints 
is violated.
+   */
+  public static DependencyGraph of(int[] heads, String[] relations) {
+    ArgumentChecks.requireNonNullArg(heads, "heads");
+    ArgumentChecks.requireNonNullArg(relations, "relations");
+    if (heads.length == 0) {
+      throw new IllegalArgumentException("a dependency graph needs at least 
one token");
+    }
+    if (heads.length != relations.length) {
+      throw new IllegalArgumentException("heads and relations must have the 
same length: "
+          + heads.length + " != " + relations.length);
+    }
+    int roots = 0;
+    int root = DependencyArc.ROOT_HEAD;
+    for (int i = 0; i < heads.length; i++) {
+      if (heads[i] == DependencyArc.ROOT_HEAD) {
+        roots++;
+        root = i;
+      } else if (heads[i] < 0 || heads[i] >= heads.length) {
+        throw new IllegalArgumentException("head of token " + i
+            + " is out of range: " + heads[i]);
+      } else if (heads[i] == i) {
+        throw new IllegalArgumentException("token " + i + " must not head 
itself");
+      }
+      if (relations[i] == null || StringUtil.isBlank(relations[i])) {
+        throw new IllegalArgumentException("relation of token " + i + " must 
not be blank");
+      }
+    }
+    if (roots != 1) {
+      throw new IllegalArgumentException("expected exactly one root, found " + 
roots);
+    }
+    checkAcyclic(heads);
+    return new DependencyGraph(heads.clone(), relations.clone(), root);
+  }
+
+  /**
+   * Rejects a cycle that is disconnected from the single root by following 
every token's
+   * head chain until it reaches the root or a token already known to reach it.
+   *
+   * @param heads The head array, already checked for range, self-heads, and 
root count.
+   * @throws IllegalArgumentException Thrown if a head chain returns to a 
token on that
+   *         same chain.
+   */
+  private static void checkAcyclic(int[] heads) {
+    final byte[] states = new byte[heads.length];
+    for (int start = 0; start < heads.length; start++) {
+      int current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
UNVISITED) {
+        states[current] = VISITING;
+        current = heads[current];
+      }
+      if (current != DependencyArc.ROOT_HEAD && states[current] == VISITING) {
+        throw new IllegalArgumentException(
+            "dependency graph contains a cycle at token " + current);
+      }
+      current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
VISITING) {
+        states[current] = VISITED;
+        current = heads[current];
+      }
+    }
+  }
+
+  /**
+   * @return The number of tokens the graph spans.
+   */
+  public int size() {
+    return heads.length;
+  }
+
+  /**
+   * Retrieves the head of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The zero-based index of the head token, or {@link 
DependencyArc#ROOT_HEAD}
+   *         when the token is the sentence root.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public int headOf(int index) {
+    checkIndex(index);
+    return heads[index];
+  }
+
+  /**
+   * Retrieves the relation label of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The label of the relation between the token and its head. Never 
{@code null}.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public String relationOf(int index) {
+    checkIndex(index);
+    return relations[index];
+  }
+
+  /**
+   * @return The zero-based index of the sentence root token.
+   */
+  public int root() {
+    return root;
+  }
+
+  /**
+   * @return All arcs of the graph in token order, one per token. Never {@code 
null}.

Review Comment:
   Should mention, that this is an unmodifiable list and always allocates a new 
list.



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencyGraph.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+import opennlp.tools.util.StringUtil;
+
+/**
+ * An immutable dependency tree over one sentence: for every token, the index 
of its head
+ * and the label of the relation to that head.
+ *
+ * <p>Token indices are zero-based positions in the sentence the graph was 
built for.
+ * Exactly one token carries the head value {@link DependencyArc#ROOT_HEAD}, 
marking it as
+ * the sentence root. Instances are immutable and safe to share between 
threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencyGraph implements Serializable {
+
+  @Serial
+  private static final long serialVersionUID = 5144949884365294061L;
+
+  /** Traversal state of a token whose head chain has not been followed yet. */
+  private static final byte UNVISITED = 0;
+
+  /** Traversal state of a token on the head chain currently being followed. */
+  private static final byte VISITING = 1;
+
+  /** Traversal state of a token whose head chain is known to reach the root. 
*/
+  private static final byte VISITED = 2;
+
+  private final int[] heads;
+  private final String[] relations;
+  private final int root;
+
+  /**
+   * Wraps already validated arrays; instances are created through {@link #of}.
+   *
+   * @param heads The validated head array, owned by the new instance.
+   * @param relations The validated relation array, owned by the new instance.
+   * @param root The index of the single token whose head is {@link 
DependencyArc#ROOT_HEAD}.
+   */
+  private DependencyGraph(int[] heads, String[] relations, int root) {
+    this.heads = heads;
+    this.relations = relations;
+    this.root = root;
+  }
+
+  /**
+   * Creates a {@link DependencyGraph} from parallel head and relation arrays.
+   *
+   * @param heads For each token, the zero-based index of its head token, or
+   *              {@link DependencyArc#ROOT_HEAD} for the sentence root. Must 
not be
+   *              {@code null} or empty, every value must be a valid token 
index or
+   *              {@link DependencyArc#ROOT_HEAD}, no token may head itself, 
and exactly
+   *              one token must be the root.
+   * @param relations For each token, the label of the relation to its head. 
Must not be
+   *                  {@code null}, must have the same length as {@code 
heads}, and no
+   *                  entry may be {@code null} or blank.
+   * @return A validated {@link DependencyGraph}. Never {@code null}.
+   * @throws IllegalArgumentException Thrown if any of the above constraints 
is violated.
+   */
+  public static DependencyGraph of(int[] heads, String[] relations) {
+    ArgumentChecks.requireNonNullArg(heads, "heads");
+    ArgumentChecks.requireNonNullArg(relations, "relations");
+    if (heads.length == 0) {
+      throw new IllegalArgumentException("a dependency graph needs at least 
one token");
+    }
+    if (heads.length != relations.length) {
+      throw new IllegalArgumentException("heads and relations must have the 
same length: "
+          + heads.length + " != " + relations.length);
+    }
+    int roots = 0;
+    int root = DependencyArc.ROOT_HEAD;
+    for (int i = 0; i < heads.length; i++) {
+      if (heads[i] == DependencyArc.ROOT_HEAD) {
+        roots++;
+        root = i;
+      } else if (heads[i] < 0 || heads[i] >= heads.length) {
+        throw new IllegalArgumentException("head of token " + i
+            + " is out of range: " + heads[i]);
+      } else if (heads[i] == i) {
+        throw new IllegalArgumentException("token " + i + " must not head 
itself");
+      }
+      if (relations[i] == null || StringUtil.isBlank(relations[i])) {
+        throw new IllegalArgumentException("relation of token " + i + " must 
not be blank");
+      }
+    }
+    if (roots != 1) {
+      throw new IllegalArgumentException("expected exactly one root, found " + 
roots);
+    }
+    checkAcyclic(heads);
+    return new DependencyGraph(heads.clone(), relations.clone(), root);
+  }
+
+  /**
+   * Rejects a cycle that is disconnected from the single root by following 
every token's
+   * head chain until it reaches the root or a token already known to reach it.
+   *
+   * @param heads The head array, already checked for range, self-heads, and 
root count.
+   * @throws IllegalArgumentException Thrown if a head chain returns to a 
token on that
+   *         same chain.
+   */
+  private static void checkAcyclic(int[] heads) {
+    final byte[] states = new byte[heads.length];
+    for (int start = 0; start < heads.length; start++) {
+      int current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
UNVISITED) {
+        states[current] = VISITING;
+        current = heads[current];
+      }
+      if (current != DependencyArc.ROOT_HEAD && states[current] == VISITING) {
+        throw new IllegalArgumentException(
+            "dependency graph contains a cycle at token " + current);
+      }
+      current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
VISITING) {
+        states[current] = VISITED;
+        current = heads[current];
+      }
+    }
+  }
+
+  /**
+   * @return The number of tokens the graph spans.
+   */
+  public int size() {
+    return heads.length;
+  }
+
+  /**
+   * Retrieves the head of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The zero-based index of the head token, or {@link 
DependencyArc#ROOT_HEAD}
+   *         when the token is the sentence root.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public int headOf(int index) {
+    checkIndex(index);
+    return heads[index];
+  }
+
+  /**
+   * Retrieves the relation label of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The label of the relation between the token and its head. Never 
{@code null}.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public String relationOf(int index) {
+    checkIndex(index);
+    return relations[index];
+  }
+
+  /**
+   * @return The zero-based index of the sentence root token.
+   */
+  public int root() {
+    return root;
+  }
+
+  /**
+   * @return All arcs of the graph in token order, one per token. Never {@code 
null}.
+   */
+  public List<DependencyArc> arcs() {
+    final List<DependencyArc> arcs = new ArrayList<>(heads.length);
+    for (int i = 0; i < heads.length; i++) {
+      arcs.add(new DependencyArc(heads[i], i, relations[i]));
+    }
+    return Collections.unmodifiableList(arcs);
+  }
+
+  /**
+   * Rejects a token index outside {@code [0, size())}.
+   *
+   * @param index The zero-based token index to check.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  private void checkIndex(int index) {
+    if (index < 0 || index >= heads.length) {
+      throw new IllegalArgumentException("token index out of range: " + index
+          + ", size: " + heads.length);
+    }
+  }
+
+  @Override

Review Comment:
   inheritDoc



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencySample.java:
##########
@@ -0,0 +1,153 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.util.Arrays;
+import java.util.Objects;
+
+import opennlp.tools.commons.Sample;
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * One dependency-annotated sentence: tokens, their part-of-speech tags, and 
the gold
+ * {@link DependencyGraph} over them. Used for training and evaluating a
+ * {@link DependencyParser}.
+ *
+ * <p>Instances are immutable and safe to share between threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencySample implements Sample {
+
+  @Serial
+  private static final long serialVersionUID = 3074843369898695370L;
+
+  private final String[] tokens;
+  private final String[] tags;
+  private final DependencyGraph graph;
+
+  /**
+   * Initializes a {@link DependencySample}.
+   *
+   * @param tokens The input tokens. Must not be {@code null} or empty and must
+   *               not contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @param graph The dependency graph over the tokens. Must not be {@code 
null} and its
+   *              {@link DependencyGraph#size()} must equal the number of 
tokens.
+   * @throws IllegalArgumentException Thrown if any parameter is {@code null},
+   *         {@code tokens} is empty, {@code tokens} or {@code tags} contains a
+   *         {@code null} entry, or the lengths of tokens, tags and graph 
disagree.
+   */
+  public DependencySample(String[] tokens, String[] tags, DependencyGraph 
graph) {
+    ArgumentChecks.requireNonNullArg(graph, "graph");
+    checkTokensAndTags(tokens, tags);
+    if (tokens.length != graph.size()) {
+      throw new IllegalArgumentException("tokens, tags and graph must agree in 
length: "
+          + tokens.length + ", " + tags.length + ", " + graph.size());
+    }
+    this.tokens = tokens.clone();
+    this.tags = tags.clone();
+    this.graph = graph;
+  }
+
+  /**
+   * Validates token and tag arrays for this sample and
+   * {@link DependencyParser#parse(String[], String[])}.
+   *
+   * @param tokens The token array. Must not be {@code null} or empty and must 
not
+   *               contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @throws IllegalArgumentException Thrown if an array is {@code null}, 
{@code tokens}
+   *         is empty, the lengths do not match, or an entry is {@code null}.
+   */
+  public static void checkTokensAndTags(String[] tokens, String[] tags) {
+    ArgumentChecks.requireNonNullArg(tokens, "tokens");
+    ArgumentChecks.requireNonNullArg(tags, "tags");
+    if (tokens.length == 0) {
+      throw new IllegalArgumentException("tokens must not be empty");
+    }
+    if (tokens.length != tags.length) {
+      throw new IllegalArgumentException("tokens and tags must have the same 
length: "
+          + tokens.length + " != " + tags.length);
+    }
+    for (int i = 0; i < tokens.length; i++) {
+      if (tokens[i] == null) {
+        throw new IllegalArgumentException("token must not be null at index " 
+ i);
+      }
+      if (tags[i] == null) {
+        throw new IllegalArgumentException("tag must not be null at index " + 
i);
+      }
+    }
+  }
+
+  /**
+   * @return The tokens of the sentence. Never {@code null}.
+   */
+  public String[] getTokens() {
+    return tokens.clone();
+  }
+
+  /**
+   * @return The part-of-speech tags aligned with the tokens. Never {@code 
null}.
+   */
+  public String[] getTags() {
+    return tags.clone();
+  }
+
+  /**
+   * @return The dependency graph over the tokens. Never {@code null}.
+   */
+  public DependencyGraph getGraph() {
+    return graph;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof DependencySample other)) {
+      return false;
+    }
+    return Arrays.equals(tokens, other.tokens) && Arrays.equals(tags, 
other.tags)
+        && graph.equals(other.graph);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(Arrays.hashCode(tokens), Arrays.hashCode(tags), graph);
+  }
+
+  @Override
+  public String toString() {

Review Comment:
   inerhitcDoc



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/depparse/ArcStandardState.java:
##########
@@ -0,0 +1,295 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.util.Arrays;
+
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * The mutable configuration of an arc-standard parse: a stack, a buffer of 
remaining

Review Comment:
   similar to above. perhaps reword in a nicer way.



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencySample.java:
##########
@@ -0,0 +1,153 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.util.Arrays;
+import java.util.Objects;
+
+import opennlp.tools.commons.Sample;
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * One dependency-annotated sentence: tokens, their part-of-speech tags, and 
the gold
+ * {@link DependencyGraph} over them. Used for training and evaluating a
+ * {@link DependencyParser}.
+ *
+ * <p>Instances are immutable and safe to share between threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencySample implements Sample {
+
+  @Serial
+  private static final long serialVersionUID = 3074843369898695370L;
+
+  private final String[] tokens;
+  private final String[] tags;
+  private final DependencyGraph graph;
+
+  /**
+   * Initializes a {@link DependencySample}.
+   *
+   * @param tokens The input tokens. Must not be {@code null} or empty and must
+   *               not contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @param graph The dependency graph over the tokens. Must not be {@code 
null} and its
+   *              {@link DependencyGraph#size()} must equal the number of 
tokens.
+   * @throws IllegalArgumentException Thrown if any parameter is {@code null},
+   *         {@code tokens} is empty, {@code tokens} or {@code tags} contains a
+   *         {@code null} entry, or the lengths of tokens, tags and graph 
disagree.
+   */
+  public DependencySample(String[] tokens, String[] tags, DependencyGraph 
graph) {
+    ArgumentChecks.requireNonNullArg(graph, "graph");
+    checkTokensAndTags(tokens, tags);
+    if (tokens.length != graph.size()) {
+      throw new IllegalArgumentException("tokens, tags and graph must agree in 
length: "
+          + tokens.length + ", " + tags.length + ", " + graph.size());
+    }
+    this.tokens = tokens.clone();
+    this.tags = tags.clone();
+    this.graph = graph;
+  }
+
+  /**
+   * Validates token and tag arrays for this sample and
+   * {@link DependencyParser#parse(String[], String[])}.
+   *
+   * @param tokens The token array. Must not be {@code null} or empty and must 
not
+   *               contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @throws IllegalArgumentException Thrown if an array is {@code null}, 
{@code tokens}
+   *         is empty, the lengths do not match, or an entry is {@code null}.
+   */
+  public static void checkTokensAndTags(String[] tokens, String[] tags) {
+    ArgumentChecks.requireNonNullArg(tokens, "tokens");
+    ArgumentChecks.requireNonNullArg(tags, "tags");
+    if (tokens.length == 0) {
+      throw new IllegalArgumentException("tokens must not be empty");
+    }
+    if (tokens.length != tags.length) {
+      throw new IllegalArgumentException("tokens and tags must have the same 
length: "
+          + tokens.length + " != " + tags.length);
+    }
+    for (int i = 0; i < tokens.length; i++) {
+      if (tokens[i] == null) {
+        throw new IllegalArgumentException("token must not be null at index " 
+ i);
+      }
+      if (tags[i] == null) {
+        throw new IllegalArgumentException("tag must not be null at index " + 
i);
+      }
+    }
+  }
+
+  /**
+   * @return The tokens of the sentence. Never {@code null}.
+   */
+  public String[] getTokens() {
+    return tokens.clone();
+  }
+
+  /**
+   * @return The part-of-speech tags aligned with the tokens. Never {@code 
null}.
+   */
+  public String[] getTags() {
+    return tags.clone();
+  }
+
+  /**
+   * @return The dependency graph over the tokens. Never {@code null}.
+   */
+  public DependencyGraph getGraph() {
+    return graph;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof DependencySample other)) {
+      return false;
+    }
+    return Arrays.equals(tokens, other.tokens) && Arrays.equals(tags, 
other.tags)
+        && graph.equals(other.graph);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(Arrays.hashCode(tokens), Arrays.hashCode(tags), graph);

Review Comment:
   inerhitcDoc



##########
opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/conllu/ConlluTagset.java:
##########
@@ -17,7 +17,43 @@
 
 package opennlp.tools.formats.conllu;
 
+import opennlp.tools.cmdline.TerminateToolException;
+
 public enum ConlluTagset {
   U,
-  X
+  X;
+
+  /**
+   * Resolves the value of a command line {@code tagset} parameter.
+   *
+   * @param parameter {@code u} for the universal tags or {@code x} for the
+   *                  language-specific tags.
+   * @return The matching {@link ConlluTagset}. Never {@code null}.
+   * @throws IllegalArgumentException Thrown if {@code parameter} is neither 
{@code u}
+   *         nor {@code x}.
+   */
+  public static ConlluTagset fromParameter(String parameter) {

Review Comment:
   Can we add those String constants as ctor arg to the enum and than just use 
the attrib?



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencyGraph.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+import opennlp.tools.util.StringUtil;
+
+/**
+ * An immutable dependency tree over one sentence: for every token, the index 
of its head
+ * and the label of the relation to that head.
+ *
+ * <p>Token indices are zero-based positions in the sentence the graph was 
built for.
+ * Exactly one token carries the head value {@link DependencyArc#ROOT_HEAD}, 
marking it as
+ * the sentence root. Instances are immutable and safe to share between 
threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencyGraph implements Serializable {

Review Comment:
   Rename to `DependencyTree`?
   
   Its documented and enforced contract is a single-rooted, acyclic tree with 
one head per token. Tree communicates those restrictions directly
   



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencyGraph.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+import opennlp.tools.util.StringUtil;
+
+/**
+ * An immutable dependency tree over one sentence: for every token, the index 
of its head
+ * and the label of the relation to that head.
+ *
+ * <p>Token indices are zero-based positions in the sentence the graph was 
built for.
+ * Exactly one token carries the head value {@link DependencyArc#ROOT_HEAD}, 
marking it as
+ * the sentence root. Instances are immutable and safe to share between 
threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencyGraph implements Serializable {
+
+  @Serial
+  private static final long serialVersionUID = 5144949884365294061L;
+
+  /** Traversal state of a token whose head chain has not been followed yet. */
+  private static final byte UNVISITED = 0;
+
+  /** Traversal state of a token on the head chain currently being followed. */
+  private static final byte VISITING = 1;
+
+  /** Traversal state of a token whose head chain is known to reach the root. 
*/
+  private static final byte VISITED = 2;
+
+  private final int[] heads;
+  private final String[] relations;
+  private final int root;
+
+  /**
+   * Wraps already validated arrays; instances are created through {@link #of}.
+   *
+   * @param heads The validated head array, owned by the new instance.
+   * @param relations The validated relation array, owned by the new instance.
+   * @param root The index of the single token whose head is {@link 
DependencyArc#ROOT_HEAD}.
+   */
+  private DependencyGraph(int[] heads, String[] relations, int root) {
+    this.heads = heads;
+    this.relations = relations;
+    this.root = root;
+  }
+
+  /**
+   * Creates a {@link DependencyGraph} from parallel head and relation arrays.
+   *
+   * @param heads For each token, the zero-based index of its head token, or
+   *              {@link DependencyArc#ROOT_HEAD} for the sentence root. Must 
not be
+   *              {@code null} or empty, every value must be a valid token 
index or
+   *              {@link DependencyArc#ROOT_HEAD}, no token may head itself, 
and exactly
+   *              one token must be the root.
+   * @param relations For each token, the label of the relation to its head. 
Must not be
+   *                  {@code null}, must have the same length as {@code 
heads}, and no
+   *                  entry may be {@code null} or blank.
+   * @return A validated {@link DependencyGraph}. Never {@code null}.
+   * @throws IllegalArgumentException Thrown if any of the above constraints 
is violated.
+   */
+  public static DependencyGraph of(int[] heads, String[] relations) {
+    ArgumentChecks.requireNonNullArg(heads, "heads");
+    ArgumentChecks.requireNonNullArg(relations, "relations");
+    if (heads.length == 0) {
+      throw new IllegalArgumentException("a dependency graph needs at least 
one token");
+    }
+    if (heads.length != relations.length) {
+      throw new IllegalArgumentException("heads and relations must have the 
same length: "
+          + heads.length + " != " + relations.length);
+    }
+    int roots = 0;
+    int root = DependencyArc.ROOT_HEAD;
+    for (int i = 0; i < heads.length; i++) {
+      if (heads[i] == DependencyArc.ROOT_HEAD) {
+        roots++;
+        root = i;
+      } else if (heads[i] < 0 || heads[i] >= heads.length) {
+        throw new IllegalArgumentException("head of token " + i
+            + " is out of range: " + heads[i]);
+      } else if (heads[i] == i) {
+        throw new IllegalArgumentException("token " + i + " must not head 
itself");
+      }
+      if (relations[i] == null || StringUtil.isBlank(relations[i])) {
+        throw new IllegalArgumentException("relation of token " + i + " must 
not be blank");
+      }
+    }
+    if (roots != 1) {
+      throw new IllegalArgumentException("expected exactly one root, found " + 
roots);
+    }
+    checkAcyclic(heads);
+    return new DependencyGraph(heads.clone(), relations.clone(), root);
+  }
+
+  /**
+   * Rejects a cycle that is disconnected from the single root by following 
every token's
+   * head chain until it reaches the root or a token already known to reach it.
+   *
+   * @param heads The head array, already checked for range, self-heads, and 
root count.
+   * @throws IllegalArgumentException Thrown if a head chain returns to a 
token on that
+   *         same chain.
+   */
+  private static void checkAcyclic(int[] heads) {
+    final byte[] states = new byte[heads.length];
+    for (int start = 0; start < heads.length; start++) {
+      int current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
UNVISITED) {
+        states[current] = VISITING;
+        current = heads[current];
+      }
+      if (current != DependencyArc.ROOT_HEAD && states[current] == VISITING) {
+        throw new IllegalArgumentException(
+            "dependency graph contains a cycle at token " + current);
+      }
+      current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
VISITING) {
+        states[current] = VISITED;
+        current = heads[current];
+      }
+    }
+  }
+
+  /**
+   * @return The number of tokens the graph spans.
+   */
+  public int size() {
+    return heads.length;
+  }
+
+  /**
+   * Retrieves the head of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The zero-based index of the head token, or {@link 
DependencyArc#ROOT_HEAD}
+   *         when the token is the sentence root.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public int headOf(int index) {
+    checkIndex(index);
+    return heads[index];
+  }
+
+  /**
+   * Retrieves the relation label of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The label of the relation between the token and its head. Never 
{@code null}.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public String relationOf(int index) {
+    checkIndex(index);
+    return relations[index];
+  }
+
+  /**
+   * @return The zero-based index of the sentence root token.
+   */
+  public int root() {
+    return root;
+  }
+
+  /**
+   * @return All arcs of the graph in token order, one per token. Never {@code 
null}.
+   */
+  public List<DependencyArc> arcs() {
+    final List<DependencyArc> arcs = new ArrayList<>(heads.length);
+    for (int i = 0; i < heads.length; i++) {
+      arcs.add(new DependencyArc(heads[i], i, relations[i]));
+    }
+    return Collections.unmodifiableList(arcs);
+  }
+
+  /**
+   * Rejects a token index outside {@code [0, size())}.
+   *
+   * @param index The zero-based token index to check.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  private void checkIndex(int index) {
+    if (index < 0 || index >= heads.length) {
+      throw new IllegalArgumentException("token index out of range: " + index
+          + ", size: " + heads.length);
+    }
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof DependencyGraph other)) {
+      return false;
+    }
+    return Arrays.equals(heads, other.heads) && Arrays.equals(relations, 
other.relations);
+  }
+
+  @Override

Review Comment:
   inheritDoc



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/depparse/ArcStandardOracle.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * The static oracle for the arc-standard system: derives the transition 
sequence that

Review Comment:
   Reword this Javadoc to explain for (non familiar) readers what it is.



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencyGraph.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+import opennlp.tools.util.StringUtil;
+
+/**
+ * An immutable dependency tree over one sentence: for every token, the index 
of its head
+ * and the label of the relation to that head.
+ *
+ * <p>Token indices are zero-based positions in the sentence the graph was 
built for.
+ * Exactly one token carries the head value {@link DependencyArc#ROOT_HEAD}, 
marking it as
+ * the sentence root. Instances are immutable and safe to share between 
threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencyGraph implements Serializable {
+
+  @Serial
+  private static final long serialVersionUID = 5144949884365294061L;
+
+  /** Traversal state of a token whose head chain has not been followed yet. */
+  private static final byte UNVISITED = 0;
+
+  /** Traversal state of a token on the head chain currently being followed. */
+  private static final byte VISITING = 1;
+
+  /** Traversal state of a token whose head chain is known to reach the root. 
*/
+  private static final byte VISITED = 2;
+
+  private final int[] heads;
+  private final String[] relations;
+  private final int root;
+
+  /**
+   * Wraps already validated arrays; instances are created through {@link #of}.
+   *
+   * @param heads The validated head array, owned by the new instance.
+   * @param relations The validated relation array, owned by the new instance.
+   * @param root The index of the single token whose head is {@link 
DependencyArc#ROOT_HEAD}.
+   */
+  private DependencyGraph(int[] heads, String[] relations, int root) {
+    this.heads = heads;
+    this.relations = relations;
+    this.root = root;
+  }
+
+  /**
+   * Creates a {@link DependencyGraph} from parallel head and relation arrays.
+   *
+   * @param heads For each token, the zero-based index of its head token, or
+   *              {@link DependencyArc#ROOT_HEAD} for the sentence root. Must 
not be
+   *              {@code null} or empty, every value must be a valid token 
index or
+   *              {@link DependencyArc#ROOT_HEAD}, no token may head itself, 
and exactly
+   *              one token must be the root.
+   * @param relations For each token, the label of the relation to its head. 
Must not be
+   *                  {@code null}, must have the same length as {@code 
heads}, and no
+   *                  entry may be {@code null} or blank.
+   * @return A validated {@link DependencyGraph}. Never {@code null}.
+   * @throws IllegalArgumentException Thrown if any of the above constraints 
is violated.
+   */
+  public static DependencyGraph of(int[] heads, String[] relations) {
+    ArgumentChecks.requireNonNullArg(heads, "heads");
+    ArgumentChecks.requireNonNullArg(relations, "relations");
+    if (heads.length == 0) {
+      throw new IllegalArgumentException("a dependency graph needs at least 
one token");
+    }
+    if (heads.length != relations.length) {
+      throw new IllegalArgumentException("heads and relations must have the 
same length: "
+          + heads.length + " != " + relations.length);
+    }
+    int roots = 0;
+    int root = DependencyArc.ROOT_HEAD;
+    for (int i = 0; i < heads.length; i++) {
+      if (heads[i] == DependencyArc.ROOT_HEAD) {
+        roots++;
+        root = i;
+      } else if (heads[i] < 0 || heads[i] >= heads.length) {
+        throw new IllegalArgumentException("head of token " + i
+            + " is out of range: " + heads[i]);
+      } else if (heads[i] == i) {
+        throw new IllegalArgumentException("token " + i + " must not head 
itself");
+      }
+      if (relations[i] == null || StringUtil.isBlank(relations[i])) {
+        throw new IllegalArgumentException("relation of token " + i + " must 
not be blank");
+      }
+    }
+    if (roots != 1) {
+      throw new IllegalArgumentException("expected exactly one root, found " + 
roots);
+    }
+    checkAcyclic(heads);
+    return new DependencyGraph(heads.clone(), relations.clone(), root);
+  }
+
+  /**
+   * Rejects a cycle that is disconnected from the single root by following 
every token's
+   * head chain until it reaches the root or a token already known to reach it.
+   *
+   * @param heads The head array, already checked for range, self-heads, and 
root count.
+   * @throws IllegalArgumentException Thrown if a head chain returns to a 
token on that
+   *         same chain.
+   */
+  private static void checkAcyclic(int[] heads) {
+    final byte[] states = new byte[heads.length];
+    for (int start = 0; start < heads.length; start++) {
+      int current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
UNVISITED) {
+        states[current] = VISITING;
+        current = heads[current];
+      }
+      if (current != DependencyArc.ROOT_HEAD && states[current] == VISITING) {
+        throw new IllegalArgumentException(
+            "dependency graph contains a cycle at token " + current);
+      }
+      current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
VISITING) {
+        states[current] = VISITED;
+        current = heads[current];
+      }
+    }
+  }
+
+  /**
+   * @return The number of tokens the graph spans.
+   */
+  public int size() {
+    return heads.length;
+  }
+
+  /**
+   * Retrieves the head of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The zero-based index of the head token, or {@link 
DependencyArc#ROOT_HEAD}
+   *         when the token is the sentence root.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public int headOf(int index) {
+    checkIndex(index);
+    return heads[index];
+  }
+
+  /**
+   * Retrieves the relation label of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The label of the relation between the token and its head. Never 
{@code null}.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public String relationOf(int index) {
+    checkIndex(index);
+    return relations[index];
+  }
+
+  /**
+   * @return The zero-based index of the sentence root token.
+   */
+  public int root() {
+    return root;
+  }
+
+  /**
+   * @return All arcs of the graph in token order, one per token. Never {@code 
null}.
+   */
+  public List<DependencyArc> arcs() {
+    final List<DependencyArc> arcs = new ArrayList<>(heads.length);
+    for (int i = 0; i < heads.length; i++) {
+      arcs.add(new DependencyArc(heads[i], i, relations[i]));
+    }
+    return Collections.unmodifiableList(arcs);
+  }
+
+  /**
+   * Rejects a token index outside {@code [0, size())}.
+   *
+   * @param index The zero-based token index to check.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  private void checkIndex(int index) {
+    if (index < 0 || index >= heads.length) {
+      throw new IllegalArgumentException("token index out of range: " + index
+          + ", size: " + heads.length);
+    }
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof DependencyGraph other)) {
+      return false;
+    }
+    return Arrays.equals(heads, other.heads) && Arrays.equals(relations, 
other.relations);
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * Arrays.hashCode(heads) + Arrays.hashCode(relations);

Review Comment:
   This hash code impl is usually quite slow. Since it cannot change after 
creation, perhaps cash the value during creation?



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencySample.java:
##########
@@ -0,0 +1,153 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.util.Arrays;
+import java.util.Objects;
+
+import opennlp.tools.commons.Sample;
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * One dependency-annotated sentence: tokens, their part-of-speech tags, and 
the gold
+ * {@link DependencyGraph} over them. Used for training and evaluating a
+ * {@link DependencyParser}.
+ *
+ * <p>Instances are immutable and safe to share between threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencySample implements Sample {
+
+  @Serial
+  private static final long serialVersionUID = 3074843369898695370L;
+
+  private final String[] tokens;
+  private final String[] tags;
+  private final DependencyGraph graph;
+
+  /**
+   * Initializes a {@link DependencySample}.
+   *
+   * @param tokens The input tokens. Must not be {@code null} or empty and must
+   *               not contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @param graph The dependency graph over the tokens. Must not be {@code 
null} and its
+   *              {@link DependencyGraph#size()} must equal the number of 
tokens.
+   * @throws IllegalArgumentException Thrown if any parameter is {@code null},
+   *         {@code tokens} is empty, {@code tokens} or {@code tags} contains a
+   *         {@code null} entry, or the lengths of tokens, tags and graph 
disagree.
+   */
+  public DependencySample(String[] tokens, String[] tags, DependencyGraph 
graph) {
+    ArgumentChecks.requireNonNullArg(graph, "graph");
+    checkTokensAndTags(tokens, tags);
+    if (tokens.length != graph.size()) {
+      throw new IllegalArgumentException("tokens, tags and graph must agree in 
length: "
+          + tokens.length + ", " + tags.length + ", " + graph.size());
+    }
+    this.tokens = tokens.clone();
+    this.tags = tags.clone();
+    this.graph = graph;
+  }
+
+  /**
+   * Validates token and tag arrays for this sample and
+   * {@link DependencyParser#parse(String[], String[])}.
+   *
+   * @param tokens The token array. Must not be {@code null} or empty and must 
not
+   *               contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @throws IllegalArgumentException Thrown if an array is {@code null}, 
{@code tokens}
+   *         is empty, the lengths do not match, or an entry is {@code null}.
+   */
+  public static void checkTokensAndTags(String[] tokens, String[] tags) {

Review Comment:
   Why public? Private + drop static?



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencySample.java:
##########
@@ -0,0 +1,153 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.util.Arrays;
+import java.util.Objects;
+
+import opennlp.tools.commons.Sample;
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * One dependency-annotated sentence: tokens, their part-of-speech tags, and 
the gold
+ * {@link DependencyGraph} over them. Used for training and evaluating a
+ * {@link DependencyParser}.
+ *
+ * <p>Instances are immutable and safe to share between threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencySample implements Sample {
+
+  @Serial
+  private static final long serialVersionUID = 3074843369898695370L;
+
+  private final String[] tokens;
+  private final String[] tags;
+  private final DependencyGraph graph;
+
+  /**
+   * Initializes a {@link DependencySample}.
+   *
+   * @param tokens The input tokens. Must not be {@code null} or empty and must
+   *               not contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @param graph The dependency graph over the tokens. Must not be {@code 
null} and its
+   *              {@link DependencyGraph#size()} must equal the number of 
tokens.
+   * @throws IllegalArgumentException Thrown if any parameter is {@code null},
+   *         {@code tokens} is empty, {@code tokens} or {@code tags} contains a
+   *         {@code null} entry, or the lengths of tokens, tags and graph 
disagree.
+   */
+  public DependencySample(String[] tokens, String[] tags, DependencyGraph 
graph) {
+    ArgumentChecks.requireNonNullArg(graph, "graph");
+    checkTokensAndTags(tokens, tags);
+    if (tokens.length != graph.size()) {
+      throw new IllegalArgumentException("tokens, tags and graph must agree in 
length: "
+          + tokens.length + ", " + tags.length + ", " + graph.size());
+    }
+    this.tokens = tokens.clone();
+    this.tags = tags.clone();
+    this.graph = graph;
+  }
+
+  /**
+   * Validates token and tag arrays for this sample and
+   * {@link DependencyParser#parse(String[], String[])}.
+   *
+   * @param tokens The token array. Must not be {@code null} or empty and must 
not
+   *               contain {@code null} entries.
+   * @param tags The part-of-speech tags aligned with {@code tokens}. Must not 
be
+   *             {@code null}, must have the same length as {@code tokens}, 
and must not
+   *             contain {@code null} entries.
+   * @throws IllegalArgumentException Thrown if an array is {@code null}, 
{@code tokens}
+   *         is empty, the lengths do not match, or an entry is {@code null}.
+   */
+  public static void checkTokensAndTags(String[] tokens, String[] tags) {
+    ArgumentChecks.requireNonNullArg(tokens, "tokens");
+    ArgumentChecks.requireNonNullArg(tags, "tags");
+    if (tokens.length == 0) {
+      throw new IllegalArgumentException("tokens must not be empty");
+    }
+    if (tokens.length != tags.length) {
+      throw new IllegalArgumentException("tokens and tags must have the same 
length: "
+          + tokens.length + " != " + tags.length);
+    }
+    for (int i = 0; i < tokens.length; i++) {
+      if (tokens[i] == null) {
+        throw new IllegalArgumentException("token must not be null at index " 
+ i);
+      }
+      if (tags[i] == null) {
+        throw new IllegalArgumentException("tag must not be null at index " + 
i);
+      }
+    }
+  }
+
+  /**
+   * @return The tokens of the sentence. Never {@code null}.
+   */
+  public String[] getTokens() {
+    return tokens.clone();
+  }
+
+  /**
+   * @return The part-of-speech tags aligned with the tokens. Never {@code 
null}.
+   */
+  public String[] getTags() {
+    return tags.clone();
+  }
+
+  /**
+   * @return The dependency graph over the tokens. Never {@code null}.
+   */
+  public DependencyGraph getGraph() {
+    return graph;
+  }
+
+  @Override

Review Comment:
   inerhitcDoc



##########
opennlp-api/src/main/java/opennlp/tools/depparse/DependencyGraph.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import opennlp.tools.commons.ThreadSafe;
+import opennlp.tools.util.ArgumentChecks;
+import opennlp.tools.util.StringUtil;
+
+/**
+ * An immutable dependency tree over one sentence: for every token, the index 
of its head
+ * and the label of the relation to that head.
+ *
+ * <p>Token indices are zero-based positions in the sentence the graph was 
built for.
+ * Exactly one token carries the head value {@link DependencyArc#ROOT_HEAD}, 
marking it as
+ * the sentence root. Instances are immutable and safe to share between 
threads.</p>
+ *
+ * @since 3.0.0
+ */
+@ThreadSafe
+public final class DependencyGraph implements Serializable {
+
+  @Serial
+  private static final long serialVersionUID = 5144949884365294061L;
+
+  /** Traversal state of a token whose head chain has not been followed yet. */
+  private static final byte UNVISITED = 0;
+
+  /** Traversal state of a token on the head chain currently being followed. */
+  private static final byte VISITING = 1;
+
+  /** Traversal state of a token whose head chain is known to reach the root. 
*/
+  private static final byte VISITED = 2;
+
+  private final int[] heads;
+  private final String[] relations;
+  private final int root;
+
+  /**
+   * Wraps already validated arrays; instances are created through {@link #of}.
+   *
+   * @param heads The validated head array, owned by the new instance.
+   * @param relations The validated relation array, owned by the new instance.
+   * @param root The index of the single token whose head is {@link 
DependencyArc#ROOT_HEAD}.
+   */
+  private DependencyGraph(int[] heads, String[] relations, int root) {
+    this.heads = heads;
+    this.relations = relations;
+    this.root = root;
+  }
+
+  /**
+   * Creates a {@link DependencyGraph} from parallel head and relation arrays.
+   *
+   * @param heads For each token, the zero-based index of its head token, or
+   *              {@link DependencyArc#ROOT_HEAD} for the sentence root. Must 
not be
+   *              {@code null} or empty, every value must be a valid token 
index or
+   *              {@link DependencyArc#ROOT_HEAD}, no token may head itself, 
and exactly
+   *              one token must be the root.
+   * @param relations For each token, the label of the relation to its head. 
Must not be
+   *                  {@code null}, must have the same length as {@code 
heads}, and no
+   *                  entry may be {@code null} or blank.
+   * @return A validated {@link DependencyGraph}. Never {@code null}.
+   * @throws IllegalArgumentException Thrown if any of the above constraints 
is violated.
+   */
+  public static DependencyGraph of(int[] heads, String[] relations) {
+    ArgumentChecks.requireNonNullArg(heads, "heads");
+    ArgumentChecks.requireNonNullArg(relations, "relations");
+    if (heads.length == 0) {
+      throw new IllegalArgumentException("a dependency graph needs at least 
one token");
+    }
+    if (heads.length != relations.length) {
+      throw new IllegalArgumentException("heads and relations must have the 
same length: "
+          + heads.length + " != " + relations.length);
+    }
+    int roots = 0;
+    int root = DependencyArc.ROOT_HEAD;
+    for (int i = 0; i < heads.length; i++) {
+      if (heads[i] == DependencyArc.ROOT_HEAD) {
+        roots++;
+        root = i;
+      } else if (heads[i] < 0 || heads[i] >= heads.length) {
+        throw new IllegalArgumentException("head of token " + i
+            + " is out of range: " + heads[i]);
+      } else if (heads[i] == i) {
+        throw new IllegalArgumentException("token " + i + " must not head 
itself");
+      }
+      if (relations[i] == null || StringUtil.isBlank(relations[i])) {
+        throw new IllegalArgumentException("relation of token " + i + " must 
not be blank");
+      }
+    }
+    if (roots != 1) {
+      throw new IllegalArgumentException("expected exactly one root, found " + 
roots);
+    }
+    checkAcyclic(heads);
+    return new DependencyGraph(heads.clone(), relations.clone(), root);
+  }
+
+  /**
+   * Rejects a cycle that is disconnected from the single root by following 
every token's
+   * head chain until it reaches the root or a token already known to reach it.
+   *
+   * @param heads The head array, already checked for range, self-heads, and 
root count.
+   * @throws IllegalArgumentException Thrown if a head chain returns to a 
token on that
+   *         same chain.
+   */
+  private static void checkAcyclic(int[] heads) {
+    final byte[] states = new byte[heads.length];
+    for (int start = 0; start < heads.length; start++) {
+      int current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
UNVISITED) {
+        states[current] = VISITING;
+        current = heads[current];
+      }
+      if (current != DependencyArc.ROOT_HEAD && states[current] == VISITING) {
+        throw new IllegalArgumentException(
+            "dependency graph contains a cycle at token " + current);
+      }
+      current = start;
+      while (current != DependencyArc.ROOT_HEAD && states[current] == 
VISITING) {
+        states[current] = VISITED;
+        current = heads[current];
+      }
+    }
+  }
+
+  /**
+   * @return The number of tokens the graph spans.
+   */
+  public int size() {
+    return heads.length;
+  }
+
+  /**
+   * Retrieves the head of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The zero-based index of the head token, or {@link 
DependencyArc#ROOT_HEAD}
+   *         when the token is the sentence root.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public int headOf(int index) {
+    checkIndex(index);
+    return heads[index];
+  }
+
+  /**
+   * Retrieves the relation label of a token.
+   *
+   * @param index The zero-based token index. Must be within {@code [0, 
size())}.
+   * @return The label of the relation between the token and its head. Never 
{@code null}.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  public String relationOf(int index) {
+    checkIndex(index);
+    return relations[index];
+  }
+
+  /**
+   * @return The zero-based index of the sentence root token.
+   */
+  public int root() {
+    return root;
+  }
+
+  /**
+   * @return All arcs of the graph in token order, one per token. Never {@code 
null}.
+   */
+  public List<DependencyArc> arcs() {
+    final List<DependencyArc> arcs = new ArrayList<>(heads.length);
+    for (int i = 0; i < heads.length; i++) {
+      arcs.add(new DependencyArc(heads[i], i, relations[i]));
+    }
+    return Collections.unmodifiableList(arcs);
+  }
+
+  /**
+   * Rejects a token index outside {@code [0, size())}.
+   *
+   * @param index The zero-based token index to check.
+   * @throws IllegalArgumentException Thrown if {@code index} is out of range.
+   */
+  private void checkIndex(int index) {
+    if (index < 0 || index >= heads.length) {
+      throw new IllegalArgumentException("token index out of range: " + index
+          + ", size: " + heads.length);
+    }
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof DependencyGraph other)) {
+      return false;
+    }
+    return Arrays.equals(heads, other.heads) && Arrays.equals(relations, 
other.relations);
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * Arrays.hashCode(heads) + Arrays.hashCode(relations);
+  }
+
+  @Override
+  public String toString() {

Review Comment:
   inheritcDoc



##########
opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/depparse/ArcStandardOracle.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 opennlp.tools.depparse;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import opennlp.tools.util.ArgumentChecks;
+
+/**
+ * The static oracle for the arc-standard system: derives the transition 
sequence that

Review Comment:
   Perhaps something like:
   
   <html><head></head><body><p>Its job is to <strong>turn a known correct 
dependency tree into the parser actions needed to reconstruct that 
tree</strong>. Those actions provide the answers used to train the parser.</p>
   <p>The parser maintains a <strong>stack</strong> of tokens being processed 
and a <strong>buffer</strong> of unread tokens. The oracle selects among three 
actions:</p>
   
   Action | Effect
   

> Add a dependency parser component
> ---------------------------------
>
>                 Key: OPENNLP-547
>                 URL: https://issues.apache.org/jira/browse/OPENNLP-547
>             Project: OpenNLP
>          Issue Type: Wish
>            Reporter: Jörn Kottmann
>            Assignee: Kristian Rickert
>            Priority: Major
>             Fix For: 3.0.0
>
>          Time Spent: 2h 20m
>  Remaining Estimate: 0h
>
> It would be nice to add a dependency parser component to OpenNLP.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to