NeuroLang for Logic Programmers#

NeuroLang is implemented on top of Datalog+/- with probabilistic extensions. The primary way to use it is the Using Datalog Embedded in Python Python-embedded frontend.

Using Datalog Embedded in Python#

This requires a first step importing the NeuroLang frontend and initialising the class:

>>> from neurolang import frontend
>>> nl = frontend.NeurolangDL()

Then, we can add some facts (connected) and rules (reachable):

>>> with nl.environment as e:
...   e.connected[0, 1] = True
...   e.connected[1, 2] = True
...   e.connected[2, 3] = True
...   e.reachable[e.x, e.y] = e.connected[e.x, e.y]
...   e.reachable[e.x, e.y] = e.reachable[e.x, e.z] & e.connected[e.z, e.y]

please note how the environment e allows for the creation of logic programming symbols dynamically.

With this we now have program loaded in memory which can be explored as:

>>> print(nl.symbols['connected'])
connected: typing.AbstractSet[typing.Tuple[int, int]] = [(0, 1), (1, 2), (2, 3)]
>>> for rule in nl.current_program:
...   print(rule)
reachable(x, y) ← connected(x, y)
reachable(x, y) ← ( reachable(x, z) ) ∧ ( reachable(z, y) )

Finally, we can solve the query:

>>> with nl.environment as e:
...   res = nl.query((e.x, e.y), e.reachable(e.x, e.y))
>>> print(res)
   0  1
0  0  1
1  1  2
2  2  3
3  0  2
4  1  3
5  0  3

Alternatively the connected table can be added more efficiently from a tuple iterable or a numpy.array as follows:

>>> nl.add_tuple_set([(0, 1), (1, 2), (2, 3)], name='connected')

Including Aggregations and Builtin Functions#

Note

The aggregation API is under active development. The examples below illustrate the intended syntax.

Suppose that now we want to obtain the number of destinations reachable by each starting point. For this we need a new aggregation function that counts the number of distinct elements. Specifically:

>>> from typing import Iterable
>>> from neurolang import frontend
>>> nl = frontend.NeurolangDL()
>>> nl.add_tuple_set([(0, 1), (1, 2), (2, 3)], name='connected')
>>> @nl.add_symbol
>>> def agg_count(x: Iterable) -> int:
>>>   return len(set(x))
>>> with nl.environment as e:
...   e.reachable[e.x, e.y] = e.connected[e.x, e.y]
...   e.reachable[e.x, e.y] = e.reachable[e.x, e.z] & e.connected[e.z, e.y]

This adds a new function agg_count to the NeuroLang interpreter, any built-in or aggregation function is added in the same manner. Then, we can then count all arrivals for starts 0 and 1:

>>> with nl.environment as e:
>>>   e.count_arrivals[e.x, agg_count(e.y)] = e.reachable(e.x, e.y)
>>>   counts = nl.query((e.x, e.c), e.count_arrivals(e.x, e.c) & (e.x < 2))
>>> print(counts)
   0  1
0  0  3
1  1  2

Adding Constraints and Open Knowledge Rules#

NeuroLang also supports tuple-generating dependencies (TGDs). We can say that a person is a parent if they have a child:

>>> from neurolang.frontend import NeurolangPDL
>>> nl = NeurolangPDL()
>>> with nl.environment as e:
...   e.parent['John', 'Carl'] = True
...   e.parent['Mary', 'Carl'] = True
...   e.parent['Pat', 'Anna'] = True
...   e.parent['Anna', 'Pete'] = True
...   e.person[e.x] = e.parent[e.x, ...]
...   e.person[e.x] = e.parent[..., e.x]
...   nl.add_constraint(e.person[e.x], e.person[e.y] & e.parent[e.y, e.x])
...   e.has_parent[e.x] = e.person[e.x] & e.parent[e.x, e.y]
>>> res = nl.solve_all()
>>> print(res)


>>> from neurolang.frontend import NeurolangPDL
>>> nl = NeurolangPDL()
>>> with nl.environment as e:
...   e.person['Pat'] = True
...   e.person['Chris'] = True
...   nl.add_constraint(e.person[e.x], e.parent[e.y, e.x])
...   e.has_grand_parent[e.x] = e.person[e.x] & e.person[e.y] & e.parent[e.y, e.x] & e.parent[e.z, e.y]
>>> res = nl.solve_all()
>>> print(res)