summaryrefslogtreecommitdiff
path: root/examples/letter_analogies/letter_analogy.py
diff options
context:
space:
mode:
Diffstat (limited to 'examples/letter_analogies/letter_analogy.py')
-rw-r--r--examples/letter_analogies/letter_analogy.py222
1 files changed, 222 insertions, 0 deletions
diff --git a/examples/letter_analogies/letter_analogy.py b/examples/letter_analogies/letter_analogy.py
new file mode 100644
index 0000000..77826ea
--- /dev/null
+++ b/examples/letter_analogies/letter_analogy.py
@@ -0,0 +1,222 @@
+"""An example TripletStructure for solving a letter analogy problem.
+
+Throughout this codebase we use the shorthand `tc` to represent the
+TripletStructure being operated on, and `rt` to represent the Runtime operating on
+that TripletStructure. I have also standardized to upper-case function names to
+differentiate the "front-end" code using ts_lib from the "plumbing" code that
+actually implements the DSL. These decisions can be undone if no one else likes
+them.
+"""
+import string # pylint: disable=deprecated-module
+from timeit import default_timer as timer
+from ts_lib import TripletStructure
+from ts_utils import RegisterPrototype, RegisterRule
+from runtime.runtime import TSRuntime
+from mapper import MapperCodelet
+from letter_tactics import SolveLetterAnalogy
+
+def Main(verbose=True):
+ """Initialize the structure and solve the letter analogy.
+ """
+ # This will hold our facts and nodes (including the rules and tactics!).
+ ts = TripletStructure()
+ # Add rules describing common letter relations, eg. 'Successor.'
+ LetterRelations(ts)
+ # Add (standardized) rules describing how to map/make analogies/join
+ # sub-structures. This can handle eg. abc -> bcd and lmn -> mno being
+ # 'abstracted' to x_1x_2x_3 -> y_1y_2y_3 with Succ(x_1, y_1), etc.
+ MapperCodelet(ts)
+
+ # Add three letter analogies, describing the problem to be solved. None
+ # indicates that the 'solution' should go there (it's indicated by marking
+ # the corresponding node with TOP).
+ LetterAnalogy(ts, ":Analogy_abc_bcd", "abc", "bcd")
+ LetterAnalogy(ts, ":Analogy_lmn_mno", "lmn", "mno")
+ LetterAnalogy(ts, ":Analogy_def_top", "def", None)
+
+ # Now that we have a structure (ts) which fully describes our problems,
+ # rules, and heuristics, we can initialize a TSRuntime to modify the
+ # structure according to the rules.
+ rt = TSRuntime(ts)
+
+ SolveLetterAnalogy(rt, verbose=verbose)
+
+ solution = ExtractLetterGroup(ts, ts["/:Analogy_def_top:To:_"])
+
+ if verbose:
+ print(f"Proposed solution: {solution}")
+
+ return solution
+
+def ExtractLetterGroup(ts, letter_group):
+ """Tries to extract a string representation of the letter group.
+
+ This is not always successful, as the representation might be invalid (eg.
+ cycles, or more than one NextPair:Right). In such cases it returns None.
+ """
+ try:
+ # NOTE: These may be ambiguous, to be sure we should have a few
+ # assert(len(...) == 1)s here.
+ letter_group = letter_group.full_name
+ head_map = ts.lookup(None, letter_group, "/:HeadPair:Container")[0][0]
+ head = ts.lookup(head_map, None, "/:HeadPair:Head")[0][1]
+ letters = ""
+ visited = set()
+ while True:
+ if head in visited:
+ return None
+ visited.add(head)
+
+ for letter in string.ascii_lowercase + string.ascii_uppercase:
+ is_letter = ts.lookup(None, head, Letter(ts, letter).full_name)
+ if is_letter:
+ letters += letter
+ break
+ else:
+ letters += "?"
+ next_map = ts.lookup(None, head, "/:NextPair:Left")
+ if not next_map:
+ break
+ next_map = next_map[0][0]
+ head = ts.lookup(next_map, None, "/:NextPair:Right")[0][1]
+ return letters
+ except IndexError:
+ return None
+
+def LetterAnalogy(ts, name, analogy_from, analogy_to):
+ """Adds nodes describing a particular letter analogy.
+
+ @name should be a unique node prefix, while @analogy_from and @analogy_to
+ should be strings describing the word analogy.
+ """
+ with ts.scope(name):
+ LetterGroup(ts, analogy_from, ts.scope(":From"))
+ LetterGroup(ts, analogy_to, ts.scope(":To"))
+ ts[":AnalogyMap"].map({
+ ts[":From:_"]: ts["/:Analogy:From"],
+ ts[":To:_"]: ts["/:Analogy:To"]
+ })
+
+def LetterGroup(ts, letters, scope):
+ """Adds nodes describing a string of letters (eg. one side of an analogy).
+
+ The entire group is represented by a node scope[:_] which is marked as the
+ owner of all other nodes.
+ """
+ with scope:
+ if letters is None:
+ ts[":IsTopMap:??"].map({ts[":_"]: ts["/:Mapper:TOP"]})
+ return
+ nodes = []
+ for i, letter in enumerate(letters):
+ node = ts[":Letter{}_{}".format(i, letter)]
+ nodes.append(node)
+ ts[":IsLetterMap:??"].map({node: Letter(ts, letter)})
+ ts[":IsOwned:??"].map({
+ scope[":_"]: ts["/:Owner"],
+ node: ts["/:Owned"],
+ })
+ for left_node, right_node in zip(nodes[:-1], nodes[1:]):
+ ts[":LRMap:??"].map({
+ left_node: ts["/:NextPair:Left"],
+ right_node: ts["/:NextPair:Right"],
+ })
+
+def Letter(ts, letter):
+ """Returns a reference to the node corresponding to character @letter.
+
+ This is sort of the "Platonic conception" of @letter.
+ """
+ return ts["/:Letter:{}".format(letter)]
+
+def LetterRelations(ts):
+ """Adds relations and rules describing strings of letters.
+ """
+ # Declaring these is not strictly necessary, as they will be created when
+ # first referenced, but I am listing them here for reference.
+ # pylint: disable=pointless-statement
+ with ts.scope(":NextPair"):
+ ts[":Left, :Right"]
+ with ts.scope(":SuccessorPair"):
+ ts[":Predecessor, :Successor"]
+ with ts.scope(":UpperPair"):
+ ts[":Lower, :Upper"]
+ with ts.scope(":HeadPair"):
+ ts[":Container, :Head"]
+
+ Headify(ts)
+
+ for predecessor, successor in zip(string.ascii_lowercase[:-1],
+ string.ascii_lowercase[1:]):
+ SuccessorPrototype(ts, predecessor, successor)
+ SuccessorPrototype(ts, predecessor.upper(), successor.upper())
+
+ for letter in string.ascii_lowercase:
+ UpperPrototype(ts, letter, letter.upper())
+
+def SuccessorPrototype(ts, predecessor, successor):
+ """Rules that identify and concretize successor pairs.
+
+ Note that this approach will create 3 rules for each pair (a, b); one that
+ creates Successor(a, b) from a and b, one that creates `a` given
+ Successor(a, b) and b, and one that creates `b` given Successor(a, b) and
+ `a`.
+
+ An alternative approach is to define three "Generic Binary Prototype"
+ rules, and then expose these as "examples" that that rule then maps
+ against. I think this is a more straight-forward approach for now, though
+ in the future (or if there's too much overhead with parsing the rules) we
+ can look at the other option.
+ """
+ with ts.scope(":Successor{}To{}".format(predecessor, successor)):
+ ts[":MA"].map({ts[":A"]: Letter(ts, predecessor)})
+ ts[":MB"].map({ts[":B"]: Letter(ts, successor)})
+ ts[":PairMap"].map({ts[":A"]: ts["/:SuccessorPair:Predecessor"]})
+ ts[":PairMap"].map({ts[":B"]: ts["/:SuccessorPair:Successor"]})
+
+ RegisterPrototype(ts, dict({
+ ":ConcretizePredecessor": {ts["/INSERT"]: [ts[":MA"]]},
+ ":ConcretizeSuccessor": {ts["/INSERT"]: [ts[":MB"]]},
+ ":ConcretizePair": {ts["/INSERT"]: [ts[":PairMap"]]},
+ }), equal=[])
+
+def UpperPrototype(ts, lowercase, uppercase):
+ """Rules for identifying and concretizing Upper pairs.
+
+ See SuccessorPrototype for notes on this implementation.
+ """
+ with ts.scope(":Upper{}To{}".format(lowercase, uppercase)):
+ ts[":MA"].map({ts[":A"]: Letter(ts, lowercase)})
+ ts[":MB"].map({ts[":B"]: Letter(ts, uppercase)})
+ ts[":PairMap"].map({ts[":A"]: ts["/:UpperPair:Lower"]})
+ ts[":PairMap"].map({ts[":B"]: ts["/:UpperPair:Upper"]})
+
+ RegisterPrototype(ts, dict({
+ ":ConcretizePredecessor": {ts["/INSERT"]: [ts[":MA"]]},
+ ":ConcretizeSuccessor": {ts["/INSERT"]: [ts[":MB"]]},
+ ":ConcretizePair": {ts["/INSERT"]: [ts[":PairMap"]]},
+ }), equal=[])
+
+def Headify(ts):
+ """Rules that identify the head letter of a letter group.
+ """
+ with ts.scope("/:HeadOfContainer"):
+ with ts.scope(":MustMap") as exist:
+ ts[":IsLeftOf"].map({ts[":LeftMost"]: ts["/:NextPair:Left"]})
+ ts[":IsMember"].map({
+ ts[":Container"]: ts["/:Owner"],
+ ts[":LeftMost"]: ts["/:Owned"],
+ })
+ with ts.scope(":NoMap"):
+ ts[":IsRightOf"].map({exist[":LeftMost"]: ts["/:NextPair:Right"]})
+ with ts.scope(":TryMap:Insert"):
+ ts[":IsHead"].map({
+ exist[":Container"]: ts["/:HeadPair:Container"],
+ exist[":LeftMost"]: ts["/:HeadPair:Head"],
+ })
+ RegisterRule(ts)
+
+if __name__ == "__main__":
+ start = timer()
+ Main()
+ print(timer() - start)
generated by cgit on debian on lair
contact matthew@masot.net with questions or feedback