Skip to content
Open
317 changes: 297 additions & 20 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

members = [
"solver",
"datalog",
"bench/data",
"bench/bench",
"env_param",
Expand Down Expand Up @@ -29,6 +30,7 @@ async-trait = "0.1"
bumpalo = { version = "3.19" }
clap = { version = "4.5", features = ["derive"] }
comfy-table = "7.0"
criterion = "0.5.1"
crossbeam-channel = "0.5"
derivative = "2.2"
derive_builder = "0.20"
Expand Down
2 changes: 2 additions & 0 deletions datalog/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
18 changes: 18 additions & 0 deletions datalog/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "aries-datalog"
version = "0.1.0"
edition = "2024"


[dependencies]
itertools = { workspace = true }

[dev-dependencies]
criterion = { workspace = true }

[[bench]]
name = "grounding"
harness = false

[[bin]]
name = "grounding_benchmark"
79 changes: 79 additions & 0 deletions datalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# A (very) minimal datalog inference engine

This crate aims to provide a very minimal datalog engine.
While the engine should be very general, its main purpose is to be used for grounding planning problems (in the aries family of planners).

The objective of the crate derives from this use-case:

- dynamic program creation: the rules are not known at compile time but discovered when parsing a planning problem file. This makes most of rust datalog engines not usable as they are design for rules known at compile time (which enables many compiler optimization)
- minimal dependencies and no-async to make it easily embeddable
- minimal and low-level API, using integers ID to represent symbols and variables and interior mutability for updating derived facts.
- reasonable performance, driven by planning use cases: this does not aim at being the fastest, but should be fast enough so that grounding is not a bottleneck on the considered planning benchmarks.

This crate was written as I did not find any existing one meeting all those requirements (the first two ones being the hardest to satisfy in conjunction in the current ecosystem).
We would happily accept improvement if they do not negatively affect the main use case.

## Example

Below is an example for encoding the usual "finding ancestors" datalog program.
An example targeting for grounding a planning problem is available in the `examples/`.


```prolog
parent(x1, x2).
parent(x2, x3).
parent(x3, x4).

ancestor(?x, ?y) :- parent(?x, ?y).
ancestor(?x, ?y) :- ancestor(?x, ?y), parent(?y, ?z).
```

The task of inferring all ancestors can be encoded as follows:

```rust
use aries_datalog::*;
// create a program that will contain the predicates, facts and rules.
let mut prog = Program::new();

let parent = prog.new_predicate(2);
parent.add([1, 2]); // parent(x1, x2).
parent.add([2, 3]);
parent.add([3, 4]);

let ancestor = prog.new_predicate(2);

// a parent is an ancestor
// ancestor(?x, ?y) :- parent(?x, ?y).
prog.add_rule(Rule::new(
ancestor.apply([Arg::Var(0), Arg::Var(1)]),
[
parent.apply([Arg::Var(0), Arg::Var(1)]),
]
));

// the parent of an ancestor is an ancestor
// ancestor(?x, ?y) :- ancestor(?x, ?y), parent(?y, ?z).
prog.add_rule(Rule::new(
ancestor.apply([Arg::Var(0), Arg::Var(2)]),
[
ancestor.apply([Arg::Var(0), Arg::Var(1)]),
parent.apply([Arg::Var(1), Arg::Var(2)]),
]
));

// run inference
prog.run();

// as a result of inference, the `ancestor` table has been populated with the result of all inferences.
assert_eq!(
ancestor.extract().rows_sized(),
&[
[1, 2],
[1, 3],
[1, 4],
[2, 3],
[2, 4],
[3, 4]
]
);
```
60 changes: 60 additions & 0 deletions datalog/benches/grounding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;

use aries_datalog::*;

fn ground(num_locs: u32, num_bots: u32) -> usize {
let mut prog = Program::new();

let robot = prog.new_predicate(1);
let loc = prog.new_predicate(1);
let connected = prog.new_predicate(2);
let at = prog.new_predicate(2);

for l in 1..=num_locs {
loc.add([l]);
connected.add([l, l + 1]); // connected(l1, l2).
}

for r in 1..=num_bots {
robot.add([r]);
at.add([r, 1]);
}

use Arg::*;

let move_applicable = prog.new_predicate(3);

let move_rule = Rule::new(
move_applicable.apply([Var(0), Var(1), Var(2)]),
[
robot.apply([Var(0)]),
loc.apply([Var(1)]),
loc.apply([Var(2)]),
at.apply([Var(0), Var(1)]),
connected.apply([Var(1), Var(2)]),
],
);
prog.add_rule(move_rule);

// at(?r, ?l) :- move_applicable(?r, _, ?l)
prog.add_rule(Rule::new(
at.apply([Var(0), Var(2)]),
[move_applicable.apply([Var(0), Var(1), Var(2)])],
));

// run inference until completion
prog.run();

move_applicable.extract().rows().count()
}

fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("ground 10/2", |b| b.iter(|| ground(black_box(10), black_box(2))));
c.bench_function("ground 30/5", |b| b.iter(|| ground(black_box(30), black_box(5))));
c.bench_function("ground 100/20", |b| b.iter(|| ground(black_box(100), black_box(20))));
c.bench_function("ground 500/40", |b| b.iter(|| ground(black_box(500), black_box(40))));
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
77 changes: 77 additions & 0 deletions datalog/examples/grounding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! This example simulates the grounding of a PDDL action.
use aries_datalog::*;

fn main() {
let mut prog = Program::new();

let loc = prog.new_predicate(1);
loc.add([1]); // loc(l1). (1 is the id of a symbol l1)
loc.add([2]); // loc(l2).
loc.add([3]);
loc.add([4]);
loc.add([6]);
loc.add([7]);

let robot = prog.new_predicate(1);
robot.add([11]); // robot(r1). (11 is the ID of the symbol r1)
robot.add([12]); // robot(r2).
robot.add([13]);
robot.add([14]);
robot.add([16]);

let connected = prog.new_predicate(2);
connected.add([1, 2]); // connected(l1, l2).
connected.add([2, 3]);
connected.add([3, 4]);
connected.add([1, 2]);
connected.add([2, 1]);
connected.add([3, 2]);
connected.add([4, 3]);
connected.add([2, 1]);
connected.add([6, 7]); // disconnected component l6, l7
connected.add([7, 6]);

let at = prog.new_predicate(2);
at.add([11, 2]); // at(r1, l2).
at.add([12, 4]); // at(r2, l4).
at.add([16, 7]);

use Arg::*;

let move_applicable = prog.new_predicate(3);

// move_applicable(?r, ?l1, l2) :-
// robot(?r),
// loc(?l1),
// loc(?l2),
// at(?r, ?l1)
// connected(?l1, ?l2).
let move_rule = Rule::new(
move_applicable.apply([Var(0), Var(1), Var(2)]),
[
robot.apply([Var(0)]),
loc.apply([Var(1)]),
loc.apply([Var(2)]),
at.apply([Var(0), Var(1)]),
connected.apply([Var(1), Var(2)]),
],
);
prog.add_rule(move_rule);

// at(?r, ?l) :- move_applicable(?r, _, ?l)
prog.add_rule(Rule::new(
at.apply([Var(0), Var(2)]),
[move_applicable.apply([Var(0), Var(1), Var(2)])],
));

// run inference until completion
prog.run();

// access the resulting variables

println!("\n == reachable locations ==\n");
at.extract().rows().for_each(|row| println!("at{row:?}"));

println!("\n == applicable actions ==\n");
move_applicable.extract().rows().for_each(|row| println!("move{row:?}"));
}
74 changes: 74 additions & 0 deletions datalog/src/bin/grounding_benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! A simple binary for performance analysis and some integration tests.

use std::time::Instant;

use aries_datalog::*;

fn main() {
let start = Instant::now();
let instances = ground(1001, 100);
println!("Instances: {instances}");
println!("Runtime: {}s", start.elapsed().as_secs_f32());
}

fn ground(num_locs: u32, num_bots: u32) -> usize {
let mut prog = Program::new();

let robot = prog.new_predicate(1);
let loc = prog.new_predicate(1);
let connected = prog.new_predicate(2);
let at = prog.new_predicate(2);

for l in 1..=num_locs {
loc.add([l]);
connected.add([l, l + 1]); // connected(l1, l2).
}

for r in 1..=num_bots {
robot.add([r]);
at.add([r, 1]);
}

use Arg::*;

let move_applicable = prog.new_predicate(3);

let move_rule = Rule::new(
move_applicable.apply([Var(0), Var(1), Var(2)]),
[
robot.apply([Var(0)]),
loc.apply([Var(1)]),
loc.apply([Var(2)]),
at.apply([Var(0), Var(1)]),
connected.apply([Var(1), Var(2)]),
],
);
prog.add_rule(move_rule);

// at(?r, ?l) :- move_applicable(?r, _, ?l)
prog.add_rule(Rule::new(
at.apply([Var(0), Var(2)]),
[move_applicable.apply([Var(0), Var(1), Var(2)])],
));

// run inference until completion
prog.run();

move_applicable.extract().rows().count()
}

#[cfg(test)]
mod test {
use crate::ground;

fn check_grounding_size(num_locs: u32, num_robots: u32) {
assert_eq!(ground(num_locs, num_robots) as u32, (num_locs - 1) * num_robots);
}

#[test]
fn test_grounding_size() {
check_grounding_size(30, 4);
check_grounding_size(1, 4);
check_grounding_size(10, 0);
}
}
11 changes: 11 additions & 0 deletions datalog/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]

pub(crate) mod merge;
mod program;
mod rules;
mod tables;

pub use crate::program::*;
pub use crate::rules::*;
pub use crate::tables::*;
Loading
Loading