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, replacex86_64-unknown-linux-gnuwithaarch64-unknown-linux-gnu,aarch64-apple-darwin,x86_64-apple-darwinorx86_64-pc-windows-msvc(a.zip). On macOS, a binary downloaded with a browser first needsxattr -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
edition2024is 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 findsg++-12tog++-14. Otherwise, name it inGOATD_CXX(GOATD_CXX=g++-11 cargo install ...). - No compiler or
make: install them withsudo apt install build-essentialon Debian or Ubuntu, orxcode-select --installon macOS.
- "feature
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,maxand the like./is in floating point:X is 4 / 2binds2.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/3whose goal calls the findall again; - a grounding that does not end, as
nat(s(X)) :- nat(X).asked for everynat(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 nop(X, Y)for anyY. - 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.
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.
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.
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)).
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.
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 aretd,closed,families,dynamic,programanddecision. They differ in speed.td,familiesanddecisionfail on some programs, with a message that says why.autoruns trials of them over rewrites of the program, under bounds on size and work that double until one finishes.--evidence-probabilityprintsP(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.--tracesummarizes 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 4prints 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.
| Input | Extension | Format name |
|---|---|---|
| ProbLog, LPAD | any other | problog |
| Bayesian network, BIF | .bif | bif |
| Bayesian network, Hugin | .net, .hugin | hugin |
| Bayesian network, GeNIe | .xdsl | xdsl |
| Bayesian or Markov network, UAI | .uai | uai |
| Dice program | .dice | dice |
| Circulette netlist | .cql | circulette |
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 leavesasks for the variables without children,--query a,bfor the variables named.--evidencenames the evidence file: a UAI.evidfile for a UAI network, an Ace.instfile 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-probabilityprints the partition function given the evidence. Where it differs from P(e), the line starts withZ:in place ofP(e):. - With
--no-queries, tddlog prints that line alone, the UAI PR task. --rescaledivides 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
flipa probabilistic fact, and the integer operations rules over the bits. iteratebecomes recursion over a step index, its count a fact (steps(10).).- The count may be the program's argument.
iterate(step, x, $1), read bytddlog program.dice 10, is writtensteps($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.
/,%andsampleare refused.data/diceon 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 --programprints it. tests/circuletteholds 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 asp1::h1; p2::h2 :- body.anda:p.asp::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 asfindall/3. Those it calls are kept, anddb(Goal)is read asGoal. - The rest is left out, with the directives: a program that plots the LPAD's answers, for example.
?- prob(Q, P).becomesquery(Q).?- prob(Q, E, P).becomesquery(Q).and anevidence(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,queriesandarguments, a list of the program's arguments as strings. - An error is an exception of its own:
tddlog.ParseError,GroundingErrororCompilationError.
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.formatnames the input's format, NULL for ProbLog.options.evidenceholds a network's evidence file, NULL for none.options.argumentspoints tooptions.num_argumentsstrings, 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.
- Ground. tddlog parses the program and grounds it bottom up, only for the atoms the queries and the evidence depend on.
- 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.
- Choose a vtree. The vtree
decides the diagrams' sizes. The default,
auto, tries several under bounds that double until one finishes. - Compile. It compiles each derived atom into a TDD by Tp-compilation. It applies the rules to diagrams until the diagrams stop changing.
- 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.
--groundprints 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
--vtreeother thanauto. The layout a run names is the one that answered. - Trial
- A layout that
autoruns under bounds on the diagrams' size and on the work that builds them.autodoubles 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.