[
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)