"""
Structures and operations for quantifier scope in DELPH-IN semantics.
"""
from __future__ import annotations
__all__ = [
"LEQ", # for backward compatibility
"LHEQ", # for backward compatibility
"OUTSCOPES", # for backward compatibility
"QEQ", # for backward compatibility
"ScopeError",
"ScopingSemanticStructure", # for backward compatibility
"conjoin",
"descendants",
"representatives",
]
from collections.abc import Callable, Iterable
from typing import (
TYPE_CHECKING,
Any,
TypeVar,
overload,
)
from delphin.__about__ import __version__ # noqa: F401
from delphin.exceptions import PyDelphinException
from delphin.sembase import (
Identifier,
Predication,
ScopalArgumentMap,
ScopeLabel,
ScopeMap,
Scopes,
ScopingSemanticStructure,
)
from delphin.util import _connected_components
if TYPE_CHECKING:
from delphin import dmrs, mrs
# Type Aliases
ID = TypeVar("ID", bound=Identifier)
P = TypeVar("P", bound=Predication)
Descendants = dict[ID, list[P]]
ScopeEqualities = Iterable[tuple[ScopeLabel, ScopeLabel]]
PredicationPriority = Callable[[P], Any] # Any should be sortable
# Exceptions
[docs]
class ScopeError(PyDelphinException):
"""Raised on invalid scope operations."""
# Module Functions
@overload
def conjoin(scopes: ScopeMap[mrs.EP], leqs: ScopeEqualities) -> Scopes[mrs.EP]: ...
@overload
def conjoin(
scopes: ScopeMap[dmrs.Node], leqs: ScopeEqualities
) -> Scopes[dmrs.Node]: ...
[docs]
def conjoin(scopes: ScopeMap, leqs: ScopeEqualities) -> ScopeMap:
"""
Conjoin multiple scopes with equality constraints.
Args:
scopes: a mapping of scope labels to predications
leqs: a list of pairs of equated scope labels
Returns:
A mapping of the labels to the predications of each conjoined
scope. The conjoined scope labels are taken arbitrarily from
each equated set).
Example:
>>> conjoined = scope.conjoin(mrs.scopes(), [("h2", "h3")])
>>> {lbl: [p.id for p in ps] for lbl, ps in conjoined.items()}
{'h1': ['e2'], 'h2': ['x4', 'e6']}
"""
scopemap: dict[ScopeLabel, list[Predication]] = {}
for component in _connected_components(list(scopes), leqs):
chosen_label = next(iter(component))
scopemap[chosen_label] = []
for label in component:
scopemap[chosen_label].extend(scopes[label])
return scopemap
[docs]
def descendants(
x: ScopingSemanticStructure[ID, P],
scopes: ScopeMap[P] | None = None,
) -> Descendants[ID, P]:
"""
Return a mapping of predication ids to their scopal descendants.
Args:
x: an MRS or a DMRS
scopes: a mapping of scope labels to predications
Returns:
A mapping of predication ids to lists of predications that are
scopal descendants.
Example:
>>> m = mrs.MRS(...) # Kim didn't think that Sandy left.
>>> descendants = scope.descendants(m)
>>> for id, ds in descendants.items():
... print(m[id].predicate, [d.predicate for d in ds])
proper_q ['named']
named []
neg ['_think_v_1', '_leave_v_1']
_think_v_1 ['_leave_v_1']
_leave_v_1 []
proper_q ['named']
named []
"""
if scopes is None:
_, scopes = x.scopes()
scargs = x.scopal_arguments(scopes=scopes)
descs: dict[ID, list[P]] = {}
for p in x.predications:
_descendants(descs, p.id, scargs, scopes)
return descs
def _descendants(
descs: dict[ID, list[P]],
id: ID,
scargs: ScopalArgumentMap[ID],
scopes: ScopeMap[P],
) -> None:
if id in descs:
return
descs[id] = []
for _role, _relation, label in scargs[id]:
assert isinstance(label, str)
for p in scopes.get(label, []):
descs[id].append(p)
_descendants(descs, p.id, scargs, scopes)
descs[id].extend(descs[p.id])
@overload
def representatives(
x: mrs.MRS,
priority: PredicationPriority[mrs.EP] | None = None,
) -> Scopes[mrs.EP]: ...
@overload
def representatives(
x: dmrs.DMRS,
priority: PredicationPriority[dmrs.Node] | None = None,
) -> Scopes[dmrs.Node]: ...
[docs]
def representatives(
x: ScopingSemanticStructure,
priority: PredicationPriority | None = None,
) -> ScopeMap:
"""
Find the scope representatives in *x* sorted by *priority*.
When predications share a scope, generally one takes another as a
non-scopal argument. For instance, the ERG analysis of a phrase
like "very old book" has the predicates `_very_x_deg`, `_old_a_1`,
and `_book_n_of` which all share a scope, where `_very_x_deg`
takes `_old_a_1` as its `ARG1` and `_old_a_1` takes `_book_n_of`
as its `ARG1`. Predications that do not take any other predication
within their scope as an argument (as `_book_n_of` above does not)
are scope representatives.
*priority* is a function that takes a :class:`Predication` object
and returns a rank which is used to to sort the representatives
for each scope. As the predication alone might not contain enough
information for useful sorting, it can be helpful to create a
function configured for the input semantic structure *x*. If
*priority* is `None`, representatives are sorted according to the
following criteria:
1. Prefer predications that are quantifiers or instances (type 'x')
2. Prefer eventualities (type 'e') over other types
3. Prefer tensed over untensed eventualities
4. Finally, prefer those appearing first in *x*
The definition of "tensed" vs "untensed" eventualities is
grammar-specific, but it is used by several large grammars. If a
grammar does something different, criterion (3) is ignored.
Criterion (4) is not linguistically motivated but is used as a
final disambiguator to ensure consistent results.
Args:
x: an MRS or a DMRS
priority: a function that maps an EP to a rank for sorting
Example:
>>> sent = "The new chef whose soup accidentally spilled quit."
>>> m = ace.parse(erg, sent).result(0).mrs()
>>> # in this example there are 4 EPs in scope h7
>>> _, scopes = m.scopes()
>>> [ep.predicate for ep in scopes["h7"]]
['_new_a_1', '_chef_n_1', '_accidental_a_1', '_spill_v_1']
>>> # there are 2 representatives for scope h7
>>> reps = scope.representatives(m)["h7"]
>>> [ep.predicate for ep in reps]
['_chef_n_1', '_spill_v_1']
"""
_, scopes = x.scopes()
ns_args = {
src: set(arg for _, arg in roleargs)
for src, roleargs in x.arguments(types="xeipu").items()
}
# compute descendants, but only keep ids
descs = {id: set(d.id for d in ds) for id, ds in descendants(x, scopes).items()}
reps: dict[ScopeLabel, list[Predication]] = {label: [] for label in scopes}
for label, scope in scopes.items():
if len(scope) == 1:
reps[label].extend(scope)
else:
for predication in scope:
others = [p.id for p in scope if p is not predication]
args = ns_args[predication.id]
# check if args are in the immediate scope
if args.intersection(others):
continue
# check if args are in scope descendants
if any(args.intersection(descs[id]) for id in others):
continue
# tests passed; predication is a candidate representative
reps[label].append(predication)
if priority is None:
priority = _make_representative_priority(x)
for label in reps:
reps[label].sort(key=priority)
return reps
_UNTENSED_VALUES = {
"",
"untensed",
}
def _make_representative_priority(x: ScopingSemanticStructure):
"""
Create a function to sort scope representatives in *x*.
"""
index = {p.id: i for i, p in enumerate(x.predications, 1)}
def representative_priority(p: Predication) -> tuple[int, Identifier]:
id = p.id
type = p.type
if x.is_quantifier(id) or type == "x":
rank = 0
elif type == "e":
tense = x.properties(id).get("TENSE", "").lower()
if tense in _UNTENSED_VALUES:
rank = 2
else:
rank = 1
else:
rank = 3
return rank, index[id]
return representative_priority
# for backward compatibility
from delphin.sembase import ScopeRelation # noqa
LEQ = ScopeRelation.LEQ
LHEQ = ScopeRelation.LHEQ
OUTSCOPES = ScopeRelation.OUTSCOPES
QEQ = ScopeRelation.QEQ