All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.stanford.nlp.trees.EnglishGrammaticalStructure Maven / Gradle / Ivy

Go to download

Stanford Parser processes raw text in English, Chinese, German, Arabic, and French, and extracts constituency parse trees.

There is a newer version: 3.9.2
Show newest version
package edu.stanford.nlp.trees;

import java.io.*;
import java.util.*;
import java.util.function.Predicate;

import edu.stanford.nlp.ling.IndexedWord;
import edu.stanford.nlp.process.Morphology;
import edu.stanford.nlp.semgraph.SemanticGraph;
import edu.stanford.nlp.semgraph.SemanticGraphEdge;
import edu.stanford.nlp.semgraph.semgrex.SemgrexMatcher;
import edu.stanford.nlp.semgraph.semgrex.SemgrexPattern;
import edu.stanford.nlp.util.*;
import static edu.stanford.nlp.trees.EnglishGrammaticalRelations.*;
import static edu.stanford.nlp.trees.GrammaticalRelation.*;

/**
 * A GrammaticalStructure for English. This is the class that produces Stanford Dependencies.
 * 

* For feeding Stanford parser trees into this class, the Stanford parser should be run with the * "-retainNPTmpSubcategories" option for best results! * * @author Bill MacCartney * @author Marie-Catherine de Marneffe * @author Christopher Manning * @author Daniel Cer (CoNLLX format and alternative user selected dependency * printer/reader interface) * @author John Bauer */ public class EnglishGrammaticalStructure extends GrammaticalStructure { private static final long serialVersionUID = -1866362375001969402L; private static final boolean DEBUG = System.getProperty("EnglishGrammaticalStructure", null) != null; /** * Construct a new {@code EnglishGrammaticalStructure} from an existing parse * tree. The new {@code GrammaticalStructure} has the same tree structure * and label values as the given tree (but no shared storage). As part of * construction, the parse tree is analyzed using definitions from * {@link GrammaticalRelation GrammaticalRelation} to populate * the new GrammaticalStructure with as many labeled grammatical * relations as it can. * * @param t Parse tree to make grammatical structure from */ public EnglishGrammaticalStructure(Tree t) { this(t, new PennTreebankLanguagePack().punctuationWordRejectFilter()); } /** * This gets used by GrammaticalStructureFactory (by reflection). DON'T DELETE. * * @param t Parse tree to make grammatical structure from * @param puncFilter Filter to remove punctuation dependencies */ public EnglishGrammaticalStructure(Tree t, Predicate puncFilter) { this(t, puncFilter, new SemanticHeadFinder(true)); } /** * Construct a new {@code GrammaticalStructure} from an existing parse * tree. The new {@code GrammaticalStructure} has the same tree structure * and label values as the given tree (but no shared storage). As part of * construction, the parse tree is analyzed using definitions from * {@link GrammaticalRelation GrammaticalRelation} to populate * the new {@code GrammaticalStructure} with as many labeled grammatical * relations as it can. * * Once upon a time this method had an extra parameter as to whether to operate * in a threadsafe manner. We decided that that was a really bad idea, and this * method now always acts in a threadsafe manner. * This method gets used by GrammaticalStructureFactory (by reflection). DON'T DELETE. * * @param t Parse tree to make grammatical structure from * @param puncFilter Filter for punctuation words * @param hf HeadFinder to use when building it */ public EnglishGrammaticalStructure(Tree t, Predicate puncFilter, HeadFinder hf) { // the tree is normalized (for index and functional tag stripping) inside CoordinationTransformer super(t, EnglishGrammaticalRelations.values(), EnglishGrammaticalRelations.valuesLock(), new CoordinationTransformer(hf), hf, puncFilter, Filters.acceptFilter()); } /** Used for postprocessing CoNLL X dependencies */ public EnglishGrammaticalStructure(List projectiveDependencies, TreeGraphNode root) { super(projectiveDependencies, root); } /** * Returns a Filter which checks dependencies for usefulness as * extra tree-based dependencies. By default, everything is * accepted. One example of how this can be useful is in the * English dependencies, where the REL dependency is used as an * intermediate and we do not want this to be added when we make a * second pass over the trees for missing dependencies. */ @Override protected Predicate extraTreeDepFilter() { return extraTreeDepFilter; } private static class ExtraTreeDepFilter implements Predicate, Serializable { @Override public boolean test(TypedDependency d) { return d != null && d.reln() != RELATIVE; } private static final long serialVersionUID = 1L; } private static final Predicate extraTreeDepFilter = new ExtraTreeDepFilter(); @Override protected void correctDependencies(List list) { if (DEBUG) { printListSorted("At correctDependencies:", list); } correctSubjPass(list); if (DEBUG) { printListSorted("After correctSubjPass:", list); } removeExactDuplicates(list); if (DEBUG) { printListSorted("After removeExactDuplicates:", list); } } private static void printListSorted(String title, Collection list) { List lis = new ArrayList<>(list); Collections.sort(lis); if (title != null) { System.err.println(title); } System.err.println(lis); } @Override protected void postProcessDependencies(List list) { if (DEBUG) { printListSorted("At postProcessDependencies:", list); } SemanticGraph sg = new SemanticGraph(list); correctWHAttachment(sg); list.clear(); list.addAll(sg.typedDependencies()); if (DEBUG) { printListSorted("After correcting WH movement", list); } convertRel(list); if (DEBUG) { printListSorted("After converting rel:", list); } } @Override protected void getExtras(List list) { addRef(list); if (DEBUG) { printListSorted("After adding ref:", list); } addExtraNSubj(list); if (DEBUG) { printListSorted("After adding extra nsubj:", list); } addStrandedPobj(list); if (DEBUG) { printListSorted("After adding stranded pobj:", list); } } // Using this makes addStrandedPobj a lot cleaner looking, but it // makes the converter roughly 2% slower. Might not be worth it. // Similar changes could be made to many of the other complicated // collapsing methods. // static final SemgrexPattern strandedPobjSemgrex = SemgrexPattern.compile("{}=head >rcmod ({} [ == {}=prepgov | >xcomp {}=prepgov | >conj {}=prepgov ]) : {}=prepgov >prep ({}=prepdep !>pcomp {} !> pobj {})"); // // Deal with preposition stranding in relative clauses. // // For example, "the only thing I'm rooting for" // // This method will add pobj(for, thing) by connecting using the rcmod and prep // private static void addStrandedPobj(List list) { // SemanticGraph graph = new SemanticGraph(list); // SemgrexMatcher matcher = strandedPobjSemgrex.matcher(graph); // while (matcher.find()) { // IndexedWord gov = matcher.getNode("prepdep"); // IndexedWord dep = matcher.getNode("head"); // TypedDependency newDep = new TypedDependency(PREPOSITIONAL_OBJECT, gov, dep); // newDep.setExtra(); // list.add(newDep); // } // } // Deal with preposition stranding in relative clauses. // For example, "the only thing I'm rooting for" // This method will add pobj(for, thing) by connecting using the rcmod and prep private static void addStrandedPobj(List list) { List depNodes = null; List newDeps = null; for (TypedDependency rcmod : list) { if (rcmod.reln() != RELATIVE_CLAUSE_MODIFIER) { continue; } IndexedWord head = rcmod.gov(); if (depNodes == null) { depNodes = Generics.newArrayList(); } else { depNodes.clear(); } depNodes.add(rcmod.dep()); for (TypedDependency connected : list) { if (connected.gov().equals(rcmod.dep()) && (connected.reln() == XCLAUSAL_COMPLEMENT || connected.reln() == CONJUNCT)) { depNodes.add(connected.dep()); } } for (IndexedWord dep : depNodes) { for (TypedDependency prep : list) { if (!prep.gov().equals(dep) || prep.reln() != PREPOSITIONAL_MODIFIER) { continue; } boolean found = false; for (TypedDependency other : list) { if (other.gov().equals(prep.dep()) && (other.reln() == PREPOSITIONAL_COMPLEMENT || other.reln() == PREPOSITIONAL_OBJECT)) { found = true; break; } } if (!found) { if (newDeps == null) { newDeps = Generics.newArrayList(); } TypedDependency newDep = new TypedDependency(PREPOSITIONAL_OBJECT, prep.dep(), head); newDeps.add(newDep); } } } } if (newDeps != null) { list.addAll(newDeps); } } /* Used by correctWHAttachment */ private static SemgrexPattern XCOMP_PATTERN = SemgrexPattern.compile("{}=root >xcomp {}=embedded >/^(dep|dobj)$/ {}=wh ?>/([di]obj)/ {}=obj"); private static Morphology morphology = new Morphology(); /** * Tries to correct complicated cases of WH-movement in * sentences such as "What does Mary seem to have?" in * which "What" should attach to "have" instead of the * control verb. * * @param sg The Semantic graph to operate on. */ private static void correctWHAttachment(SemanticGraph sg) { /* Semgrexes require a graph with a root. */ if (sg.getRoots().isEmpty()) return; SemanticGraph sgCopy = sg.makeSoftCopy(); SemgrexMatcher matcher = XCOMP_PATTERN.matcher(sgCopy); while (matcher.findNextMatchingNode()) { IndexedWord root = matcher.getNode("root"); IndexedWord embeddedVerb = matcher.getNode("embedded"); IndexedWord wh = matcher.getNode("wh"); IndexedWord dobj = matcher.getNode("obj"); /* Check if the object is a WH-word. */ if (wh.tag().startsWith("W")) { boolean reattach = false; /* If the control verb already has an object, then we have to reattach th WH-word to the verb in the embedded clause. */ if (dobj != null) { reattach = true; } else { /* If the control verb can't have an object, we also have to reattach. */ String lemma = morphology.lemma(root.value(), root.tag()); if (lemma.matches(EnglishPatterns.NP_V_S_INF_VERBS_REGEX)) { reattach = true; } } if (reattach) { SemanticGraphEdge edge = sg.getEdge(root, wh); if (edge != null) { sg.removeEdge(edge); sg.addEdge(embeddedVerb, wh, DIRECT_OBJECT, Double.NEGATIVE_INFINITY, false); } } } } } /** * What we do in this method is look for temporary dependencies of * the type "rel". These occur in sentences such as "I saw the man * who you love". In that case, we should produce dobj(love, who). * On the other hand, in the sentence "... which Mr. Bush was * fighting for", we should have pobj(for, which). */ private static void convertRel(List list) { List newDeps = new ArrayList<>(); for (TypedDependency rel : list) { if (rel.reln() != RELATIVE) { continue; } boolean foundPrep = false; for (TypedDependency prep : list) { // todo: It would also be good to add a rule here to prefer ccomp nsubj over dobj if there is a ccomp with no subj // then we could get right: Which eco-friendly options do you think there will be on the new Lexus? if (prep.reln() != PREPOSITIONAL_MODIFIER) { continue; } if (!prep.gov().equals(rel.gov())) { //Try to find a clausal complement with a preposition without an //object. For sentences such as "What am I good at?" boolean hasCompParent = false; for (TypedDependency prep2 : list) { if (prep2.reln() == XCLAUSAL_COMPLEMENT || prep2.reln() == ADJECTIVAL_COMPLEMENT || prep2.reln() == CLAUSAL_COMPLEMENT || prep2.reln() == ROOT) { if (prep.gov().equals(prep2.dep()) && prep2.gov().equals(rel.gov())) { hasCompParent = true; break; } } } if ( ! hasCompParent) continue; } // at this point, we have two dependencies as in the Mr. Bush // example. it should be rel(fighting, which) and // prep(fighting, for). We now look to see if there is a // corresponding pobj associated with the dependent of the // prep relation. If not, we will connect the dep of the prep // relation and the head of the rel relation. Otherwise, the // original rel relation will become a dobj. boolean foundPobj = false; for (TypedDependency pobj : list) { if (pobj.reln() != PREPOSITIONAL_OBJECT && pobj.reln() != PREPOSITIONAL_COMPLEMENT) { continue; } if (!pobj.gov().equals(prep.dep())) { continue; } // we did find a pobj/pcomp, so it is not necessary to // change this rel. foundPobj = true; break; } if (!foundPobj) { foundPrep = true; TypedDependency newDep = new TypedDependency(PREPOSITIONAL_OBJECT, prep.dep(), rel.dep()); newDeps.add(newDep); rel.setReln(KILL); // break; // only put it in one place (or do we want to allow across-the-board effects? } } if (!foundPrep) { rel.setReln(DIRECT_OBJECT); } } filterKill(list); for (TypedDependency dep : newDeps) { if (!list.contains(dep)) { list.add(dep); } } } /** * Alters a list in place by removing all the KILL relations */ private static void filterKill(Collection deps) { List filtered = Generics.newArrayList(); for (TypedDependency dep : deps) { if (dep.reln() != KILL) { filtered.add(dep); } } deps.clear(); deps.addAll(filtered); } /** * Destructively modifies this {@code Collection} * by collapsing several types of transitive pairs of dependencies. * If called with a tree of dependencies and both CCprocess and * includeExtras set to false, then the tree structure is preserved. *

*
prepositional object dependencies: pobj
*
* prep(cat, in) and pobj(in, hat) are collapsed to * prep_in(cat, hat)
*
prepositional complement dependencies: pcomp
*
* prep(heard, of) and pcomp(of, attacking) are * collapsed to prepc_of(heard, attacking)
*
conjunct dependencies
*
* cc(investors, and) and * conj(investors, regulators) are collapsed to * conj_and(investors,regulators)
*
possessive dependencies: possessive
*
* possessive(Montezuma, 's) will be erased. This is like a collapsing, but * due to the flatness of NPs, two dependencies are not actually composed.
*
For relative clauses, it will collapse referent
*
* ref(man, that) and dobj(love, that) are collapsed * to dobj(love, man)
*
*/ @Override protected void collapseDependencies(List list, boolean CCprocess, Extras includeExtras) { if (DEBUG) { printListSorted("collapseDependencies: CCproc: " + CCprocess + " includeExtras: " + includeExtras, list); } correctDependencies(list); if (DEBUG) { printListSorted("After correctDependencies:", list); } eraseMultiConj(list); if (DEBUG) { printListSorted("After collapse multi conj:", list); } collapse2WP(list); if (DEBUG) { printListSorted("After collapse2WP:", list); } collapseFlatMWP(list); if (DEBUG) { printListSorted("After collapseFlatMWP:", list); } collapse2WPbis(list); if (DEBUG) { printListSorted("After collapse2WPbis:", list); } collapse3WP(list); if (DEBUG) { printListSorted("After collapse3WP:", list); } collapsePrepAndPoss(list); if (DEBUG) { printListSorted("After PrepAndPoss:", list); } collapseConj(list); if (DEBUG) { printListSorted("After conj:", list); } if (includeExtras.doRef) { addRef(list); if (DEBUG) { printListSorted("After adding ref:", list); } if (includeExtras.collapseRef) { collapseReferent(list); if (DEBUG) { printListSorted("After collapse referent:", list); } } } if (CCprocess) { treatCC(list); if (DEBUG) { printListSorted("After treatCC:", list); } } if (includeExtras.doSubj) { addExtraNSubj(list); if (DEBUG) { printListSorted("After adding extra nsubj:", list); } correctSubjPass(list); if (DEBUG) { printListSorted("After correctSubjPass:", list); } } removeDep(list); if (DEBUG) { printListSorted("After remove dep:", list); } Collections.sort(list); if (DEBUG) { printListSorted("After all collapse:", list); } } @Override protected void collapseDependenciesTree(List list) { collapseDependencies(list, false, Extras.NONE); } /** * Does some hard coding to deal with relation in CONJP. For now we deal with: * but not, if not, instead of, rather than, but rather GO TO negcc
* as well as, not to mention, but also, & GO TO and. * * @param conj The head dependency of the conjunction marker * @return A GrammaticalRelation made from a normalized form of that * conjunction. */ private static GrammaticalRelation conjValue(String conj) { String newConj = conj.toLowerCase(); if (newConj.equals("not") || newConj.equals("instead") || newConj.equals("rather")) { newConj = "negcc"; } else if (newConj.equals("mention") || newConj.equals("to") || newConj.equals("also") || newConj.contains("well") || newConj.equals("&")) { newConj = "and"; } return EnglishGrammaticalRelations.getConj(newConj); } private static void treatCC(Collection list) { // Construct a map from tree nodes to the set of typed // dependencies in which the node appears as dependent. Map> map = Generics.newHashMap(); // Construct a map of tree nodes being governor of a subject grammatical // relation to that relation Map subjectMap = Generics.newHashMap(); // Construct a set of TreeGraphNodes with a passive auxiliary on them Set withPassiveAuxiliary = Generics.newHashSet(); // Construct a map of tree nodes being governor of an object grammatical // relation to that relation // Map objectMap = new // HashMap(); List rcmodHeads = Generics.newArrayList(); List prepcDep = Generics.newArrayList(); for (TypedDependency typedDep : list) { if (!map.containsKey(typedDep.dep())) { // NB: Here and in other places below, we use a TreeSet (which extends // SortedSet) to guarantee that results are deterministic) map.put(typedDep.dep(), new TreeSet<>()); } map.get(typedDep.dep()).add(typedDep); if (typedDep.reln().equals(AUX_PASSIVE_MODIFIER)) { withPassiveAuxiliary.add(typedDep.gov()); } // look for subjects if (typedDep.reln().getParent() == NOMINAL_SUBJECT || typedDep.reln().getParent() == SUBJECT || typedDep.reln().getParent() == CLAUSAL_SUBJECT) { if (!subjectMap.containsKey(typedDep.gov())) { subjectMap.put(typedDep.gov(), typedDep); } } // look for objects // this map was only required by the code commented out below, so comment // it out too // if (typedDep.reln() == DIRECT_OBJECT) { // if (!objectMap.containsKey(typedDep.gov())) { // objectMap.put(typedDep.gov(), typedDep); // } // } // look for rcmod relations if (typedDep.reln() == RELATIVE_CLAUSE_MODIFIER) { rcmodHeads.add(typedDep.gov()); } // look for prepc relations: put the dependent of such a relation in the // list // to avoid wrong propagation of dobj if (typedDep.reln().toString().startsWith("prepc")) { prepcDep.add(typedDep.dep()); } } // System.err.println(map); // if (DEBUG) System.err.println("Subject map: " + subjectMap); // if (DEBUG) System.err.println("Object map: " + objectMap); // System.err.println(rcmodHeads); // create a new list of typed dependencies Collection newTypedDeps = new ArrayList<>(list); // find typed deps of form conj(gov,dep) for (TypedDependency td : list) { if (EnglishGrammaticalRelations.getConjs().contains(td.reln())) { IndexedWord gov = td.gov(); IndexedWord dep = td.dep(); // look at the dep in the conjunct Set gov_relations = map.get(gov); // System.err.println("gov " + gov); if (gov_relations != null) { for (TypedDependency td1 : gov_relations) { // System.err.println("gov rel " + td1); IndexedWord newGov = td1.gov(); // in the case of errors in the basic dependencies, it // is possible to have overlapping newGov & dep if (newGov.equals(dep)) { continue; } GrammaticalRelation newRel = td1.reln(); if (newRel != ROOT) { if (rcmodHeads.contains(gov) && rcmodHeads.contains(dep)) { // to prevent wrong propagation in the case of long dependencies in relative clauses if (newRel != DIRECT_OBJECT && newRel != NOMINAL_SUBJECT) { if (DEBUG) { System.err.println("Adding new " + newRel + " dependency from " + newGov + " to " + dep + " (subj/obj case)"); } newTypedDeps.add(new TypedDependency(newRel, newGov, dep)); } } else { if (DEBUG) { System.err.println("Adding new " + newRel + " dependency from " + newGov + " to " + dep); } newTypedDeps.add(new TypedDependency(newRel, newGov, dep)); } } } } // propagate subjects // look at the gov in the conjunct: if it is has a subject relation, // the dep is a verb and the dep doesn't have a subject relation // then we want to add a subject relation for the dep. // (By testing for the dep to be a verb, we are going to miss subject of // copula verbs! but // is it safe to relax this assumption?? i.e., just test for the subject // part) // CDM 2008: I also added in JJ, since participial verbs are often // tagged JJ String tag = dep.tag(); if (subjectMap.containsKey(gov) && (tag.startsWith("VB") || tag.startsWith("JJ")) && ! subjectMap.containsKey(dep)) { TypedDependency tdsubj = subjectMap.get(gov); // check for wrong nsubjpass: if the new verb is VB or VBZ or VBP or JJ, then // add nsubj (if it is tagged correctly, should do this for VBD too, but we don't) GrammaticalRelation relation = tdsubj.reln(); if (relation == NOMINAL_PASSIVE_SUBJECT) { if (isDefinitelyActive(tag)) { relation = NOMINAL_SUBJECT; } } else if (relation == CLAUSAL_PASSIVE_SUBJECT) { if (isDefinitelyActive(tag)) { relation = CLAUSAL_SUBJECT; } } else if (relation == NOMINAL_SUBJECT) { if (withPassiveAuxiliary.contains(dep)) { relation = NOMINAL_PASSIVE_SUBJECT; } } else if (relation == CLAUSAL_SUBJECT) { if (withPassiveAuxiliary.contains(dep)) { relation = CLAUSAL_PASSIVE_SUBJECT; } } if (DEBUG) { System.err.println("Adding new " + relation + " dependency from " + dep + " to " + tdsubj.dep() + " (subj propagation case)"); } newTypedDeps.add(new TypedDependency(relation, dep, tdsubj.dep())); } // propagate objects // cdm july 2010: This bit of code would copy a dobj from the first // clause to a later conjoined clause if it didn't // contain its own dobj or prepc. But this is too aggressive and wrong // if the later clause is intransitive // (including passivized cases) and so I think we have to not have this // done always, and see no good "sometimes" heuristic. // IF WE WERE TO REINSTATE, SHOULD ALSO NOT ADD OBJ IF THERE IS A ccomp // (SBAR). // if (objectMap.containsKey(gov) && // dep.tag().startsWith("VB") && ! objectMap.containsKey(dep) // && ! prepcDep.contains(gov)) { // TypedDependency tdobj = objectMap.get(gov); // if (DEBUG) { // System.err.println("Adding new " + tdobj.reln() + " dependency from " // + dep + " to " + tdobj.dep() + " (obj propagation case)"); // } // newTypedDeps.add(new TypedDependency(tdobj.reln(), dep, // tdobj.dep())); // } } } list.clear(); list.addAll(newTypedDeps); } private static boolean isDefinitelyActive(String tag) { // we should include VBD, but don't as it is often a tagging mistake. return tag.equals("VB") || tag.equals("VBZ") || tag.equals("VBP") || tag.startsWith("JJ"); } /** * This rewrites the "conj" relation to "conj_word" and deletes cases of the * "cc" relation providing this rewrite has occurred (but not if there is only * something like a clause-initial and). For instance, cc(elected-5, and-9) * conj(elected-5, re-elected-11) becomes conj_and(elected-5, re-elected-11) * * @param list List of dependencies. */ private static void collapseConj(Collection list) { List govs = Generics.newArrayList(); // find typed deps of form cc(gov, dep) for (TypedDependency td : list) { if (td.reln() == COORDINATION) { // i.e. "cc" IndexedWord gov = td.gov(); GrammaticalRelation conj = conjValue(td.dep().value()); if (DEBUG) { System.err.println("Set conj to " + conj + " based on " + td); } // find other deps of that gov having reln "conj" boolean foundOne = false; for (TypedDependency td1 : list) { if (td1.gov().equals(gov)) { if (td1.reln() == CONJUNCT) { // i.e., "conj" // change "conj" to the actual (lexical) conjunction if (DEBUG) { System.err.println("Changing " + td1 + " to have relation " + conj); } td1.setReln(conj); foundOne = true; } else if (td1.reln() == COORDINATION) { conj = conjValue(td1.dep().value()); if (DEBUG) { System.err.println("Set conj to " + conj + " based on " + td1); } } } } // register to remove cc from this governor if (foundOne) { govs.add(gov); } } } // now remove typed dependencies with reln "cc" if we have successfully // collapsed for (Iterator iter = list.iterator(); iter.hasNext();) { TypedDependency td2 = iter.next(); if (td2.reln() == COORDINATION && govs.contains(td2.gov())) { iter.remove(); } } } /** * This method will collapse a referent relation such as follows. e.g.: * "The man that I love … " ref(man, that) dobj(love, that) -> * dobj(love, man) */ private static void collapseReferent(Collection list) { // find typed deps of form ref(gov, dep) // put them in a List for processing; remove them from the set of deps List refs = new ArrayList<>(); for (Iterator iter = list.iterator(); iter.hasNext();) { TypedDependency td = iter.next(); if (td.reln() == REFERENT) { refs.add(td); iter.remove(); } } // now substitute target of referent where possible for (TypedDependency ref : refs) { IndexedWord dep = ref.dep();// take the relative word IndexedWord ant = ref.gov();// take the antecedent for (TypedDependency td : list) { // the last condition below maybe shouldn't be necessary, but it has // helped stop things going haywire a couple of times (it stops the // creation of a unit cycle that probably leaves something else // disconnected) [cdm Jan 2010] if (td.dep().equals(dep) && td.reln() != REFERENT && !td.gov().equals(ant)) { if (DEBUG) { System.err.print("referent: changing " + td); } td.setDep(ant); td.setExtra(); if (DEBUG) { System.err.println(" to " + td); } } } } } /** * Look for ref rules for a given word. We look through the * children and grandchildren of the rcmod dependency, and if any * children or grandchildren depend on a that/what/which/etc word, * we take the leftmost that/what/which/etc word as the dependent * for the ref TypedDependency. */ private static void addRef(Collection list) { List newDeps = new ArrayList<>(); for (TypedDependency rcmod : list) { if (rcmod.reln() != RELATIVE_CLAUSE_MODIFIER) { // we only add ref dependencies across relative clauses continue; } IndexedWord head = rcmod.gov(); IndexedWord modifier = rcmod.dep(); TypedDependency leftChild = null; for (TypedDependency child : list) { if (child.gov().equals(modifier) && EnglishPatterns.RELATIVIZING_WORD_PATTERN.matcher(child.dep().value()).matches() && (leftChild == null || child.dep().index() < leftChild.dep().index())) { leftChild = child; } } // TODO: could be made more efficient TypedDependency leftGrandchild = null; for (TypedDependency child : list) { if (!child.gov().equals(modifier)) { continue; } for (TypedDependency grandchild : list) { if (grandchild.gov().equals(child.dep()) && EnglishPatterns.RELATIVIZING_WORD_PATTERN.matcher(grandchild.dep().value()).matches() && (leftGrandchild == null || grandchild.dep().index() < leftGrandchild.dep().index())) { leftGrandchild = grandchild; } } } TypedDependency newDep = null; if (leftGrandchild != null && (leftChild == null || leftGrandchild.dep().index() < leftChild.dep().index())) { newDep = new TypedDependency(REFERENT, head, leftGrandchild.dep()); } else if (leftChild != null) { newDep = new TypedDependency(REFERENT, head, leftChild.dep()); } if (newDep != null) { newDeps.add(newDep); } } for (TypedDependency newDep : newDeps) { if (!list.contains(newDep)) { newDep.setExtra(); list.add(newDep); } } } /** * Add extra nsubj dependencies when collapsing basic dependencies. *
* In the general case, we look for an aux modifier under an xcomp * modifier, and assuming there aren't already associated nsubj * dependencies as daughters of the original xcomp dependency, we * add nsubj dependencies for each nsubj daughter of the aux. *
* There is also a special case for "to" words, in which case we add * a dependency if and only if there is no nsubj associated with the * xcomp and there is no other aux dependency. This accounts for * sentences such as "he decided not to" with no following verb. */ private static void addExtraNSubj(Collection list) { List newDeps = new ArrayList<>(); for (TypedDependency xcomp : list) { if (xcomp.reln() != XCLAUSAL_COMPLEMENT) { // we only add extra nsubj dependencies to some xcomp dependencies continue; } IndexedWord modifier = xcomp.dep(); IndexedWord head = xcomp.gov(); boolean hasSubjectDaughter = false; boolean hasAux = false; List subjects = Generics.newArrayList(); List objects = Generics.newArrayList(); for (TypedDependency dep : list) { // already have a subject dependency if ((dep.reln() == NOMINAL_SUBJECT || dep.reln() == NOMINAL_PASSIVE_SUBJECT) && dep.gov().equals(modifier)) { hasSubjectDaughter = true; break; } if (dep.reln() == AUX_MODIFIER && dep.gov().equals(modifier)) { hasAux = true; } if ((dep.reln() == NOMINAL_SUBJECT || dep.reln() == NOMINAL_PASSIVE_SUBJECT) && dep.gov().equals(head)) { subjects.add(dep.dep()); } if (dep.reln() == DIRECT_OBJECT && dep.gov().equals(head)) { objects.add(dep.dep()); } } // if we already have an nsubj dependency, no need to add an extra nsubj if (hasSubjectDaughter) { continue; } if ((modifier.value().equalsIgnoreCase("to") && hasAux) || (!modifier.value().equalsIgnoreCase("to") && !hasAux)) { continue; } // In general, we find that the objects of the verb are better // for extra nsubj than the original nsubj of the verb. For example, // "Many investors wrote asking the SEC to require ..." // There is no nsubj of asking, but the dobj, SEC, is the extra nsubj of require. // Similarly, "The law tells them when to do so" // Instead of nsubj(do, law) we want nsubj(do, them) if (objects.size() > 0) { for (IndexedWord object : objects) { TypedDependency newDep = new TypedDependency(NOMINAL_SUBJECT, modifier, object); newDeps.add(newDep); } } else { for (IndexedWord subject : subjects) { TypedDependency newDep = new TypedDependency(NOMINAL_SUBJECT, modifier, subject); newDeps.add(newDep); } } } for (TypedDependency newDep : newDeps) { if (!list.contains(newDep)) { newDep.setExtra(); list.add(newDep); } } } /** * This method corrects subjects of verbs for which we identified an auxpass, * but didn't identify the subject as passive. * * @param list List of typedDependencies to work on */ private static void correctSubjPass(Collection list) { // put in a list verbs having an auxpass List list_auxpass = new ArrayList<>(); for (TypedDependency td : list) { if (td.reln() == AUX_PASSIVE_MODIFIER) { list_auxpass.add(td.gov()); } } for (TypedDependency td : list) { // correct nsubj if (td.reln() == NOMINAL_SUBJECT && list_auxpass.contains(td.gov())) { // System.err.println("%%% Changing subj to passive: " + td); td.setReln(NOMINAL_PASSIVE_SUBJECT); } if (td.reln() == CLAUSAL_SUBJECT && list_auxpass.contains(td.gov())) { // System.err.println("%%% Changing subj to passive: " + td); td.setReln(CLAUSAL_PASSIVE_SUBJECT); } // correct unretrieved poss: dep relation in which the dependent is a // PRP$ or WP$ // cdm: Now done in basic rules. The only cases that this still matches // are (1) tagging mistakes where PRP in dobj position is mistagged PRP$ // or a couple of parsing errors where the dependency is wrong anyway, so // it's probably okay to keep it a dep. So I'm disabling this. // String tag = td.dep().tag(); // if (td.reln() == DEPENDENT && (tag.equals("PRP$") || tag.equals("WP$"))) { // System.err.println("%%% Unrecognized basic possessive pronoun: " + td); // td.setReln(POSSESSION_MODIFIER); // } } } private static boolean inConjDeps(TypedDependency td, List> conjs) { for (Triple trip : conjs) { if (td.equals(trip.first())) { return true; } } return false; } private static void collapsePrepAndPoss(Collection list) { // Man oh man, how gnarly is the logic of this method.... Collection newTypedDeps = new ArrayList<>(); // Construct a map from tree nodes to the set of typed // dependencies in which the node appears as governor. // cdm: could use CollectionValuedMap here! Map> map = Generics.newHashMap(); List vmod = Generics.newArrayList(); for (TypedDependency typedDep : list) { if (!map.containsKey(typedDep.gov())) { map.put(typedDep.gov(), new TreeSet<>()); } map.get(typedDep.gov()).add(typedDep); if (typedDep.reln() == VERBAL_MODIFIER) { // look for aux deps which indicate this was a to-be verb boolean foundAux = false; for (TypedDependency auxDep : list) { if (auxDep.reln() != AUX_MODIFIER) { continue; } if (!auxDep.gov().equals(typedDep.dep()) || !auxDep.dep().value().equalsIgnoreCase("to")) { continue; } foundAux = true; break; } if (!foundAux) { vmod.add(typedDep.dep()); } } } // System.err.println("here's the vmod list: " + vmod); // Do preposition conjunction interaction for // governor p NP and p NP case ... a lot of special code cdm jan 2006 for (TypedDependency td1 : list) { if (td1.reln() != PREPOSITIONAL_MODIFIER) { continue; } IndexedWord td1Dep = td1.dep(); SortedSet possibles = map.get(td1Dep); if (possibles == null) { continue; } // look for the "second half" // unique: the head prep and whether it should be pobj Pair prepDep = null; TypedDependency ccDep = null; // treat as unique // list of dep and prepOtherDep and pobj (or pcomp) List> conjs = new ArrayList<>(); Set otherDtrs = new TreeSet<>(); // first look for a conj(prep, prep) (there might be several conj relations!!!) boolean samePrepositionInEachConjunct = true; int conjIndex = -1; for (TypedDependency td2 : possibles) { if (td2.reln() == CONJUNCT) { IndexedWord td2Dep = td2.dep(); String td2DepPOS = td2Dep.tag(); if (td2DepPOS.equals("IN") || td2DepPOS.equals("TO")) { samePrepositionInEachConjunct = samePrepositionInEachConjunct && td2Dep.value().equals(td1Dep.value()); Set possibles2 = map.get(td2Dep); boolean pobj = true;// default of collapsing preposition is prep_ TypedDependency prepOtherDep = null; if (possibles2 != null) { for (TypedDependency td3 : possibles2) { IndexedWord td3Dep = td3.dep(); String td3DepPOS = td3Dep.tag(); // CDM Mar 2006: I put in disjunction here when I added in // PREPOSITIONAL_OBJECT. If it catches all cases, we should // be able to delete the DEPENDENT disjunct // maybe better to delete the DEPENDENT disjunct - it creates // problem with multiple prep (mcdm) if ((td3.reln() == PREPOSITIONAL_OBJECT || td3.reln() == PREPOSITIONAL_COMPLEMENT) && (!(td3DepPOS.equals("IN") || td3DepPOS.equals("TO"))) && prepOtherDep == null) { prepOtherDep = td3; if (td3.reln() == PREPOSITIONAL_COMPLEMENT) { pobj = false; } } else { otherDtrs.add(td3); } } } if (conjIndex < td2Dep.index()) { conjIndex = td2Dep.index(); } conjs.add(new Triple<>(td2, prepOtherDep, pobj)); } } } // end td2:possibles if (conjs.isEmpty()) { continue; } // if we have a conj under a preposition dependency, we look for the other // parts String td1DepPOS = td1Dep.tag(); for (TypedDependency td2 : possibles) { // we look for the cc linked to this conjDep // the cc dep must have an index smaller than the dep of conjDep if (td2.reln() == COORDINATION && td2.dep().index() < conjIndex) { ccDep = td2; } else { IndexedWord td2Dep = td2.dep(); String td2DepPOS = td2Dep.tag(); // System.err.println("prepDep find: td1.reln: " + td1.reln() + // "; td2.reln: " + td2.reln() + "; td1DepPos: " + td1DepPOS + // "; td2DepPos: " + td2DepPOS + "; index " + index + // "; td2.dep().index(): " + td2.dep().index()); if ((td2.reln() == DEPENDENT || td2.reln() == PREPOSITIONAL_OBJECT || td2.reln() == PREPOSITIONAL_COMPLEMENT) && (td1DepPOS.equals("IN") || td1DepPOS.equals("TO") || td1DepPOS.equals("VBG")) && prepDep == null && (!(td2DepPOS.equals("RB") || td2DepPOS.equals("IN") || td2DepPOS.equals("TO")))) { // same index trick, in case we have multiple deps // I deleted this to see if it helped [cdm Jan 2010] && // td2.dep().index() < index) prepDep = new Pair<>(td2, td2.reln() != PREPOSITIONAL_COMPLEMENT); } else if (!inConjDeps(td2, conjs)) {// don't want to add the conjDep // again! otherDtrs.add(td2); } } } if (prepDep == null || ccDep == null) { continue; // we can't deal with it in the hairy prep/conj interaction case! } if (DEBUG) { // ccDep must be non-null given test above System.err.println("!! Conj and prep case:"); System.err.println(" td1 (prep): " + td1); System.err.println(" Kids of td1 are: " + possibles); System.err.println(" prepDep: " + prepDep); System.err.println(" ccDep: " + ccDep); System.err.println(" conjs: " + conjs); System.err.println(" samePrepositionInEachConjunct: " + samePrepositionInEachConjunct); System.err.println(" otherDtrs: " + otherDtrs); } // check if we have the same prepositions in the conjunction if (samePrepositionInEachConjunct) { // conjDep != null && prepOtherDep != // null && // OK, we have a conjunction over parallel PPs: Fred flew to Greece and // to Serbia. GrammaticalRelation reln = determinePrepRelation(map, vmod, td1, td1, prepDep.second()); TypedDependency tdNew = new TypedDependency(reln, td1.gov(), prepDep.first().dep()); newTypedDeps.add(tdNew); if (DEBUG) { System.err.println("PrepPoss Conj branch (two parallel PPs) adding: " + tdNew); System.err.println(" removing: " + td1 + " " + prepDep + " " + ccDep); } td1.setReln(KILL);// remember these are "used up" prepDep.first().setReln(KILL); ccDep.setReln(KILL); for (Triple trip : conjs) { TypedDependency conjDep = trip.first(); TypedDependency prepOtherDep = trip.second(); if (prepOtherDep == null) { // CDM July 2010: I think this should only ever happen if there is a // misparse, but it has happened in such circumstances. You have // something like (PP in or in (NP Serbia)), with the two // prepositions the same. We just clean up the mess. if (DEBUG) { System.err.println(" apparent misparse: same P twice with only one NP object (prepOtherDep is null)"); System.err.println(" removing: " + conjDep); } ccDep.setReln(KILL); } else { TypedDependency tdNew2 = new TypedDependency(conjValue(ccDep.dep().value()), prepDep.first().dep(), prepOtherDep.dep()); newTypedDeps.add(tdNew2); if (DEBUG) { System.err.println(" adding: " + tdNew2); System.err.println(" removing: " + conjDep + " " + prepOtherDep); } prepOtherDep.setReln(KILL); } conjDep.setReln(KILL); } // promote dtrs that would be orphaned for (TypedDependency otd : otherDtrs) { if (DEBUG) { System.err.print("Changed " + otd); } otd.setGov(td1.gov()); if (DEBUG) { System.err.println(" to " + otd); } } // Now we need to see if there are any TDs that will be "orphaned" // by this collapse. Example: if we have: // dep(drew, on) // dep(on, book) // dep(on, right) // the first two will be collapsed to on(drew, book), but then // the third one will be orphaned, since its governor no // longer appears. So, change its governor to 'drew'. // CDM Feb 2010: This used to not move COORDINATION OR CONJUNCT, but now // it does, since they're not automatically deleted // Some things in possibles may have already been changed, so check gov if (DEBUG) { System.err.println("td1: " + td1 + "; possibles: " + possibles); } for (TypedDependency td2 : possibles) { // if (DEBUG) { // System.err.println("[a] td2.reln " + td2.reln() + " td2.gov " + // td2.gov() + " td1.dep " + td1.dep()); // } if (td2.reln() != KILL && td2.gov().equals(td1.dep())) { // && td2.reln() // != COORDINATION // && td2.reln() // != CONJUNCT if (DEBUG) { System.err.println("Changing " + td2 + " to have governor of " + td1 + " [a]"); } td2.setGov(td1.gov()); } } continue; // This one has been dealt with successfully } // end same prepositions // case of "Lufthansa flies to and from Serbia". Make it look like next // case :-) // that is, the prepOtherDep should be the same as prepDep ! for (Triple trip : conjs) { if (trip.first() != null && trip.second() == null) { trip.setSecond(new TypedDependency(prepDep.first().reln(), trip.first().dep(), prepDep.first().dep())); trip.setThird(prepDep.second()); } } // we have two different prepositions in the conjunction // in this case we need to add a node // "Bill jumped over the fence and through the hoop" // prep_over(jumped, fence) // conj_and(jumped, jumped) // prep_through(jumped, hoop) // Extra complication: // If "jumped" is already part of a conjunction, we should add the new one off that rather than chaining IndexedWord conjHead = td1.gov(); for (TypedDependency td3 : list) { if (td3.dep().equals(td1.gov()) && td3.reln().equals(CONJUNCT)) { conjHead = td3.gov(); } } GrammaticalRelation reln = determinePrepRelation(map, vmod, td1, td1, prepDep.second()); TypedDependency tdNew = new TypedDependency(reln, td1.gov(), prepDep.first().dep()); newTypedDeps.add(tdNew); if (DEBUG) { System.err.println("ConjPP (different preps) adding: " + tdNew); System.err.println(" deleting: " + td1 + " " + prepDep.first() + " " + ccDep); } td1.setReln(KILL);// remember these are "used up" prepDep.first().setReln(KILL); ccDep.setReln(KILL); // so far we added the first prep grammatical relation int copyNumber = 1; for (Triple trip : conjs) { TypedDependency conjDep = trip.first(); TypedDependency prepOtherDep = trip.second(); boolean pobj = trip.third(); // OK, we have a conjunction over different PPs // we create a new node; // in order to make a distinction between the original node and its copy // we set the "copyCount" variable in the IndexedWord // existence of copyCount > 0 is checked at printing (toString method of // TypedDependency) IndexedWord label = td1.gov().makeSoftCopy(copyNumber); copyNumber++; // now we add the conjunction relation between conjHead (either td1.gov // or what it is itself conjoined with) and the copy // the copy has the same label as td1.gov() but is another TreeGraphNode // todo: Or that's the plan; there are a couple of knock on changes to fix before we can do this! // TypedDependency tdNew2 = new TypedDependency(conjValue(ccDep.dep().value()), conjHead, label); TypedDependency tdNew2 = new TypedDependency(conjValue(ccDep.dep().value()), td1.gov(), label); newTypedDeps.add(tdNew2); // now we still need to add the second prep grammatical relation // between the copy and the dependent of the prepOtherDep node TypedDependency tdNew3; GrammaticalRelation reln2 = determinePrepRelation(map, vmod, conjDep, td1, pobj); tdNew3 = new TypedDependency(reln2, label, prepOtherDep.dep()); newTypedDeps.add(tdNew3); if (DEBUG) { System.err.println(" adding: " + tdNew2 + " " + tdNew3); System.err.println(" deleting: " + conjDep + " " + prepOtherDep); } conjDep.setReln(KILL); prepOtherDep.setReln(KILL); // promote dtrs that would be orphaned for (TypedDependency otd : otherDtrs) { // special treatment for prepositions: the original relation is // likely to be a "dep" and we want this to be a "prep" if (otd.dep().tag().equals("IN")) { otd.setReln(PREPOSITIONAL_MODIFIER); } otd.setGov(td1.gov()); } } // Now we need to see if there are any TDs that will be "orphaned" off // the first preposition // by this collapse. Example: if we have: // dep(drew, on) // dep(on, book) // dep(on, right) // the first two will be collapsed to on(drew, book), but then // the third one will be orphaned, since its governor no // longer appears. So, change its governor to 'drew'. // CDM Feb 2010: This used to not move COORDINATION OR CONJUNCT, but now // it does, since they're not automatically deleted for (TypedDependency td2 : possibles) { if (td2.reln() != KILL) { // && td2.reln() != COORDINATION && // td2.reln() != CONJUNCT) { if (DEBUG) { System.err.println("Changing " + td2 + " to have governor of " + td1 + " [b]"); } td2.setGov(td1.gov()); } } // end for different prepositions } // for TypedDependency td1 : list // below here is the single preposition/possessor basic case!! for (TypedDependency td1 : list) { if (td1.reln() == KILL) { continue; } IndexedWord td1Dep = td1.dep(); String td1DepPOS = td1Dep.tag(); // find all other typedDeps having our dep as gov Set possibles = map.get(td1Dep); if (possibles != null && (td1.reln() == PREPOSITIONAL_MODIFIER || td1.reln() == POSSESSION_MODIFIER || td1.reln() == CONJUNCT)) { // look for the "second half" boolean pobj = true;// default for prep relation is prep_ for (TypedDependency td2 : possibles) { if (td2.reln() != COORDINATION && td2.reln() != CONJUNCT) { IndexedWord td2Dep = td2.dep(); String td2DepPOS = td2Dep.tag(); if ((td1.reln() == POSSESSION_MODIFIER || td1.reln() == CONJUNCT)) { if (td2.reln() == POSSESSIVE_MODIFIER) { if ( ! map.containsKey(td2Dep)) { // if 's has no kids of its own (it shouldn't!) td2.setReln(KILL); } } } else if ((td2.reln() == PREPOSITIONAL_OBJECT || td2.reln() == PREPOSITIONAL_COMPLEMENT) && (td1DepPOS.equals("IN") || td1DepPOS.equals("TO") || td1DepPOS.equals("VBG")) && (!(td2DepPOS.equals("RB") || td2DepPOS.equals("IN") || td2DepPOS.equals("TO"))) && !isConjWithNoPrep(td2.gov(), possibles)) { // we don't collapse preposition conjoined with a non-preposition // to avoid disconnected constituents // OK, we have a pair td1, td2 to collapse to td3 if (DEBUG) { System.err.println("(Single prep/poss base case collapsing " + td1 + " and " + td2); } // check whether we are in a pcomp case: if (td2.reln() == PREPOSITIONAL_COMPLEMENT) { pobj = false; } GrammaticalRelation reln = determinePrepRelation(map, vmod, td1, td1, pobj); TypedDependency td3 = new TypedDependency(reln, td1.gov(), td2.dep()); if (DEBUG) { System.err.println("PP adding: " + td3 + " deleting: " + td1 + ' ' + td2); } // add it to map to deal with recursive cases like "achieved this (PP (PP in part) with talent)" map.get(td3.gov()).add(td3); newTypedDeps.add(td3); td1.setReln(KILL);// remember these are "used up" td2.setReln(KILL);// remember these are "used up" } } } // for TypedDependency td2 } // Now we need to see if there are any TDs that will be "orphaned" // by this collapse. Example: if we have: // dep(drew, on) // dep(on, book) // dep(on, right) // the first two will be collapsed to on(drew, book), but then // the third one will be orphaned, since its governor no // longer appears. So, change its governor to 'drew'. // CDM Feb 2010: This used to not move COORDINATION OR CONJUNCT, but now // it does, since they're not automatically deleted if (possibles != null && td1.reln() == KILL) { for (TypedDependency td2 : possibles) { if (td2.reln() != KILL) { // && td2.reln() != COORDINATION && // td2.reln() != CONJUNCT) { if (DEBUG) { System.err.println("Changing " + td2 + " to have governor of " + td1 + " [c]"); } td2.setGov(td1.gov()); } } } } // for TypedDependency td1 // now remove typed dependencies with reln "kill" and add new ones. for (Iterator iter = list.iterator(); iter.hasNext();) { TypedDependency td = iter.next(); if (td.reln() == KILL) { if (DEBUG) { System.err.println("Removing dep killed in poss/prep (conj) collapse: " + td); } iter.remove(); } } list.addAll(newTypedDeps); } // end collapsePrepAndPoss() /** Work out prep relation name. pc is the dependency whose dep() is the * preposition to do a name for. topPrep may be the same or different. * Among the daughters of its gov is where to look for an auxpass. */ private static GrammaticalRelation determinePrepRelation(Map> map, List vmod, TypedDependency pc, TypedDependency topPrep, boolean pobj) { // handling the case of an "agent": // the governor of a "by" preposition must have an "auxpass" dependency // or be the dependent of a "vmod" relation // if it is the case, the "agent" variable becomes true boolean agent = false; String preposition = pc.dep().value().toLowerCase(); if (preposition.equals("by")) { // look if we have an auxpass Set aux_pass_poss = map.get(topPrep.gov()); if (aux_pass_poss != null) { for (TypedDependency td_pass : aux_pass_poss) { if (td_pass.reln() == AUX_PASSIVE_MODIFIER) { agent = true; } } } // look if we have a vmod if (!vmod.isEmpty() && vmod.contains(topPrep.gov())) { agent = true; } } GrammaticalRelation reln; if (agent) { reln = AGENT; } else { // for prepositions, use the preposition // for pobj: we collapse into "prep"; for pcomp: we collapse into "prepc" if (pobj) { reln = EnglishGrammaticalRelations.getPrep(preposition); } else { reln = EnglishGrammaticalRelations.getPrepC(preposition); } } return reln; } // used by collapse2WP(), collapseFlatMWP(), collapse2WPbis() KEPT IN // ALPHABETICAL ORDER private static final String[][] MULTIWORD_PREPS = { { "according", "to" }, { "across", "from" }, { "ahead", "of" }, { "along", "with" }, { "alongside", "of" }, { "apart", "from" }, { "as", "for" }, { "as", "from" }, { "as", "of" }, { "as", "per" }, { "as", "to" }, { "aside", "from" }, { "away", "from" }, { "based", "on" }, { "because", "of" }, { "close", "by" }, { "close", "to" }, { "contrary", "to" }, { "compared", "to" }, { "compared", "with" }, { "due", "to" }, { "depending", "on" }, { "except", "for" }, { "exclusive", "of" }, { "far", "from" }, { "followed", "by" }, { "inside", "of" }, { "instead", "of" }, { "irrespective", "of" }, { "next", "to" }, { "near", "to" }, { "off", "of" }, { "out", "of" }, { "outside", "of" }, { "owing", "to" }, { "preliminary", "to" }, { "preparatory", "to" }, { "previous", "to" }, { "prior", "to" }, { "pursuant", "to" }, { "regardless", "of" }, { "subsequent", "to" }, { "such", "as" }, { "thanks", "to" }, { "together", "with" } }; // used by collapse3WP() KEPT IN ALPHABETICAL ORDER private static final String[][] THREEWORD_PREPS = { { "by", "means", "of" }, { "in", "accordance", "with" }, { "in", "addition", "to" }, { "in", "case", "of" }, { "in", "front", "of" }, { "in", "lieu", "of" }, { "in", "place", "of" }, { "in", "spite", "of" }, { "on", "account", "of" }, { "on", "behalf", "of" }, { "on", "top", "of" }, { "with", "regard", "to" }, { "with", "respect", "to" } }; /** * Given a list of typedDependencies, returns true if the node "node" is the * governor of a conj relation with a dependent which is not a preposition * * @param node * A node in this GrammaticalStructure * @param list * A list of typedDependencies * @return true If node is the governor of a conj relation in the list with * the dep not being a preposition */ private static boolean isConjWithNoPrep(IndexedWord node, Collection list) { for (TypedDependency td : list) { if (td.gov().equals(node) && td.reln() == CONJUNCT) { // we have a conjunct // check the POS of the dependent String tdDepPOS = td.dep().tag(); if (!(tdDepPOS.equals("IN") || tdDepPOS.equals("TO"))) { return true; } } } return false; } /** * Collapse multiword preposition of the following format: * prep|advmod|dep|amod(gov, mwp[0])
* dep(mpw[0],mwp[1])
* pobj|pcomp(mwp[1], compl) or pobj|pcomp(mwp[0], compl)
* -> prep_mwp[0]_mwp[1](gov, compl)
* * prep|advmod|dep|amod(gov, mwp[1])
* dep(mpw[1],mwp[0])
* pobj|pcomp(mwp[1], compl) or pobj|pcomp(mwp[0], compl)
* -> prep_mwp[0]_mwp[1](gov, compl) *

* * The collapsing has to be done at once in order to know exactly which node * is the gov and the dep of the multiword preposition. Otherwise this can * lead to problems: removing a non-multiword "to" preposition for example!!! * This method replaces the old "collapsedMultiWordPreps" * * @param list * list of typedDependencies to work on */ private static void collapse2WP(Collection list) { Collection newTypedDeps = new ArrayList<>(); for (String[] mwp : MULTIWORD_PREPS) { // first look for patterns such as: // X(gov, mwp[0]) // Y(mpw[0],mwp[1]) // Z(mwp[1], compl) or Z(mwp[0], compl) // -> prep_mwp[0]_mwp[1](gov, compl) collapseMultiWordPrep(list, newTypedDeps, mwp[0], mwp[1], mwp[0], mwp[1]); // now look for patterns such as: // X(gov, mwp[1]) // Y(mpw[1],mwp[0]) // Z(mwp[1], compl) or Z(mwp[0], compl) // -> prep_mwp[0]_mwp[1](gov, compl) collapseMultiWordPrep(list, newTypedDeps, mwp[0], mwp[1], mwp[1], mwp[0]); } } /** * Collapse multiword preposition of the following format: * prep|advmod|dep|amod(gov, mwp0) dep(mpw0,mwp1) pobj|pcomp(mwp1, compl) or * pobj|pcomp(mwp0, compl) -> prep_mwp0_mwp1(gov, compl) *

* * @param list List of typedDependencies to work on, * @param newTypedDeps List of typedDependencies that we construct * @param str_mwp0 First part of the multiword preposition to construct the collapsed * preposition * @param str_mwp1 Second part of the multiword preposition to construct the * collapsed preposition * @param w_mwp0 First part of the multiword preposition that we look for * @param w_mwp1 Second part of the multiword preposition that we look for */ private static void collapseMultiWordPrep(Collection list, Collection newTypedDeps, String str_mwp0, String str_mwp1, String w_mwp0, String w_mwp1) { // first find the multiword_preposition: dep(mpw[0], mwp[1]) // the two words should be next to another in the sentence (difference of // indexes = 1) IndexedWord mwp0 = null; IndexedWord mwp1 = null; TypedDependency dep = null; for (TypedDependency td : list) { if (td.gov().value().equalsIgnoreCase(w_mwp0) && td.dep().value().equalsIgnoreCase(w_mwp1) && Math.abs(td.gov().index() - td.dep().index()) == 1) { mwp0 = td.gov(); mwp1 = td.dep(); dep = td; } } if (mwp0 == null) { return; } // now search for prep|advmod|dep|amod(gov, mwp0) IndexedWord governor = null; TypedDependency prep = null; for (TypedDependency td1 : list) { if ((td1.reln() == PREPOSITIONAL_MODIFIER || td1.reln() == ADVERBIAL_MODIFIER || td1.reln() == ADJECTIVAL_MODIFIER || td1.reln() == DEPENDENT || td1.reln() == MULTI_WORD_EXPRESSION) && td1.dep().equals(mwp0)) { // we found prep|advmod|dep|amod(gov, mwp0) prep = td1; governor = prep.gov(); } } if (prep == null) { return; } // search for the complement: pobj|pcomp(mwp1,X) // or for pobj|pcomp(mwp0,X) // There may be more than one in weird constructions; if there are several, // take the one with the LOWEST index! TypedDependency pobj = null; TypedDependency newtd = null; for (TypedDependency td2 : list) { if ((td2.reln() == PREPOSITIONAL_OBJECT || td2.reln() == PREPOSITIONAL_COMPLEMENT) && (td2.gov().equals(mwp1) || td2.gov().equals(mwp0))) { if (pobj == null || pobj.dep().index() > td2.dep().index()) { pobj = td2; // create the new gr relation GrammaticalRelation gr; if (td2.reln() == PREPOSITIONAL_COMPLEMENT) { gr = EnglishGrammaticalRelations.getPrepC(str_mwp0 + '_' + str_mwp1); } else { gr = EnglishGrammaticalRelations.getPrep(str_mwp0 + '_' + str_mwp1); } if (governor != null) { newtd = new TypedDependency(gr, governor, pobj.dep()); } } } } if (pobj == null || newtd == null) { return; } // only if we found the three parts, set to KILL and remove // and add the new one // Necessarily from the above: prep != null, dep != null, pobj != null, newtd != null if (DEBUG) { System.err.println("Removing " + prep + ", " + dep + ", and " + pobj); System.err.println(" and adding " + newtd); } prep.setReln(KILL); dep.setReln(KILL); pobj.setReln(KILL); newTypedDeps.add(newtd); // now remove typed dependencies with reln "kill" // and promote possible orphans for (TypedDependency td1 : list) { if (td1.reln() != KILL) { if (td1.gov().equals(mwp0) || td1.gov().equals(mwp1)) { // CDM: Thought of adding this in Jan 2010, but it causes // conflicting relations tmod vs. pobj. Needs more thought // maybe restrict pobj to first NP in PP, and allow tmod for a later // one? if (td1.reln() == TEMPORAL_MODIFIER) { // special case when an extra NP-TMP is buried in a PP for // "during the same period last year" td1.setGov(pobj.dep()); } else { td1.setGov(governor); } } if (!newTypedDeps.contains(td1)) { newTypedDeps.add(td1); } } } list.clear(); list.addAll(newTypedDeps); } /** * Collapse multi-words preposition of the following format: advmod|prt(gov, * mwp[0]) prep(gov,mwp[1]) pobj|pcomp(mwp[1], compl) -> * prep_mwp[0]_mwp[1](gov, compl) *

* * @param list * List of typedDependencies to work on */ private static void collapse2WPbis(Collection list) { Collection newTypedDeps = new ArrayList<>(); for (String[] mwp : MULTIWORD_PREPS) { newTypedDeps.clear(); IndexedWord mwp0 = null; IndexedWord mwp1 = null; IndexedWord governor = null; TypedDependency prep = null; TypedDependency dep = null; TypedDependency pobj = null; TypedDependency newtd = null; // first find the first part of the multi_preposition: advmod|prt(gov, mwp[0]) for (TypedDependency td : list) { if (td.dep().value().equalsIgnoreCase(mwp[0]) && (td.reln() == PHRASAL_VERB_PARTICLE || td.reln() == ADVERBIAL_MODIFIER || td.reln() == DEPENDENT || td.reln() == MULTI_WORD_EXPRESSION)) { // we found advmod(gov, mwp0) or prt(gov, mwp0) governor = td.gov(); mwp0 = td.dep(); dep = td; } } // now search for the second part: prep(gov, mwp1) // the two words in the mwp should be next to another in the sentence // (difference of indexes = 1) if (mwp0 == null || governor == null) { continue; } for (TypedDependency td1 : list) { if (td1.reln() == PREPOSITIONAL_MODIFIER && td1.dep().value().equalsIgnoreCase(mwp[1]) && Math.abs(td1.dep().index() - mwp0.index()) == 1 && td1.gov().equals(governor)) {// we // found // prep(gov, // mwp1) mwp1 = td1.dep(); prep = td1; } } if (mwp1 == null) { continue; } // search for the complement: pobj|pcomp(mwp1,X) for (TypedDependency td2 : list) { if (td2.reln() == PREPOSITIONAL_OBJECT && td2.gov().equals(mwp1)) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrep(mwp[0] + '_' + mwp[1]); newtd = new TypedDependency(gr, governor, pobj.dep()); } if (td2.reln() == PREPOSITIONAL_COMPLEMENT && td2.gov().equals(mwp1)) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrepC(mwp[0] + '_' + mwp[1]); newtd = new TypedDependency(gr, governor, pobj.dep()); } } if (pobj == null) { return; } // only if we found the three parts, set to KILL and remove // and add the new one // now prep != null, pobj != null and newtd != null prep.setReln(KILL); dep.setReln(KILL); pobj.setReln(KILL); newTypedDeps.add(newtd); // now remove typed dependencies with reln "kill" // and promote possible orphans for (TypedDependency td1 : list) { if (td1.reln() != KILL) { if (td1.gov().equals(mwp0) || td1.gov().equals(mwp1)) { td1.setGov(governor); } if (!newTypedDeps.contains(td1)) { newTypedDeps.add(td1); } } } list.clear(); list.addAll(newTypedDeps); } } /** * Collapse 3-word preposition of the following format:
* This will be the case when the preposition is analyzed as a NP
* prep(gov, mwp0)
* X(mwp0,mwp1)
* X(mwp1,mwp2)
* pobj|pcomp(mwp2, compl)
* -> prep_mwp[0]_mwp[1]_mwp[2](gov, compl) *

* * It also takes flat annotation into account:
* prep(gov,mwp0)
* X(mwp0,mwp1)
* X(mwp0,mwp2)
* pobj|pcomp(mwp0, compl)
* -> prep_mwp[0]_mwp[1]_mwp[2](gov, compl) *

* * * @param list * List of typedDependencies to work on */ private static void collapse3WP(Collection list) { Collection newTypedDeps = new ArrayList<>(); // first, loop over the prepositions for NP annotation for (String[] mwp : THREEWORD_PREPS) { newTypedDeps.clear(); IndexedWord mwp0 = null; IndexedWord mwp1 = null; IndexedWord mwp2 = null; TypedDependency dep1 = null; TypedDependency dep2 = null; // first find the first part of the 3word preposition: dep(mpw[0], mwp[1]) // the two words should be next to another in the sentence (difference of // indexes = 1) for (TypedDependency td : list) { if (td.gov().value().equalsIgnoreCase(mwp[0]) && td.dep().value().equalsIgnoreCase(mwp[1]) && Math.abs(td.gov().index() - td.dep().index()) == 1) { mwp0 = td.gov(); mwp1 = td.dep(); dep1 = td; } } // find the second part of the 3word preposition: dep(mpw[1], mwp[2]) // the two words should be next to another in the sentence (difference of // indexes = 1) for (TypedDependency td : list) { if (td.gov().equals(mwp1) && td.dep().value().equalsIgnoreCase(mwp[2]) && Math.abs(td.gov().index() - td.dep().index()) == 1) { mwp2 = td.dep(); dep2 = td; } } if (dep1 != null && dep2 != null) { // now search for prep(gov, mwp0) IndexedWord governor = null; TypedDependency prep = null; for (TypedDependency td1 : list) { if (td1.reln() == PREPOSITIONAL_MODIFIER && td1.dep().equals(mwp0)) {// we // found // prep(gov, // mwp0) prep = td1; governor = prep.gov(); } } // search for the complement: pobj|pcomp(mwp2,X) TypedDependency pobj = null; TypedDependency newtd = null; for (TypedDependency td2 : list) { if (td2.reln() == PREPOSITIONAL_OBJECT && td2.gov().equals(mwp2)) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrep(mwp[0] + '_' + mwp[1] + '_' + mwp[2]); if (governor != null) { newtd = new TypedDependency(gr, governor, pobj.dep()); } } if (td2.reln() == PREPOSITIONAL_COMPLEMENT && td2.gov().equals(mwp2)) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrepC(mwp[0] + '_' + mwp[1] + '_' + mwp[2]); if (governor != null) { newtd = new TypedDependency(gr, governor, pobj.dep()); } } } // only if we found the governor and complement parts, set to KILL and // remove // and add the new one if (prep != null && pobj != null && newtd != null) { prep.setReln(KILL); dep1.setReln(KILL); dep2.setReln(KILL); pobj.setReln(KILL); newTypedDeps.add(newtd); // now remove typed dependencies with reln "kill" // and promote possible orphans for (TypedDependency td1 : list) { if (td1.reln() != KILL) { if (td1.gov().equals(mwp0) || td1.gov().equals(mwp1) || td1.gov().equals(mwp2)) { td1.setGov(governor); } if (!newTypedDeps.contains(td1)) { newTypedDeps.add(td1); } } } list.clear(); list.addAll(newTypedDeps); } } } // second, loop again looking at flat annotation for (String[] mwp : THREEWORD_PREPS) { newTypedDeps.clear(); IndexedWord mwp0 = null; IndexedWord mwp1 = null; IndexedWord mwp2 = null; TypedDependency dep1 = null; TypedDependency dep2 = null; // first find the first part of the 3word preposition: dep(mpw[0], mwp[1]) // the two words should be next to another in the sentence (difference of // indexes = 1) for (TypedDependency td : list) { if (td.gov().value().equalsIgnoreCase(mwp[0]) && td.dep().value().equalsIgnoreCase(mwp[1]) && Math.abs(td.gov().index() - td.dep().index()) == 1) { mwp0 = td.gov(); mwp1 = td.dep(); dep1 = td; } } // find the second part of the 3word preposition: dep(mpw[0], mwp[2]) // the two words should be one word apart in the sentence (difference of // indexes = 2) for (TypedDependency td : list) { if (td.gov().equals(mwp0) && td.dep().value().equalsIgnoreCase(mwp[2]) && Math.abs(td.gov().index() - td.dep().index()) == 2) { mwp2 = td.dep(); dep2 = td; } } if (dep1 != null && dep2 != null) { // now search for prep(gov, mwp0) IndexedWord governor = null; TypedDependency prep = null; for (TypedDependency td1 : list) { if (td1.dep().equals(mwp0) && td1.reln() == PREPOSITIONAL_MODIFIER) {// we // found // prep(gov, // mwp0) prep = td1; governor = prep.gov(); } } // search for the complement: pobj|pcomp(mwp0,X) TypedDependency pobj = null; TypedDependency newtd = null; for (TypedDependency td2 : list) { if (td2.gov().equals(mwp0) && td2.reln() == PREPOSITIONAL_OBJECT) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrep(mwp[0] + '_' + mwp[1] + '_' + mwp[2]); if (governor != null) { newtd = new TypedDependency(gr, governor, pobj.dep()); } } if (td2.gov().equals(mwp0) && td2.reln() == PREPOSITIONAL_COMPLEMENT) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrepC(mwp[0] + '_' + mwp[1] + '_' + mwp[2]); if (governor != null) { newtd = new TypedDependency(gr, governor, pobj.dep()); } } } // only if we found the governor and complement parts, set to KILL and // remove // and add the new one if (prep != null && pobj != null && newtd != null) { prep.setReln(KILL); dep1.setReln(KILL); dep2.setReln(KILL); pobj.setReln(KILL); newTypedDeps.add(newtd); // now remove typed dependencies with reln "kill" // and promote possible orphans for (TypedDependency td1 : list) { if (td1.reln() != KILL) { if (td1.gov().equals(mwp0) || td1.gov().equals(mwp1) || td1.gov().equals(mwp2)) { td1.setGov(governor); } if (!newTypedDeps.contains(td1)) { newTypedDeps.add(td1); } } } list.clear(); list.addAll(newTypedDeps); } } } } /* * * While upgrading, here are some lists of common multiword prepositions which * we might try to cover better. (Also do corpus counts for same?) * * (Prague Dependency Treebank) as per CRIT except for RESTR but for RESTR * apart from RESTR away from RESTR aside from RESTR as from TSIN ahead of * TWHEN back of LOC, DIR3 exclusive of* RESTR instead of SUBS outside of LOC, * DIR3 off of DIR1 upwards of LOC, DIR3 as of TSIN because of CAUS inside of * LOC, DIR3 irrespective of REG out of LOC, DIR1 regardless of REG according * to CRIT due to CAUS next to LOC, RESTR owing to* CAUS preparatory to* TWHEN * prior to* TWHEN subsequent to* TWHEN as to/for REG contrary to* CPR close * to* LOC, EXT (except the case named in the next table) near to LOC, DIR3 * nearer to LOC, DIR3 preliminary to* TWHEN previous to* TWHEN pursuant to* * CRIT thanks to CAUS along with ACMP together with ACMP devoid of* ACMP void * of* ACMP * * http://www.keepandshare.com/doc/view.php?u=13166 * * according to ahead of as far as as well as by means of due to far from in * addition to in case of in front of in place of in spite of inside of * instead of in to (into) near to next to on account of on behalf of on top * of on to (onto) out of outside of owing to prior to with regards to * * www.eslmonkeys.com/book/learner/prepositions.pdf According to Ahead of * Along with Apart from As for As to Aside from Because of But for Contrary * to Except for Instead of Next to Out of Prior to Thanks to */ /** * Collapse multi-words preposition of the following format, which comes from * flat annotation. This handles e.g., "because of" (PP (IN because) (IN of) * ...), "such as" (PP (JJ such) (IN as) ...) *

* prep(gov, mwp[1]) dep(mpw[1], mwp[0]) pobj(mwp[1], compl) -> * prep_mwp[0]_mwp[1](gov, compl) * * @param list List of typedDependencies to work on */ private static void collapseFlatMWP(Collection list) { Collection newTypedDeps = new ArrayList<>(); for (String[] mwp : MULTIWORD_PREPS) { newTypedDeps.clear(); IndexedWord mwp1 = null; IndexedWord governor = null; TypedDependency prep = null; TypedDependency dep = null; TypedDependency pobj = null; // first find the multi_preposition: dep(mpw[1], mwp[0]) for (TypedDependency td : list) { if (Math.abs(td.gov().index() - td.dep().index()) == 1 && td.gov().value().equalsIgnoreCase(mwp[1]) && td.dep().value().equalsIgnoreCase(mwp[0])) { mwp1 = td.gov(); dep = td; } } if (mwp1 == null) { continue; } // now search for prep(gov, mwp1) for (TypedDependency td1 : list) { if (td1.dep().equals(mwp1) && td1.reln() == PREPOSITIONAL_MODIFIER) { // we found prep(gov, mwp1) prep = td1; governor = prep.gov(); } } if (prep == null) { continue; } // search for the complement: pobj|pcomp(mwp1,X) for (TypedDependency td2 : list) { if (td2.gov().equals(mwp1) && td2.reln() == PREPOSITIONAL_OBJECT) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrep(mwp[0] + '_' + mwp[1]); newTypedDeps.add(new TypedDependency(gr, governor, pobj.dep())); } if (td2.gov().equals(mwp1) && td2.reln() == PREPOSITIONAL_COMPLEMENT) { pobj = td2; // create the new gr relation GrammaticalRelation gr = EnglishGrammaticalRelations.getPrepC(mwp[0] + '_' + mwp[1]); newTypedDeps.add(new TypedDependency(gr, governor, pobj.dep())); } } if (pobj == null) { return; } // only if we found the three parts, set to KILL and remove // we know prep != null && dep != null && dep != null prep.setReln(KILL); dep.setReln(KILL); pobj.setReln(KILL); // now remove typed dependencies with reln "kill" // and promote possible orphans for (TypedDependency td1 : list) { if (td1.reln() != KILL) { if (td1.gov().equals(mwp1)) { td1.setGov(governor); } if (!newTypedDeps.contains(td1)) { newTypedDeps.add(td1); } } } list.clear(); list.addAll(newTypedDeps); } } /** * This method gets rid of multiwords in conjunctions to avoid having them * creating disconnected constituents e.g., * "bread-1 as-2 well-3 as-4 cheese-5" will be turned into conj_and(bread, * cheese) and then dep(well-3, as-2) and dep(well-3, as-4) cannot be attached * to the graph, these dependencies are erased * * @param list List of words to get rid of multiword conjunctions from */ private static void eraseMultiConj(Collection list) { // find typed deps of form cc(gov, x) for (TypedDependency td1 : list) { if (td1.reln() == COORDINATION) { IndexedWord x = td1.dep(); // find typed deps of form dep(x,y) and kill them for (TypedDependency td2 : list) { if (td2.gov().equals(x) && (td2.reln() == DEPENDENT || td2.reln() == MULTI_WORD_EXPRESSION || td2.reln() == COORDINATION || td2.reln() == ADVERBIAL_MODIFIER || td2.reln() == NEGATION_MODIFIER || td2.reln() == AUX_MODIFIER)) { td2.setReln(KILL); } } } } filterKill(list); } /** * Remove duplicate relations: it can happen when collapsing stranded * prepositions. E.g., "What does CPR stand for?" we get dep(stand, what), and * after collapsing we also get prep_for(stand, what). * * @param list A list of typed dependencies to check through */ private static void removeDep(Collection list) { Set prepRels = Generics.newHashSet(EnglishGrammaticalRelations.getPreps()); prepRels.addAll(EnglishGrammaticalRelations.getPrepsC()); for (TypedDependency td1 : list) { if (prepRels.contains(td1.reln())) { // if we have a prep_ relation IndexedWord gov = td1.gov(); IndexedWord dep = td1.dep(); for (TypedDependency td2 : list) { if (td2.reln() == DEPENDENT && td2.gov().equals(gov) && td2.dep().equals(dep)) { td2.setReln(KILL); } } } } // now remove typed dependencies with reln "kill" for (Iterator iter = list.iterator(); iter.hasNext();) { TypedDependency td = iter.next(); if (td.reln() == KILL) { if (DEBUG) { System.err.println("Removing duplicate relation: " + td); } iter.remove(); } } } /** * Find and remove any exact duplicates from a dependency list. * For example, the method that "corrects" nsubj dependencies can * turn them into nsubjpass dependencies. If there is some other * source of nsubjpass dependencies, there may now be multiple * copies of the nsubjpass dependency. If the containing data type * is a List, they may both now be in the List. */ private static void removeExactDuplicates(Collection list) { Set set = new TreeSet<>(list); list.clear(); list.addAll(set); } public static List readCoNLLXGrammaticalStructureCollection(String fileName) throws IOException { return readCoNLLXGrammaticalStructureCollection(fileName, EnglishGrammaticalRelations.shortNameToGRel, new FromDependenciesFactory()); } public static EnglishGrammaticalStructure buildCoNLLXGrammaticalStructure(List> tokenFields) { return (EnglishGrammaticalStructure) buildCoNLLXGrammaticalStructure(tokenFields, EnglishGrammaticalRelations.shortNameToGRel, new FromDependenciesFactory()); } public static class FromDependenciesFactory implements GrammaticalStructureFromDependenciesFactory { @Override public EnglishGrammaticalStructure build(List tdeps, TreeGraphNode root) { return new EnglishGrammaticalStructure(tdeps, root); } } } // end class EnglishGrammaticalStructure





© 2015 - 2024 Weber Informatics LLC | Privacy Policy