tddlog guide

Write programs for tddlog, run them from the command line, and call it from Rust, Python and C. Every worked example opens in the playground. For how tddlog compares with other systems, see the results by program type, by baseline and by integration.

Getting started

tddlog computes exact probabilities for ProbLog programs and for Bayesian and Markov networks. It also reads Dice programs, Circulette netlists and LPADs (below).

  • In the browser: the playground, with nothing to install.
  • Download the binary for your platform from the release, with its examples/:
    curl -L https://github.com/Tractables/tddlog/releases/download/v0.1.1/tddlog-v0.1.1-x86_64-unknown-linux-gnu.tar.gz | tar xz
    cd tddlog-v0.1.1-x86_64-unknown-linux-gnu
    ./tddlog examples/alarm.pl
    For other platforms, replace x86_64-unknown-linux-gnu with aarch64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-apple-darwin or x86_64-pc-windows-msvc (a .zip). On macOS, a binary downloaded with a browser first needs xattr -d com.apple.quarantine tddlog.
  • From Python: pip install tddlog. For Rust and C, see below.
  • From crates.io, with Rust 1.88 or newer and a C++20 compiler:
    cargo install --locked tddlog
    If the build fails
    • "feature edition2024 is required": this cargo is older than 1.88. Install Rust from rustup, then run . "$HOME/.cargo/env".
    • "unrecognized command line option '-std=c++20'": this g++ is older than 11. Install a newer one; the build finds g++-12 to g++-14. Otherwise, name it in GOATD_CXX (GOATD_CXX=g++-11 cargo install ...).
    • No compiler or make: install them with sudo apt install build-essential on Debian or Ubuntu, or xcode-select --install on macOS.

The commands below run in the archive's directory, or in a checkout of the repository (the few that read tests/ need the checkout). Put tddlog on your PATH to run it by name.

A first program

examples/alarm.pl is the alarm example of Fierens et al. (2015):

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)).

Each p::fact. is true with probability p, independently of the others, and the rules derive the other atoms. Each query asks for its atom's probability, given the evidence that John calls:

$ tddlog examples/alarm.pl
burglary:       0.357143
earthquake:     0.714286
calls(mary):    0.400000

To check this by hand: the alarm goes off with probability 1 − 0.9 × 0.8 = 0.28, so John calls with probability 0.28 × 0.7 = 0.196. That gives P(burglary | John calls) = 0.1 × 0.7 / 0.196 = 0.357143.

--evidence-probability also prints P(e), with its base-10 logarithm, which stays exact even where P(e) is too small for a floating-point number:

$ tddlog --evidence-probability examples/alarm.pl
burglary:       0.357143
earthquake:     0.714286
calls(mary):    0.400000
P(e):   1.960000e-1     log10 -0.707744

Open this program in the playground to edit it and run it again.

The language

tddlog reads ProbLog without cut. A program for tddlog is a program ProbLog reads too, and the tests check tddlog's answers against ProbLog's.

Facts and rules

edge(1,2).                           % a fact
0.6::edge(2,3).                      % a probabilistic fact
path(X,Y) :- edge(X,Y).              % rules
path(X,Y) :- edge(X,Z), path(Z,Y).
0.7::alarm :- burglary.              % a probabilistic rule
  • A name that starts with a lower-case letter is a constant or a predicate. One that starts with an upper-case letter or _ is a variable.
  • A probability is a number or a fraction, such as 1/3::a.
  • A probabilistic rule makes one independent choice for each instance of its variables. Under 0.6::q(X) :- p(X)., each q(X) whose p(X) holds is true with probability 0.6.
  • A body is a conjunction (,) and may hold disjunctions (;) in parentheses.
  • % and /* start comments.

Annotated disjunctions

0.2::color(red); 0.5::color(green); 0.3::color(blue).
0.15::headlights(bright); 0.75::headlights(dim); 0.1::headlights(off) :- battery(weak).

An annotated disjunction makes one choice among its heads: here the color is red with probability 0.2, green with 0.5 and blue with 0.3.

  • Probabilities that sum to less than one leave the rest to none of the heads.
  • With a body, as on the second line, the choice is made where the body holds, once for each instance of the variables.
  • A table of a Bayesian network is one annotated disjunction per row.

Negation

b :- \+ a.
c :- not(a).
\+ d :- b.

\+ a, or not(a), holds in a world where a cannot be derived: negation as failure. A negated head, as on the third line, makes d false wherever b holds, as in ProbLog.

Negation through recursion

A negated literal may reach back into its rule's own recursion (examples below). tddlog then reads each world by its well-founded model, which is ProbLog's semantics. Where some world leaves an atom undefined, the program has no distribution, and tddlog reports that and gives no answer.

Arithmetic and comparisons

n(1). n(2). n(3).
0.5::x(N) :- n(N).
s(S) :- x(A), x(B), A < B, S is A + B.

is evaluates arithmetic as ProbLog and Python do. A positive atom of the body must bind the variables of an expression or a comparison. tddlog orders the body so that those atoms come first.

The operators
  • Arithmetic: +, -, *, /, // (rounded down), mod, **, abs, min, max and the like. / is in floating point: X is 4 / 2 binds 2.0.
  • Arithmetic comparisons, which evaluate both sides: <, >, =<, >=, =:= and =\=.
  • Unification: = and \=.
  • Comparison of terms in ProbLog's standard order: ==, \==, @< and the like.

Terms, lists and findall

0.4::item(a). 0.7::item(b). 0.5::item(c).
cost(a, 3). cost(b, 4). cost(c, 5).
first_two :- nth1(1, [a, b, c], X), nth1(2, [a, b, c], Y), item(X), item(Y).
total(S) :- findall(C, cost(_, C), L), sum_list(L, S).
query(first_two).
query(total(_)).

Arguments may be compound terms, f(X, g(Y)), and lists, [a, b | T]. The list predicates of ProbLog's library are built in, and findall/3 collects the solutions of a goal.

The built-in predicates

Built in unless the program defines its own:

  • member/2, memberchk/2, append/3, append/2, length/2, nth0/3, nth1/3, last/2, reverse/2, sort/2, flatten/2;
  • sum_list/2, max_list/2, min_list/2;
  • subset/2, intersection/3, union/3, subtract/3, delete/3;
  • between/3, plus/3, compare/3, is_list/1.

N1 = N - 1 binds N1 to the term N - 1, which is evaluates.

findall over a probabilistic goal

Over a deterministic goal, findall/3 collects the solutions in ProbLog's order. The list may then be counted, summed or sorted under probabilistic facts, as total above.

Over a probabilistic goal, the answer is a distribution over lists, each solution listed once per proof. The order of such a list follows tddlog's proofs, which may differ from ProbLog's engine. A program that reads the order (nth1/3, the list's head) may answer differently. Uses that ignore the order agree: length/2, sum_list/2, member/2, sort/2.

Evidence and queries

evidence(calls(john), true).
evidence(calls(mary), false).
evidence(alarm).                     % the same as evidence(alarm, true)
query(burglary).
query(\+ burglary).
query(path(1,X)).                    % each path(1,X) that can hold

The queries are answered given all the evidence. A query with variables asks for each of its instances that can hold in some world.

  • A rule whose head is a query, as query(path(1,X)) :- target(X)., asks for the instances its body allows.
  • Evidence that no world satisfies is reported as evidence is inconsistent: probability zero.

What tddlog does not read

Each of these stops tddlog with a message and exit status 1:

$ tddlog choice.pl
parse error: line 1: if-then-else is not supported
  • strings, cut and if-then-else;
  • built-ins beyond arithmetic, comparison, the list predicates and findall/3;
  • a predicate without clauses in a query, an observation or a call, which ProbLog refuses too;
  • a findall/3 whose goal calls the findall again;
  • a grounding that does not end, as nat(s(X)) :- nat(X). asked for every nat(X);
  • facts with variables (p(X).) and rules that are not range restricted.
The details
  • A misspelt predicate name stops tddlog. A call that no query and no observation reaches is left alone.
  • Range restriction: each variable of a head, of a negated literal and of a comparison must occur in a positive atom of the body. A query or a call can bind a head's variables.
  • A variable that only a negated atom has is read as ProbLog reads it. \+ p(X, _) means no p(X, Y) for any Y.
  • Of ProbLog 2.3's 109 system tests, tddlog passes 52, reports as errors the 9 that ProbLog expects to fail, and reports the other 48 as unsupported (tests/problog_suite.py).

Worked examples

Programs from the repository's examples/, each with its answers. tests/it/examples.rs checks them.

A Bayesian network with evidence

examples/bn_car_evidence.pl is a car that does not start, a network of thirteen variables (examples/car.net). Each variable's table is one annotated disjunction per row of its parents' values. A row certain of one value is a rule:

0.3::weather(cold); 0.5::weather(mild); 0.2::weather(hot).
0.7::battery_age(new); 0.3::battery_age(old).
0.1::lights_left_on(yes); 0.9::lights_left_on(no).
0.2::battery(charged); 0.4::battery(weak); 0.4::battery(flat) :- battery_age(new), lights_left_on(yes).
0.9::battery(charged); 0.08::battery(weak); 0.02::battery(flat) :- battery_age(new), lights_left_on(no).
0.05::battery(charged); 0.35::battery(weak); 0.6::battery(flat) :- battery_age(old), lights_left_on(yes).
0.6::battery(charged); 0.3::battery(weak); 0.1::battery(flat) :- battery_age(old), lights_left_on(no).
0.95::headlights(bright); 0.04::headlights(dim); 0.01::headlights(off) :- battery(charged).
0.15::headlights(bright); 0.75::headlights(dim); 0.1::headlights(off) :- battery(weak).
0.1::headlights(dim); 0.9::headlights(off) :- battery(flat).
0.97::starter(ok); 0.03::starter(faulty).
0.97::cranks(yes); 0.02::cranks(slowly); 0.01::cranks(no) :- battery(charged), starter(ok).
0.1::cranks(slowly); 0.9::cranks(no) :- battery(charged), starter(faulty).
0.3::cranks(yes); 0.6::cranks(slowly); 0.1::cranks(no) :- battery(weak), starter(ok).
0.05::cranks(slowly); 0.95::cranks(no) :- battery(weak), starter(faulty).
0.05::cranks(slowly); 0.95::cranks(no) :- battery(flat), starter(ok).
cranks(no) :- battery(flat), starter(faulty).
0.55::fuel(full); 0.35::fuel(low); 0.1::fuel(empty).
0.9::fuel_gauge(full); 0.08::fuel_gauge(low); 0.02::fuel_gauge(empty) :- fuel(full).
0.1::fuel_gauge(full); 0.8::fuel_gauge(low); 0.1::fuel_gauge(empty) :- fuel(low).
0.3::fuel_gauge(low); 0.7::fuel_gauge(empty) :- fuel(empty).
0.8::plugs(ok); 0.2::plugs(fouled) :- weather(cold).
0.95::plugs(ok); 0.05::plugs(fouled) :- weather(mild).
0.97::plugs(ok); 0.03::plugs(fouled) :- weather(hot).
0.99::spark(yes); 0.01::spark(no) :- plugs(ok), battery(charged).
0.9::spark(yes); 0.1::spark(no) :- plugs(ok), battery(weak).
spark(no) :- plugs(ok), battery(flat).
0.4::spark(yes); 0.6::spark(no) :- plugs(fouled), battery(charged).
0.2::spark(yes); 0.8::spark(no) :- plugs(fouled), battery(weak).
spark(no) :- plugs(fouled), battery(flat).
0.98::fires(yes); 0.02::fires(no) :- spark(yes), fuel(full).
0.95::fires(yes); 0.05::fires(no) :- spark(yes), fuel(low).
fires(no) :- spark(yes), fuel(empty).
fires(no) :- spark(no), fuel(full).
fires(no) :- spark(no), fuel(low).
fires(no) :- spark(no), fuel(empty).
0.99::starts(yes); 0.01::starts(no) :- cranks(yes), fires(yes).
starts(no) :- cranks(yes), fires(no).
0.6::starts(yes); 0.4::starts(no) :- cranks(slowly), fires(yes).
starts(no) :- cranks(slowly), fires(no).
starts(no) :- cranks(no), fires(yes).
starts(no) :- cranks(no), fires(no).
evidence(weather(cold)).
evidence(headlights(bright)).
evidence(fuel_gauge(low)).
evidence(starts(no)).
query(fuel(full)).
query(fuel(low)).
query(fuel(empty)).
query(plugs(ok)).
query(plugs(fouled)).
query(starter(ok)).
query(starter(faulty)).

On a cold morning the headlights are bright, the fuel gauge reads low and the car does not start. The queries ask whether it is the fuel, the plugs or the starter:

$ tddlog --evidence-probability examples/bn_car_evidence.pl
fuel(full):     0.087223
fuel(low):      0.621333
fuel(empty):    0.291445
plugs(ok):      0.533975
plugs(fouled):  0.466025
starter(ok):    0.901450
starter(faulty):        0.098550
P(e):   2.262810e-2     log10 -1.645352

An empty tank goes from 0.10 to 0.29, fouled plugs from 0.20 on a cold day to 0.47, and a faulty starter from 0.03 to 0.10. The last line is the probability of the four observations together. tddlog also reads this network from its file.

Open it in the playground.

Reachability in a graph with cycles

examples/cycle_graph.pl asks how likely a path is from node 1 to each other node. Each of the graph's fourteen edges is present with its own probability:

0.6::edge(1,2). 0.5::edge(2,3). 0.7::edge(3,1). 0.4::edge(3,4). 0.8::edge(4,5).
0.3::edge(5,3). 0.6::edge(5,6). 0.5::edge(6,4). 0.4::edge(2,6). 0.3::edge(6,1).
0.5::edge(4,7). 0.6::edge(7,2). 0.4::edge(7,5). 0.7::edge(1,5).
path(X,Y) :- edge(X,Y).
path(X,Y) :- edge(X,Z), path(Z,Y).
query(path(1,2)).
query(path(1,3)).
query(path(1,4)).
query(path(1,5)).
query(path(1,6)).
query(path(1,7)).
$ tddlog examples/cycle_graph.pl
path(1,2):      0.632256
path(1,3):      0.460356
path(1,4):      0.399624
path(1,5):      0.754432
path(1,6):      0.571699
path(1,7):      0.199812

The graph has cycles, such as 1, 2, 3 and back to 1. tddlog handles the recursion through them.

How tddlog handles the cycles

A world's paths are the least fixpoint of the two rules. tddlog grounds the rules only for the paths that start at node 1, which are all the queries need. It then writes the reachability without recursion before it compiles the program: it eliminates the nodes one at a time, as Floyd and Warshall's transitive closure does. After each step, what connects two nodes is a path whose inner nodes have all been eliminated. Eliminating node k adds to each such connection from i to j the paths that go from i to k and on from k to j. Each eliminated path(1,k) is defined from the nodes eliminated after it, so no cycle is left. A connection that joins several paths and goes on becomes an atom of its own, named for what it means: '$via'(path(1,2),path(1,4),2) holds where a path leads from 2 to 4 whose inner nodes are among the first two eliminated, 3 and 6. tddlog --ground --vtree closed examples/cycle_graph.pl prints the program without recursion, the order of the elimination first. Every new atom is fixed by the edges, so the probabilities are unchanged, and the atoms share their parts: the paths they cover are never listed one by one.

Open it in the playground.

Integers in binary

examples/adder.pl adds two numbers with a ripple-carry adder. Each bit of a number is a random choice of its own, as in Dice and Alea.jl, so a number from 0 to 7 costs three choices:

bit(0). bit(1). bit(2).
0.7::x(0). 0.3::x(1). 0.8::x(2).
0.4::y(0). 0.6::y(1). 0.2::y(2).

% exactly one of x(I) and y(I)
half(I) :- x(I), \+ y(I).
half(I) :- y(I), \+ x(I).
% the carry into bit I + 1
carry(J) :- x(I), y(I), J is I + 1.
carry(J) :- half(I), carry(I), J is I + 1.
% bit I of the sum
sum(I) :- half(I), \+ carry(I).
sum(I) :- bit(I), \+ half(I), carry(I).
sum(3) :- carry(3).
at_least_8 :- sum(3).

evidence(sum(0), false).
query(sum(1)).
query(sum(2)).
query(at_least_8).
query(x(0)).

x(I) is bit I of x, bit 0 the least significant, so x is odd with probability 0.7. The evidence says that the sum is even. at_least_8 compares the sum with a constant by reading its bits.

$ tddlog --evidence-probability examples/adder.pl
sum(1): 0.491304
sum(2): 0.496870
at_least_8:     0.505913
x(0):   0.608696
P(e):   4.600000e-1     log10 -0.337242

tddlog reads a Dice program as a program like this one.

Check it by hand

The sum is odd where exactly one of x and y is, with probability 0.7 × 0.6 + 0.3 × 0.4 = 0.54, so P(e) = 0.46. Both are odd with probability 0.7 × 0.4 = 0.28, so P(x odd | e) = 0.28 / 0.46 = 0.608696. The other answers sum the 64 combinations of the six bits. The test checks them against that sum and against ProbLog.

Open it in the playground.

Negation through recursion

A rule may read the negation of an atom that depends on the rule's own head. Three programs show what tddlog then does.

A game

In examples/win.pl, a position of a game is won if some move leads from it to a position that is not won. Each move is present with probability 0.5:

0.5::move(1,2). 0.5::move(1,3). 0.5::move(2,3). 0.5::move(3,4).
win(X) :- move(X,Y), \+ win(Y).
query(win(1)).
query(win(2)).
query(win(3)).
$ tddlog examples/win.pl
win(1): 0.562500
win(2): 0.250000
win(3): 0.500000

The rule defines win through its own negation, so the program is not stratified. The moves form no cycle, so each world still has one reading.

Check it by hand

Position 4 has no moves and is lost, so position 3 is won where the move from 3 to 4 is present: 0.5. Position 2 is won where the move to 3 is present and 3 is lost: 0.5 × 0.5 = 0.25. Position 1 is won through 2 with probability 0.5 × 0.75 = 0.375, through 3 with 0.5 × 0.5 = 0.25, and through both with 0.0625. So P(win(1)) = 0.375 + 0.25 − 0.0625 = 0.5625.

--ground prints the program tddlog compiles, in which win(3) has become a probabilistic fact:

$ tddlog --ground examples/win.pl
0.5::move(1,2).
0.5::move(1,3).
0.5::move(2,3).
0.5::win(3).
win(1) :- move(1,2), \+win(2).
win(1) :- move(1,3), \+win(3).
win(2) :- move(2,3), \+win(3).
query(win(1)).
query(win(2)).
query(win(3)).

Open it in the playground.

Gear wheels

In examples/gearwheels.pl (Bogaerts and Van den Broeck, 2015, Example 4.13), two gear wheels turn each other, and a button toggles each wheel from time 0 to time 1. turns1 depends on its own negation:

0.3::button1(0).
0.6::button2(0).
turns1(0) :- turns2(0).
turns2(0) :- turns1(0).
turns1(1) :- turns2(1).
turns2(1) :- turns1(1).
turns1(1) :- turns1(0), \+button1(0).
turns2(1) :- turns2(0), \+button2(0).
turns1(1) :- \+turns1(0), button1(0).
turns2(1) :- \+turns2(0), button2(0).
query(turns1(0)).
query(turns1(1)).
query(turns2(1)).
$ tddlog examples/gearwheels.pl
turns1(0):      0.000000
turns1(1):      0.720000
turns2(1):      0.720000

At time 0 nothing starts the wheels, and the well-founded model does not let the two support each other, so neither turns. At time 1 a wheel turns where its button is pressed, and then both turn: 1 − 0.7 × 0.4 = 0.72.

Open it in the playground.

A program with no distribution

Where the well-founded model leaves an atom undefined, the program has no distribution. In examples/unsound.pl, from the same paper, a :- \+b. and b :- \+a. leave both undefined in every world. tddlog stops with exit status 1:

$ tddlog examples/unsound.pl
compilation error: atom a is undefined in the well-founded model of some possible world (negation through a cycle); no probability

The command line

tddlog [options] program.pl [argument ...] prints one atom:<tab>probability line per query, in the program's order, as ProbLog does.

Options

--evidence-probability
After the queries, print P(e) and its base-10 logarithm.
--no-queries
With --evidence-probability, print P(e) alone, as for a partition function. tddlog then compiles only what the observations depend on.
--precision <n>
Digits after the decimal point, 6 by default.
--vtree <strategy>
How to build the vtree. auto, the default, tries the other strategies. Every strategy that answers gives the same answers.
--ground
Print the relevant ground program and exit. ProbLog reads the output.
--program
Print the ProbLog program tddlog reads the input as, with the arguments in place.
--stats
Print what tddlog did to stderr: sizes, times and the trials of auto.
--trace <file>
Write each stage of the run to the file as JSON. The playground's Trace box shows the same stages.

tddlog --help lists the rest.

More on the options and the exit status
  • --vtree: the strategies are td, closed, families, dynamic, program and decision. They differ in speed. td, families and decision fail on some programs, with a message that says why. auto runs trials of them over rewrites of the program, under bounds on size and work that double until one finishes.
  • --evidence-probability prints P(e):<tab>p<tab>log10 l, p in scientific notation and l its base-10 logarithm.
  • --precision: a probability under 10−4 that is not zero is written in scientific notation, as ProbLog writes it.
  • --trace summarizes what each stage took in and gave out, or how far the run got before its error. The answers on stdout are the same.
  • Exit status 0: answers, also where the evidence is inconsistent.
  • Exit status 1: a program tddlog cannot answer, with the reason on stderr.
  • Exit status 2: options it does not accept, checked before it reads the program. These are an unknown option, one given twice, or one that the others leave unused.

Program arguments

The arguments after a ProbLog program or an LPAD take its places $1, $2, ..., as a shell script's arguments do. One program then serves every size, probability and query. examples/chain.pl:

$2::link(I) :- between(1, $1, I).
reach(0).
reach(I) :- between(1, $1, I), J is I - 1, reach(J), link(I).
query(reach($3)).

tddlog examples/chain.pl 10 0.9 4 answers it for a chain of 10 links of probability 0.9, queried at node 4.

The rules for arguments
  • Each argument is read as a term: a number, an atom, a list or a compound.
  • Places inside comments and quoted atoms are left alone.
  • A place without an argument, an argument without a place and an argument that is not a term are errors.
  • tddlog --program examples/chain.pl 10 0.9 4 prints the program with the arguments in place, for a system that does not take them.

Networks and other languages

tddlog reads each of these inputs as a ProbLog program, which --program prints. The command line takes the format from the file's extension. The playground's Format menu and the bindings take its name.

InputExtensionFormat name
ProbLog, LPADany otherproblog
Bayesian network, BIF.bifbif
Bayesian network, Hugin.net, .huginhugin
Bayesian network, GeNIe.xdslxdsl
Bayesian or Markov network, UAI.uaiuai
Dice program.dicedice
Circulette netlist.cqlcirculette

Alea.jl and Roulette have tddlog backends of their own. The results by integration compare each with its own inference.

Bayesian and Markov networks

tddlog answers a discrete network's marginals given the evidence:

tddlog examples/car.net --query leaves
tddlog tests/regress/markov.uai --evidence tests/regress/markov.uai.evid --evidence-probability
tddlog --program examples/car.net > car.pl
  • By default, it queries every value of every unobserved variable.
  • --query leaves asks for the variables without children, --query a,b for the variables named.
  • --evidence names the evidence file: a UAI .evid file for a UAI network, an Ace .inst file for any other.

From the bindings, pass the network's text, its format and the evidence file's text. They query every variable.

solution = tddlog.solve(open("examples/car.net").read(), format="hugin",
                        evidence=open("examples/car.inst").read())

Open the car network's file in the playground.

Partition functions, rescaling and the encoding
  • For a UAI network, --evidence-probability prints the partition function given the evidence. Where it differs from P(e), the line starts with Z: in place of P(e):.
  • With --no-queries, tddlog prints that line alone, the UAI PR task.
  • --rescale divides every row by its sum, for a file that writes its tables in percent. A UAI file is read as written.
  • A Markov network's factors each become an observed atom. Network files in docs/design.md has the encoding.
  • The benchmarks give --program's output to the other systems that read ProbLog.

Dice programs

tddlog program.dice answers a Dice program's result. tddlog --program program.dice prints the ProbLog program it is read as, with integers as bits, as in the adder.

Open a random walk in the playground.

How a Dice program is read
  • An integer of w bits becomes w atoms, each flip a probabilistic fact, and the integer operations rules over the bits.
  • iterate becomes recursion over a step index, its count a fact (steps(10).).
  • The count may be the program's argument. iterate(step, x, $1), read by tddlog program.dice 10, is written steps($1)., one program of every count.
  • A recursive function is inlined up to Dice's limit of ten calls.
  • A list is held as Dice holds one, its length and ten elements.
  • /, % and sample are refused.
  • data/dice on the baselines branch holds the Dice paper's programs.

Circulette netlists

tddlog reads a .cql file as a netlist of Circulette, a tool for the fault analysis of digital circuits on Roulette. It answers Circulette's queries: every gate's fault, and every pair and triple of faults, given that the circuit fails.

How a netlist is read
  • The circuit is evaluated for one cycle with its inputs and registers false.
  • Each logic gate the outputs read is faulty with probability 1 - p-clean, which flips its output.
  • The circuit fails where a bit of an output or of a register's next value is one. The failure is the evidence.
  • The program states the netlist as facts, with one rule for each kind of gate over its operands' bits. tddlog --program prints it.
  • tests/circulette holds netlists written for the tests.

Alea.jl programs

pr(x; algo = Tddlog()) answers an Alea.jl program with tddlog. The backend is on the branch tddlog of Tractables/Alea.jl. It hands tddlog the Boolean circuit Alea.jl builds: each flip a probabilistic fact, each gate a rule, each observation evidence.

write_problog(io, x) writes that program, here with ALEA a checkout of the branch:

julia --project=ALEA -e 'using Alea; include("examples/alea_dice.jl");
  open(io -> write_problog(io, model(); query = QUERY, label = "alea_dice.jl"), "alea_dice.pl", "w")'
The queries

Each component of the value is queried. For a Boolean, that is its probability. Under query = :pr, it is the probability of each world of a number's bits. Under query = :expectation, it is each bit's probability, of which the expectation is a weighted sum. write_problog's documentation names the queries.

Roulette programs

Roulette's tddlog engine runs a Roulette program unchanged, with tddlog in place of RSDD. It is on the branch tddlog of Tractables/roulette, a fork of Roulette. The circuit Roulette builds for each query goes to tddlog as ProbLog. Without --tddlog, the engine writes one program for all the queries:

racket -l- roulette/engine/tddlog PROGRAM.rkt > PROGRAM.pl
racket -l- roulette/engine/tddlog --tddlog tddlog PROGRAM.rkt

LPADs

tddlog reads a logic program with annotated disjunctions (an LPAD) written for cplint or PITA. A file that holds :- begin_lpad. or :- begin_plp. is read as the ProbLog program that --program prints.

tddlog tests/lpad/coin.cplint.pl
tddlog --program tests/lpad/coin.cplint.pl > coin.pl
How an LPAD is read
  • The LPAD is the clauses between :- begin_lpad. and :- end_lpad.
  • h1:p1 ; h2:p2 :- body. is read as p1::h1; p2::h2 :- body. and a:p. as p::a. A head without a probability takes what the others leave.
  • The clauses outside the markers are cplint's database clauses. The LPAD calls them through db(Goal) or the goal of a built-in such as findall/3. Those it calls are kept, and db(Goal) is read as Goal.
  • The rest is left out, with the directives: a program that plots the LPAD's answers, for example.
  • ?- prob(Q, P). becomes query(Q).
  • ?- prob(Q, E, P). becomes query(Q). and an evidence(L). for each literal L of the conjunction E.
  • A program has one set of evidence, that of the file's first query. A query asked under other evidence, or under an intervention do(L), is left out. A comment and standard error name it.
  • A clause that uses what tddlog does not read stops tddlog with its message.

From Rust, Python and C

Each binding takes the program's text and the command line's options. It returns the answers in the order of the program's queries. For another format, pass the text with the format's name.

Rust

Add the crate to Cargo.toml. The build needs a C++ compiler, as in Getting started.

[dependencies]
tddlog = "0.1"

tddlog::solve takes the program, Options and a function that receives what --stats would print:

let program = "
    0.1::burglary.
    0.2::earthquake.
    0.7::hears_alarm(john).
    alarm :- burglary.
    alarm :- earthquake.
    calls(X) :- alarm, hears_alarm(X).
    evidence(calls(john), true).
    query(burglary).
";
let options = tddlog::Options { evidence_probability: true, ..tddlog::Options::default() };
let solution = tddlog::solve(program, &options, &mut |_| {})?;
// `answers` is `None` where the evidence is inconsistent.
for answer in solution.answers.as_deref().unwrap_or_default() {
    println!("{answer}: {}", answer.probability);
}

The API documentation describes Options, Solution and tddlog::trace::Trace, which writes the run's stages as --trace does. tddlog::read reads an input in any format as the program solve takes:

let format: tddlog::Format = "hugin".parse()?;
let (net, inst) = (std::fs::read("examples/car.net")?, std::fs::read("examples/car.inst")?);
let read = tddlog::network::ReadOptions { evidence: Some(format.evidence(&inst)), ..Default::default() };
let input = tddlog::read(&net, format, &read)?;
let solution = tddlog::solve(&input.program, &options, &mut |_| {})?;
// P(e), or a Markov network's partition function.
let p = input.evidence_probability(&solution);

Python

import tddlog

solution = tddlog.solve(open("examples/alarm.pl").read(), evidence_probability=True)
for query, probability in solution.answers.items():
    print(f"{query}: {probability:.6f}")
print(f"P(e) = {solution.evidence_probability:.6f}")

pip install tddlog installs it. solve takes the options as keywords. The Python page has the rest.

Wheels, keywords and errors
  • Wheels: CPython 3.10 or later, on Linux and macOS (x86-64 and ARM64) and Windows x86-64. Elsewhere pip builds from source.
  • Keywords: format, evidence, vtree, evidence_probability, queries and arguments, a list of the program's arguments as strings.
  • An error is an exception of its own: tddlog.ParseError, GroundingError or CompilationError.

C

TddlogOptions options = tddlog_options_default();
options.evidence_probability = true;
TddlogSolution *solution;
if (tddlog_solve(program, &options, &solution) != TDDLOG_OK) {
    fprintf(stderr, "%s\n", tddlog_last_error_message());
    return 1;
}
for (size_t i = 0; i < tddlog_solution_num_answers(solution); i++)
    printf("%s:\t%f\n", tddlog_solution_query(solution, i),
           tddlog_solution_probability(solution, i));
tddlog_solution_free(solution);

The C library comes prebuilt in the release's C archives, or installs with CMake. The C page covers building, linking and the header.

The other options
  • options.format names the input's format, NULL for ProbLog.
  • options.evidence holds a network's evidence file, NULL for none.
  • options.arguments points to options.num_arguments strings, the program's arguments.
  • CMake installs a CMake package and a pkg-config file.

How it works

tddlog compiles the program into tree decision diagrams (TDDs) and counts the queries on them. The site's How it works walks through a run, and the playground's Trace box shows these stages for any program.

  1. Ground. tddlog parses the program and grounds it bottom up, only for the atoms the queries and the evidence depend on.
  2. Simplify. It simplifies the acyclic part of the ground program as a logic synthesis tool simplifies a circuit, and merges gates that compute the same function.
  3. Choose a vtree. The vtree decides the diagrams' sizes. The default, auto, tries several under bounds that double until one finishes.
  4. Compile. It compiles each derived atom into a TDD by Tp-compilation. It applies the rules to diagrams until the diagrams stop changing.
  5. Count. It conjoins the evidence and counts each query against it. The weighted count of a diagram is the probability of the worlds it holds in.

The TDDs are those of the tididi library. docs/design.md describes each stage and the decisions behind it.

The methods and their papers
  • TDDs: a tree decision diagram is a canonical decision diagram shaped by a vtree (Capelli, Choi, Mengel, Muñoz and Van den Broeck, SAT 2026).
  • Simplification: the derived atoms outside recursion, the gates, are built into an and-inverter graph. Each gate is the OR of its rules' bodies, each body the AND of its literals. The two-input ANDs are hashed as they are created, so a gate the program writes twice is one node.
  • Equal gates: a gate may compute an earlier gate's function by other structure, or its complement, or a constant. tddlog finds candidates by simulating the graph on random inputs and proves them equal with a SAT solver, as in Mishchenko, Chatterjee, Brayton and Eén (2006). It merges such gates into the earlier ones, with exceptions that docs/design.md lists. The bit-blasted circuits that Alea.jl writes compute many of their gates more than once.
  • Tp-compilation: Vlasselaer et al., 2015 and 2016. The rules are applied component by component.
  • Negation through recursion takes the well-founded fixpoint of Bogaerts and Van den Broeck (2015).

The results compare tddlog with the other exact systems: by program type, by baseline, by integration and on the circuits.

Terms

The playground's account of a run and the results page use these words.

Ground program
The program without variables that tddlog compiles. --ground prints it. Its facts are the probabilistic facts and the choices of the annotated disjunctions, whose values make a world. Its derived atoms are the ones the rules define.
Component
Derived atoms that depend on each other through the rules (a strongly connected component). tddlog compiles the components in the order of their dependencies. It iterates a cyclic component, as in the reachability example, until its formulas stop changing.
TDD
A tree decision diagram, the representation tddlog compiles into. It is canonical: a function has one TDD on a given vtree.
Vtree
A binary tree whose leaves are the diagrams' variables. Every diagram of a run is built on the same vtree. The vtree decides the diagrams' sizes: one function can take a few pairs on one vtree and millions on another.
Pairs
The size of a TDD, as a BDD's size is the number of its nodes. A node of a TDD is a list of pairs, each a child on the vtree node's left and one on its right. The playground's "pairs at most" is the largest diagram a run built.
Formula
The TDD of a derived atom. It holds in exactly the worlds in which the atom does.
Layout
A way to build the vtree, with the rewrite of the program that it compiles: one of the strategies of --vtree other than auto. The layout a run names is the one that answered.
Trial
A layout that auto runs under bounds on the diagrams' size and on the work that builds them. auto doubles the bounds each turn, and the first trial to finish answers. The bounds are counts, never seconds, so a run takes the same decisions on every machine.
Part
Some queries depend on a part of the program much smaller than the whole. tddlog then answers the queries in groups, each on its own part, with a layout and a vtree of its own.
P(e)
The probability of the evidence: the sum of the probabilities of the worlds in which every observation holds. Its base-10 logarithm stays exact where P(e) is smaller than the least floating-point number.