tddlog for Python

Python bindings for tddlog, built with PyO3 and maturin. tddlog computes exact marginal probabilities for programs in the Datalog fragment of ProbLog, by compiling them bottom up into tree decision diagrams.

Install

pip install tddlog

Wheels cover CPython 3.10 and later on Linux x86-64, macOS arm64 and Windows x86-64. Anywhere else, pip builds from the source distribution, which needs a Rust toolchain (1.88 or newer) and a C++20 compiler.

Use

import tddlog

solution = tddlog.solve("""
    0.1::burglary.
    0.2::earthquake.
    0.7::hears_alarm(john).
    0.4::hears_alarm(mary).
    alarm :- burglary.
    alarm :- earthquake.
    calls(X) :- alarm, hears_alarm(X).
    evidence(calls(john), true).
    query(burglary).
    query(earthquake).
    query(calls(mary)).
""", evidence_probability=True)

for query, probability in solution.answers.items():
    print(f"{query}: {probability:.6f}")
print(f"P(e) = {solution.evidence_probability:.6f}")

prints

burglary: 0.357143
earthquake: 0.714286
calls(mary): 0.400000
P(e) = 0.196000

solution.answers maps each query, as ProbLog prints it (\+p for query(\+p)), to its probability, in the order of the program’s queries. It is None where the evidence is inconsistent. examples/ holds this program and one built from Python data.

Options

solve takes the command line’s options under its names:

  • vtree: the vtree strategy, "auto" by default, or one of the layouts tddlog --help lists ("td", "closed", "families", "families:0", "dynamic", "program", "program:fanin", "decision"). Every strategy that applies to a program gives the same answers; they differ in how fast they get there.

  • evidence_probability=True: also compute P(e), the probability of the evidence, as solution.evidence_probability and, exact below the least float, solution.log10_evidence_probability. Both are None otherwise. The evidence no query depends on is then compiled rather than dropped.

  • queries=False: leave out the program’s queries and compute P(e) alone, as for a partition function. It needs evidence_probability=True.

An unknown strategy, or options that ask for nothing, raise ValueError.

Errors

A program tddlog cannot answer raises a subclass of tddlog.Error whose message is the one the command line prints:

  • tddlog.ParseError: the program is not in the language tddlog reads;

  • tddlog.GroundingError: it uses a construct outside the supported fragment or has a rule that is not range restricted;

  • tddlog.CompilationError: the strategy asked for does not apply to the program, or the well-founded model of some world leaves an atom undefined (negation through a cycle with no two-valued reading).

The supported language is described in the guide.

Threads

The interpreter lock is released for the whole of a solve, so a caller can solve several programs at once from Python threads. Each solve runs on the thread that calls it.

Building from source

The extension builds the tddlog sources in the repository, so it needs a Rust toolchain, a C++20 compiler for the tree-decomposition solvers goatd vendors, and maturin. The three notice files are copied in from the repository root first, since pyproject.toml cannot reference files above its own directory:

mkdir -p bindings/python/notices
cp LICENSE NOTICE THIRD-PARTY.md bindings/python/notices/
pip install maturin
maturin build --release --manifest-path bindings/python/Cargo.toml

maturin develop --release installs the extension into the active virtualenv, and pytest bindings/python/tests tests it.

Examples

examples/alarm.py is the program above; its docstring works the answers out by hand:

"""The alarm example of Fierens et al. (2015): the probabilities of three
queries given that John calls, and the probability that he calls.

P(alarm) = 1 - 0.9 * 0.8 = 0.28 and P(calls(john)) = 0.28 * 0.7 = 0.196, so
P(burglary | calls(john)) = 0.1 * 0.7 / 0.196 = 0.357143. Run it with

    python examples/alarm.py

to print

    burglary: 0.357143
    earthquake: 0.714286
    calls(mary): 0.400000
    P(e) = 0.196000 (log10 -0.707744)
"""

import tddlog

PROGRAM = """
0.1::burglary.
0.2::earthquake.
0.7::hears_alarm(john).
0.4::hears_alarm(mary).
alarm :- burglary.
alarm :- earthquake.
calls(X) :- alarm, hears_alarm(X).
evidence(calls(john), true).
query(burglary).
query(earthquake).
query(calls(mary)).
"""

solution = tddlog.solve(PROGRAM, evidence_probability=True)
for query, probability in solution.answers.items():
    print(f"{query}: {probability:.6f}")
print(
    f"P(e) = {solution.evidence_probability:.6f}"
    f" (log10 {solution.log10_evidence_probability:.6f})"
)

examples/graph.py builds a program from Python data:

"""A program built from Python data: the probability that a path joins two
nodes of a graph whose edges are present independently, ProbLog's system
test 7. ProbLog answers path(1,5) 0.25824 and path(1,6) 0.2167296.

Run it with

    python examples/graph.py

to print

    path(1,5): 0.258240
    path(1,6): 0.216730
"""

import tddlog

EDGES = {
    (1, 2): 0.6,
    (1, 3): 0.1,
    (2, 5): 0.4,
    (2, 6): 0.3,
    (3, 4): 0.3,
    (4, 5): 0.8,
    (5, 6): 0.2,
}
QUERIES = [(1, 5), (1, 6)]

facts = [f"{p}::edge({a},{b})." for (a, b), p in EDGES.items()]
rules = [
    "path(X,Y) :- edge(X,Y).",
    "path(X,Y) :- edge(X,Z), path(Z,Y).",
]
queries = [f"query(path({a},{b}))." for a, b in QUERIES]
program = "\n".join(facts + rules + queries)

solution = tddlog.solve(program)
for query, probability in solution.answers.items():
    print(f"{query}: {probability:.6f}")

The guide describes the language tddlog reads, with worked examples that open in the playground.

Reference

tddlog.solve(program, *, vtree='auto', evidence_probability=False, queries=True)

Parse, ground, compile and count program, a ProbLog program as text, and return the answers to its queries.

vtree is the command line’s --vtree: "auto", the default, or one of the layouts it lists. evidence_probability=True also computes P(e), compiling the evidence no query depends on rather than dropping it. queries=False leaves out the program’s queries, as --no-queries does, and needs evidence_probability=True.

An unknown vtree, or options that ask for nothing, raise ValueError; a program tddlog cannot answer raises a subclass of tddlog.Error. The interpreter lock is released for the whole run.

class tddlog.Solution

The answers to a program’s queries, and the probability of its evidence where solve was asked for it.

answers

A dict from each query, as ProbLog prints it (\+ before a negative one), to its probability, in the program’s order; None where the evidence is inconsistent.

evidence_probability

P(e), the probability of the evidence: 1.0 with none, 0.0 where it is inconsistent, and 0.0 too where it is under the least float, which log10_evidence_probability still gives. None unless solve was called with evidence_probability=True.

log10_evidence_probability

The base-10 logarithm of P(e), exact beyond a float’s range: -inf where the evidence is inconsistent. None unless solve was called with evidence_probability=True.

exception tddlog.Error

A program tddlog could not answer: the base of ParseError, GroundingError and CompilationError.

exception tddlog.ParseError

The program is not in the language tddlog reads.

exception tddlog.GroundingError

The program parses but cannot be ground: an unsupported construct or a rule that is not range restricted.

exception tddlog.CompilationError

The ground program could not be compiled and counted under the vtree strategy asked for, or the well-founded model of some world leaves an atom undefined.