#!/usr/bin/env python3
from __future__ import annotations
from collections import Counter, defaultdict, deque
from typing import TYPE_CHECKING
from fosf.syntax.base import FrozenDisjunctiveSort, URIFeature, URISort
from fosf.syntax.constraints import DisjunctiveClause
if TYPE_CHECKING:
from fosf.syntax.constraints import RootedClause, RootedSolvedClause
from fosf.syntax import DisjunctiveSort, Feature, Sort, Tag
[docs]
class Term:
"""
Represent an OSF term X:s(f1 -> t1, ..., fn -> tn).
"""
def __init__(
self,
X: Tag,
s: Sort | None = None,
subterms: dict[Feature, list[Term]] | None = None,
):
"""
Parameters
----------
X : Tag
The root tag of the term.
s : Sort | None
The root sort of the term. If None, when the OSF term is processes, it is assumed to be
the top sort in a given Sort taxonomy.
subterms: dict[Feature, list[Term]] | None
The subterms of the OSF term. In a non-normal OSF term, the same feature may
point to different subterms.
Attributes
----------
X : Tag
The root tag of the term.
s : Sort
The root sort of the term.
subterms : dict[Feature, list[Term]]
Possibly empty dict mapping each feature to a list of subterms.
"""
self.X = X
self.s = s
self.subterms = defaultdict(list) if subterms is None else subterms
[docs]
def dfs(self):
"""
Generate subterms depth-first.
"""
stack = [self]
while stack:
yield (term := stack.pop())
for _, subterm in term.iter_subterms():
stack.append(subterm)
[docs]
def bfs(self):
"""
Generate subterms breadth-first.
"""
queue = deque([self])
while queue:
yield (term := queue.popleft())
for _, subterm in term.iter_subterms():
queue.append(subterm)
[docs]
def to_clause(self) -> RootedClause:
"""
Transform the OSF term into an equivalent rooted clause.
Returns
-------
RootedClause
The rooted clause corresponding to the OSF term.
"""
from fosf.syntax.constraints import RootedClause
clause = RootedClause(self.X)
for c in self.generate_constraints():
clause.add(c)
return clause
[docs]
def generate_constraints(self):
"""
Generate the OSF constraints expressed by the OSF term.
"""
from fosf.syntax.constraints import FeatureConstraint, SortConstraint
if self.s is not None:
yield SortConstraint(self.X, self.s)
for term in self.dfs():
for f, subterm in term.iter_subterms():
yield FeatureConstraint(term.X, f, subterm.X)
if subterm.s is not None:
yield SortConstraint(subterm.X, subterm.s)
[docs]
def tag_counts(self) -> Counter:
"""
Return the counts of :class:`Tag`'s appearing in the OSF term.
"""
out = Counter()
for term in self.dfs():
out[term.X] += 1
return out
[docs]
def sorts(self) -> set[Sort]:
"""
Return the set of :class:`Sort`'s appearing in the OSF term.
"""
out = set()
for term in self.dfs():
if term.s is not None:
if isinstance(term.s, DisjunctiveSort):
out.add(term.s.freeze())
else:
out.add(term.s)
return out
[docs]
def prettify(self, spaces=0, feature="", namespace_manager=None):
"Prettify the OSF Term for printing."
out = " " * spaces
if feature:
if isinstance(feature, URIFeature) and namespace_manager:
feature = feature.n3(namespace_manager)
out += f"{feature} -> "
out += f"{self.X}"
if self.s is not None:
s = self.s
if isinstance(s, URISort) and namespace_manager:
s = s.n3(namespace_manager)
if isinstance(s, (DisjunctiveSort, FrozenDisjunctiveSort)) and namespace_manager:
s = "{ " + ", ".join({ds.n3(namespace_manager) for ds in s}) + " }"
out += f" : {s}"
if self.subterms:
out += "("
out += "\n"
for feature, term in self.iter_subterms():
out += term.prettify(spaces + 4, feature, namespace_manager)
if self.subterms:
out += " " * spaces + ")\n"
return out
[docs]
def pretty_print(self, spaces=0, feature="", namespace_manager=None):
"Pretty-print the OSF Term."
print(self.prettify(spaces, feature, namespace_manager))
def __repr__(self):
out = f"{self.__class__.__name__}(X={self.X!r}"
if self.s is not None:
out += f", s={self.s!r}"
if self.subterms:
out += f", subterms={dict(self.subterms)!r}"
out += ")"
return out
def __str__(self):
out = f"{self.X}"
if self.s is not None:
out += f" : {self.s}"
if self.subterms:
pairs = [f"{f} -> {str(subterm)}" for f, subterm in self.iter_subterms()]
out += f"({', '.join(pairs)})"
return out
def __getitem__(self, key: Feature):
return self.subterms.get(key, None)
def __eq__(self, other: Term):
return (
self.X == other.X and self.s == other.s and self.subterms == other.subterms
)
[docs]
def iter_subterms(self):
"""
Generate (feature, subterm) pairs.
"""
for feature, subterms in self.subterms.items():
for subterm in subterms:
yield (feature, subterm)
[docs]
def tag_to_sort(self) -> dict[Tag, set[Sort]]:
"""
Return a mapping from each :class:`Tag` to its set of :class:`Sort`'s.
"""
out = defaultdict(set)
for term in self.dfs():
if term.s:
out[term.X].add(term.s)
return out
def to_sparql(
self,
vars=None,
prefixes=None,
base=None,
include_prefixes: bool = True,
simplify=False,
):
from fosf.utils.sparql import term_to_query
term = self
if simplify:
if not isinstance(term, (NormalTerm, DisjunctiveTerm)):
print(
"Skipping simplification step.",
"Only NormalTerms and DisjunctiveTerm can be simplified.",
f"Found {type(term)}.",
)
else:
from fosf.utils.terms import simplify_disjunctive_term
term = simplify_disjunctive_term(term)
return term_to_query(term, vars, prefixes, base, include_prefixes)
[docs]
class NormalTerm(Term):
"""
Represent an OSF term in normal form.
"""
def __init__(
self,
X: Tag,
s: Sort | None = None,
subterms: dict[Feature, NormalTerm] | None = None,
):
"""
Parameters
----------
X : Tag
The root tag.
s : Sort | None
The root sort. If None, when the OSF term is processes, it is assumed to be
the top sort in a given Sort taxonomy.
subterms: dict[Feature, NormalTerm] | None
The subterms of the OSF term. In a normal OSF term, each feature may point to
at most one (normal) subterm.
Attributes
----------
X : Tag
The root tag of the term.
s : Sort
The root sort of the term.
subterms : dict[Feature, Term]
Possibly empty dict mapping each feature to a unique subterm.
"""
self.X = X
self.s = s
self.subterms = {} if subterms is None else subterms
[docs]
def to_clause(self) -> RootedSolvedClause:
"""
Transform the OSF term into an equivalent rooted solved clause.
Returns
-------
RootedSolvedClause
The rooted solved clause corresponding to the OSF term.
"""
from fosf.syntax.constraints import (
FeatureConstraint,
RootedSolvedClause,
SortConstraint,
)
clause = RootedSolvedClause(self.X)
for term in self.dfs():
if term.s is not None:
clause.add(SortConstraint(term.X, term.s))
for f, subterm in term.subterms.items():
clause.add(FeatureConstraint(term.X, f, subterm.X))
return clause
[docs]
def equivalent_to(self, other: NormalTerm) -> bool:
"""
Return whether this Term is equivalent to another normal term.
"""
this_clause = self.to_clause()
other_clause = other.to_clause()
return this_clause.equivalent_to(other_clause)
[docs]
def iter_subterms(self):
"""
Generate (feature, subterm) pairs.
"""
yield from self.subterms.items()
[docs]
def tag_to_sort(self) -> dict[Tag, Sort]:
"""
Return a mapping from each :class:`Tag` to its :class:`Sort`.
"""
out = dict()
for term in self.dfs():
if term.s:
out[term.X] = term.s
return out
[docs]
class DisjunctiveTerm(Term):
"""
Represent a disjunctive OSF term.
"""
def __init__(self, X: Tag, terms: list[Term]):
self.X = X
self.terms = terms
def __repr__(self):
out = f"{self.__class__.__name__}("
out += f"X={self.X!r}, "
out += f"terms={self.terms!r}"
out += ")"
return out
def __str__(self):
terms = " ; ".join(str(term) for term in self.terms)
if self.X is None:
out = f"{{ {terms} }}"
else:
var = f"{self.X}"
out = f"{var} : {{ {terms} }}"
return out
[docs]
def iter_subterms(self):
"""
Generate (feature, subterm) pairs.
"""
for term in self.terms:
yield from term.iter_subterms()
[docs]
def prettify(self, spaces=0, feature="", namespace_manager=None):
out = " " * spaces
if feature:
if isinstance(feature, URIFeature) and namespace_manager is not None:
feature = feature.n3(namespace_manager)
out += f"{feature} -> "
if self.X is not None:
out += f"{self.X} : {{"
else:
out += "{"
out += "\n"
for i, term in enumerate(self.terms):
if i > 0:
out += " " * spaces + " ;\n"
out += term.prettify(spaces + 4, namespace_manager=namespace_manager) + "\n"
out += " " * spaces + "}\n"
return out
[docs]
def pretty_print(self, spaces=0, feature="", namespace_manager=None):
"Pretty-print the OSF Term."
print(self.prettify(spaces, feature, namespace_manager))
def __iter__(self):
return iter(self.terms)
[docs]
def tag_counts(self):
"""
Return the counts of :class:`Tag`'s appearing in the OSF term.
"""
out = Counter([self.X])
for term in self.terms:
out.update(term.tag_counts())
return out
[docs]
def to_clause(self):
clauses = [term.to_clause() for term in self.terms]
return DisjunctiveClause(clauses)
[docs]
def generate_constraints(self):
msg = "The method 'generate_constraints' is currently not implemented for DisjunctiveTerms"
return NotImplementedError(msg)
[docs]
def sorts(self):
msg = "The method 'sorts' is currently not implemented for DisjunctiveTerms"
return NotImplementedError(msg)
def __getitem__(self, key: Feature):
return NotImplementedError()
def __eq__(self, other: Term):
return NotImplementedError()
[docs]
def tag_to_sort(self):
return NotImplementedError()