potassco / clingo

🤔 A grounder and solver for logic programs.
https://potassco.org/clingo
MIT License
603 stars 80 forks source link

Using python scripts during grounding when using the C/C++ API #322

Closed marklaw closed 3 years ago

marklaw commented 3 years ago

Hi,

Is there a simple way to call python functions during grounding when using the C++ API? Please correct me if I'm wrong, but I think this is not currently supported.

I've started down the line of trying to use a C++ API grounding callback to pass the callback onto python myself, but I got stuck in converting Clingo::SymbolSpan arguments to a PyObject. I assume there is a function somewhere in libpyclingo that could do this for me, but I couldn't find it. I'm also not sure this is the most efficient approach, so any better suggestions would be greatly appreciated.

I'm aware it might sound a bit of an odd use case that I am using the C++ API and I also need python functions. My use case is that I am developing systems written in C++ that call Clingo using the C++ API, but I would like users to be able to write custom functions that can be evaluated during grounding without needing to write them in C++ and recompile my systems.

Thanks in advance,

Mark

rkaminsk commented 3 years ago

The clingo library as such does not even link against the Python and Lua libraries. For this to work, another library including the code from libpyclingo would have to be included. Then, you could call clingo_register_python_ to enable execution of Python scripts. There is no target to build such a library.

marklaw commented 3 years ago

Yes, I did this actually. But unfortunately, although the Python script is being executed when the program is ground (i.e. if I put a print statement in the main script, it gets printed), but the functions themselves are not being called by the grounder. Is this because when I use the C++ API, it expects that all functions will be handled by the C++ callback?

I can post a simple minimal example of what I have tried if it would help.

rkaminsk commented 3 years ago

I think that if you pass a context object to ground, then the globally registered scripts are ignored. If you pass a nullptr, then it should work. You could register another script to call functions to avoid using the context callback.

marklaw commented 3 years ago

Thanks for your help! Sorry if I'm being dim, but I'm not sure what you mean by passing a context object to ground. The ground function takes two arguments (the program parts and an optional callback, which is nullptr by default).

On registering another script, is this not what clingo_registerpython does? Can I just copy the code from clingo_register_python_, or call clingo_register_python_ again (if so, at what point should this be executed? just before grounding?). Or, do I need to completely rewrite the functions used by clingo_register_python_? If so, what will be the difference between my customised versions and those used by clingo_register_python_?

For reference, this is what I am currently doing:

#include <string>
#include <clingo.hh>
#include <pyclingo.h>
#include <iostream>

std::string program = R"ESC(
#script (python)
import clingo
print("script executed")

def ident(x):
  print("ident function called ident({x})")
  return clingo.Number(x)
#end.

p(@ident(2)).
p(test).
)ESC";

int main(int argc, char *argv[]) {
  using namespace std;

  clingo_register_python_();

  Clingo::Logger logger = [](Clingo::WarningCode, char const *message) { cout << message << endl;};

  Clingo::Control ctl({{}, logger, 20});
  cout << "grounding..." << endl;

  ctl.add("", {}, program.c_str());
  ctl.ground({{"", {}}});

  cout << "solving..." << endl;

  for(auto& m : ctl.solve())
    for(auto& atom : m.symbols())
      cout << atom << " ";

  cout << endl;

  return 0;
}

And I see the following output:

grounding...
script executed
solving...
p(test)
rkaminsk commented 3 years ago

As far as I can see, it should work like this. Or at least there should be a message because it has to be

def ident(x)
    return clingo.Number(x.number)

Maybe it is just the message that gets gobbled but it is working otherwise.

The identity function should also be written like this:

def ident(x)
    return x
marklaw commented 3 years ago

Sorry, that was me going from my larger program to a minimal example (in my larger program I was doing something with x and returning something else). I've corrected the identity function, but it still produces the same output for me.

I know this is an odd use case, so understand if you don't have the time to fix it. Is there anything else I can try to get it working myself?

rkaminsk commented 3 years ago

I will have a look. But it might take some time because I am currently working on another project and also vacation is ahead.

marklaw commented 3 years ago

Okay, thanks!

rkaminsk commented 3 years ago

This should be fixed now.

marklaw commented 3 years ago

Great. Thanks for your help!