Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A sketch of DAS integration #446

Merged
merged 6 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions python/sandbox/das_gate/dasgate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from hyperon import *
from hyperon.ext import register_atoms

import os
import re
import json

from hyperon_das import DasAPI
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe include instructions about where to find the hyperon_das module / or how to setup & run / connect to the DAS more generally

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, their repo is private now, so this PR was more for information than for the in-depth review. Of course, the link to DAS will be provided, once it is open.

from hyperon_das.das import QueryOutputFormat as Format
from hyperon_das.pattern_matcher import (
Link,
Node,
Variable,
And,
Or,
Not,
PatternMatchingAnswer,
)

class DASpace(AbstractSpace):

def __init__(self, unwrap=True):
super().__init__()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not technically needed since AbstractSpace is pure-virtual, but also harmless.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see. I'd still keep it here, because if some useful functionality is introduced in the parent class, then it will not be missed in DASpace as well. I'm also ok to remove it if Abstract part is a strong hint that no concrete functionality will be ever introduced.

self.das = DasAPI('hash_table')
self.unwrap = unwrap

def _atom2dict(self, atom):
if isinstance(atom, ExpressionAtom):
return {
"type": "Expression",
"targets": [self._atom2dict(ch) for ch in atom.get_children()]
}
else:
return {
"type": "Symbol",
"name": repr(atom)
}

def _atom2query(self, atom):
if isinstance(atom, ExpressionAtom):
targets = atom.get_children()
if isinstance(targets[0], SymbolAtom) and targets[0].get_name() == ',':
return And([self._atom2query(ch) for ch in targets[1:]])
return Link("Expression", ordered=True,
targets=[self._atom2query(ch) for ch in targets])
else:
if isinstance(atom, VariableAtom):
return Variable(repr(atom))
else:
return Node("Symbol", repr(atom))

def _handle2atom(self, h):
try:
return S(self.das.get_node_name(h))
except Exception as e:
return E(*[self._handle2atom(ch) for ch in self.das.get_link_targets(h)])

def query(self, query_atom):
query = self._atom2query(query_atom)
answer = PatternMatchingAnswer()
new_bindings_set = BindingsSet.empty()
if not query.matched(self.das.db, answer):
return new_bindings_set
for a in answer.assignments:
bindings = Bindings()
for var, val in a.mapping.items():
# remove '$', because it is automatically added
bindings.add_var_binding(V(var[1:]), self._handle2atom(val))
new_bindings_set.push(bindings)
return new_bindings_set

def add(self, atom):
dc = self._atom2dict(atom)
if "name" in dc:
self.das.add_node(dc)
else:
self.das.add_link(dc)

#def remove(self, atom):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without remove and replace implementations, an exception will be raised if they're called.

Do we want to modify the Space contract so that a Space can gracefully decline to implement these functions in the same way a Space can decline to implement atom_count and atoms_iter? My feeling is "no", but you have a more coherent conceptual vision for MeTTa than I do.

It seems to me that remove and replace are different from atom_count and atoms_iter in that remove and replace affect the Space. Even in a space where it's impossible to "forget" (e.g. an append-only journal) it's still possible to indicate that an atom is disused or has been superseded.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without remove and replace implementations, an exception will be raised if they're called.

This is perfectly ok.

Do we want to modify the Space contract so that a Space can gracefully decline to implement these functions

What behavior will be in this case for remove, when it is not implemented? I believe it should be exception. A space may not implement some required functionality, but attempts to use the missing functionality should be prevented, and the user should be informed that there is no such functionality and it cannot be used.

in the same way a Space can decline to implement atom_count and atoms_iter?

Do you mean that if atoms_iter is not implemented, and get-atoms is executed in MeTTa, a MeTTa Error-expression is returned, while if remove-atom is executed the exception is raised, which is not turned into Error, but the whole MeTTa runner stops? I'm ok with this difference.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if remove-atom is executed the exception is raised, which is not turned into Error, but the whole MeTTa runner stops? I'm ok with this difference.

Does it really work by this way? I believe the exception should be wrapped into Error as well.

# pass
#def replace(self, from_atom, to_atom):
# pass
#def atom_count(self):
# return self.das.count_atoms()
#def atoms_iter(self):
# return iter(self.atoms_list)

@register_atoms(pass_metta=True)
def das_atoms(metta):
newDASpaceAtom = OperationAtom('new-das', lambda: [G(SpaceRef(DASpace()))], unwrap=False)
return {
r"new-das": newDASpaceAtom,
}

21 changes: 21 additions & 0 deletions python/sandbox/das_gate/test_das.metta
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
!(extend-py! dasgate)

!(bind! &das (new-das))

!(add-atom &das (Test (Test 2)))
!(add-atom &das (Best (Test 2)))
!(add-atom &das Test)

; The simplest match with grounding the variable in the node
!(match &das ($v1 (Test 2)) (This $v1 works))
; The simplest match with grounding the variable in the link
!(match &das (Test $v2) (This $v2 works))

!(add-atom &das (Rest (Test 3)))
!(add-atom &das (Test (Test 3)))

; Compositional (And) query test
!(match &das (, (Best $x) ($v $x)) ($v $x))
; !(match &das (, ($v1 $x) (Test $x)) ($v1 Test $x))

; !(match &das ($v1 ($v2 2)) (This $v1 works))