Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic python tracing support #382

Merged
merged 3 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@ jobs:
- name: install pycodestyle
run: pip install pycodestyle
- name: run pycodestyle
run: pycodestyle nemo-python
run: pycodestyle nemo-python --max-line-length 90
142 changes: 112 additions & 30 deletions nemo-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ use std::{collections::HashSet, fs::read_to_string};

use nemo::{
datatypes::Double,
execution::ExecutionEngine,
execution::{tracing::trace::ExecutionTrace, ExecutionEngine},
io::{resource_providers::ResourceProviders, OutputFileManager, RecordWriter},
model::{
types::primitive_logical_value::PrimitiveLogicalValueT, Constant, NumericLiteral,
RdfLiteral, XSD_STRING,
chase_model::{ChaseAtom, ChaseFact},
types::primitive_logical_value::PrimitiveLogicalValueT,
Constant, NumericLiteral, RdfLiteral, XSD_STRING,
},
};

use pyo3::{create_exception, exceptions::PyNotImplementedError, prelude::*};
use pyo3::{create_exception, exceptions::PyNotImplementedError, prelude::*, types::PyDict};

create_exception!(module, NemoError, pyo3::exceptions::PyException);

Expand Down Expand Up @@ -134,34 +135,38 @@ impl NemoLiteral {
#[pyclass]
struct NemoResults(Box<dyn Iterator<Item = Vec<PrimitiveLogicalValueT>> + Send>);

fn logical_value_to_python(py: Python<'_>, v: PrimitiveLogicalValueT) -> PyResult<&PyAny> {
fn constant_to_python<'a>(py: Python<'a>, v: &Constant) -> PyResult<&'a PyAny> {
let decimal = py.import("decimal")?.getattr("Decimal")?;
match v {
PrimitiveLogicalValueT::Any(rdf) => match rdf {
Constant::Abstract(c) => Ok(c.to_string().into_py(py).into_ref(py)),
Constant::NumericLiteral(NumericLiteral::Integer(i)) => Ok(i.into_py(py).into_ref(py)),
Constant::NumericLiteral(NumericLiteral::Double(d)) => {
Ok(f64::from(d).into_py(py).into_ref(py))
}
// currently we pack decimals into strings, maybe this should change
Constant::NumericLiteral(_) => decimal.call1((rdf.to_string(),)),
Constant::StringLiteral(s) => Ok(s.into_py(py).into_ref(py)),
Constant::RdfLiteral(lit) => (|| {
let lit = match lit {
RdfLiteral::DatatypeValue { value, datatype } => NemoLiteral {
value,
language: None,
datatype,
},
RdfLiteral::LanguageString { value, tag } => NemoLiteral {
value,
language: Some(tag),
datatype: RDF_LANG_STRING.to_string(),
},
};
Ok(Py::new(py, lit)?.to_object(py).into_ref(py))
})(),
},
Constant::Abstract(c) => Ok(c.to_string().into_py(py).into_ref(py)),
Constant::NumericLiteral(NumericLiteral::Integer(i)) => Ok(i.into_py(py).into_ref(py)),
Constant::NumericLiteral(NumericLiteral::Double(d)) => {
Ok(f64::from(*d).into_py(py).into_ref(py))
}
// currently we pack decimals into strings, maybe this should change
Constant::NumericLiteral(_) => decimal.call1((v.to_string(),)),
Constant::StringLiteral(s) => Ok(s.into_py(py).into_ref(py)),
Constant::RdfLiteral(lit) => (|| {
let lit = match lit {
RdfLiteral::DatatypeValue { value, datatype } => NemoLiteral {
value: value.clone(),
language: None,
datatype: datatype.clone(),
},
RdfLiteral::LanguageString { value, tag } => NemoLiteral {
value: value.clone(),
language: Some(tag.clone()),
datatype: RDF_LANG_STRING.to_string(),
},
};
Ok(Py::new(py, lit)?.to_object(py).into_ref(py))
})(),
}
}

fn logical_value_to_python(py: Python<'_>, v: PrimitiveLogicalValueT) -> PyResult<&PyAny> {
match v {
PrimitiveLogicalValueT::Any(rdf) => constant_to_python(py, &rdf),
PrimitiveLogicalValueT::String(s) => Ok(String::from(s).into_py(py).into_ref(py)),
PrimitiveLogicalValueT::Integer(i) => Ok(i64::from(i).into_py(py).into_ref(py)),
PrimitiveLogicalValueT::Float64(d) => {
Expand All @@ -170,6 +175,77 @@ fn logical_value_to_python(py: Python<'_>, v: PrimitiveLogicalValueT) -> PyResul
}
}

#[pyclass]
struct NemoFact(ChaseFact);

#[pymethods]
impl NemoFact {
fn predicate(&self) -> String {
self.0.predicate().to_string()
}

fn constants<'a>(&self, py: Python<'a>) -> PyResult<Vec<&'a PyAny>> {
self.0
.terms()
.iter()
.map(|c| constant_to_python(py, c))
.collect()
}

fn __repr__(&self) -> String {
self.0.to_string()
}
}

#[pyclass]
struct NemoTrace(ExecutionTrace);

#[pymethods]
impl NemoTrace {
fn subtraces(&self) -> Option<Vec<NemoTrace>> {
match &self.0 {
ExecutionTrace::Fact(_) => None,
ExecutionTrace::Rule(_, subtraces) => {
Some(subtraces.iter().map(|t| NemoTrace(t.clone())).collect())
}
}
}

fn fact(&self) -> Option<NemoFact> {
match &self.0 {
ExecutionTrace::Fact(f) => Some(NemoFact(f.clone())),
ExecutionTrace::Rule(_, _) => None,
}
}

fn rule(&self) -> Option<String> {
match &self.0 {
ExecutionTrace::Fact(_) => None,
ExecutionTrace::Rule(rule, _) => Some(rule.to_string()),
}
}

fn dict(&self, py: Python) -> PyResult<PyObject> {
trace_to_dict(&self.0, py)
}
}

fn trace_to_dict(trace: &ExecutionTrace, py: Python) -> PyResult<PyObject> {
let result = PyDict::new(py);
match &trace {
ExecutionTrace::Fact(fact) => result.set_item("fact", fact.to_string())?,
ExecutionTrace::Rule(rule, subtraces) => {
result.set_item("rule", rule.to_string())?;
let subtraces: Vec<_> = subtraces
.iter()
.map(|trace| trace_to_dict(trace, py))
.collect::<PyResult<_>>()?;
result.set_item("subtraces", subtraces)?;
}
};
Ok(result.to_object(py))
}

#[pymethods]
impl NemoResults {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
Expand Down Expand Up @@ -206,6 +282,12 @@ impl NemoEngine {
Ok(())
}

fn trace(&self, fact: String) -> PyResult<Option<NemoTrace>> {
let parsed_fact = nemo::io::parser::parse_fact(fact).py_res()?;
let trace = self.0.trace(parsed_fact).py_res()?;
Ok(trace.map(NemoTrace))
}

fn write_result(
&mut self,
predicate: String,
Expand Down
34 changes: 33 additions & 1 deletion nemo-python/tests/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import csv
from decimal import Decimal

from nmo_python import load_string, NemoEngine, NemoOutputManager, NemoLiteral
from nmo_python import (
load_string,
NemoEngine,
NemoOutputManager,
NemoLiteral,
)


class TestExample(unittest.TestCase):
Expand All @@ -15,8 +20,15 @@ def setUp(self):
data(hello, world) .
data(py, 3.14) .
data(msg, "hello world"@en) .
data(3.14, circle).

calculated(?x, !v) :- data(?y, ?x) .

interesting(py).
interesting(msg).

interesting(?x) :- data(?x, ?y), interesting(?y).
interesting(?y) :- data(?x, ?y), interesting(?x).
"""

self.engine = NemoEngine(load_string(self.rules))
Expand All @@ -31,6 +43,7 @@ def setUp(self):
NemoLiteral("hello world", lang="en"),
"__Null#9223372036854775813",
],
["circle", "__Null#9223372036854775814"],
]

self.expected_serialized_result = [
Expand All @@ -51,6 +64,7 @@ def setUp(self):
'"hello world"@en',
"<__Null#9223372036854775813>",
],
["circle", "<__Null#9223372036854775814>"],
]

def test_result(self):
Expand All @@ -69,6 +83,24 @@ def test_output(self):
results = list(csv.reader(results_file))
self.assertEqual(results, self.expected_serialized_result)

def test_trace(self):
trace = self.engine.trace("interesting(circle)")
expected_trace = {
"rule": "interesting(circle) :- data(3.14, circle), interesting(3.14) .",
"subtraces": [
{"fact": "data(3.14, circle)"},
{
"rule": "interesting(3.14) :- data(py, 3.14), interesting(py) .",
"subtraces": [
{"fact": "data(py, 3.14)"},
{"fact": "interesting(py)"},
],
},
],
}

self.assertEqual(trace.dict(), expected_trace)


if __name__ == "__main__":
unittest.main(verbosity=2)
4 changes: 2 additions & 2 deletions nemo/src/execution/tracing/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ascii_tree::write_tree;
use crate::model::{chase_model::ChaseFact, Rule, VariableAssignment};

/// Identifies an atom within the head of a rule
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct RuleApplication {
rule: Rule,
assignment: VariableAssignment,
Expand Down Expand Up @@ -35,7 +35,7 @@ impl std::fmt::Display for RuleApplication {
}

/// Represents the derivation tree for one derived fact
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum ExecutionTrace {
/// Fact was given as input
Fact(ChaseFact),
Expand Down