From 27433cff2da2435d58f14fc555604f27fba903da Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Fri, 16 Sep 2022 16:37:40 +0200 Subject: [PATCH 01/11] First documentation of sva --- sva.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/sva.py b/sva.py index 0b2f8e3..e5a3d5b 100644 --- a/sva.py +++ b/sva.py @@ -8,6 +8,9 @@ from treebankfunctions import (copymodifynode, find1, getattval, getdetof, getheadof, getlemma, indextransform, inverted, lbrother, nominal, rbrother, simpleshow, showtree) +from typing import List +from sastatypes import SynTree, UttId + debug = False @@ -376,7 +379,23 @@ def getsvacorrectedutt(snode, thepv, tokens, metadata): return results -def getsvacorrections(tokensmd, rawtree, uttid): +def getsvacorrections(tokensmd: TokenListMD, rawtree: SynTree, uttid: UttId) -> List[TokenListMD]: + ''' + + :param tokensmd: the inpu sequencxe of tokens plus metadata + :param rawtree: the syntacti structuire of the original utterance + :param uttid: the identifier of the utterance + :return: a list of pairs (token sequence , metadata) + + The function *getsvacorrections* generates alternative token sequences plus associated metadata for an input token + sequence with associated metadata for an utternace with id *uttid* and (inflated) syntactic structure *rawtree*. + + The function performs the following steps: + * It transforms all index nodes in the syntactic structure to nodes of their antecedents + (except for the *rel* attribute) by means of the function *indextransform* from the module *treebankfunctons*. See :ref:`indexnodeexpansion`. + * It looks up all finite verb forms in the structure by means of the function *getpvs* + + ''' debug = False if debug: showtree(rawtree, text='rawtree') From 47eacb5d3143df36beee4a06cd5770c2635263d8 Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Wed, 21 Sep 2022 14:56:53 +0200 Subject: [PATCH 02/11] AVn, Ond, Subject, Vr.. updates plus updated Documentation --- Documentation/Tarsp.rst | 66 +++++++++++++++++-------------- macros/newimperatives.txt | 27 +++++++------ macros/sastamacros1.txt | 11 ++++++ methods/TARSP Index Current.xlsx | Bin 25728 -> 25679 bytes 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/Documentation/Tarsp.rst b/Documentation/Tarsp.rst index d78fcba..870dba5 100644 --- a/Documentation/Tarsp.rst +++ b/Documentation/Tarsp.rst @@ -92,9 +92,9 @@ Explanation: *Wh*-questions are defined as follows:: - Tarsp_whq = """((@cat="whq" and @rel="--") or (@cat="whsub"))""" + Tarsp_whq = """((@cat="whq" and @rel="--") or (@cat="whsub") or (@cat="whrel" and @rel="--"))""" -covering both main clauses (*whq*) and subordinate clauses (*whsub*). +covering both main clauses (*whq*) and subordinate clauses (*whsub*), but also clauses that have been analysed by Alpino as independent (relation="--" ) relative clauses, which usually are actually wh-questions. .. _imperatives: @@ -241,10 +241,12 @@ Language measures such as *WBVC*, *OndVC*, *OndW*, *OndWB*, *OndWBVC*, and sever where:: - subject = """(@rel="su")""" + subject = """(@rel="su" and parent::node[(@cat="smain" or @cat="sv1" or @cat="ssub")])""" +Here we only count as subjects those nodes (full or index nodes) that have a finite clause node as parent. +Overt subjects never occur in nonfinite clauses, and subject index nodes should not be counted inside nonfinite clauses. Subject index nodes do occur in finite clauses, e.g., as the "trace" of a wh-movement. -It differs from the definition of :ref:`T063_Ond` because T063 has to exclude subjects of nonfinite nodes (which is covered in the composed language measures by the conditions on mood) and to cover an additional case (existential *er* as a subject (though maybe that should be included here as well) +It differs from the definition of :ref:`T063_Ond` because T063 has to cover an additional case (existential *er* as a subject (though maybe that should be included here as well) * **Tarsp_B**: see :ref:`T007_B` * **Tarsp_VC**: is defined as follows:: @@ -435,16 +437,23 @@ T006: Avn * **Implementation**: Xpath * **Query** defined as:: - //node[@pt="vnw" and @vwtype="aanw" and @lemma!="hier" and @lemma!="daar" and @lemma!="er" and - @rel!="det" and (not(@positie) or @positie!="prenom") ] + AVn = """(%coreavn% or %avnrel%)"""" + coreavn = """(@pt="vnw" and @vwtype="aanw" and @lemma!="hier" and @lemma!="daar" and @lemma!="er" and @rel!="det" and (not(@positie) or @positie!="prenom") )""" + avnrel = """(%diedatrel% and parent::node[@cat="rel" and @rel!="mod"])""" + diedatrel = """(@pt="vnw" and @vwtype="betr" and @rel="rhd" and (@lemma="die" or @lemma="dat"))""" +The query for *AVn* consists of two subcases: the core case, and the case of *die* and * dat* incorrectly analysed as a relative pronoun in an independent relative clause. -* The query with pt equal to *vnw* and vwtype equal to *aanw* selects demonstrative pronouns, but +The core case (*coreavn*): + +* which has pt equal to *vnw* and vwtype equal to *aanw* selects demonstrative pronouns, but * these include R-pronouns, so they are explicitly excluded * the relation must not be *det* (otherwise the pronouns are not used independently) * and if a *position* attribute is present it should not have the value *prenom* (otherwise it is not used independently) -* **Schlichting**: "Aanwijzend Voornaamwoord: 'die', 'dit', 'deze', 'dat' zelfstandig gebruikt." fully covered. +The relative case (*avnrel*) covers the relative pronouns *die* and *dat* (*diedatrel*) in an independent relative clause (i.e. *rel* is not equal to *mod*). + +* **Schlichting**: "Aanwijzend Voornaamwoord: 'die', 'dit', 'deze', 'dat' zelfstandig gebruikt." Fully covered. .. _T007_B: @@ -2052,18 +2061,17 @@ T063: Ond Here, **FullOnd** is defined as:: - """((%subject% and (@pt or @cat) ) or %erx%)""" + FullOnd = """(%subject% or %erx%)""" -where **subject** and **erx** are defined as:: +where **subject** (see :ref:`zinsdelen`) and **erx** are defined as:: - subject = """(@rel="su")""" + subject = """(@rel="su" and parent::node[(@cat="smain" or @cat="sv1" or @cat="ssub")])""" erx = """((@rel="mod" and @lemma="er" and ../node[@rel="su" and @begin>=../node[@rel="mod" and @lemma="er"]/@end]) or (@rel="mod" and @lemma="er" and ../node[@rel="su" and not(@pt) and not(@cat)]) ) """ -The condition on the presence of *pt* or *cat* is present to exclude (empty) subject of infinitives and participle clauses, e.g. in *Hij heeft gezwommen* *hij* is an antecedent of an (empty) node acting as the subject of the past participle *gezwommen*, and we do not want to include that. The **erx** macro is to ensure that so-called *expletive er* also counts a subject. We implemented this in the following manner: *er* is considered (also) expletive (and thus must count as a subject): @@ -2071,7 +2079,6 @@ The **erx** macro is to ensure that so-called *expletive er* also counts a subje * if there is an empty subject, to cover cases such as *wie zwom* **er**. Note that in *wie heeft* **er** gezwommen* this *er* is considered a subject because of the empty subject of the participial clause, which is perhaps not what we want. -**Remark**: The condition on the presence of *pt* or *cat* incorrectly excludes *wie* in *wie doet dat*, *wie heeft dat gedaan*: *wie* is a *whd* an an antecedent to an index node with grammatical relation *su* (see e.g. VKLTarsp, sample 3, utterance 25 *weet ik niet* **wie** *daarin zit*. It has no consequences for the scores because *Ond* is not in the form and because in language measures such as OndWB etc a different definition of subject is used. It probably is better to replace the condition on *pt* and *cat* by a condition on the parent node, viz. that it must have as value for the *cat* attribute from one of the values from *smain*, *sv1*, or *ssub* (categories for finite clauses or finite clause bodies). * **Schlichting**: "Dit is de persoon of de zaak die de handeling van het werkwoord uitvoert. Wanneer het onderwerp van een zin in het meervoud staat, staat de persoonsvorm van die zin ook in het meervoud." @@ -2402,7 +2409,7 @@ Straightforward implementation:: Tarsp_OndW = """(%declarative% and %Ond% and (%Tarsp_W% or node[%Tarsp_onlyWinVC%]) and - %realcomplormodnodecount% = 0 )""" + %realcomplormodnodecount% = 1 )""" See section :ref:`composedmeasures` for details. @@ -3604,6 +3611,7 @@ T110: Vr This language measure actually does not exist. It has been included and implemented because the code *Vr* occurs in the Schlichting appendix (example 20, p. 91, and example 22, p. 92). Example 20 should have been annotated as *Vr(XY)*, and example 22 as *Vr4*. +.. _VrXY: T111: Vr(XY) """""""""""" @@ -3624,13 +3632,14 @@ T111: Vr(XY) Straightforward implementation:: - Tarsp_VrXY = """(%Tarsp_whq% and - node[@rel="whd"] and - node[@cat="sv1" and - @rel="body" and - %realcomplormodnodecount% = 1 - ])""" + Tarsp_VrXY = """(%Tarsp_whq% and + node[%Tarsp_whqhead%] and + node[%whqbody% and %realcomplormodnodecount% <= 1])""" +where *Tarsp_whqhead* and *whqbody* cover both wh-questions (main and subordinate) and independent relatives:: + + Tarsp_whqhead = """(@rel="whd" or @rel="rhd") """ + whqbody = """((@cat="sv1" or @cat="ssub") and @rel="body")""" See section :ref:`composedmeasures` for details. @@ -3662,12 +3671,10 @@ T112: Vr4 Straightforward implementation:: Tarsp_Vr4 = """(%Tarsp_whq% and - node[@rel="whd"] and - node[@cat="sv1" and - @rel="body" and - %realcomplormodnodecount% = 2 - ])""" + node[%Tarsp_whqhead%] and + node[%whqbody% and %realcomplormodnodecount% = 2])""" +For *Tarsp_whqhead* and *whqbody*, see :ref:`VrXY` See section :ref:`composedmeasures` for details. @@ -3694,13 +3701,12 @@ T113: Vr5+ Straightforward implementation:: Tarsp_Vr5plus = """(%Tarsp_whq% and - node[@rel="whd"] and - node[@cat="sv1" and - @rel="body" and - %realcomplormodnodecount% > 2 - ])""" + node[%Tarsp_whqhead%] and + node[%whqbody% and %realcomplormodnodecount% > 2])""" + +For *Tarsp_whqhead* and *whqbody*, see :ref:`VrXY` See section :ref:`composedmeasures` for details. diff --git a/macros/newimperatives.txt b/macros/newimperatives.txt index 85af4d8..03b45fb 100644 --- a/macros/newimperatives.txt +++ b/macros/newimperatives.txt @@ -1,6 +1,6 @@ -subject = """(@rel="su")""" +subject = """(@rel="su" and parent::node[(@cat="smain" or @cat="sv1" or @cat="ssub")])""" questionmark = """(@pt="let" and @word="?")""" exclamationmark = """(@pt="let" and @word="!")""" periodmark = """(@pt="let" and @word=".")""" @@ -9,7 +9,7 @@ topcontainsexclamationmark = """ (ancestor::node[@cat="top" and node[%exclamatio topcontainsperiodmark = """ (ancestor::node[@cat="top" and node[%periodmark%]])""" Ond = """node[%subject%]""" -FullOnd = """((%subject% and (@pt or @cat) ) or %erx%)""" +FullOnd = """(%subject% or %erx%)""" @@ -60,8 +60,9 @@ nonfincat = """(@cat="inf" or @cat="ppart")""" nonfinvc = """(@rel="vc" and %nonfincat%) """ realcomplormodnode = """node[%realcomplormod%]""" -realcomplormod = """(not(%particlesvp%) and not(%indexnode%) and not(%nonfinvc%) and not(@rel="hd"))""" +realcomplormod = """(not(%particlesvp%) and (not(%indexnode%) or %includeindexnode%) and not(%nonfinvc%) and not(@rel="hd"))""" indexnode = """(@index and not (@cat or @pt or @pos))""" +includeindexnode = """(%indexnode% and parent::node[@cat="ssub" and parent::node[@cat="rel" ]])""" suindexnode = """(%indexnode% and @rel="su") """ nonfinindexnode = """(%indexnode% and parent::node[%nonfinvc%])""" @@ -111,7 +112,7 @@ Tarsp_OndVC = """(%Ond% and node[%Tarsp_Basic_VC%] and count(node) = 2) """ Tarsp_OndBVC = """(%Ond% and node[%Tarsp_Basic_B%] and node[%Tarsp_Basic_VC%] and count(node) = 3) """ -Tarsp_OndW = """(%declarative% and %Ond% and (%Tarsp_W% or node[%Tarsp_onlyWinVC%]) and %realcomplormodnodecount% = 0 )""" +Tarsp_OndW = """(%declarative% and %Ond% and (%Tarsp_W% or node[%Tarsp_onlyWinVC%]) and %realcomplormodnodecount% = 1 )""" Tarsp_onlyWinVC = """(@rel="vc" and node[@rel="hd" and @pt="ww" and %realcomplormodnodecount% = 0])""" @@ -162,19 +163,21 @@ Tarsp_WOnd4 = """(%ynquery% and not(%basicimperative%) and %topcontainsquestionm Tarsp_WOnd5plus = """(%ynquery% and not(%basicimperative%) and %topcontainsquestionmark% and %Tarsp_W% and %Ond% and %realcomplormodnodecount% =4) """ -Tarsp_whq = """((@cat="whq" and @rel="--") or (@cat="whsub"))""" -] +Tarsp_whq = """((@cat="whq" and @rel="--") or (@cat="whsub") or (@cat="whrel" and @rel="--"))""" + +Tarsp_whqhead = """(@rel="whd" or @rel="rhd") """ +whqbody = """((@cat="sv1" or @cat="ssub") and @rel="body")""" Tarsp_VrXY = """(%Tarsp_whq% and - node[@rel="whd"] and - node[@cat="sv1" and @rel="body" and %realcomplormodnodecount% = 1])""" + node[%Tarsp_whqhead%] and + node[%whqbody% and %realcomplormodnodecount% <= 1])""" Tarsp_Vr4 = """(%Tarsp_whq% and - node[@rel="whd"] and - node[@cat="sv1" and @rel="body" and %realcomplormodnodecount% = 2])""" + node[%Tarsp_whqhead%] and + node[%whqbody% and %realcomplormodnodecount% = 2])""" Tarsp_Vr5plus = """(%Tarsp_whq% and - node[@rel="whd"] and - node[@cat="sv1" and @rel="body" and %realcomplormodnodecount% > 2])""" + node[%Tarsp_whqhead%] and + node[%whqbody% and %realcomplormodnodecount% > 2])""" Tarsp_BBX = """((@cat="top" and count(.//node[@pt!='let' and @pt!='vg'])=3 and diff --git a/macros/sastamacros1.txt b/macros/sastamacros1.txt index 5e14838..2db9859 100644 --- a/macros/sastamacros1.txt +++ b/macros/sastamacros1.txt @@ -179,6 +179,17 @@ singlewordbw = """ (@pt="bw" or %Rpronoun% or %adjadv%) """ +diedatrel = """(@pt="vnw" and @vwtype="betr" and @rel="rhd" and (@lemma="die" or @lemma="dat"))""" + +avnrel = """(%diedatrel% and parent::node[@cat="rel" and @rel!="mod"])""" + +v2problem = """//node[@pt="ww" and @wvorm="pv" and + @begin != parent::node[@cat="ssub"]/parent::node[@cat="rel" and @rel!="mod"]/node[%diedatrel% ]/@end + ]""" + +coreavn = """(@pt="vnw" and @vwtype="aanw" and @lemma!="hier" and @lemma!="daar" and @lemma!="er" and @rel!="det" and (not(@positie) or @positie!="prenom") )""" + +AVn = """(%coreavn% or %avnrel%)"""" corephrase = """(@cat="np" or @cat="pp" or @cat="advp" or @cat="ap")""" diff --git a/methods/TARSP Index Current.xlsx b/methods/TARSP Index Current.xlsx index 7f814951fdc027f19567369cb9b8226035bb9410..8a36391e8722bd270ea612a2f21eebcd78207b2b 100644 GIT binary patch delta 16372 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 E0g3w?&;S4c From c5ebe4d91d91301ccc82eb80e4e8639261fe675a Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Fri, 30 Sep 2022 19:29:37 +0200 Subject: [PATCH 03/11] updated basicreplacements and Vobij and Vo/bij --- alpinoparsing.py | 4 + basicreplacements.py | 23 +++- external_functions.py | 4 +- macrolength.py | 26 +++++ macros/sastamacros1.txt | 3 + methods/TARSP Index Current.xlsx | Bin 25679 -> 25632 bytes queryfunctions.py | 44 +++++++- sva.py | 100 ++++++++++++++++-- test_adjacency.py | 167 +++++++++++++++++++++++++++++ test_vobij.py | 175 +++++++++++++++++++++++++++++++ treebankfunctions.py | 78 +++++++++++++- 11 files changed, 605 insertions(+), 19 deletions(-) create mode 100644 macrolength.py create mode 100644 test_adjacency.py create mode 100644 test_vobij.py diff --git a/alpinoparsing.py b/alpinoparsing.py index 5a92af9..d9a9ab6 100644 --- a/alpinoparsing.py +++ b/alpinoparsing.py @@ -25,11 +25,15 @@ #from config import SDLOGGER #from sastatypes import SynTree, URL +urllibrequestversion = urllib.request.__version__ + alpino_special_symbols_pattern = r'[\[\]]' alpino_special_symbols_re = re.compile(alpino_special_symbols_pattern) gretelurl = 'https://gretel.hum.uu.nl/api/src/router.php/parse_sentence/' +#gretelurl = 'http://gretel.hum.uu.nl/api/src/router.php/parse_sentence/' previewurltemplate = 'https://gretel.hum.uu.nl/ng/tree?sent={sent}&xml={xml}' +#previewurltemplate = 'http://gretel.hum.uu.nl/ng/tree?sent={sent}&xml={xml}' emptypattern = r'^\s*$' emptyre = re.compile(emptypattern) diff --git a/basicreplacements.py b/basicreplacements.py index 4415f8a..e8637b4 100644 --- a/basicreplacements.py +++ b/basicreplacements.py @@ -139,14 +139,24 @@ ('aleen', 'alleen', orth, typo, typorepl.format(wrong='alleen', correct='alleen'), dp), ('heef', 'heeft', pron, infpron, codared, dp), ('saan', 'staan', pron, wrongpron, onsetred, dp), + ('saan', 'gaan', pron, wrongpron, wrongpron, dp+2), + ('jerke', 'werken', pron, wrongpron, wrongpron, dp), ('taan', 'staan', pron, wrongpron, onsetred, dp), ("a'maal", 'allemaal', pron, infpron, redpron, dp), ('taan', 'staan', pron, wrongpron, onsetred, dp), ('beurt', 'gebeurt', pron, wrongpron, prefixdrop, dp), ('dahaar', 'daar', pron, emphasis, voweldup, dp), ('desu', 'deze', pron, infpron, vzdevoicing, dp), + ('tan', 'dan', pron, infpron, initdev, dp), + ('tat', 'dat', pron, infpron, initdev, dp), + ('tit', 'dit', pron, infpron, initdev, dp), + ('lape', 'slapen', pron, infpron, f'{onsetred}+{fndrop}', dp), + ('vas', 'vast', pron, infpron, codared, dp), + ('datte', 'dat', pron, infpron, emphasis, dp), + ('omdatte', 'dat', pron, infpron, emphasis, dp), + ('cirtus', 'circus', pron, wrongpron, typorepl.format(wrong='t', correct='c'), dp) - ] + \ + ] + \ ervzvariants + \ innereplacements + \ innureplacements @@ -217,10 +227,12 @@ ('of-t-ie', ['of', 'ie'], pron, infpron, t_ie, dp), ('as-t-ie', ['als', 'ie'], pron, infpron, t_ie, dp), ("dit's", ["dit", "is"], pron, infpron, contract, dp), - ("dat's", ["dat", "is"], pron, infpron, contract, dp) + ("dat's", ["dat", "is"], pron, infpron, contract, dp), + ("datte", ['dat', 'ie'], pron, infpron, contract, dp+2), + ("omdatte", ['omdat', 'ie'], pron, infpron, contract, dp+2) ] + \ - closesyllshortprepexpansions + \ - innuclosedsyllshortprepexpansions + closesyllshortprepexpansions + # + innuclosedsyllshortprepexpansions # put off does not lead to improvement #: The dictionary *basicexpansions* maps a contracted word form to a list of 4-tuples @@ -288,7 +300,8 @@ def getmeta4CHATreplacements(wrongword: str, correctword: str) -> KnownReplaceme 'wieken', 'paarden', 'stoelen', 'ramen', 'strepen', 'planten', 'groeten', 'flessen', 'boeren', 'punten', 'tranen'], 'teilen'), (['snel', 'wit', 'kort', 'dicht'], 'mooi'), - (['witte'], 'mooie') + (['witte'], 'mooie'), + (['wel', 'niet'], 'ietsjes') #find a different adverb that does not get inside constituents (ietsjes?) ] diff --git a/external_functions.py b/external_functions.py index 1ea9264..9ad6e90 100644 --- a/external_functions.py +++ b/external_functions.py @@ -19,7 +19,7 @@ from imperatives import wx, wxy, wxyz, wxyz5, wondx, wond4, wond5plus from TARSPscreening import tarsp_screening from TARSPpostfunctions import vutotaal, gofase, gtotaal, pf2, pf3, pf4, pf5, pf6, pf7, pf -from queryfunctions import xneg_x, xneg_neg, VzN +from queryfunctions import xneg_x, xneg_neg, VzN, vobij, voslashbij from dedup import mlux, samplesize, neologisme, onvolledig, correct from STAPpostfunctions import BB_totaal, GLVU, GL5LVU from ASTApostfunctions import wordcountperutt, countwordsandcutoff, KMcount, finietheidsindex, getnounlemmas,\ @@ -63,7 +63,7 @@ def oldgetfname(f: Callable) -> str: # Initialisation thetarspfunctions = [getcompounds, sziplus6, xenx, vr5plus, wx, wxy, wxyz, wxyz5, wondx, wond4, wond5plus, tarsp_screening, vutotaal, gofase, gtotaal, pf2, pf3, pf4, pf5, pf6, pf7, pf, xneg_x, xneg_neg, - mktarspform, VzN] + mktarspform, VzN, vobij, voslashbij] thestapfunctions = [BB_totaal, GLVU, GL5LVU, makestapform] diff --git a/macrolength.py b/macrolength.py new file mode 100644 index 0000000..05edc04 --- /dev/null +++ b/macrolength.py @@ -0,0 +1,26 @@ +from macros import macrodict, expandmacros + +resultlist = [] +for macroname, macrovalue in macrodict.items(): + expandedmacrovalue = expandmacros(macrovalue) + resultlist.append((macroname, len(expandedmacrovalue))) + +querylist = ['//node[( %declarative% )]', '//node[( %Ond% )', + '//node[( %Tarsp_B_X_count% = 3)]', '//node[%declarative% and %Ond%]', + '//node[%declarative% and %Tarsp_B_X_count% = 3]'] +#Invalid query: unknown error + +queryresultlist = [] +for query in querylist: + expandedquery = expandmacros(query) + queryresultlist.append((query, len(expandedquery))) + +print('\n\nQueries:\n') +sortedqueryresultlist = sorted(queryresultlist, key= lambda x: x[1], reverse=True) +for (query, lvalue) in sortedqueryresultlist: + print(f'{query}\t{lvalue}') + +print('\n\nMacros:\n') +sortedresults = sorted(resultlist, key=lambda x: x[1], reverse=True) +for (name, lvalue) in sortedresults: + print(f'{name}\t{lvalue}') \ No newline at end of file diff --git a/macros/sastamacros1.txt b/macros/sastamacros1.txt index 2db9859..9fa2054 100644 --- a/macros/sastamacros1.txt +++ b/macros/sastamacros1.txt @@ -34,6 +34,9 @@ JO_kijken_naar = """ parent::node[@cat="pp" and ] """ +PQ_e = """@end""" +PQ_b = """@begin""" + robusttopicdrop = """(@cat="sv1" and ../node[@lemma="."])""" Tarsp_hww = """ diff --git a/methods/TARSP Index Current.xlsx b/methods/TARSP Index Current.xlsx index 8a36391e8722bd270ea612a2f21eebcd78207b2b..fed8fcf0564f74c15c7d1e07e63041c3a7757801 100644 GIT binary patch delta 17293 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

1!9X=>ymPc*JA4nn@A(LTNQ`~e(9T% z(;(q_weRK15W^s5b`d;0PHCpv^H2H$M5k!MxQwzO?!tv||0UCvwf0>Nb+q>)>aXPV zp^ZV;I2z6>H4Q?hDe|)O*@O?c{GD-vW{Q@90_p?cDP!Npm2niYXWD}&tsk~!Z5`j- zGx$n#9>$8=4%rhZ=P2fdn*u>P`KSP^L#uhcWz`0|rrKOnJn6;uM$_?*GN_3P|LP&n zMV6C|s9Im~{s>0D4xSdawPX%w2RxazTq6r7MQ;d{?G1?C;?9vtND8%;KxNJMpP9OU zW!?su@)MGAzjn4AstD<0hrYR*5XT1UQ1{k6j*aIuaFVkwdRNed!{US3U3+FMmY?$i z($ZCJ8h+;@AXximETvf@{oy>kVr_<}N}1KKAS2$jt*PffUot1%soo0EB|!4}V$qOt zR0rX2_3M-f$q^Q>R|rI6Iv_@rV~e>Lp=U2lLo8KQ*t=MvWNbBdvX;L!hvkSCVjT4b z(5(R1r0m>KIICmvEF}Q-33C@|d(<~?T^!84eWSA9m}s*7ejfXVYlXN{JugPn&PcwI zmLH+?D+CTswt7GZp+Ie>DD;X-L2Xi~jYjGr)t29t=DC3&A^C^xo9w}|eEj%RVUnd@ zqwT_v)RAhsIdcrZnSK*eR_lK&Tn5Z`@78 zZB=^_a=V&|kXL&Zc`>o#rLxdG^^a25_?QV$u3|a9+x2c?mLSji@0~UepZnw0<9w2g zw_|-grp~hsvh&&4dR{dyZqhRqd{#}YvFQA6xeFojxa;g1tdjX|f-=eKO7onQ7|m+4 zDr&C@mWm?lUCAKG3sJR}_l;4VqE#lszMf7CI6&p!r`ctO8KFZGCGNC7W@aS?W8_#| zWUEx^Nae8C&P3-!-;*6VOm2r^zU*uy2rv`V*xL8?uG($Ba?QHGeM9*x%hyRovtgN$ z^+l`I7`fWd^FFd3Z<_JfGmC5m-mTC)5=h(2)SD@TsC5z4;(3l5((GTM${(c+??dw# zo>55j{sHCT@pr?J=3e_-VuszIVR*QRqq)oo)~=C~_ssndWOSXfCIDn6X*mx3?PXhq z!LHfBV(qQNgFudDvb=MQ+aIH3tJ(j#>eYJ zbSJqg5iXEfX+S!w(Z<|g8i7xFyNv1ZGlZ1+!gLHW=b_ zHix_IKrgnqBbq?bq4zzpNHP+PKX)f&;p7jyQ1ch2wK<_xWdF`t z)^XRf`Wlj9sP*Kp94NS>6Eh!flYaiyG(Y?huPZ@@ZPAA1l>W9JWLBqSDBF$)KqcRA z4LDZ&R#=29c_R+GWW!-2Se>;%0>-C4xc3WK+zHMR;n(l~vpI{ue?G~Qzg;?o`KZ%g z{k77Qwr z#mh@x#VehUSf_H{o7M1EljGzkKQx3yg_pLm$A&G)Wjj`^k~(8W(w0rzl3FA-HZ@7V zkvY}b9aJxVXV>buNnPEh@l2eWvJ;tfrr#+FV*a~Id68^T7hmMw*LxT}92MG`rkJW6 zVEp+l>O$+!tqTIH&M9raed_F%?%V0!7rW;EzML|*aP!RvXBTun{KWnwweI_ko>%Fs z?oDNw;u2)Au<>?~`gf!1==qh#HQnhIeS0TMhA$U1k!N-_}0g{Cajt*Q5z-$@BaE8u&CTE*9_Mi+?R+|LXONx6T{AKkFOv zWIt+H#+dqUPJh)ATZPUO2g^P8fp^VM=1$?;oEB%!3_1N`VPYdEd<*?#?WAy~bEcEu zn~F}JnWV+zYs6D0OEvU!5FK`5KeA$B z^+G~rhO=2xk`nqQUrb|*4!G$4!Xpx`g2BWSOV|1#-6Tm4)55jR&I$g>Ylm`uM(8os z9C}tBMjxiqt-B0zZZ4kQ9chWYR6-Rv5^e?1e>TGhS+##g(l30J(%*p7zavLq?}`5` zWfZRutGJ0Y+X*L#gyEXORyJMjiJ`5RuT;RAPD{(EEyt|GM-HJtzt7H7jczAdW{c7r zIrrge_>3ehiA-CfqJ89uD!n-n&fC7xy$8*uiqonoXNoDrGi90C;i%bKGzk~~DZ~W) z@-2Q{Qq|-lI7YLK%%FpLKZ;byQ$%nNitJ;^ZeRQ)9^;DefcenQ=WKzg9D^=H0C~$eM1ESk^ zvYQYWNP?%`sWB7!?}>3)c;A=B-}fy5Wm^l>FW9L)S8MCK%idc4nTaf_VBW{-h!mc- zU8-NHx*?a2?;6ym`Px_#(6Cx-E?WNnL4B{P>oKeSwP3Y2XddQt{32_%Igl`sf+UAf zzcf%B%+RuP^(zl^d2$y`MGx`hREQ$;d+uXSgC1gB{oYN}--x0tj2V=oIvadoV0vK^ zh4wUB+hY>8xm0j$m_*~@Wav}=X}1y&k1S*w`-j6G?8jFU-;i{^#SGY_qFGGG?w{kk z{uI3D5lKu9R4I;S^mMKXvwt+_;!2noDGB(TG+s{~JtM=bMp)1M1eb8Q$`7wK!OFey z?H^lj>(Bp*TzVG$E_U?if(=IpES4#5&bmLYX^rrH=())3UU!>_Ar(np{?c!M<`8qt zsrLu?wH!|w{>-S^I1#O>Bb8$C?~}yb0nuDR(b6!ed&GJqPqIZ6JR8q_Onm3;A@;ky zlyI25Hflg=f9S&6>)$xmW=8j7r&>(q4#VtMMP5u#(hFOx)NyZ1gDt#AZluHlOzXSb zS^){EYvHa>9#4Tg!#|H6q0f;47lqI#dY7x0dHZ$8x{!3`{I;~n4UuD{Zae+fz!O;ly!7=yQ1R={MfQ1ok4 zpIPQyw&QkBW3W+n*J^+#&y*%Sa@}I{k(MaOuv&wWuE1@YZ8!4GD;SV+B3wSX zsRC}8O1YaDn}?JGkT2FW^5RqSMwmjFzVt_}Uza!X>m{?FI8%3H(gi-|e5)wCtswpi z(vx(GG|%2`2Y4OmkZA-43)b~65D(E@yf_$^1x6SsCYs{~?>qL;s3qld88ZRwxzj_g zru8h7^`V}0^)a4?$(xeVp1-=TxcD7zWRsu7!f*MpE(U+Y@dkxgM7SV3a-X&+Vi~Av zt$1X9ANdf{a8TZwUt2#zPt{;JZaD0_NcKRiPhgrOW@yN^=pRh5v;=g9o(? zz-~LSUTv&85kvruk z|D%NNs7IJLi9NZd^jEXQ&$c)*jnp?cuMKt)eDY-bj%UPWmhKf$Nc9__moQn|G4=DN z1z%Y@+$~X7*;-2#5heOZP%hBso&r$7G@aw~mYu6k~Z_40% z=v`_IAi6qQetoeu*Yk4v)wbsmIs!aj%_sJlafzD?(C-(?cv~K18TqqJ2D-=bUGGMp z+nO2qA*j-3!jJ`HyGn$Q*r*^WG&!UA!O`Xl@rGr^KLLMG$TlU@oXPv9WOOWp5hjW> zZ!RK|w{A$EYNB$cB}=h$_dHqM`~|5rw(}*6UvuHyJJRnK;pBr46{48BrZF9E=jIqk zJT8!*2xa5Kb^u9CBtCfBTQQ#+@f8rzw8syTYrk?^&k31bY3h;nSph% zN=me)q8JZ@R0=vgCbD`K>}_KG{)7K}e(zYj<{F&*d!*=w-`LYg2@XD2OJ-z$srq<@ z25I3Y>;p}x%eJ+C!>Or;)Kg&f_|EQq+5ENm#~&78t?gI5$h2i0lnBp_H?E-ROyb1It0c+owCoRFL5Bc`mv^msKA8)hx9rxmf}Ihl45$UavW>%Df(`il|duT>+FNXLOo$zRX;Z(Ste{|k(HQQs^pJ5qid)wTyA^76Sd9d zW8oC^f)92-_h!`>WzO^&J+u1eJqU_M0ouc#hIDr7C44C>ZTJSbC8vAobDOf^=NbdN zBeGPa2|GvMy*Cbv)7W_b2Axc`D`zFz7|q+I-c>vz6OE6C!1!+z$unBPHQOrrAzJ8j zWkKH%NPQE_4iw_q=3a6g# z!*mq5f<>~&Ff@r(PsGiRT9u{vtal>u3QZY0mzQ?vH1uUe=_tuJ zv#Tn^3vtp%C(`JrQWNf>QfIb@EQz5I7AD`!9q5o+Z^XFJ&UYn$`=d&(mDdZ*`IqXS zLz|xb{QprmdjPEn_lgGLL3>meCB-s7*Pr1(dt%vv&^_m*ol>BE76Qr3Eo(~CfKjp0 zq>9zs`-3-?F2=QNDy!$RNw8y#);;0VNAVHr7Q~c%fPzDkcN8G&{VtgDkGdFiDx2{% zb2x@Z7xIzEsV{rMM=*;DcYx&MXKPAg{5+I>zGV9K<#>BZ4-Y>Y7UDBav1TPj*|pGo zD~nI?Ga{Uk?SDBqAx&(zmJK|sW~hjpae=%uJE}4#i(46r`#g^niC`!{afD*9mqZ zz5Q?z*zEJZFe>egnAqk46)(q{-kECqYXs!zZ!I1}6tIdfm%-6U1R0%QtxG@p)sEXW zO&RK4!f0C2GQ4l-B5Ww`ScVIM6!w)=md>M0Ix(Zk&_`JnXT^X`e{FYvpVuiDxtN2i zRIjR>T9Oi&gpXqt06=S}Q$*$=->MYuyG}P(oWe@8%zJd`fjA*qhnOYB{&fNtemK(=%V;Ko?w@wDej{vL0*G5I93>6xZrV|?(xbE^qnyF^h$d9^-VfE=kk#qw#&UlSXxSKlQ%MDIu z#1h4Miyp5etR3^ws82}e2M3rlMAt)A1!9j4)PeW9>1$t}3kgrO{o#{@xs%H_(P$5p z%qu>qs*IZ&mdH3uMV&M~AcLnkiQI$9n+(XYNdUOu+Rr>aEr;(V_=BY){k2iJcL!yu zj+S$bFN*cF7YXo7)yES-`wOAeWtdGY8xRr*|MqW3X4dF-6@i@IigeNX0NmS=XgnQw z&o`9%RI+bqR6&?3kY`-U_kV>_A7P@eeiwo)1m=m4ZK(S%75F-)lGeQzNBM0N$`~Q2 z0GsDD2eJ#lEIXMKjddNAMayYUbah~pmhDsN=hJ3VmR?{+MUT+Oa&=ClW6O-9LARn- zLc4?bZ_KlgV0CS|RN3sK*PttZ8mnl-4D#y}c<=tXAjQ6{AKwu6^Vj|~4o=KQlq9sR z<0CQ0hxq?6^}rRPKYyVfSpjLve^mz@bYoD#8PPwJ=q*9~q<*2Cpf&OLcHRd_<)*0b z+wD9v*QF}o(4Url4DGtK+zA#(eI_ewmmXcyLwQfk4u9&7OY}OcZZg&}c~i?i{=`8wx?f_LQ27 z{f;J5K09Cu$A`4fen52*HeS|JN?orwE^MQ-2{z~nEry-Rfu5vH(7PxT0NJ^W7N^)s0zAjr*E)Eg*Tcj z{6Y4tf~6XeBBSGXAg?FCe0MN)X&# zU8};$3>uMR^R5>Nd$3nKd~Ur1voS^o-4F@W-ULtBVvqe9Ap0QxBG2hsFTjj=pwC|( zXw!T9ES>!nHTQLKBM(e~S(9py7>Fo}jY-vde7Qwfx6qRDw1GP~@GkA{&Qo0MN4W6L zQ<>b-a*++KFY0?ru^UKRt!(b8Re4+U)!!E7nlbx${PZdi{Ci0ngJk+H8Yd7oS7%z4 z*ePfJi96aE;Bzuar z`^K6fOHJlh0`M~bNSo%M{MJ^3=`mmml)En#V*s_uQ0;d8GQnV#LUkr=lIwXYeSUm6 zdZ||poFOdFSLv^&1#gmK9wqVWFk5i$>Hojy?gbH_@m%$PdB48qs>dqoMqVB~0hB$P zX?VdPKd}F_6$lNxVh9p;{d%Zep-is?@=InG`Ynh6LSE{&8_~Jw_z>Ay-og3!L9YPN zMO&LMqUfScvN7kfCSKw0-GBUy_zC%emS`@ea&@M{U`&Ig;vL4<|7D2R{d$F>*NMvc z?~U=ft1SmJ?KV`f1P+Z#uq!=T!u+W`Qr*7TBm@shpG!w%!7kfiiZMpZHf6FSobI;uL>}mHKLB0u7XRMX) zhjjS#*|on-!zf@dMee|Syl4}h_+Dh-94k8#2xj27PLcMaMT=~zuDco11T9KFIf1Ht>`4L}sN#{qN~Qu08KTL=Ye5`xUwKMwDX0ldspUi~j@8yfHPh zm)?yEarFls8!A}hF38*FxuPaH4T^{M)xdCpFSMEFv>v!}K>i91uDmPJcnY9uji5?C z2xc)UP#tj&v#-Rj-2nf2PHoU#69)`d(P}70`?5M1Lm>W)UoVL>DykbvHLs^fYt!V- zlx_jCzO%&}PDsQZ{VY=cu7P#mR>p6*!pP`Sm!kUv1pyi$`UIcw&@cHPrFALjEz)>F zBJEwFDAsM*=M_-<4N0T_$$nlNq&JCSQW5^mV@;^ zd%oj4q1ELoMR{1^fftX7d0IoGd|5pd6)XtSxt<|S(#=dr9X#L-i3+3MIsETysB!&Q zU{z|AK29^(b0ge}ZWvR%$;E$dJmuD;*AfqDur{M%NE!k|R8h{r=pocu1KlYy#_8=K zc<#Eib}_^ZjS{=g(B~-$&Npbrh9rQD5<^+U&tS$<_zG^|8;C$M0@^y~@u(9snJ&FO zDKbXGfLIbW-jb~*!P1#=#2x1ShnFVor6w1zw~XDTO)!+J$CV~mz zU@S1sz!Z>hm#a`at_5hM30w#j7F#5LM{}TnRL=7oJ)%2Z>%K{947mHG^v?a%=D@l} z*)RYkt}2-}`yYs?zK4=W!(b;lNr2;_KoH>!$sm~eyV9=61Opf16ks$QlEJRa>)^OS2#OVV@*Y;E)Peeo^E&4bb8E3)oK+O+Q z520i}SFyX?IZ|&b*`hf3B1^@15q!CFPejz6nVx=am0p-JJ65Nk2{VfK5v&C#$`#-) zBXZmoQlPFY3UtP?J^+hkuU*xS&K;92^f9*6cC>7IZb|kLmhQyMD zhyjSOu_SL@8-mIu4nqGdGT96F%$FD{t<|Xa#tH^nM^QHDqpke^3+L7>C>Q^Ku?pBQ z`$)?m_^i#6I(f8N(_ZZ|xEFAO=zvPs1P5WR8aRD`bj8&x`Tru(4e$%a^`JJy)#x9o zNkX1N@I$^?G+3A1?E|UFg5m1d_V@b*zLlig#}%<;IE)Oi#D1VFTqJ+)FQN&tVlTyx zhTWlM&`t5E>{rd7OAUM=m{myJ>R>z@R8uHPL*3ggO{>R&HwSY0@2YNe$UBE1L{%#b zdEeTA%qCD`@)u2n17&zYPSB> zwZhrDgho63Kk@shdA$@7pKKZqcrM&4F@dexs8M7sq9Mt6=mF0Ig;*_h+IjQi*fQQ26n%DR+9R z_z1Ys=g^lN*k^kps*5GKULJhQXODXZA!u{tc^5d7nj|QI@4ycrK{8rpclxJ zI#|;rT`J^#XYae!lanR>aJ78W1&D{l^^8KroK)GmK;@0&KVkOKV4c)!mB2}xS8@=2 zNoV3q2b!e$4+S-6RVR^eMR`>m=`;uk>qd5uF>Mvs^jMqR1T8@0Z0S@49Af_C?%bxc zs#q@`BeNP*eNaO+UB6+JN3(eqUXh0Ru@nQ8GsHoysx<%be(u8@rzE+pOw$Ia+Ee-J zZQS%m{nz6LjgERlQxv)&anLf{C@r;SJ)AeBBaAxBw(CtaKIG15{8eNEFb^w|#B(5( zozc(#m3X6qbjJtm?TYo(^|dLMJVp(nSL<-2R#cyyu-+60ToFgH3M-TQp;Ae5(hAbb zr_7(bDob!GyUoDJM<4(H^Da(-^1s#amV&5!f6#rKG3@FCBKztBVW9g}> zo3Gb7H_>y7Q~>1^TpmT=oQPi~x?B&m9*JMp$0QF1J_uWvMm$D9(?cC6ZE$77$-H7w z`t%1{IX2#P+`-%EsOP6!{)Ya-)mfFJACHkv&5{Sq=zn?8@nrqeg_Jt~+jt3Xv0yN{ zEuO9t@qaaxUj??`tsaerJS(S>6#~^Zf_n0*I)@?PctcT3=%hYfmb#~Is%2KhGvfry zAnLjwvB}5xKh{Ct`Wouqc2IkLOJH~Sg;?uBaVP3+y8Q8LPtfDN%-5BSbrMiuJi-escikGtdsFw$AQJ$s4++0;42BSOZ7lfd#--53SCR&O)*?=)$I%DA!ZoaL(euHnH_BWx@#*B#b^w7RBp zYM%a=-Y1#AYcx`@5oGn#Hu!Q_3Qo`sXaKzm8vFwb>_R6+TjRN1w1O6Y1g(=Fnym1i zqLt8ZH>|N4Jjc!I`FN)=fc6~%ex0cne44h=#xYuDT5te+iG|R5iTfFELHVO}fl5{L znRQh&XEsi3!;Q9IL)P^qvRa9Z8XXNSy~MvFUCP&YTK-bAu8Kc*g-TR^R_a#T?g51} z>XQ9=$KGkR&NwAoMd}-bMons=td&aPn@>Xpxk2BoKC>$r)^hg=6#U(gRPmNb7;JH2 zMY1ST=5!RG@;mnZ+Fo9y%1@URq?Q35tol(FrKbHGarwqrB<3ml!Bgho1FXiHikI^E zW-VYc(WXnJq$IkncygM&ODEkn7YAJUU$O_p+QPvusDSuiC=-8Dtc4Trf1wE&oNeC!_f z);GsSXoU6MJH3}h=t#XLH*Ay1O(p0kVRFa+{!6Jnb>SsOwFF45wZe`)3{$aCugzB! zwz^+Q&-^>@eo!;?O0A$h74ieQF)@~=y0ijslgET0kmb{jUC{+ImpLjv5t!ddUsN-^ zch8AcW34&l$QJ(a+I6|?5etxfxthPz&*jt@~o+L02Idu zwqEbgA1MLy?w32@fPDDwdKS@|_L)iQBsT#z4_cmM^PZ<99khJw4R}l|Eg)T2PYdc& zczXS94gCJ~RueCm!ieGx zC%&J>RmZ}p;xA}O10XN3j4mZKv|T$vWed;3i`@uNCj|T>IQG>}e!%4GG$6XUc^y?& z=nV0gw54r*YlYWS1;O9!Swu$91x#v2om^tH*kxmqX&%=X> zwKuovy>gmwuAuf>lO||sc|%CLS4>GniA=7bx#~iE0+*NvptVR>CxBjuLX>*L*_$Qt zmrH0;ax&+Kvj_1qlZH!_rA7|Qfyb3sg^GP)PB00*jP&79<2LCs z{dE;X+aVby0G~gh-|ntc)wc92vzyy<*#SZ1gv*>U;)TTvF1UpIqC1$bai;PORWRw& zJs&c8AVp98qSAf0RBLlZW5{S(~%KRN?i=a_aIbH%d3y|9GvgB zmY8LTYy75^8$bsl+@VhAFjS+NA#;8I_=B`ZwLQxK7*T9gTeZe&9rG3OMnPJks}C=& znVhSabxE{%g@OA;ca0)<8%q`H#5Fh;m3R=7cn~%lA500tUd(XP315->>yk0MtGuRb z7(@GTY%@&)9f{14QHT*G-w&@LBjOr9qk00!(;q1rCefe}rXV0@H0d*6QG=y9iT33R zADul=`0=`xL1cH&eQ<+zwOz(KSELCcMy^q13xrzv8-%dOvwG!jcr`w9KSWT{b z)EGVMkC%P~Sxw6^ddApV*ERk4f;jc4U~EC^dQ??t)UEcf5S_P!HpiA`TtNfcR$v8A zK}6C~%9Ui7VcUho*m1wIdQ7j+n0Fp>(VtF{r&bRGo2;}9ALc;mpr+p-<h!bt z=KLh^{a=RwjIRVLY3PCu*vJ*eAz@x3-L=MF<_=Ez>e!EEj21KPz3{(}k;zu^B+XyT zPx0-C8-rO_ufza3H^7TD*Ado%10edtm`s7Y4)U_wufwfQtAk8g6ehZ6YFV2J06x-!wM!oIUs(%As7q>UVg(z#B19B0bAkR)_m{ncz@BCe+z+{|w>Lo#Qop7BD{##2yjPl4?wcegI}9G>eoVG)Kp%k$e-!2X{v`n2Owuy#$N;XOpyuf zb|mKryOyf=(eQpvotyj4fT76+Lw#8j7&^T`Avd_H zYb3|%XsoQmOfv}96nNzhWcHlL6xx6fT=4K(uJS`S$}YdiUS{2 zFFlJi4><&!L%(48S1+YH)~|j(MKq=5I`1uT0cV52OIGtn7hmlgJl&6J!G(%1t3I&f zBM&gSr}OSt&I;dY6X@1t)+ixM0p?X}X1+`5uiB|qKSM$qbjU+O@9`mLG(6WzrK0&c z#OXSv=Z%Cw;n}ym z1t88JNe0bNMU4@sFZT%au)nFCpI^Im(Jo;tm z%@Em@AIv1uP_;N_pD=cEGbCvotNpLeFy>!)eyFwKIYH$JDi{&ziU&V}5tJJZ=6vYZ zi+`b%FRH+=iM(SP7;jUr@?kCE#!187ey8aTsEDu9nD%7^!|jB$czS31A{hHuDem$^ zMCnasM6jA$k863>7NKI*Y!dh{*Gq+sQ&fKc}NsEt%SW zxS2naO(DPTnjDj1Huy``QQ2DiTn+lygmN=Jb@Exc1OgEnMe2ZMIM^_yIXKABP>==S zmni7PEv@t=<_q{s#TNw6vNVwjIgQ-w?xoLzH8l&Y1CiWn5<%@^D@(_{L~9K<5)dk{ z6n2f=h`G(bpEoa@$7y$$Wn0XJrGl$Vr)p}5SO?O%*U{+H?zo$5%93E@;@T4CVXZ>^ zQ^dXz!7XgMjKus)Cqg4vC?;4Yu1d88M3xDR-eu`f!r^ntx3IB>v0I`$!B~YfTpUC0 zZlSc&eXvQLcW!Z;%OX}@G%84FLDlT4agnkFO*S-WkZ5juod51hfV%%Rp_cYp-ja>3 zLG#OL+~QA0a<=Om%J`-3!qp)@f=mPg*_wbr_@LBTO9J4`#MxtAKIO&9;9gSIu!R5JDaJyFgK$FH z%29{fzA_ibw$!!V;UU{+Q4pZ4!DD+RLYW}+qT>JKO8_Q zI`4N*Jqz7)i!I1q zpZ=(}UH@$X+(q+K-1sEs#7myq;{YQ{f3W2z70e9B9co$@9U8Z68ZBJQdXD%>r=L_4 z_x4G1=;P+V8hkltVyG*e8xv#c#S!v|@#Tu9wZ48Oae? z@ng2d)I)nZjN17u$*?r@4xQZn_P@8P-2Fa}0Rgy2MMBDr))HgV-Ss`kAmG)~2DZBU z&w1Yi*V5hXL%`#t-;CR*;S~|9_0PfI&#zAchR3CVi@Mc+W|wg{qGuP}#A4Gq8xqq| zw32xC-TSg;UYyHt)r?M+MK2VoPL@&ZGkV1%XXkq_FPN#7^Q@tzI_mQ!Gu}7L1kziN z6&Z%_hPn1VRwX)$gY3>10)S5fLZ_zOp^{y3LxS!cOv0(N&l@iiAD{G&M8XcyE}#)g z=u$sgBsRBBFI0Gp{EnJ;Pth?yim%`rR%Taq^lWWO+6z#0T8>!op%(iVuHy zI;}qc(T3bzozzaIgXak=oXrg3h=$#8KN8&?l4c8HPy{>QJ|qrFN2)gO|7y6>;qw(* zbH)7n;fazX4g3< zl#5+qNkz7QutEx{@p2@9{SRQ!30W6cXGfQ;q0W$wKezMaq(%ZvnpD~|0C$A4-;ioxCg?x)oxT7CdULNkEQ1%toH4Gzxh z-bL=IEhF!Z*~%JM!Kd)99BMB4=(rW*E>gyB%5R=wpT*M4$Z9oV$=@*^w6Hb(nF`PQ)s(Cq2Sb>3=Bi%BESVD=duNsjLqaqhywJei4(i&%-%GBorX{ANHySGbUO-C}KX{1Co zrLz3A{?tnVm|93Y1iR@HRm3@AP;opwPHjaD>k2Qt_{umlk^TKt+>YA)_!;$LSubg@ z5u>?vqgYbcXUFc8l%-p1$FmfV5}Rb+0Zk|_LiU%OC+eb_*z|(oW3uFZhGQ;OQe1}N zXKZ(m85iiaHx4yi@+dAU*86{47&_TR-!?`Xhgckxym4U?`Qf5te&O_&cF7T4I5551NiT& zEZ>ykl9hb(5Sjlx z;{W`Zw4@ZKZ!pv{$}WC};cTS4cci+j;HoS~*Mk9$ekEnG;82jIL zwAcemAQ|$b8`U4cjp_^u=?8aI9VHShWmK36Icgvo>SN#K+Lv6J(8T{|7Lr1s7-4vc z1b7LdHJ^G9x<77oZ(*~4YI>1IhGsOj!w0mE8UMJkz1i7FeeeIQj&cN+a-=_)3@wlh z&E0F_{*Sj1WuCcA7x`2aHUyu|bwBav&}8t}4khqL{S}T|HwP~Nq>88r;MC>_)gu1T+fjkfw#|Nj zyP=dCszMDcBh6H6?IPOKt1gNmz^Zd$tvwNcN}+mMBXi6clrf*GP!p8(C>BorouIsj z66Wep?8Gd&(iyLSI;u7J*C6CQuJQpQSQtER&!e!D2HP9{vLpMv%V<^1@BimOuxqol z*rs)4#D-)vHg+!nZkURrSy46X0QP%Y0@HY`_qH-HJI;_Zk%xh6ngdp{%kZfASrURz zpnJox0cQfu>~WY~*-^%N03Dm1JdSQt~S89Y!b$HWfP zP287{n8VcSFxjk&L+CR)7-?UOY0o0+);-!isd;(y$X>O{?rvG$S59wLP65Zbh_3Ut z{9_JOyUe~W#~`fe{v?C(c`mbWei%a``?>C(qbO#i&22*>Z}=`DTUb?H>w(@(c2qAR zkcpUyXwGGJKPOh9$S;LOQjgQC*bG-@hB~4acTky%r7EMEoNI33cq!pWv!iDb@KyLr z?1O+IdG&W;GM%we_9Lo@?zyj1GG5GNP2C9qS*oJJ`BNGfgq=qqrphbQAZN`UW#(a92-hC=|I9y7+O2i5AIQdleNH!1U2Liu?D1?Td zf1+LSN2$Hnn7bE5WTtHOA3)%*hwxvCSs(P?hg*;Zvf?UQ+xzMF#V%>vHM6kK(|fG~ zVhuxy6@t8u%QlGB&73|Mk^#5AnbdZ5qX)ccFNlj_P}HN7KuVPTtJZluBZ7amkkb-dxWi!$<7hL6#wdw% zm(rRYU0${59d(hu;WZqo`{AouuT`l5C!+gjv*Xb?0`RWH3UT+$IO+SmhWPYx?j40YoHc1B= zBu!jYkm)iBvH1%sC6B>yios-G!P}4R%`6~!;n+#|RQXZK(_>X+2aUVzDQ-*vwD>K1 zSg(j|TG=W?f8gGviP(J@r97$DC(s)SnJF!~5^jc41Yjl_&dh)Eq4fzq*+^e_)glevx(C_^X#IB>m3bu1f^h$!W-Muq)$1 zR@c3$A>w$dHnp%>^T9?OL${)t&c0`tMc*oYUj+My2z~y3rk)}AoKA=iZ9d4L^@-Pz z`a~Oe8D1Mu8s4ZI)~oKz!c__0!K?SYy4jAuuoI)|V_b+c9?6@b|>TJDpSg zklac8gp(7Jti-*w)ni?4YJmnEiLTdw^}?hX@fYw9O5_OFdZYe+(o5re?>Vt~?Q#DC z(5{6+MJd4;i99*0Z`>$Z8+9c)8(gvu-UGivS20Kfy$1yRy=f;__DQH|-!0J+v2h8o zbYr+~e;??NcWJ?QiJTciSfppx4aeYL*N&oaY*J)Jcv#k8j@-?cC<%J-asfzY&@-J8 zM==vs;on>OZt;nNb^k@g6p(K#;$kWQUt#gHxOKpC2Y*}R6t`(NPC=ig0C|ra>_*6S z<47#6aw=GAb9_=QoN}d3JUfx$ZQ&V0x`26y4lG*Q%hJjwg^MXG_AVxiL=0YYrI&#N z^4YaOc=*QAzQhCuTQF-*W?&fl%>Z0egZLVD%MIkV(*s!h^#)yLMmr4yXKP_zLeyW>)dXb%~sXu zRW9?vI1h{U%99bQj}?_B&&)n?m8`}Sn>Z>}*qVqr>|fMVw8kR&blcR~=bX}+$g)NyLp38W5Vi5bFno5#`+SRviT&sglSF{lEuFU5(66FI+&vtq6 zg&jtIde%(Z$7N~g16~v07gSjcJknpuFU=$Da7Hm1`1rrtylkHw#AGNyA(-ryb^bu5 zr{)ru-M8N94^>C~Bw-HL#dCzw;i>2E3O|hQ%{8Sh_nq`V!ru#c&~VCfZ5r5p+a{cL zl0vX9aM@zn14}A6#N6R zAw<=QMH}Z3JwT@wW}CY6B|EGXVM^0BJ~9Y6=??CAb$L$G#w8V8ud8cv)>HbS|23O+ z)7$4JfKfn%m4k_$+*8q44sVtdnJ%Wt!y|S=hdzlm{R4WFK=`&m%f{G!51(9-Ig(qk zCfUAn?1`T2sBx-&?O&XIk=HEzP6p1&eUhoG~S@=g}>iaW5N6NRWQ}>gXF3O z*2&s;cl?ZX)nNS`RkUU@cNk?5T~@7Hz@q-7|8iJf_AL{}$ecc>>n2Hg10pl&x=AhG zwtC7)LeNN)>&&;K42*p4_M-#XZ-vr85T@S{+P z?qK+i*-ITY(kFP$2+&xXcw9lJ=(-IM39E2FEuu1*@KX?T>#(g5%zw?xOBE_EJUhBE z+Sn7>IS5|!jhK~LBp|3`$vvFs?~^AujB=@!3fuWCzOc8uhM(ha+)AoFw^uxc^#mbQQ?!}hM>5AvxHRV8_E#nOv zK#wpmYb0>W*+z09w&r(ZN}*JP#-bSOR}{W#Q$U)keiX&y-j$BJxF{fI0)IKHGFU5}QZHn%_AlCI8zd(cu6 z>&nrQ>0VJG$-eo~JnsM+RnJrK(FlIrMu`BVi*hn`B3`9!%XJX?9EN9$lGqr0N}CEe$11XB2zdKsII8H3?(OFxF-Qh?tk`JGammai1IE zr?^(sT!5n6DM^E0C8K?$9K~e_dJVdbY(86 zlJk_bW<1)iv(sV9eXiWLM?bC{XD>@Dx-<1;VZtZY^;(V1K6#s(tZPoO8UNcX5GQv0 z&EFcMgSX#)yY-;%!ars2$L&6)x<^GKE=Ds>ez^NyZ^6c^hyGhIOpC4D)%(6>|6h5? z7V_FiX(ra1yH!<@o99NWGuHbz8;Bge@B4>uiT^B)r3Q1e8&v<@Jp5=;r$leQZ}s+v z3npyq`u$DphJf+cjkeGKRh{!){4#x_wfusTuu~F(=iT`{;<|j+C4HH4?vD49jKfP3 zDm4||qAz63l96}avuIDqhXtp&rkr%Jo~B~@G_>d1u@I5uv)2O*-bS`>d9*2nwchyK zim)9?HHX6*nD6gDz$&3OBXI}gu26p$>m?6tH&umKatp{f^Rap?HvGu(@0(OW>%Y zdcAY~w#~Bm*}b*q-^Jz?8>N3TImGO~`Rts`pv@b)C{*xHaczJS2k^^s$EdxVEj(%}TWln1G zWW_`gY1G*Q;O1#KNS%Bk$&qP(kOs#kaJzLi69a=F YTnC7;Yx0g{J4Ua`-;&!vFvP diff --git a/queryfunctions.py b/queryfunctions.py index c0412e6..1a6af9e 100644 --- a/queryfunctions.py +++ b/queryfunctions.py @@ -1,5 +1,9 @@ from macros import expandmacros -from treebankfunctions import get_left_siblings, getattval, parent +from treebankfunctions import get_left_siblings, getattval, parent, find1, adjacent + +from typing import Callable, List +from sastatypes import SynTree + nietxpath = './/node[@lemma="niet"]' wordxpath = './/node[@pt]' @@ -12,6 +16,16 @@ #vzn4xpath = expandmacros(vzn4basexpath) +voslashbijxpath = expandmacros(""".//node[node[@pt="vz" and @rel="hd"] and + node[@rel="obj1" and + ((@index and not(@word or @cat)) or + (%Rpronoun%) + )]]""") +vobijxpath = expandmacros('.//node[%Vobij%]') + +notadjacent = lambda n1, n2, t: not adjacent(n1, n2, t) + + def xneg(stree): nodepairs = [] nietnodes = stree.xpath(nietxpath) @@ -53,3 +67,31 @@ def VzN(stree): results += stree.xpath(vzn3xpath) #results += stree.xpath(vzn4xpath) # does not belong here after all, these will be scored under Vo/Bij return results + +def auxvobij(stree: SynTree, pred: Callable[[SynTree, SynTree, SynTree], bool]): + RPnodes = stree.xpath(voslashbijxpath) + results = [] + for RPnode in RPnodes: + # find the head node + headnode = find1(RPnode, 'node[@rel="hd"]') + + # find the obj1node + obj1node = find1(RPnode, 'node[@rel="obj1"]') + + # check if they are adjacent + if headnode is not None and obj1node is not None: + if pred(headnode, obj1node, stree): + results.append(RPnode) + return results + + +def vobij(stree: SynTree) -> List[SynTree]: + results1 = stree.xpath(vobijxpath) + results2 = auxvobij(stree, adjacent) + results = results1 + results2 + return results + + +def voslashbij(stree: SynTree) -> List[SynTree]: + results = auxvobij(stree, notadjacent) + return results diff --git a/sva.py b/sva.py index e5a3d5b..bd08ab5 100644 --- a/sva.py +++ b/sva.py @@ -17,23 +17,34 @@ nominalpts = ['n', 'vnw', 'tw'] nominalisablepts = ['adj', 'ww'] +#: The constant *normalsentencexpath* defines a query for a finite verb form +#: with *smain* or *sv1* as parent node that is the only phrase under the top node, +#: possibly accompanied by punctuation signs. normalsentencexpath = """.//node[@pt="ww" and @wvorm="pv" and parent::node[(@cat="smain" or @cat="sv1") and parent::node[@cat="top" and count(node[@cat or @pt!="let"])=1]]]""" +#: The constant *normalwhqsentencexpath* defines a query for a finite verb form with as parent a node with +#: *cat*=*sv1* which has a parent node with *cat*=*whq* that is the only phrase under the top node, +#: possibly accompanied by punctuation signs. normalwhqsentencexpath = """ //node[@pt="ww" and @wvorm="pv" and parent::node[( @cat="sv1") and parent::node[@cat="whq" and parent::node[@cat="top" and count(node[@cat or @pt!="let"])=1]]]]""" - +#: The constant *abnormalobj2sentencexpath* defines a query for a finite verb of which the parent contains +#: as indirect object (*obj2*) a pronoun or the word *zij*. (*zij* can also be analysed, usually incorrectly +#: in the SASTA context, as the subjunctive form of the verb *zijn*). +#: example sentence to which this applies: *mij lukt het ook* abnormalobj2sentencexpath = """.//node[@pt="ww" and @wvorm="pv" and parent::node[node[@rel="obj2" and (@pt="vnw" or @word="zij")]]] """ - +#: The constant *abnormalobj2xpath* searches for nodes that are an indirect object and either a pronoun or the word "zij" +#: and that have a sibling finite verb. @I do not understand this anymore@ abnormalobj2xpath = """.//node[@rel="obj2" and (@pt="vnw" or @word="zij") and parent::node[node[@pt="ww" and @wvorm="pv"]]]""" +#: The constant *subjxpath* defines a query to search for a subject that is a sibling of a finite verb. subjxpath = """.//node[@rel="su" and parent::node[node[@pt="ww" and @wvorm="pv"]]]""" zijsgnodestringtemplate = """ @@ -253,7 +264,27 @@ def getpotsubjs(tree): return results -def getpvs(tokensmd, tree, uttid): +def getpvs(tokensmd: TokenListMD, tree: SynTree, uttid: UttId) -> List[SynTree]: + """ + The function *getpvs* searches for finite verbs and potentially finite verbs in the structure + + * It first looks for words that are explicitly marked as finite verbs (*@pt="ww" and @wvorm="pv"*) + except for the words *ja*, *zij* and *kijk*. + * If none has been found, it identifies infinitrves as potential finite verbs (infinitives in Dutch are always + homophonous to present tense plural finite verbs). However, such an infinitive should not be preceded by explicit + infinitive markers such as *te* or *aan het* (as determined by the functions *ahi* and *ti*): + + * .. autofunction:: sva::ahi + + * .. autofunction:: sva::ti + + If multiple infinitives are found, arbitrarily the first one is selected. + + **Remark** We should actually select the first one that is not preceded by *te* or *aan het*. + + It the seelcted node is not immediately preceded by *te* or *aan het* + @@hier verder@@ + """ if tree is None: return [] else: @@ -264,8 +295,8 @@ def getpvs(tokensmd, tree, uttid): if pvs != []: thepv = pvs[0] if not ahi(thepv, tree) and not ti(thepv, tree): - rb = rbrother(thepv, tree) - lb = lbrother(thepv, tree) + rb = rbrother(thepv, tree) # use infl_rbrother instead for inflated trees + lb = lbrother(thepv, tree) # use infl_lbrother instead for inflated trees lblemma = getattval(lb, 'lemma') lbrel = getattval(lb, 'rel') if nominal(rb) or getattval(rb, 'pt') == 'lid' or (lblemma in ['je', 'het'] and lbrel == 'det'): @@ -336,7 +367,17 @@ def zijnimperativeok(vnode): return result -def ahi(node, tree): +def ahi(node: SynTree, tree: SynTree) -> bool: + ''' + The function *ahi* checks whether a given node *node* is immediately preceded by the words *aan* and *het* in + syntactic structure *tree*. + + **Remark** The function currently works for standard syntactic structures, not for inflated + syntactic structures. + :param node: the node for which it is checked whether it is immediately preceded by *aan* and *het* + :param tree: the syntactic structure that contains the node *node*. + :return: True if *aan het* immediately precedes *node*, False otherwise + ''' nodepos = int(getattval(node, 'end')) p1end = str(nodepos - 2) p2end = str(nodepos - 1) @@ -346,7 +387,18 @@ def ahi(node, tree): return result -def ti(node, tree): +def ti(node: SynTree, tree: SynTree) -> bool: + ''' + The function *ti* checks whether a given node *node* is immediately preceded by the word *te* in + syntactic structure *tree* + + **Remark** The function currently works for standard syntactic structures, not for inflated + syntactic structures. + + :param node: the node for which it is checked whether it is immediately preceded by *te* + :param tree: the syntactic structure that contains the node *node*. + :return: True if *te* immediately precedes *node*, False otherwise + ''' nodepos = int(getattval(node, 'end')) p1end = str(nodepos - 1) p1 = find1(tree, '//node[@pt and @end={} and @lemma="te"]'.format(p1end)) @@ -382,18 +434,48 @@ def getsvacorrectedutt(snode, thepv, tokens, metadata): def getsvacorrections(tokensmd: TokenListMD, rawtree: SynTree, uttid: UttId) -> List[TokenListMD]: ''' - :param tokensmd: the inpu sequencxe of tokens plus metadata + :param tokensmd: the input sequence of tokens plus metadata :param rawtree: the syntacti structuire of the original utterance :param uttid: the identifier of the utterance :return: a list of pairs (token sequence , metadata) The function *getsvacorrections* generates alternative token sequences plus associated metadata for an input token - sequence with associated metadata for an utternace with id *uttid* and (inflated) syntactic structure *rawtree*. + sequence with associated metadata for an utterance with id *uttid* and (inflated) syntactic structure *rawtree*. The function performs the following steps: * It transforms all index nodes in the syntactic structure to nodes of their antecedents (except for the *rel* attribute) by means of the function *indextransform* from the module *treebankfunctons*. See :ref:`indexnodeexpansion`. * It looks up all finite verb forms in the structure by means of the function *getpvs* + * ..autofunction:: sva::getpvs + + * It only deals with structures that contain at most one finite verb + * Next, it searches for the subject or for potential subjects in the structure. How it searches depend on the structure of the sentence. + * normal sentences that match the Xpath expression *normalsentencexpath*: + + .. autodata:: sva::normalsentencexpath + + In this case the query *subjxpath* is used to search for the subject(s). + Of the subjects found (if any), the first one is selected as the subject. + Normally, there is only one subject in a clause. + + * abnormalobj2 sentences that match the Xpath expression *abnormalobj2sentencexpath*: + + .. autodata:: sva::abnormalobj2sentencexpath + + In this case, potential subjects are searched for, as described below. + + * normal whquestion sentences that match the Xpath expression *normalwhqsentencexpath*: + + .. autodata:: sva::normalwhqsentencexpath + + In this case the query *subjxpath* is used to search for the subject(s). + Of the subjects found (if any), the first one is selected as the subject. + Normally, there is only one subject in a clause. + + The query *subjxpath* is defined as follows: + + .. autodata:: sva::subjxpath + ''' debug = False diff --git a/test_adjacency.py b/test_adjacency.py new file mode 100644 index 0000000..e1b2d15 --- /dev/null +++ b/test_adjacency.py @@ -0,0 +1,167 @@ +from lxml import etree +from treebankfunctions import adjacent, find1, getnodeyield, immediately_precedes, immediately_follows + +streestrings = {} +streestrings[1] = ''' + + + + + + + + + + + + + + + en uhm en uhm hij hij is nogal + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + +streestrings[2] = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ik heb een ik heb een ik heb een man met wie ik wil gaan trouwen uhm + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + +strees = {} +for el in streestrings: + strees[el] = etree.fromstring(streestrings[el]) + +def getnode(stree, word): + result = find1(stree, f'//node[@word="{word}"]') + return result + +# is nogal + +thetree = strees[1] +node1 = getnode(thetree, 'is') +node2 = getnode(thetree, 'nogal') +result = adjacent(node1, node2, thetree) +assert result == True + +thetree = strees[2] +node1 = getnode(thetree, 'gaan') +node2 = getnode(thetree, 'trouwen') +result = adjacent(node1, node2, thetree) +assert result == True + +thetree = strees[2] +node1 = getnode(thetree, 'wil') +node2 = getnode(thetree, 'gaan') +result = adjacent(node1, node2, thetree) +assert result == True + +thetree = strees[2] +node1 = getnode(thetree, 'wil') +node2 = getnode(thetree, 'trouwen') +result = adjacent(node1, node2, thetree) +assert result == False + +thetree = strees[2] +yieldnodes = getnodeyield(thetree) +for i, n in enumerate(yieldnodes): + if i > 0: + assert adjacent(yieldnodes[i-1], n, thetree) + assert immediately_precedes(yieldnodes[i-1], n, thetree) + assert immediately_follows(n, yieldnodes[i - 1], thetree) + if i < len(yieldnodes) - 1: + assert adjacent(n, yieldnodes[i+1], thetree) + assert immediately_precedes(n, yieldnodes[i+1], thetree) + assert immediately_follows(yieldnodes[i+1], n, thetree) \ No newline at end of file diff --git a/test_vobij.py b/test_vobij.py new file mode 100644 index 0000000..0d00ede --- /dev/null +++ b/test_vobij.py @@ -0,0 +1,175 @@ +from lxml import etree + +from queryfunctions import vobij, voslashbij + + + +# find the cases written separately + + +streestrings = {} +streestrings[1] = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + allemaal varkens zit erin . + +""" +streestrings[2] = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + die kan der op weer . +""" +streestrings[3] = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + daar heb wij ook een boekje van + Q#ng1664379677|daar hebben wij ook een boekje van|1|1|-3.187513080109999 + + + +""" + +strees = {i: etree.fromstring(ts) for i, ts in streestrings.items() } + +# reference dictionary contains tuples with (1) VoBij reference results; (2) Voslashbij reference results +reference = {} +reference[1] = (['40'], []) +reference[2] = (['30'], []) +reference[3] = ([], ['30']) + +def test(): + for i, stree in strees.items(): + results = [n.attrib['begin'] for n in vobij(stree)] + try: + assert results == reference[i][0] + except AssertionError as e: + print(f'Vobij: i={i}, reference={reference[i][0]}, =/= results={results}') + raise AssertionError + + results = [n.attrib['begin'] for n in voslashbij(stree)] + try: + assert results == reference[i][1] + except AssertionError as e: + print(f'Vo/bij: i={i}, reference={reference[i][1]}, =/= results={results}') + raise AssertionError + + +def main(): + # define tests + test() + + +if __name__ == '__main__': + main() diff --git a/treebankfunctions.py b/treebankfunctions.py index 73450c6..2545888 100644 --- a/treebankfunctions.py +++ b/treebankfunctions.py @@ -104,6 +104,56 @@ def md2XMLElement(self): countcompoundxpath = 'count(.//node[contains(@lemma, "_")])' +def adjacent(node1: SynTree, node2: SynTree, stree: SynTree) -> bool: + ''' + :param node1: + :param node2: + :param stree: syntactic structure containing *node1* and *node2* + :return: True if *node1* is adjacent to *node2* in *stree*, False otherwise + + The function *adjacent* determines whether *node1* is adjacent to *node1* in syntactic structure *stree*, + and it works correctly in inflated syntactic structures. The two nodes must be nodes for words. + ''' + yieldnodes = getnodeyield(stree) + for i, n in enumerate(yieldnodes): + if yieldnodes[i] == node1: + prec = yieldnodes[i-1] if i > 0 else None + succ = yieldnodes[i+1] if i < len(yieldnodes) - 1 else None + result = prec == node2 or succ == node2 + return result + return False + + +def immediately_precedes(node1: SynTree, node2: SynTree, stree: SynTree) -> bool: + ''' + :param node1: + :param node2: + :param stree: syntactic structure containing *node1* and *node2* + :return: True if *node1* immediately precedes *node2* in *stree*, False otherwise + + The function *immediately_precedes* determines whether *node1* immediately precedes *node1* in syntactic structure *stree*, + and it works correctly in inflated syntactic structures. The two nodes must be nodes for words. + ''' + yieldnodes = getnodeyield(stree) + for i, n in enumerate(yieldnodes): + if yieldnodes[i] == node1: + succ = yieldnodes[i+1] if i < len(yieldnodes) - 1 else None + result = succ == node2 + return result + return False + +def immediately_follows(node1: SynTree, node2: SynTree, stree: SynTree) -> bool: + ''' + :param node1: + :param node2: + :param stree: syntactic structure containing *node1* and *node2* + :return: True if *node1* immediately follows *node2* in *stree*, False otherwise + + The function *immediately_follows* determines whether *node1* immediately follows *node1* in syntactic structure *stree*, + and it works correctly in inflated syntactic structures. The two nodes must be nodes for words. + ''' + return immediately_precedes(node2, node1, stree) + def countav(stree: SynTree, att: str, val: str) -> int: countattvalxpath = countattvalxpathtemplate.format(att=att, val=val) result = stree.xpath(countattvalxpath) @@ -1467,19 +1517,43 @@ def getspan(node: SynTree) -> Span: return nodespan -def lbrother(node: SynTree, tree: SynTree) -> SynTree: +def lbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: nodebegin = getattval(node, 'begin') condition = lambda n: getattval(n, 'end') == nodebegin result = findfirstnode(tree, condition) return result -def rbrother(node: SynTree, tree: SynTree) -> SynTree: +def rbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: nodeend = getattval(node, 'end') condition = lambda n: getattval(n, 'begin') == nodeend result = findfirstnode(tree, condition) return result +def infl_lbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: + ''' + :param node: the node for the relevant word + :param tree: the syntactic structure that contains *node* + :return: The function *infl_lbrother* returns the node for the word that immediately precedes the word for *node* if there is one, otherwise None + ''' + nodeyield = getnodeyield(tree) + for i, n in enumerate(nodeyield): + if nodeyield[i] == n and i > 0: + return nodeyield[i-1] + return None + +def infl_rbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: + ''' + :param node: the node for the relevant word + :param tree: the syntactic structure that contains *node* + :return: The function *infl_lbrother* returns the node for the word that immediately follows the word for *node* if there is one, otherwise None + ''' + nodeyield = getnodeyield(tree) + for i, n in enumerate(nodeyield): + if nodeyield[i] == n and i < len(nodeyield) - 1: + return nodeyield[i+1] + return None + def findfirstnode(tree: SynTree, condition: Callable[[SynTree], bool]) -> SynTree: if condition(tree): From 8cdb98f3f75af37b6c8bbd65930502c4391a4ad2 Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Mon, 3 Oct 2022 13:17:42 +0200 Subject: [PATCH 04/11] Updated Vobij+Vo/bij and associated Documentation --- Documentation/Tarsp.rst | 36 +++++++----------------- macros/sastamacros1.txt | 2 +- queryfunctions.py | 62 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/Documentation/Tarsp.rst b/Documentation/Tarsp.rst index 870dba5..23da796 100644 --- a/Documentation/Tarsp.rst +++ b/Documentation/Tarsp.rst @@ -3488,28 +3488,16 @@ T106: Vo/bij * **Original**: yes * **In form**: yes * **Page**: 71 -* **Implementation**: Xpath with macros +* **Implementation**: Python function * **Query** defined as:: - //node[node[@pt="vz" and @rel="hd"] and - node[@rel="obj1" and - ((@index and not(@pt or @cat)) or - (@end < ../node[@rel="hd"]/@begin) - )]] - - -The query searches for a node - -* containing a head adposition node, and -* containing a node with grammatical relation *obj1* - - * which is an "empty" node - * and which precedes the head adposition + voslashbij + +.. autofunction:: queryfunctions::voslashbij * **Schlichting**: "Voornaamwoordelijk bijwoord, gesplitst. Het gesplitste voornaamwoordelijk bijwoord behoeft niet gescoord te worden bij Vobij, kolom Voornaamwoorden in Fase IV, alleen hier bij de Woordgroepen in Fase V." -* **Remark** Schlichting only gives examples with nonadjacent Rpronouns and adpositions. We agreed with Rob Zwitserlood that adjacent R-pronoun + adposition, even if written separately, is to be counted under *Vobij*. However, the notion "adjacent" is not so easy to define in inflated tree structure. This still has to be done. -* **Remark** The condition *@index and not(@pt or @cat)* is better replaced by a macro defined as *@index and not(@word or @cat)*. +* **Remark** Schlichting only gives examples with nonadjacent Rpronouns and adpositions. We agreed with Rob Zwitserlood that adjacent R-pronoun + adposition, even if written separately, is to be counted under *Vobij*. However, the notion "adjacent" is not so easy to define in inflated tree structures. This has been done in a python function *adjacent* in the module treebankfunctions, and for this reason this query must also be defined as a python function. @@ -3525,24 +3513,20 @@ T107: Vobij * **Original**: yes * **In form**: yes * **Page**: 80 -* **Implementation**: Xpath with macros +* **Implementation**: Python function * **Query** defined as:: - //node[%Vobij%] + vobij + +.. autofunction:: queryfunctions::vobij -the macro **Vobij** is defined as follows:: - - Vobij = """(@pt="bw" and (contains(@frame,"er_adverb" ) or contains(@frame, "tmp_adverb") or @lemma="daarom") and - @lemma!="er" and @lemma!="daar" and @lemma!="hier" and - (starts-with(@lemma, 'er') or starts-with(@lemma, 'daar') or starts-with(@lemma, 'hier')) - )""" * **Schlichting**: "Voornaamwoordelijk bijwoord. Het voornaamwoordelijk bijwoord is een combinatie van 'er', 'daar', 'hier', 'waar' met een voorzetsel (bijv. 'aan', 'bij', 'voor') of een bijwoord ('af', 'heen', 'toe')" +* **Remark** Separately wiritten but adjacen R-pronoun + adposition cases are also considered *Vobij* * **Remark** Alpino considers words such as 'af', 'heen', and 'toe' as postpositions, not as adverbs. -* **Remark** *waar* is lacking. It does not occur in the VKLtarsp data, in the Auris data only *waarom* occurs. diff --git a/macros/sastamacros1.txt b/macros/sastamacros1.txt index 9fa2054..8ed6ec5 100644 --- a/macros/sastamacros1.txt +++ b/macros/sastamacros1.txt @@ -512,7 +512,7 @@ robustdelpv = """(not(@rel="dp" and @begin > ancestor::node[@cat="top"]/descenda delpv = """(%coredelpv% and %robustdelpv%)""" Vobij = """(@pt="bw" and (contains(@frame,"er_adverb" ) or contains(@frame, "tmp_adverb") or @lemma="daarom") and -@lemma!="er" and @lemma!="daar" and @lemma!="hier" and (starts-with(@lemma, 'er') or starts-with(@lemma, 'daar') or starts-with(@lemma, 'hier')))""" +@lemma!="er" and @lemma!="daar" and @lemma!="hier" and @lemma!="waar" and (starts-with(@lemma, 'er') or starts-with(@lemma, 'daar') or starts-with(@lemma, 'hier') or starts-with(@lemma, 'waar')))""" Tarsp_VzN = """(%vzn1xpath% or %vzn2xpath% ) """ diff --git a/queryfunctions.py b/queryfunctions.py index 1a6af9e..a086adb 100644 --- a/queryfunctions.py +++ b/queryfunctions.py @@ -15,12 +15,28 @@ #vzn4basexpath = './/node[node[@pt="vz" and @rel="hd" and ../node[%Rpronoun% and @rel="obj1" and @end <= ../node[@rel="hd"]/@begin]]]' #vzn4xpath = expandmacros(vzn4basexpath) - +#: The constant *voslahbijxpath* selects nodes (PPs) that contain an adposition and an R-pronoun or a index node +#: coindexed with an R-pronoun. +#: +#: **Remark** It is not actually checked whether the indexed node has an R-pronoun as its antecedent +#: +#: **Remark** We may have to do something special for *pobj1* +#: voslashbijxpath = expandmacros(""".//node[node[@pt="vz" and @rel="hd"] and node[@rel="obj1" and ((@index and not(@word or @cat)) or (%Rpronoun%) )]]""") + +#: The constant *vobijxpath* uses the macro *Vobij* to identify adverbial pronouns. +#: The macro **Vobij** is defined as follows:: +#: +#: Vobij = """(@pt="bw" and (contains(@frame,"er_adverb" ) or contains(@frame, "tmp_adverb") or @lemma="daarom") and +#: @lemma!="er" and @lemma!="daar" and @lemma!="hier" and @lemma!="waar" and +#: (starts-with(@lemma, 'er') or starts-with(@lemma, 'daar') or +# starts-with(@lemma, 'hier') or starts-with(@lemma, 'waar')) +#: )""" +#: vobijxpath = expandmacros('.//node[%Vobij%]') notadjacent = lambda n1, n2, t: not adjacent(n1, n2, t) @@ -68,7 +84,21 @@ def VzN(stree): #results += stree.xpath(vzn4xpath) # does not belong here after all, these will be scored under Vo/Bij return results -def auxvobij(stree: SynTree, pred: Callable[[SynTree, SynTree, SynTree], bool]): +def auxvobij(stree: SynTree, pred: Callable[[SynTree, SynTree, SynTree], bool]) -> List[SynTree]: + ''' + + :param stree: the syntactic structure to be analysed + :param pred: a predicate that the results found must satisfy + :return: a list of matching nodes + + The function *auxvobij* finds nodes that are found by the *voslashbijxpath* and selects from these those + that satisfy the predicate *pred*. It is used to distinguish cases of R-pronoun + adposition that are *adjacent* + (which should be analysed as TARSP *Vobij*) from those that are not adjacent (which should be analysed as TARSP + Vo/Bij). + + .. autodata:: queryfunctions::voslashbijxpath + + ''' RPnodes = stree.xpath(voslashbijxpath) results = [] for RPnode in RPnodes: @@ -86,6 +116,22 @@ def auxvobij(stree: SynTree, pred: Callable[[SynTree, SynTree, SynTree], bool]): def vobij(stree: SynTree) -> List[SynTree]: + ''' + + :param stree: syntactic structure to be analysed + :return: List of matching nodes + + The function *vobij* uses the Xpath expression *vobijxpath* and the function *auxvobij* to obtain its resulting nodes: + + * The *vobijxpath* expression matches with so-called adverbial pronouns: + + .. autodata:: queryfunctions::vobijxpath + + * The function *auxvobij* finds adjacent R-pronoun + adposition cases: + + .. autofunction:: queryfunctions::auxvobij + + ''' results1 = stree.xpath(vobijxpath) results2 = auxvobij(stree, adjacent) results = results1 + results2 @@ -93,5 +139,17 @@ def vobij(stree: SynTree) -> List[SynTree]: def voslashbij(stree: SynTree) -> List[SynTree]: + ''' + + :param stree: syntactic structuire to be analysed + :return: List of matching nodes + + The function *voslashbij* uses the function *auxvobij* to find non-adjacent R-pronoun + adposition cases: + + .. autofunction:: queryfunctions::auxvobij + :noindex: + + + ''' results = auxvobij(stree, notadjacent) return results From a5c2f8864e978f569c3dd58d0c4d8abfb3e0c283 Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Mon, 10 Oct 2022 15:32:50 +0200 Subject: [PATCH 05/11] Updated disambiguation replacements: added condition, condition in scorefunction earlier in the tuple. --- basicreplacements.py | 47 +++++++++++++++++--------- corrector.py | 18 +++++++--- correcttreebank.py | 2 +- macros/sastamacros1.txt | 2 +- sastatypes.py | 2 ++ test_indexexpansion.py | 74 +++++++++++++++++++++++++++++++++++++++++ treebankfunctions.py | 10 ++++-- 7 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 test_indexexpansion.py diff --git a/basicreplacements.py b/basicreplacements.py index e8637b4..24a1279 100644 --- a/basicreplacements.py +++ b/basicreplacements.py @@ -2,7 +2,9 @@ from metadata import bpl_word, bpl_node, defaultpenalty from deregularise import correctinflection from typing import Dict, List, Tuple -from sastatypes import ReplacementMode +from sastatypes import ReplacementMode, TokenTreePredicate, SynTree +from sastatoken import Token +from treebankfunctions import find1 BasicExpansion = Tuple[str, List[str], str, str, str, int] BasicReplacement = Tuple[str, str, str, str, str, int] @@ -230,8 +232,8 @@ ("dat's", ["dat", "is"], pron, infpron, contract, dp), ("datte", ['dat', 'ie'], pron, infpron, contract, dp+2), ("omdatte", ['omdat', 'ie'], pron, infpron, contract, dp+2) - ] + \ - closesyllshortprepexpansions + ] + # + closesyllshortprepexpansions # put off does not lead to improvement # + innuclosedsyllshortprepexpansions # put off does not lead to improvement @@ -270,6 +272,20 @@ def getmeta4CHATreplacements(wrongword: str, correctword: str) -> KnownReplaceme bpl_word) return result +#: dttp = default token tree predicate +dtp = lambda token, tree: True + +def welnietttp(token: Token, stree: SynTree) -> bool: + """ + The function *welniettp* checks whether *token* has been analysed as a verb in *stree* + :param token: input token + :param stree: input syntactic structure + :return: True if *token* has been analysed as a verb in *stree*, False otherwise. + """ + beginval = str(token.pos) + wordnode = find1(stree, f'.//node[@pt="ww" and @begin="{beginval}"]') + result = wordnode is not None + return result # keer removed #: The constant *disambiguation_replacements* contains a list of tuples. Each tuple @@ -283,12 +299,13 @@ def getmeta4CHATreplacements(wrongword: str, correctword: str) -> KnownReplaceme #: * plural nouns that can also be a verb (e.g. *planten*) are replaced by the word *teilen*, which is only a noun; #: * adjectives without an *-e* ending that can also be a verb (e.g. *dicht*) are replaced by the word *mooi*, which is only an adjective; #: * adjectives with an *-e* ending that can also be a verb (*e.g. witte*) are replaced by the word *mooie*, which is only an adjective. +#: * the words *wel* and *niet* are replaced by a nonambiguous adverb (*ietsjes*) if they are parsed as a verb in the original parse #: #: **Remark** Currently no distinction is made between singular *count* and singular *mass* nouns: they are both replaced by the same word. This may have to be adapted. -disambiguation_replacements: List[Tuple[List[str], str]] = \ - [(['huis', 'water', 'paard', 'werk', 'stuur', 'feest', 'snoep', 'geluid', +disambiguation_replacements: List[Tuple[TokenTreePredicate, List[str], str]] = \ + [(dtp, ['huis', 'water', 'paard', 'werk', 'stuur', 'feest', 'snoep', 'geluid', 'kwartet', 'kruis'], 'gas'), - (['toren', 'fiets', 'puzzel', 'boom', 'vis', 'melk', 'zon', 'pot', 'klok', + (dtp, ['toren', 'fiets', 'puzzel', 'boom', 'vis', 'melk', 'zon', 'pot', 'klok', 'school', 'boer', 'lepel', 'jas', 'tuin', 'fles', 'lucht', 'emmer', 'maan', 'kachel', 'kwak', 'verf', 'hop', 'kam', 'spiegel', 'klap', 'stal', 'lijm', 'lift', 'kat', 'wagen', 'schep', 'kus', 'wind', 'borstel', 'duim', 'strik', 'klik', 'pleister', @@ -296,27 +313,27 @@ def getmeta4CHATreplacements(wrongword: str, correctword: str) -> KnownReplaceme 'punt', 'post', 'gom', 'tap', 'kraanwagen', 'drup', 'wieg', 'kriebel', 'pit', 'zaag', 'slof', 'deuk', 'hark', 'jeuk', 'stift', 'aard', 'hamster', 'kiek', 'haak', 'schroef', 'tape', 'vorm', 'klem', 'mot', 'druppel'], 'teil'), - (['bomen', 'kussen', 'kaarten', 'beesten', 'weken', 'huizen', 'apen', 'poten', + (dtp, ['bomen', 'kussen', 'kaarten', 'beesten', 'weken', 'huizen', 'apen', 'poten', 'wieken', 'paarden', 'stoelen', 'ramen', 'strepen', 'planten', 'groeten', 'flessen', 'boeren', 'punten', 'tranen'], 'teilen'), - (['snel', 'wit', 'kort', 'dicht'], 'mooi'), - (['witte'], 'mooie'), - (['wel', 'niet'], 'ietsjes') #find a different adverb that does not get inside constituents (ietsjes?) + (dtp, ['snel', 'wit', 'kort', 'dicht'], 'mooi'), + (dtp, ['witte'], 'mooie'), + (welnietttp, ['wel', 'niet'], 'ietsjes') #find a different adverb that does not get inside constituents (ietsjes?) ] -def getdisambiguationdict() -> Dict[str, str]: +def getdisambiguationdict() -> Dict[str, Tuple[TokenTreePredicate, str]]: ''' - :return: a dictionary with word:replacement items (both of type string) + :return: a dictionary with words as key and a tuple of a condition and a replacement as values - The function *getdisambiguationdict* creates a dictionary with word:replacement + The function *getdisambiguationdict* creates a dictionary with word:(cond, replacement) items. It selects its content from the constant *disambiguation_replacements*: .. autodata:: basicreplacements::disambiguation_replacements :no-value: ''' disambiguationdict = {} - for ws, repl in disambiguation_replacements: + for cond, ws, repl in disambiguation_replacements: for w in ws: - disambiguationdict[w] = repl + disambiguationdict[w] = cond, repl return disambiguationdict diff --git a/corrector.py b/corrector.py index 77d11d7..6966e2e 100644 --- a/corrector.py +++ b/corrector.py @@ -973,6 +973,15 @@ def getalternativetokenmds(tokenmd: TokenMD, method: MethodName, tokens: List[To newwords = [r] newtokenmds = updatenewtokenmds(newtokenmds, token, newwords, beginmetadata, name=n, value=v, cat=c, backplacement=bpl_word, penalty=p) + # next replaced by conditions in basicreplacements.py + # if token.word.lower() in {'wel', 'niet'}: + # beginval = str(token.pos) + # wordnode = find1(tree, f'.//node[@pt="ww" and @begin="{beginval}"]') + # if wordnode is not None: + # newwords = ['ietsjes'] + # newtokenmds = updatenewtokenmds(newtokenmds, token, newwords, beginmetadata, + # name='Disambiguation', value='Avoid unknown reading', + # cat='Lexicon', backplacement=bpl_wordlemma) moemoetxpath = './/node[@lemma="moe" and @pt!="n" and not(%onlywordinutt%) and (@rel="--" or @rel="dp" or @rel="predm" or @rel="nucl")]' expanded_moemoetxpath = expandmacros(moemoetxpath) @@ -1068,10 +1077,11 @@ def getalternativetokenmds(tokenmd: TokenMD, method: MethodName, tokens: List[To # replaceambiguous words with one reading not known by the child by a nonambiguous word with the same properties if method in {'tarsp', 'stap'}: if token.word.lower() in disambiguationdict: - newword = disambiguationdict[token.word.lower()] - newtokenmds = updatenewtokenmds(newtokenmds, token, [newword], beginmetadata, - name='Disambiguation', value='Avoid unknown reading', - cat='Lexicon', backplacement=bpl_wordlemma) + cond, newword = disambiguationdict[token.word.lower()] + if cond(token, tree): + newtokenmds = updatenewtokenmds(newtokenmds, token, [newword], beginmetadata, + name='Disambiguation', value='Avoid unknown reading', + cat='Lexicon', backplacement=bpl_wordlemma) # ...en -> e: groten -> grote (if adjective); goten -> grote diff --git a/correcttreebank.py b/correcttreebank.py index bcba457..3505309 100644 --- a/correcttreebank.py +++ b/correcttreebank.py @@ -778,7 +778,7 @@ def getorigutt(stree: SynTree) -> Optional[str]: def scorefunction(obj: Alternative) -> TupleNint: - return (-obj.unknownwordcount, -obj.unknownnouncount, -obj.unknownnamecount, -obj.dpcount, -obj.dhyphencount, + return (-obj.unknownwordcount, -obj.unknownnouncount, -obj.unknownnamecount, -obj.ambigcount, -obj.dpcount, -obj.dhyphencount, -obj.complsucount, -obj.badcatcount, -obj.basicreplaceecount, -obj.ambigcount, -obj.hyphencount, -obj.subjunctivecount, obj.dimcount, obj.compcount, obj.supcount, obj.compoundcount, obj.sucount, obj.svaok, -obj.deplusneutcount, diff --git a/macros/sastamacros1.txt b/macros/sastamacros1.txt index 8ed6ec5..1decca3 100644 --- a/macros/sastamacros1.txt +++ b/macros/sastamacros1.txt @@ -511,7 +511,7 @@ robustdelpv = """(not(@rel="dp" and @begin > ancestor::node[@cat="top"]/descenda delpv = """(%coredelpv% and %robustdelpv%)""" -Vobij = """(@pt="bw" and (contains(@frame,"er_adverb" ) or contains(@frame, "tmp_adverb") or @lemma="daarom") and +Vobij = """(@pt="bw" and (contains(@frame,"er_adverb" ) or contains(@frame, "tmp_adverb") or contains(@frame, "waar_adverb") or @lemma="daarom") and @lemma!="er" and @lemma!="daar" and @lemma!="hier" and @lemma!="waar" and (starts-with(@lemma, 'er') or starts-with(@lemma, 'daar') or starts-with(@lemma, 'hier') or starts-with(@lemma, 'waar')))""" Tarsp_VzN = """(%vzn1xpath% or %vzn2xpath% ) """ diff --git a/sastatypes.py b/sastatypes.py index 7fa306c..59f0803 100644 --- a/sastatypes.py +++ b/sastatypes.py @@ -62,6 +62,8 @@ ExactResultsFilter = Callable[[Query, ExactResultsDict, ExactResult], bool] Targets = int Treebank = etree.Element +TreePredicate = Callable[[SynTree], bool] +TokenTreePredicate = Callable[[Token, SynTree], bool] URL = str UttTokenDict = Dict[UttId, List[Token]] UttWordDict = Dict[UttId, List[str]] diff --git a/test_indexexpansion.py b/test_indexexpansion.py new file mode 100644 index 0000000..37b5a20 --- /dev/null +++ b/test_indexexpansion.py @@ -0,0 +1,74 @@ +from lxml import etree +from treebankfunctions import indextransform + + +streestrings = {} +streestrings[1] = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Het blauw-groene logo met de N dat alle Nederlandstalige scholen in Brussel dragen , is uitgegroeid tot een kwaliteitslabel . + + Q#dpc-vla-001171-nl-sen.p.114.s.7|Het blauw-groene logo met de N dat alle Nederlandstalige scholen in Brussel dragen , is uitgegroeid tot een kwaliteitslabel .| + + +""" + +strees = {i: etree.fromstring(streestrings[i]) for i in streestrings} + + +def test(): + for i in strees: + newtree = indextransform(strees[i]) + etree.dump(newtree) + + +def main(): + test() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/treebankfunctions.py b/treebankfunctions.py index 2545888..1610430 100644 --- a/treebankfunctions.py +++ b/treebankfunctions.py @@ -1159,6 +1159,8 @@ def uniquenodes(nodelist: List[SynTree]) -> List[SynTree]: return resultlist +# this does not take into account that the antecedent itself can contain an indexed node, +# which must be replaced by an antecedent that may itself contain an index node, etc. def getindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: indexednodes = {} if stree is not None: @@ -1169,6 +1171,8 @@ def getindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: return indexednodes + + def nodecopy(node: SynTree) -> SynTree: ''' copies a node without its children @@ -1204,7 +1208,9 @@ def indextransform(stree: SynTree) -> SynTree: ''' indexednodesmap = getindexednodesmap(stree) - + # for ind, tree in indexednodesmap.items(): + # print(ind) + #etree.dump(tree) result = indextransform2(stree, indexednodesmap) return result @@ -1767,7 +1773,7 @@ def deletewordnodes2(tree: SynTree, begins: List[Position]) -> SynTree: childbeginint = int(childbegin) if childbeginint in begins and childless(child): tree.remove(child) - if 'cat' in child.attrib and childless(child): # if its children have been deleted earlier + elif 'cat' in child.attrib and childless(child): # if its children have been deleted earlier tree.remove(child) # tree begin en end bijwerken if tree. tag == 'node': From 8af42e328a414f625630836aea71dd8ff0af3d46 Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Tue, 11 Oct 2022 15:32:13 +0200 Subject: [PATCH 06/11] Updated indexexpansion and added its documentation --- Documentation/auxiliarymodules.rst | 13 +++ test_indexexpansion.py | 28 ++++++- treebankfunctions.py | 124 +++++++++++++++++++++++++++-- 3 files changed, 157 insertions(+), 8 deletions(-) diff --git a/Documentation/auxiliarymodules.rst b/Documentation/auxiliarymodules.rst index 9169f76..24d5449 100644 --- a/Documentation/auxiliarymodules.rst +++ b/Documentation/auxiliarymodules.rst @@ -21,3 +21,16 @@ Deregularise ------------ .. automodule:: deregularise + +.. _treebankfunctions: + +Treebankfunctions +----------------- + +.. _indextransform: + +Expansion of bare index nodes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: treebankfunctions::indextransform + diff --git a/test_indexexpansion.py b/test_indexexpansion.py index 37b5a20..ae521fd 100644 --- a/test_indexexpansion.py +++ b/test_indexexpansion.py @@ -1,6 +1,8 @@ from lxml import etree -from treebankfunctions import indextransform +from treebankfunctions import indextransform, getstree +import os +bareindexnodexpath = './/node[@index and not(@cat) and not(@word)]' streestrings = {} streestrings[1] = """ @@ -61,14 +63,36 @@ strees = {i: etree.fromstring(streestrings[i]) for i in streestrings} + + def test(): for i in strees: - newtree = indextransform(strees[i]) + idxnodes = strees[i].xpath(bareindexnodexpath) + if idxnodes == []: + print('No bare index nodes in tree {i}') + newtree = newindextransform(strees[i]) etree.dump(newtree) +def testwholelassy(): + lassykleinpath = r'D:\Dropbox\various\Resources\LASSY\LASSY-Klein\Lassy-Klein\Treebank' + for root, dirs, files in os.walk(lassykleinpath): + print(f'Processing {root}...') + for filename in files: + base, ext = os.path.splitext(filename) + if ext == '.xml': + fullname = os.path.join(root, filename) + fullstree = getstree(fullname) + stree = fullstree.getroot() + expansion = indextransform(stree) + if expansion.xpath(bareindexnodexpath) != []: + print(fullname) + etree.dump(stree) + etree.dump(expansion) + def main(): test() + #testwholelassy() if __name__ == '__main__': main() \ No newline at end of file diff --git a/treebankfunctions.py b/treebankfunctions.py index 1610430..2e8251c 100644 --- a/treebankfunctions.py +++ b/treebankfunctions.py @@ -1161,7 +1161,7 @@ def uniquenodes(nodelist: List[SynTree]) -> List[SynTree]: # this does not take into account that the antecedent itself can contain an indexed node, # which must be replaced by an antecedent that may itself contain an index node, etc. -def getindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: +def oldgetindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: indexednodes = {} if stree is not None: for node in stree.iter(): @@ -1170,12 +1170,82 @@ def getindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: indexednodes[theindex] = node return indexednodes +def getindexednodesmap(basicdict: Dict[str, SynTree]) -> Dict[str, SynTree]: + """ + + :param basicdict: dictionary of index - SynTree items in which the syntactic structure can contain bare index nodes + :return: a dictionary for each item in *basicdict* in which the bare index nodes have been replaced by their antecedents + + The function *getindexednodesmap* creates a new dictionary for each item in *basicdict* in which the bare index nodes have been replaced by + their antecedents by applying the function *expandtree*: + + .. autofunction:: treebankfunctions::expandtree + + """ + newdict = {} + for i, tree in basicdict.items(): + newdict[i] = expandtree(tree, basicdict, newdict) + return newdict + +def expandtree(tree: SynTree, basicdict:Dict[str, SynTree], newdict: Dict[str, SynTree]) -> Dict[str, SynTree]: + """ + + :param tree: input syntactic structure + :param basicdict: dictionary with index - SynTree items where the syntactic structure can contain bare index nodes + :param newdict: a dictionary, initially empty, that is filled by this function with index - SynTree items where the syntactic structure does not contain any bare index nodes + :return: a syntactic structure based on *tree* in which all bare index nodes have been replaced by their antecedents that do not contain any bare index nodes. + + The function *expandtree* expands a syntactic structure as follows: + + * if the top node is a bare index node with index *theindex*: + + * it is replaced by the newdict[theindex] if *theindex* is in *newdict* + * it is replaced by the expansion of basicdict[theindex] otherwise. This is a recursive call. + This recursion cannot go on forever since a node with index *idx* cannot contain a node with index *idx* + (the underlying type is a directed **acyclic** graph). Once this expansion has been created, + the expansion is assigned to newdict[idx] + + * otherwise the function is called recursively to all children of the top node, creating a new child list, which is + appended to a copy if the top node. + + """ + if bareindexnode(tree): + theindex = getattval(tree, 'index') + if theindex in newdict: + result = newdict[theindex] + else: + result = expandtree(basicdict[theindex], basicdict, newdict) + newdict[theindex] = result + else: + newtree = nodecopy(tree) + for child in tree: + newchild = expandtree(child, basicdict, newdict) + newtree.append(newchild) + result = newtree + return result +def getbasicindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: + """ + + :param stree: input syntactic structure + :return: dictionary with index - SynTree items in which each SynTree is the antecedent for bare + index nodes with this index. These antecedents can contain bare index nodes themselves. + + The function *getbasicindexednodesmap* simply assigns a node that is not a bare index node with index *theindex* to + the resulting dictionary *indexednodes* at key *theindex*. + """ + indexednodes = {} + if stree is not None: + for node in stree.iter(): + if 'index' in node.attrib and not bareindexnode(node): + theindex = node.attrib['index'] + indexednodes[theindex] = node + return indexednodes def nodecopy(node: SynTree) -> SynTree: ''' - copies a node without its children + The function *nodecopy* copies a node without its children :param node: node, an lxml.etree Element :return: a node with no children, otherwise a copy of the input node ''' @@ -1189,8 +1259,8 @@ def nodecopy(node: SynTree) -> SynTree: def bareindexnode(node: SynTree) -> bool: - result = terminal( - node) and 'index' in node.attrib and 'postag' not in node.attrib and 'cat' not in node.attrib and 'pt' not in node.attrib and 'pos' not in node.attrib + result = node.tag == 'node' and terminal(node) and 'index' in node.attrib and \ + 'word' not in node.attrib and 'cat' not in node.attrib # print(props2str(get_node_props(node)), result, file=sys.stderr) return (result) @@ -1200,7 +1270,7 @@ def terminal(node: SynTree) -> bool: return result -def indextransform(stree: SynTree) -> SynTree: +def oldindextransform(stree: SynTree) -> SynTree: ''' produces a new stree in which all index nodes are replaced by their antecedent nodes :param stree: input stree @@ -1214,10 +1284,52 @@ def indextransform(stree: SynTree) -> SynTree: result = indextransform2(stree, indexednodesmap) return result +def indextransform(stree: SynTree) -> SynTree: + ''' + :param stree: input stree + :return: stree with all index nodes replaced by the nodes of their antecedents + + The function *indextransform* produces a new stree in which all index nodes are replaced by their antecedent nodes. + It first gathers the antecedents of bare index nodes in a dictionary (*basicindexednodesmap*) of index-SynTree + items by means of the function *getbasicindexednodesmap*. + + .. autofunction:: treebankfunctions::getbasicindexednodesmap + + The antecedents can contain bare index nodes themselves. So, in a second step, each antecedent is expanded + so that bare index nodes are replaced by their antecedents. This is done by the function *getindexednodesmap*, + which creates a new dictionary of index-SynTree items called *indexnodesmap* + + .. autofunction:: treebankfunctions::getindexednodesmap + + Finally, the input tree is transformed by the function *indextransform2*, which uses *indexnodesmap*: + + .. autofunction:: treebankfunctions::indextransform2 + + ''' + + basicindexednodesmap = getbasicindexednodesmap(stree) + # for ind, tree in indexednodesmap.items(): + # print(ind) + #etree.dump(tree) + indexnodesmap = getindexednodesmap(basicindexednodesmap) + result = indextransform2(stree, indexnodesmap) + return result + + ##deze robuust maken tegen andere nodes dan node (metadata, alpino_ds etc) ## waarschijnlijk is node.tag == 'node'in baseindexnode voldoende def indextransform2(stree: SynTree, indexednodesmap: Dict[str, SynTree]) -> SynTree: + """ + The function *indextransform2* takes as input a syntactic structure *stree* and an index-SynTree dictionary. + It creates a new tree in which each bare index node in *stree* with index *i* is replaced by its antecedent + (i.e. indexednodesmap[i]), except for the grammatical relation attribute *rel*. + + :param stree: input syntactic structure + :param indexednodesmap: dictionary with index - SynTree items. No bare index nodes occur in the syntactic structures + :return: new tree in which each bare index node in *stree* with index *i* is replaced by its antecedent (i.e. indexednodesmap[i]), except for the grammatical relation attribute *rel*. + + """ if stree is None: return None else: @@ -1903,7 +2015,7 @@ def updatetokenpos2(node: SynTree, tokenposdict: PositionMap): def updateindexnodes(stree: SynTree) -> SynTree: #presupposes that the non bareindex nodes have been adapted already - indexednodesmap = getindexednodesmap(stree) + indexednodesmap = getbasicindexednodesmap(stree) newstree = deepcopy(stree) for node in newstree.iter(): if node.tag == 'node': From f78a54b1ef2de482929c54198c4e994bfc36403f Mon Sep 17 00:00:00 2001 From: Jan Odijk Date: Fri, 18 Nov 2022 15:40:22 +0100 Subject: [PATCH 07/11] Updated treebankfunctions and initial versions for auchann inclusdion --- CHAT_Annotation.py | 4 +++- sasta_explanation.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ sastatok.py | 13 ++++++----- stringfunctions.py | 18 +++++++++++++--- test_explanation.py | 23 ++++++++++++++++++++ treebankfunctions.py | 20 +++++++++++++---- 6 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 sasta_explanation.py create mode 100644 test_explanation.py diff --git a/CHAT_Annotation.py b/CHAT_Annotation.py index edc006f..e421d24 100644 --- a/CHAT_Annotation.py +++ b/CHAT_Annotation.py @@ -9,6 +9,8 @@ CHAT = 'CHAT' +CHAT_explanation = 'Explanation' + monadic = 1 dyadic = 2 @@ -642,7 +644,7 @@ def result(x, y): return SDLOGGER.warning(msg.format(x, y)) CHAT_SimpleScopedRegex(r'\[!!\]', keep, False, monadic), simplescopedmetafunction), # Duration to be added here @@ - CHAT_Annotation('Explanation', '8.3:69', '10.3:73', + CHAT_Annotation(CHAT_explanation, '8.3:69', '10.3:73', CHAT_ComplexRegex((r'\[=', anybutrb, r'\]'), (keep, eps), False), complexmetafunction), CHAT_Annotation('Replacement', '8.3:69', '10.3:73', diff --git a/sasta_explanation.py b/sasta_explanation.py new file mode 100644 index 0000000..c9ade99 --- /dev/null +++ b/sasta_explanation.py @@ -0,0 +1,51 @@ + +from sastatypes import SynTree +#import CHAT_Annotation as schat # put off because it causes an error: AttributeError: module 'CHAT_Annotation' has no attribute 'wordpat' + +import treebankfunctions as tbf +#import find1, iswordnode, getattval +import stringfunctions as strf +from typing import Optional + +space = ' ' +CHAT_explanation = 'Explanation' +explannwordlistxpath = f'.//xmeta[@name="{CHAT_explanation}"]/@annotationwordlist' +explannposlistxpath = f'.//xmeta[@name="{CHAT_explanation}"]/@annotationposlist' + + +def finalmultiwordexplanation(stree: SynTree) -> Optional[str]: + #get the multiword explanation and the last tokenposition is occupies + + + explannwrdliststr = tbf.find1(stree, explannwordlistxpath) + explannwrdlist = strf.string2list(explannwrdliststr, quoteignore=True) + + explannposliststr = tbf.find1(stree, explannposlistxpath) + explannposlist = strf.string2list(explannposliststr) + + + ismultiword = len(explannwrdlist) > 1 + + if ismultiword: + # any token in the tree with begin > last tokenposition of explanation can only be an interpunction sign + # check whether it is the last one ignoring interpunction + + explannposlast = int(explannposlist[-1]) + + explisfinal = True + for node in stree.iter(): + if explisfinal: + if tbf.iswordnode(node): + beginstr = tbf.getattval(node, 'begin') + if beginstr != '': + begin = int(beginstr) + if begin > explannposlast: + nodept = tbf.getattval(node, 'pt') + if nodept not in {'let'}: + explisfinal = False + + if explisfinal: + result = space.join(explannwrdlist) + else: + result = None + return result diff --git a/sastatok.py b/sastatok.py index 6072d60..198579a 100644 --- a/sastatok.py +++ b/sastatok.py @@ -1,6 +1,9 @@ import re -from CHAT_Annotation import CHAT_patterns, interpunction, wordpat + + +import CHAT_Annotation as sastachat +#from CHAT_Annotation import CHAT_patterns, interpunction, wordpat from sastatoken import stringlist2tokenlist @@ -20,7 +23,7 @@ def alts(pats, grouping=False): #interpunction = r'[!\?\.,;]' # add colon separated by spaces #word = r'[^!\?;\.\[\]<>\s]+' -word = wordpat +word = sastachat.wordpat scope = r'<.+?>' scopeorword = alts([scope, word]) myrepetition = r'\[x\s*[0-9]+\s*\]' @@ -39,13 +42,13 @@ def alts(pats, grouping=False): #sastaspecials = [r'\[::', r'\[=', r'\[:', r'\[=\?', r'\[x', r'\<', r'\>', r'\[\?\]', r'\[/\]', r'\[//\]', r'\[///\]', r'\[%', r'\]'] -sastaspecials = list(CHAT_patterns) -sastapatterns = sorted(sastaspecials, key=lambda x: len(x), reverse=True) + [word, interpunction] +sastaspecials = list(sastachat.CHAT_patterns) +sastapatterns = sorted(sastaspecials, key=lambda x: len(x), reverse=True) + [word, sastachat.interpunction] fullsastapatterns = alts(sastapatterns) fullsastare = re.compile(fullsastapatterns) allpatterns = [realwordreplacement, replacement, myrepetition, alternativetranscription, commentonmainline, bestguess, retracing] -sortedallpatterns = sorted(allpatterns, key=lambda x: len(x), reverse=True) + [word, interpunction] +sortedallpatterns = sorted(allpatterns, key=lambda x: len(x), reverse=True) + [word, sastachat.interpunction] fullpattern = alts(sortedallpatterns) #print(fullpattern) fullre = re.compile(fullpattern) diff --git a/stringfunctions.py b/stringfunctions.py index da8e00a..f18c5c9 100644 --- a/stringfunctions.py +++ b/stringfunctions.py @@ -9,6 +9,9 @@ tab = '\t' comma = ',' +csvre = "'[^']+'|[^,' ]+" #for selecting nonempty tokens from a csvstring ; comma between single quotes is allowed +csvpat = re.compile(csvre) + wpat = r'^.*\w.*$' wre = re.compile(wpat) allhyphenspat = r'^-+$' @@ -399,21 +402,30 @@ def allconsonants(inval: str) -> bool: return result -def string2list(liststr: str) -> List[str]: +def string2list(liststr: str, quoteignore=False) -> List[str]: ''' The function string2list turns a string surrounded by [ ] into a list by splitting it on a comma Examples: * "[1,2,3]" becomes ['1', '2', '3'] * "[]" becomes [] * "[ ]" becomes [" "] + * "['Jan', 'Piet']" becomes ["'Jan'", "'Piet'"] if quoteignore = False + * "['Jan', 'Piet']" becomes ['Jan', 'Piet'] if quoteignore = True + * comma's between single quotes are allowed ''' if liststr is None or len(liststr) == 2: return [] elif liststr[0] == '[' and liststr[-1] == ']': core = liststr[1:-1] - parts = core.split(comma) - return parts + parts = csvpat.findall(core) + strippedparts = [part.strip() for part in parts] + if quoteignore: + cleanparts1 = [part.strip("'") for part in strippedparts] + cleanparts = [part.strip('"') for part in cleanparts1] + else: + cleanparts = strippedparts + return cleanparts else: return [] diff --git a/test_explanation.py b/test_explanation.py new file mode 100644 index 0000000..e1692e1 --- /dev/null +++ b/test_explanation.py @@ -0,0 +1,23 @@ +import sasta_explanation +import treebankfunctions as tbf +from auchann.align_words import align_words + +print(dir(sasta_explanation)) +def test(): + infullname = r"D:\Dropbox\jodijk\Utrecht\Projects\SASTADATA\Auris\outtreebanks\DLD07_corrected.xml" + fulltreebank = tbf.getstree(infullname) + treebank = fulltreebank.getroot() + for tree in treebank: + origutt = tbf.find1(tree, './/meta[@name="origutt"]/@value') + cleanutt = tbf.find(tree, './/sentence/@value') + explanationstr = sasta_explanation.finalmultiwordexplanation(tree) + if explanationstr is not None: + alignment = align_words(cleanutt, explanationstr) + else: + alignment = None + if explanationstr is not None: + print(f' Orig:{origutt}\nClean:{cleanutt}\n Expl:{explanationstr}\nAlign:{alignment}\n\n') + + +if __name__ == '__main__': + test() \ No newline at end of file diff --git a/treebankfunctions.py b/treebankfunctions.py index 2e8251c..aa286bd 100644 --- a/treebankfunctions.py +++ b/treebankfunctions.py @@ -63,12 +63,14 @@ def md2XMLElement(self): allcats = ['smain', 'np', 'ppart', 'ppres', 'pp', 'ssub', 'inf', 'cp', 'du', 'ap', 'advp', 'ti', 'rel', 'whrel', 'whsub', 'conj', 'whq', 'oti', 'ahi', 'detp', 'sv1', 'svan', 'mwu', 'top', 'cat', 'part'] +#part occurs but is not official allpts = ['let', 'spec', 'bw', 'vg', 'lid', 'vnw', 'tw', 'ww', 'adj', 'n', 'tsw', 'vz'] openclasspts = ['bw', 'ww', 'adj', 'n'] clausecats = ['smain', 'ssub', 'inf', 'cp', 'ti', 'rel', 'whrel', 'whsub', 'whq', 'oti', 'ahi', 'sv1', 'svan'] +clausebodycats = ['smain', 'ssub', 'inf', 'sv1', 'ppart', 'ppres'] trueclausecats = ['smain', 'cp', 'rel', 'whrel', 'whsub', 'whq', 'sv1', 'svan'] @@ -1211,11 +1213,15 @@ def expandtree(tree: SynTree, basicdict:Dict[str, SynTree], newdict: Dict[str, """ if bareindexnode(tree): theindex = getattval(tree, 'index') + therel = getattval(tree, 'rel') if theindex in newdict: - result = newdict[theindex] + result = deepcopy(newdict[theindex]) + result.attrib['rel'] = therel else: - result = expandtree(basicdict[theindex], basicdict, newdict) - newdict[theindex] = result + result1 = expandtree(basicdict[theindex], basicdict, newdict) + newdict[theindex] = result1 + result = deepcopy(newdict[theindex]) + result.attrib['rel'] = therel else: newtree = nodecopy(tree) for child in tree: @@ -1223,6 +1229,7 @@ def expandtree(tree: SynTree, basicdict:Dict[str, SynTree], newdict: Dict[str, newtree.append(newchild) result = newtree return result + def getbasicindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: """ @@ -1260,7 +1267,7 @@ def nodecopy(node: SynTree) -> SynTree: def bareindexnode(node: SynTree) -> bool: result = node.tag == 'node' and terminal(node) and 'index' in node.attrib and \ - 'word' not in node.attrib and 'cat' not in node.attrib + 'word' not in node.attrib and 'lemma' not in node.attrib and 'cat' not in node.attrib # print(props2str(get_node_props(node)), result, file=sys.stderr) return (result) @@ -1543,6 +1550,11 @@ def testindextransform() -> None: newstree = indextransform(stree) simpleshow(newstree) +def getyieldstr(stree:SynTree) -> str: + theyield = getyield(stree) + theyieldstr = space.join(theyield) + return theyieldstr + def adaptsentence(stree: SynTree) -> SynTree: # adapt the sentence From 5cfe251259077b94ba1af58a542c796958397a10 Mon Sep 17 00:00:00 2001 From: Sheean Spoel Date: Tue, 6 Dec 2022 14:22:27 +0100 Subject: [PATCH 08/11] Added exports for mwe_query --- .github/workflows/tests.yml | 52 +++++++++++++++++++++ README.md | 6 +++ alpinoparsing.py | 7 +-- celexlexicon.py | 31 ++++++------- celexlexicon/__init__.py | 0 celexlexicon/dutch/__init__.py | 0 deregularise.py | 8 ++-- lexicon.py | 7 +-- metadata.py | 2 +- pypi/MANIFEST.in | 2 + pypi/__config__.py | 14 ++++++ pypi/include.txt | 19 ++++++++ pypi/prepare.sh | 8 ++-- pypi/setup.py | 15 ++++-- sastatypes.py | 9 ++-- stringfunctions.py | 2 +- treebankfunctions.py | 85 +++++++++++++++++++++------------- 17 files changed, 191 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 celexlexicon/__init__.py create mode 100644 celexlexicon/dutch/__init__.py create mode 100644 pypi/include.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..497737a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,52 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Unit tests + +on: + push: + branches: + - 'develop' + - 'main' + - 'feature/**' + - 'bugfix/**' + - 'hotfix/**' + - 'release/**' + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.10'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Prepare PIP package + run: | + cd pypi + ./prepare.sh + mypy sastadev + # - name: mypy + # run: | + # pip install mypy + # cd pypi + # mypy sastadev + - name: Lint with flake8 + run: | + flake8 $(cat pypi/include.txt | grep \.py\$) --count --max-complexity=12 --max-line-length=127 --statistics + # - name: Run unit tests + # run: | + # pip install pytest + # python -m pytest diff --git a/README.md b/README.md index 56207a3..76e6ed6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ # Sastadev +[![Actions Status](https://github.com/UUDigitalHumanitieslab/auchann/workflows/Unit%20tests/badge.svg)](https://github.com/UUDigitalHumanitieslab/auchann/actions) + +[pypi sastadev](https://pypi.org/project/sastadev) + Method definitions for use in SASTA Copy `default_config.py` to your own `config.py` in the `sastadev` directory, and change what you need. ## Upload to PyPi +Specify the files which should be included in the package in `pypi/include.txt`. + ```bash cd pypi ./prepare.sh diff --git a/alpinoparsing.py b/alpinoparsing.py index d9a9ab6..0755a6d 100644 --- a/alpinoparsing.py +++ b/alpinoparsing.py @@ -19,7 +19,6 @@ from memoize import memoize import logging -from typing import Optional #from sastatypes import SynTree, URL #from config import SDLOGGER @@ -90,6 +89,8 @@ def parse(origsent: str, escape: bool = True): return None #def previewurl(stree: SynTree) -> URL: + + def previewurl(stree): ''' The function *previewurl* returns the URL to preview the input SynTree *stree* in the GreTEL application. @@ -122,9 +123,9 @@ def escape_alpino_input(instr: str) -> str: result = '' for c in instr: if c == '[': - newc = '\[' + newc = '\\[' elif c == ']': - newc = '\]' + newc = '\\]' else: newc = c result += newc diff --git a/celexlexicon.py b/celexlexicon.py index f09dbc7..140de82 100644 --- a/celexlexicon.py +++ b/celexlexicon.py @@ -41,7 +41,6 @@ posre = re.compile(pospattern) - # dml columns IdNum, Head, Inl, MorphStatus, MorphCnt, DerComp, Comp, Def, Imm, \ ImmSubCat, ImmAllo, ImmSubst, StrucLab, StrucAllo, StrucSubst, Sepa = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 @@ -151,21 +150,21 @@ def dcoiphi2celexpv(thesubj: SynTree, thepv: SynTree, inversion: bool) -> str: celex2dcoimap: Dict[str, Dict[str, str]] =\ - {'te1': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, - 'te2': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, - 'te2t': {'pvtijd': 'tgw', 'pvagr': 'met-t', 'wvorm': 'pv'}, - 'te3': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, - 'te3t': {'pvtijd': 'tgw', 'pvagr': 'met-t', 'wvorm': 'pv'}, - 'te2I': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, - 'tm': {'pvtijd': 'tgw', 'pvagr': 'mv', 'wvorm': 'pv'}, - 've': {'pvtijd': 'verl', 'pvagr': 'ev', 'wvorm': 'pv'}, - 'vm': {'pvtijd': 'verl', 'pvagr': 'mv', 'wvorm': 'pv'}, - 'i': {'wvorm': 'inf', 'positie': 'vrij', 'buiging': 'zonder'}, - 'pv': {'wvorm': 'vd', 'positie': 'vrij', 'buiging': 'zonder'}, - 'pt': {'wvorm': 'td', 'positie': 'vrij', 'buiging': 'zonder'}, - 'pvE': {'wvorm': 'vd', 'buiging': 'met-e', 'positie': 'prenom'}, - 'ptE': {'wvorm': 'td', 'buiging': 'met-e', 'positie': 'prenom'} - } + {'te1': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, + 'te2': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, + 'te2t': {'pvtijd': 'tgw', 'pvagr': 'met-t', 'wvorm': 'pv'}, + 'te3': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, + 'te3t': {'pvtijd': 'tgw', 'pvagr': 'met-t', 'wvorm': 'pv'}, + 'te2I': {'pvtijd': 'tgw', 'pvagr': 'ev', 'wvorm': 'pv'}, + 'tm': {'pvtijd': 'tgw', 'pvagr': 'mv', 'wvorm': 'pv'}, + 've': {'pvtijd': 'verl', 'pvagr': 'ev', 'wvorm': 'pv'}, + 'vm': {'pvtijd': 'verl', 'pvagr': 'mv', 'wvorm': 'pv'}, + 'i': {'wvorm': 'inf', 'positie': 'vrij', 'buiging': 'zonder'}, + 'pv': {'wvorm': 'vd', 'positie': 'vrij', 'buiging': 'zonder'}, + 'pt': {'wvorm': 'td', 'positie': 'vrij', 'buiging': 'zonder'}, + 'pvE': {'wvorm': 'vd', 'buiging': 'met-e', 'positie': 'prenom'}, + 'ptE': {'wvorm': 'td', 'buiging': 'met-e', 'positie': 'prenom'} + } def celexpv2dcoi(word: str, infl: str, lemma: str) -> Dict[str, str]: diff --git a/celexlexicon/__init__.py b/celexlexicon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/celexlexicon/dutch/__init__.py b/celexlexicon/dutch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deregularise.py b/deregularise.py index 3ae1cf8..af23b40 100644 --- a/deregularise.py +++ b/deregularise.py @@ -303,11 +303,11 @@ def CV(thestr): def dup(thestr): - return(thestr + thestr) + return thestr + thestr def endsin(stem, thechar): - return(stem[-1] == thechar) + return stem[-1] == thechar def startswithprefix(stem): @@ -486,7 +486,7 @@ def getstems(el): takesge = False else: takesge = True - return(stem, stemFS, takesge) + return stem, stemFS, takesge def makepastpartwithe(stem, stemFS, takesge, prefix='ge'): @@ -627,7 +627,7 @@ def makeparadigm(word, forms): #: two strings (corrected form, metadata) as value. This dictionary is filled by #: reading from the file with the name in the constant *correctionfilename* upon #: initialisation of the module *deregularise* -correction: Dict[str, Tuple[str,str]] = {} +correction: Dict[str, Tuple[str, str]] = {} correctionfile = open(os.path.join(SD_DIR, correctionfilename), 'r', encoding='utf8') myreader = csv.reader(correctionfile, delimiter=tab) for row in myreader: diff --git a/lexicon.py b/lexicon.py index b30d047..a28d12d 100644 --- a/lexicon.py +++ b/lexicon.py @@ -13,8 +13,8 @@ import treebankfunctions from namepartlexicon import namepart_isa_namepart, namepart_isa_namepart_uc -from typing import Any, Dict, List, Optional, Tuple -from sastatypes import CELEXPosCode, CELEX_INFL, DCOITuple, DeHet, Lemma, SynTree, WordInfo +from typing import Any, Dict, List, Optional +from sastatypes import CELEX_INFL, DCOITuple, Lemma, SynTree, WordInfo space = ' ' @@ -39,7 +39,6 @@ dets[het] = ['het', 'dat', 'dit', 'ons', 'welk', 'ieder', 'elk', 'zulk'] - def isa_namepart(word: str) -> bool: ''' is the word a name part @@ -58,8 +57,6 @@ def isa_namepart_uc(word: str) -> bool: return namepart_isa_namepart_uc(word) - - def lookup(dct: Dict[str, Any], key: str) -> str: ''' looks up key in dct, if so it returns dct[key] else '' diff --git a/metadata.py b/metadata.py index 25c659b..aaa063a 100644 --- a/metadata.py +++ b/metadata.py @@ -104,4 +104,4 @@ def mkSASTAMeta(token, nwt, name, value, cat, subcat=None, penalty=defaultpenalt insertion = 'Insertion' smallclause = 'Small Clause Treatment' tokenmapping = 'Token Mapping' -insertiontokenmapping = 'Insertion Token Mapping' \ No newline at end of file +insertiontokenmapping = 'Insertion Token Mapping' diff --git a/pypi/MANIFEST.in b/pypi/MANIFEST.in index 8321013..d46e486 100644 --- a/pypi/MANIFEST.in +++ b/pypi/MANIFEST.in @@ -1,2 +1,4 @@ include sastadev/LICENSE include sastadev/*.txt +include sastadev/celexlexicon/dutch/*.txt +include sastadev/names/nameparts/namepartlexicon.csv diff --git a/pypi/__config__.py b/pypi/__config__.py index 04d32d3..0e91dd4 100644 --- a/pypi/__config__.py +++ b/pypi/__config__.py @@ -1,3 +1,17 @@ #!/usr/bin/env python3 +import logging import os.path as op +import sentence_parser + +# logging object +SDLOGGER = logging.getLogger() + SD_DIR = op.dirname(op.abspath(__file__)) + +# Alpino +ALPINO_HOST = 'localhost' +ALPINO_PORT = 7001 + +# Function to parse a sentence with Alpino +# Should take a string as input and return an lxml.etree +PARSE_FUNC = sentence_parser.parse diff --git a/pypi/include.txt b/pypi/include.txt new file mode 100644 index 0000000..a05ef6d --- /dev/null +++ b/pypi/include.txt @@ -0,0 +1,19 @@ +__init__.py +alpinoparsing.py +celexlexicon +celexlexicon.py +deregularise.py +inflectioncorrection.tsv.txt +lexicon.py +LICENSE +memoize.py +metadata.py +namepartlexicon.py +names +py.typed +query.py +sastatoken.py +sastatypes.py +sentence_parser.py +stringfunctions.py +treebankfunctions.py diff --git a/pypi/prepare.sh b/pypi/prepare.sh index 54dacc7..bdb7961 100755 --- a/pypi/prepare.sh +++ b/pypi/prepare.sh @@ -2,8 +2,10 @@ find sastadev/ -type f -not -name '.gitignore' -delete TARGET=$PWD/../pypi/sastadev/ cp __config__.py $TARGET/config.py -cd .. -cp LICENSE __init__.py deregularise.py inflectioncorrection.tsv.txt py.typed $TARGET -cd pypi +while read SOURCE +do + cp -r ../$SOURCE $TARGET +done < include.txt + python setup.py sdist diff --git a/pypi/setup.py b/pypi/setup.py index 751b175..828c25d 100644 --- a/pypi/setup.py +++ b/pypi/setup.py @@ -1,15 +1,22 @@ -from setuptools import setup, find_packages +from setuptools import setup + +with open('README.md') as file: + long_description = file.read() setup( name='sastadev', - python_requires='>=3.5, <4', - version='0.0.2', + python_requires='>=3.7, <4', + version='0.0.3', description='Linguistic functions for SASTA tool', + long_description=long_description, + long_description_content_type="text/markdown", author='Digital Humanities Lab, Utrecht University', author_email='digitalhumanities@uu.nl', url='https://github.com/UUDigitalHumanitieslab/sastadev', license='BSD-3-Clause', include_package_data=True, packages=['sastadev'], - package_data={'sastadev': ['*.txt', 'LICENSE', 'py.typed']} + package_data={ + 'sastadev': ['*.txt', 'LICENSE', 'py.typed'] + } ) diff --git a/sastatypes.py b/sastatypes.py index 59f0803..e04697c 100644 --- a/sastatypes.py +++ b/sastatypes.py @@ -2,8 +2,8 @@ This module contains definitions of types used in multiple modules ''' -from typing import Dict, List, Any, Tuple, Callable, Pattern, Optional, NewType, Union -from lxml import etree # type: ignore +from typing import Dict, List, Tuple, Callable, Optional, Union +from lxml import etree # type: ignore from collections import Counter from query import Query from sastatoken import Token @@ -18,7 +18,7 @@ LocationName = str DCOIPt = str DeHet = str -CELEX_INFL =str +CELEX_INFL = str DCOITuple = Tuple Lemma = str CorrectionMode = str # Literal['0','1','n'] @@ -72,6 +72,3 @@ #CoreQueryFunction = Callable[[SynTree], List[SynTree]] #PostQueryFunction = Callable[[SynTree, allresults.AllResults], List[SynTree]] #QueryFunction = Union[CoreQueryFunction, PostQueryFunction] - - - diff --git a/stringfunctions.py b/stringfunctions.py index f18c5c9..b6b9572 100644 --- a/stringfunctions.py +++ b/stringfunctions.py @@ -9,7 +9,7 @@ tab = '\t' comma = ',' -csvre = "'[^']+'|[^,' ]+" #for selecting nonempty tokens from a csvstring ; comma between single quotes is allowed +csvre = "'[^']+'|[^,' ]+" # for selecting nonempty tokens from a csvstring ; comma between single quotes is allowed csvpat = re.compile(csvre) wpat = r'^.*\w.*$' diff --git a/treebankfunctions.py b/treebankfunctions.py index aa286bd..512600c 100644 --- a/treebankfunctions.py +++ b/treebankfunctions.py @@ -3,9 +3,9 @@ ''' -import sys +# import sys import re -import logging +# import logging from copy import copy, deepcopy from lxml import etree from config import SDLOGGER @@ -16,7 +16,7 @@ from sastatoken import Token from metadata import Meta -from typing import Any, AnyStr, Callable, Dict, List, Match, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple from sastatypes import FileName, OptPhiTriple, PhiTriple, Position, PositionMap, PositionStr, Span, SynTree, UttId @@ -39,6 +39,7 @@ def md2XMLElement(self): result = etree.Element('meta', type=self.type, name=self.name, value=self.value) return result + #: The constant *min_sasta_length* sets the minimum length a word must have to count as #: a real, though unknown, word min_sasta_length = 9 @@ -119,8 +120,8 @@ def adjacent(node1: SynTree, node2: SynTree, stree: SynTree) -> bool: yieldnodes = getnodeyield(stree) for i, n in enumerate(yieldnodes): if yieldnodes[i] == node1: - prec = yieldnodes[i-1] if i > 0 else None - succ = yieldnodes[i+1] if i < len(yieldnodes) - 1 else None + prec = yieldnodes[i - 1] if i > 0 else None + succ = yieldnodes[i + 1] if i < len(yieldnodes) - 1 else None result = prec == node2 or succ == node2 return result return False @@ -139,11 +140,12 @@ def immediately_precedes(node1: SynTree, node2: SynTree, stree: SynTree) -> bool yieldnodes = getnodeyield(stree) for i, n in enumerate(yieldnodes): if yieldnodes[i] == node1: - succ = yieldnodes[i+1] if i < len(yieldnodes) - 1 else None + succ = yieldnodes[i + 1] if i < len(yieldnodes) - 1 else None result = succ == node2 return result return False + def immediately_follows(node1: SynTree, node2: SynTree, stree: SynTree) -> bool: ''' :param node1: @@ -156,6 +158,7 @@ def immediately_follows(node1: SynTree, node2: SynTree, stree: SynTree) -> bool: ''' return immediately_precedes(node2, node1, stree) + def countav(stree: SynTree, att: str, val: str) -> int: countattvalxpath = countattvalxpathtemplate.format(att=att, val=val) result = stree.xpath(countattvalxpath) @@ -194,6 +197,7 @@ def getmeta(syntree: SynTree, attname: str, treebank: bool = True) -> Optional[s result = getqueryresult(syntree, xpathquery=thequery) return result + def normalizedword(stree: SynTree) -> Optional[str]: if stree is None: result = None @@ -330,7 +334,7 @@ def lastconstituentof(stree: SynTree) -> SynTree: return result -def getsentence(syntree: SynTree, treebank: bool =True) -> Optional[str]: +def getsentence(syntree: SynTree, treebank: bool = True) -> Optional[str]: prefix = "." if treebank else "" thequery = prefix + sentencexpathquery result = getqueryresult(syntree, xpathquery=thequery) @@ -457,7 +461,7 @@ def getconjphi(node: SynTree) -> OptPhiTriple: conjs = node.xpath('node[@rel="cnj"]') conjphis = [getphi(conj) for conj in conjs] startphi = ('3', 'getal', 'genus') - curphi: OptPhiTriple = startphi + curphi: OptPhiTriple = startphi for conjphi in conjphis: curphi = merge(curphi, conjphi) if curphi is not None: @@ -562,8 +566,8 @@ def number2intstring(numberstr: str) -> str: return result -def getqueryresult(syntree: SynTree, xpathquery: Optional[str] =None, \ - noxpathquery: Callable[[SynTree], List[str]] =None) -> Optional[str]: +def getqueryresult(syntree: SynTree, xpathquery: Optional[str] = None, + noxpathquery: Callable[[SynTree], List[str]] = None) -> Optional[str]: if syntree is None: result = None else: @@ -811,6 +815,7 @@ def sasta_long(node: SynTree) -> bool: result = len(word) >= min_sasta_length return result + def spec_noun(node: SynTree) -> bool: ''' The function *spec_noun* checks whether the node is node of *pt* *spec* which is a @@ -969,6 +974,7 @@ def short_nucl_n(node: SynTree) -> bool: result = pt == 'n' and rel == 'nucl' and sasta_short(word) return result + #: The constant *sasta_pseudonyms* list the strings that replace names for #: pseudonymisation purposes. sasta_pseudonyms = ['NAAM', 'VOORNAAM', 'ACHTERNAAM', 'ZIEKENHUIS', 'STRAAT', 'PLAATS', 'PLAATSNAAM', 'KIND', 'BEROEP', @@ -1029,7 +1035,7 @@ def recognised_wordnodepos(node: SynTree, pos: str) -> bool: word = getattval(node, 'word') lcword = word.lower() result = lex.informlexiconpos(word, pos) or lex.informlexiconpos(lcword, pos) or \ - iscompound(node) or isdiminutive(node) or lex.isa_namepart_uc(word) + iscompound(node) or isdiminutive(node) or lex.isa_namepart_uc(word) return result @@ -1092,6 +1098,7 @@ def recognised_lemmanodepos(node: SynTree, pos: str) -> bool: ##@@need to add a variant that returns a string + def simpleshow(stree: SynTree, showchildren: bool = True, newline: bool = True) -> None: simpleshow2(stree, showchildren) if newline: @@ -1172,6 +1179,7 @@ def oldgetindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: indexednodes[theindex] = node return indexednodes + def getindexednodesmap(basicdict: Dict[str, SynTree]) -> Dict[str, SynTree]: """ @@ -1184,12 +1192,13 @@ def getindexednodesmap(basicdict: Dict[str, SynTree]) -> Dict[str, SynTree]: .. autofunction:: treebankfunctions::expandtree """ - newdict = {} + newdict = {} for i, tree in basicdict.items(): newdict[i] = expandtree(tree, basicdict, newdict) return newdict -def expandtree(tree: SynTree, basicdict:Dict[str, SynTree], newdict: Dict[str, SynTree]) -> Dict[str, SynTree]: + +def expandtree(tree: SynTree, basicdict: Dict[str, SynTree], newdict: Dict[str, SynTree]) -> Dict[str, SynTree]: """ :param tree: input syntactic structure @@ -1230,6 +1239,7 @@ def expandtree(tree: SynTree, basicdict:Dict[str, SynTree], newdict: Dict[str, result = newtree return result + def getbasicindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: """ @@ -1249,7 +1259,6 @@ def getbasicindexednodesmap(stree: SynTree) -> Dict[str, SynTree]: return indexednodes - def nodecopy(node: SynTree) -> SynTree: ''' The function *nodecopy* copies a node without its children @@ -1267,11 +1276,13 @@ def nodecopy(node: SynTree) -> SynTree: def bareindexnode(node: SynTree) -> bool: result = node.tag == 'node' and terminal(node) and 'index' in node.attrib and \ - 'word' not in node.attrib and 'lemma' not in node.attrib and 'cat' not in node.attrib + 'word' not in node.attrib and 'lemma' not in node.attrib and 'cat' not in node.attrib # print(props2str(get_node_props(node)), result, file=sys.stderr) return (result) ##herdefinieren want met UD hebben terminale nodes wel children (maar geen children met tag=node) + + def terminal(node: SynTree) -> bool: result = isinstance(node, etree._Element) and node is not None and len(node) == 0 return result @@ -1286,11 +1297,12 @@ def oldindextransform(stree: SynTree) -> SynTree: indexednodesmap = getindexednodesmap(stree) # for ind, tree in indexednodesmap.items(): - # print(ind) - #etree.dump(tree) + # print(ind) + #etree.dump(tree) result = indextransform2(stree, indexednodesmap) return result + def indextransform(stree: SynTree) -> SynTree: ''' :param stree: input stree @@ -1316,16 +1328,15 @@ def indextransform(stree: SynTree) -> SynTree: basicindexednodesmap = getbasicindexednodesmap(stree) # for ind, tree in indexednodesmap.items(): - # print(ind) - #etree.dump(tree) + # print(ind) + #etree.dump(tree) indexnodesmap = getindexednodesmap(basicindexednodesmap) result = indextransform2(stree, indexnodesmap) return result - -##deze robuust maken tegen andere nodes dan node (metadata, alpino_ds etc) -## waarschijnlijk is node.tag == 'node'in baseindexnode voldoende +# deze robuust maken tegen andere nodes dan node (metadata, alpino_ds etc) +# waarschijnlijk is node.tag == 'node'in baseindexnode voldoende def indextransform2(stree: SynTree, indexednodesmap: Dict[str, SynTree]) -> SynTree: """ The function *indextransform2* takes as input a syntactic structure *stree* and an index-SynTree dictionary. @@ -1374,7 +1385,7 @@ def getstree(fullname: FileName) -> SynTree: except OSError as e: SDLOGGER.error('OS Error: {}; file: {}'.format(e, fullname)) return None - except: + except Exception: SDLOGGER.error('Error: Unknown error in file {}'.format(fullname)) return None @@ -1550,7 +1561,8 @@ def testindextransform() -> None: newstree = indextransform(stree) simpleshow(newstree) -def getyieldstr(stree:SynTree) -> str: + +def getyieldstr(stree: SynTree) -> str: theyield = getyield(stree) theyieldstr = space.join(theyield) return theyieldstr @@ -1634,7 +1646,7 @@ def get_parentandindex(node: SynTree, stree: SynTree) -> Optional[Tuple[SynTree, return (stree, idx) else: chresult = get_parentandindex(node, child) - if chresult != None: + if chresult is not None: return chresult idx += 1 return None @@ -1649,17 +1661,18 @@ def getspan(node: SynTree) -> Span: def lbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: nodebegin = getattval(node, 'begin') - condition = lambda n: getattval(n, 'end') == nodebegin + def condition(n): return getattval(n, 'end') == nodebegin result = findfirstnode(tree, condition) return result def rbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: nodeend = getattval(node, 'end') - condition = lambda n: getattval(n, 'begin') == nodeend + def condition(n): return getattval(n, 'begin') == nodeend result = findfirstnode(tree, condition) return result + def infl_lbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: ''' :param node: the node for the relevant word @@ -1669,9 +1682,10 @@ def infl_lbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: nodeyield = getnodeyield(tree) for i, n in enumerate(nodeyield): if nodeyield[i] == n and i > 0: - return nodeyield[i-1] + return nodeyield[i - 1] return None + def infl_rbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: ''' :param node: the node for the relevant word @@ -1681,7 +1695,7 @@ def infl_rbrother(node: SynTree, tree: SynTree) -> Optional[SynTree]: nodeyield = getnodeyield(tree) for i, n in enumerate(nodeyield): if nodeyield[i] == n and i < len(nodeyield) - 1: - return nodeyield[i+1] + return nodeyield[i + 1] return None @@ -1754,7 +1768,7 @@ def find1(tree: SynTree, xpathquery: str) -> SynTree: return result -def getxmetatreepositions(tree: SynTree, xmetaname: str, poslistname: str ='annotationposlist') -> List[PositionStr]: +def getxmetatreepositions(tree: SynTree, xmetaname: str, poslistname: str = 'annotationposlist') -> List[PositionStr]: query = ".//xmeta[@name='{}']".format(xmetaname) xmeta = find1(tree, query) if xmeta is None: @@ -1872,17 +1886,21 @@ def olddeletewordnodes(tree: SynTree, begins: List[Position]) -> SynTree: return newtree ##redefine: no children with tag == 'node' (because of UD extensions ) + + def childless(node: SynTree): children = [ch for ch in node] result = children == [] return result + def deletewordnodes(tree: SynTree, begins: List[Position]) -> SynTree: newtree = deepcopy(tree) newtree = deletewordnodes2(newtree, begins) newtree = adaptsentence(newtree) return newtree + def deletewordnodes2(tree: SynTree, begins: List[Position]) -> SynTree: if tree is None: return tree @@ -1894,12 +1912,12 @@ def deletewordnodes2(tree: SynTree, begins: List[Position]) -> SynTree: for child in tree: if child.tag == 'node': childbegin = getattval(child, 'begin') - childbeginint = int(childbegin) + childbeginint = int(childbegin) if childbeginint in begins and childless(child): tree.remove(child) elif 'cat' in child.attrib and childless(child): # if its children have been deleted earlier tree.remove(child) - # tree begin en end bijwerken + # tree begin en end bijwerken if tree. tag == 'node': newchildren = [n for n in tree] if newchildren != []: @@ -1992,6 +2010,7 @@ def updatetokenpos(stree: SynTree, tokenposdict: PositionMap) -> SynTree: return finaltree + def updatetokenpos2(node: SynTree, tokenposdict: PositionMap): if node is None: return node @@ -2024,7 +2043,6 @@ def updatetokenpos2(node: SynTree, tokenposdict: PositionMap): return node - def updateindexnodes(stree: SynTree) -> SynTree: #presupposes that the non bareindex nodes have been adapted already indexednodesmap = getbasicindexednodesmap(stree) @@ -2039,6 +2057,7 @@ def updateindexnodes(stree: SynTree) -> SynTree: node.attrib['end'] = newend return newstree + def treewithtokenpos(thetree: SynTree, tokenlist: List[Token]) -> SynTree: resulttree = deepcopy(thetree) thetreeleaves = getnodeyield(thetree) From 05dea624b7baa28e8cf635c1a1e601df5d89ee17 Mon Sep 17 00:00:00 2001 From: Sheean Spoel Date: Tue, 6 Dec 2022 14:25:13 +0100 Subject: [PATCH 09/11] Trigger test --- .github/workflows/tests.yml | 11 +++-------- README.md | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 497737a..53e295a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,15 +4,10 @@ name: Unit tests on: - push: - branches: - - 'develop' - - 'main' - - 'feature/**' - - 'bugfix/**' - - 'hotfix/**' - - 'release/**' workflow_dispatch: + push: + paths-ignore: + - '**.md' jobs: build: diff --git a/README.md b/README.md index 76e6ed6..aa62a72 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sastadev -[![Actions Status](https://github.com/UUDigitalHumanitieslab/auchann/workflows/Unit%20tests/badge.svg)](https://github.com/UUDigitalHumanitieslab/auchann/actions) +[![Actions Status](https://github.com/UUDigitalHumanitieslab/sastadev/workflows/Unit%20tests/badge.svg)](https://github.com/UUDigitalHumanitieslab/sastadev/actions) [pypi sastadev](https://pypi.org/project/sastadev) From 44f1a543797751242db2cdd0a85c4fc779e019a0 Mon Sep 17 00:00:00 2001 From: Sheean Spoel Date: Tue, 6 Dec 2022 14:27:47 +0100 Subject: [PATCH 10/11] removed mypy --- .github/workflows/tests.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 53e295a..0e820cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,12 +32,6 @@ jobs: run: | cd pypi ./prepare.sh - mypy sastadev - # - name: mypy - # run: | - # pip install mypy - # cd pypi - # mypy sastadev - name: Lint with flake8 run: | flake8 $(cat pypi/include.txt | grep \.py\$) --count --max-complexity=12 --max-line-length=127 --statistics From b74a9844a5a39869964296d135e7713c59b50ab5 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Tue, 31 Jan 2023 10:23:08 +0100 Subject: [PATCH 11/11] Rename celexlexicon to prevent wrong imports --- celexlexicon.py | 4 ++-- {celexlexicon => celexlexicondata}/__init__.py | 0 .../dutch/DMLCD.txt | 0 .../dutch/DMWCDOK.txt | 0 .../dutch/DSLCD.txt | 0 .../dutch/__init__.py | 0 top3000.py | 15 ++++++++++----- 7 files changed, 12 insertions(+), 7 deletions(-) rename {celexlexicon => celexlexicondata}/__init__.py (100%) rename {celexlexicon => celexlexicondata}/dutch/DMLCD.txt (100%) rename {celexlexicon => celexlexicondata}/dutch/DMWCDOK.txt (100%) rename {celexlexicon => celexlexicondata}/dutch/DSLCD.txt (100%) rename {celexlexicon => celexlexicondata}/dutch/__init__.py (100%) diff --git a/celexlexicon.py b/celexlexicon.py index 140de82..ad5883a 100644 --- a/celexlexicon.py +++ b/celexlexicon.py @@ -5,7 +5,7 @@ * DML (dmlcd.txt): lemmas and their morphological properties * DSL (dslcd.txt): lemmas and their syntactic properties -These files can be found in the folder celexlexicon/dutch in the code folder. +These files can be found in the folder celexlexicondata/dutch in the code folder. We store these in the celexlexicon module in python dictionaries: @@ -52,7 +52,7 @@ # initialisation # read the celex lexicon -inputfolder = os.path.join(SD_DIR, 'celexlexicon', 'dutch') +inputfolder = os.path.join(SD_DIR, 'celexlexicondata', 'dutch') dmwfilename = 'DMWCDOK.txt' dmwfullname = os.path.join(inputfolder, dmwfilename) diff --git a/celexlexicon/__init__.py b/celexlexicondata/__init__.py similarity index 100% rename from celexlexicon/__init__.py rename to celexlexicondata/__init__.py diff --git a/celexlexicon/dutch/DMLCD.txt b/celexlexicondata/dutch/DMLCD.txt similarity index 100% rename from celexlexicon/dutch/DMLCD.txt rename to celexlexicondata/dutch/DMLCD.txt diff --git a/celexlexicon/dutch/DMWCDOK.txt b/celexlexicondata/dutch/DMWCDOK.txt similarity index 100% rename from celexlexicon/dutch/DMWCDOK.txt rename to celexlexicondata/dutch/DMWCDOK.txt diff --git a/celexlexicon/dutch/DSLCD.txt b/celexlexicondata/dutch/DSLCD.txt similarity index 100% rename from celexlexicon/dutch/DSLCD.txt rename to celexlexicondata/dutch/DSLCD.txt diff --git a/celexlexicon/dutch/__init__.py b/celexlexicondata/dutch/__init__.py similarity index 100% rename from celexlexicon/dutch/__init__.py rename to celexlexicondata/dutch/__init__.py diff --git a/top3000.py b/top3000.py index 9e70289..3092c02 100644 --- a/top3000.py +++ b/top3000.py @@ -42,6 +42,7 @@ from typing import Dict, List, Tuple from sastatypes import SynTree, DCOIPt + def ishuman(node: SynTree) -> bool: ''' The function ishuman determines whether the node node is human @@ -49,11 +50,12 @@ def ishuman(node: SynTree) -> bool: lemma = getattval(node, 'lemma') pt = getattval(node, 'pt') vwtype = getattval(node, 'vwtype') - result = (lemma, pt ) in semlexicon and 'human' in semlexicon[(lemma, pt)] + result = (lemma, pt) in semlexicon and 'human' in semlexicon[(lemma, pt)] result = result or vwtype == 'pers' result = result or namepart_isa_namepart(lemma) return result + def isanimate(node: SynTree) -> bool: ''' The function isanimate determines whether the nde node is animate @@ -61,7 +63,7 @@ def isanimate(node: SynTree) -> bool: lemma = getattval(node, 'lemma') pt = getattval(node, 'pt') - result = (lemma, pt ) in semlexicon and 'animate' in semlexicon[(lemma, pt)] + result = (lemma, pt) in semlexicon and 'animate' in semlexicon[(lemma, pt)] return result @@ -71,15 +73,17 @@ def transitivity(node: SynTree, tr: str) -> bool: ''' lemma = getattval(node, 'lemma') pt = getattval(node, 'pt') - result = (lemma, pt ) in semlexicon and tr in trlexicon[(lemma, pt)] + result = (lemma, pt) in semlexicon and tr in trlexicon[(lemma, pt)] return result + def transitive(node: SynTree) -> bool: ''' The function transitive determines whether node is transitive ''' return transitivity(node, 'tr') + def pseudotr(node: SynTree) -> bool: ''' The function pseudotr determines whether node is pseudotransitive @@ -93,9 +97,10 @@ def intransitive(node: SynTree) -> bool: ''' return transitivity(node, 'intr') + semicolon = ';' -filename = os.path.join(SD_DIR, r'top3000\Woordenlijsten Current.xlsx') +filename = os.path.join(SD_DIR, 'top3000', 'Woordenlijsten Current.xlsx') lexiconheader, lexicondata = getxlsxdata(filename) @@ -120,4 +125,4 @@ def intransitive(node: SynTree) -> bool: genlexicon[(lemma, pt)] = gens #next statement for debugging purposes -junk = 0 \ No newline at end of file +junk = 0