gecode_solver.cpp
author David Douard <david.douard@logilab.fr>
Thu, 08 Aug 2013 08:57:51 +0200
branchstable
changeset 717 5cad47e1ffbf
parent 712 0f967bf3a5de
permissions -rw-r--r--
[gecode] fix build against gecode 4.2.0 (closes #167329) memory statistics have been removed from the search engines.

#include <Python.h>
#include <iostream>
#include <string.h>
#include <exception>
#include "gecode/kernel.hh"
#include "gecode/int.hh"
#include "gecode/search.hh"

#if 1
#define debug(fmt,...)
#else
#define debug(fmt,...) printf(fmt, ##__VA_ARGS__)
#endif


#define PM_VERSION(a,b,c) ((a<<16)+(b<<8)+(c))
// There is no easy way to test for gecode version here
// so the build system must pass GE_VERSION accordingly
// by default we build for 3.1.0 if GECODE_VERSION exists

#ifndef GE_VERSION
#ifndef GECODE_VERSION
#define GE_VERSION PM_VERSION(2,1,2)
#else
#define GE_VERSION PM_VERSION(3,1,0)
#endif
#endif

#if GE_VERSION < PM_VERSION(2,0,0)
#define SELF this
#define INT_VAR_NONE BVAR_NONE
#define INT_VAL_MIN BVAL_MIN

#elif GE_VERSION < PM_VERSION(3,0,0)
#define SELF this
#define SET_VAR_SIZE_MAX SET_VAR_MAX_CARD
#define SET_VAL_MIN_INC SET_VAL_MIN
#else
#define SELF (*this)
#define convexHull convex
#endif

#if GE_VERSION >= PM_VERSION(4, 0, 0)
#define INT_VAR_NONE INT_VAR_NONE()
#define INT_VAL_MIN INT_VAL_MIN()
#endif

using namespace std;
using namespace Gecode;

#define USE_CLOCK
#ifdef USE_CLOCK
#include <ctime>

/// Timer interface stolen from gecode examples
class Timer {
private:
    clock_t t0;
public:
    void start(void);
    double stop(void);
};

forceinline void
Timer::start(void) {
    t0 = clock();
}

forceinline double
Timer::stop(void) {
    return (static_cast<double>(clock()-t0) / CLOCKS_PER_SEC) * 1000.0;
}
#else
#include <sys/time.h>
#include <unistd.h>

/// Timer interface stolen from gecode examples
class Timer {
private:
    struct timeval t0;
public:
    void start(void);
    double stop(void);
};

forceinline void
Timer::start(void) {
    gettimeofday( &t0, NULL );
}
forceinline double
Timer::stop(void) {
    struct timeval t1;
    gettimeofday( &t1, NULL );
    return (t1.tv_sec - t0.tv_sec)+1e-6*(t1.tv_usec - t0.tv_usec);
}
#endif

enum {
    _AND = 0,
    _OR  = 1,
    _EQ  = 2,
    _EQV = 3
};

class RqlError : public exception {
};

class RqlContext {
    /** Context holding the problem for solving Rql constraints
	we keep the info as Python objects and parse the problem
	during the creation of the Gecode problem
    */
public:
    RqlContext(long nvars, PyObject* domains,
	       long nvalues, PyObject* constraints, PyObject* sols):
	solutions(-1), // return every solutions
	time(-1),    // time limit in case the problem is too big
	fails(-1),     // ?? used by GecodeStop ...
	nvars(nvars),  // Number of variables
	nvalues(nvalues), // Number of values
	constraints(constraints), // A python list, holding the root of the problem
	sols(sols),    // an empty list that will receive the solutions
	domains(domains), // A PyList of PyList, one for each var,
	                  // holding the allowable integer values
	verbosity(false)  // can help debugging
    {
    }

    long solutions;
    long time;
    long fails;
    long nvars;
    long nvalues;
    PyObject* constraints;
    PyObject* sols;
    PyObject* domains;
    bool verbosity;
};

class RqlSolver : public Space {
/* A gecode Space
   this is a strange beast that requires special methods and
   behavior (mostly, copy and (bool,share,space) constructor
*/
protected:
    /* The variables we try to find values for
       these are the only 'public' variable of the
       problem.

       we use a lot more intermediate variables but
       they shouldn't be member of the space
     */
    IntVarArray  variables;

public:
    RqlSolver(const RqlContext& pb):
	variables(SELF,     // all gecode variable keep a reference to the space
		  pb.nvars, // number of variables
		  0,        // minimum domain value
		  pb.nvalues-1) // max domain value (included)
    {
	/* Since we manipulate Boolean expression and
	   we need to assign truth value to subexpression
	   eg (a+b)*(c+d) will be translated as :
	   root = x1 * x2
	   x1 = a+b
	   x2 = c+d
	   root = True
	*/
	BoolVar root(SELF, 1,1);
	
	set_domains( pb.domains );
	add_constraints( pb.constraints, root );

	/* the branching strategy, there must be one,
	   changing it might improve performance, but
	   in out case, we almost never propagate (ie
	   gecode solves the problem during its creation)
	*/
	branch(SELF, variables, INT_VAR_NONE, INT_VAL_MIN);
    }

    ~RqlSolver() {};

    RqlSolver(bool share, RqlSolver& s) : Space(share,s)
    {
	/* this is necessary for the solver to fork space
	   while branching
	*/
	variables.update(SELF, share, s.variables);
    }

    void set_domains( PyObject* domains )
    {
	PyObject* ovalues;
	if (!PyList_Check(domains)) {
	    throw RqlError();
	}
	int n = PyList_Size( domains );
	for(int var=0 ;var<n; ++var) {
	    /* iterate of domains which should contains
	       list of values
	       domains[0] contains possible values for var[0]...
	    */
	    int i, nval;
	    ovalues = PyList_GetItem( domains, var );
	    if (!PyList_Check(ovalues)) {
		throw RqlError();
	    }
	    nval = PyList_Size( ovalues );

	    /* It's a bit cumbersome to construct an IntSet, but
	       it's the only way to reduce an integer domain to
	       a discrete set
	    */
	    int* vals = new int[nval];
	    for(i=0;i<nval;++i) {
		//refcount ok, borrowed ref
		vals[i] = PyInt_AsLong( PyList_GetItem( ovalues, i ) );
		if (vals[i]<0) {
		    /* we don't have negative values and PyInt_AsLong returns
		       -1 if the object is not an Int */
		    delete [] vals;
		    throw RqlError();
		}
	    }
	    IntSet gvalues(vals,nval);
	    dom(SELF, variables[var], gvalues);
	    delete [] vals;
	}
    }

    /* Dispatch method from Node to specific node type */
    void add_constraints( PyObject* desc, BoolVar& var )
    {
	long type;

	if (!PyList_Check(desc)) {
	    throw RqlError();
	}
	/* the first element of each list (node) is 
	   a symbolic Int from _AND, _OR, _EQ, _EQV
	*/
	type = PyInt_AsLong( PyList_GetItem( desc, 0 ) );
	
	switch(type) {
	case _AND:
	    add_and( desc, var );
	    break;
	case _OR:
	    add_or( desc, var );
	    break;
	case _EQ:
	    add_equality( desc, var );
	    break;
	case _EQV:
	    add_equivalence( desc, var );
	    break;
	default:
	    throw RqlError();
	}
    }

    /* retrieve an int from a list, throw error if int is <0 */
    long get_uint( PyObject* lst, int index ) {
	PyObject* val;
	val = PyList_GetItem(lst, index);
	if (val<0) {
	    throw RqlError();
	}
	return PyInt_AsLong(val);
    }

    /* post gecode condition for Var == Value
       we can't use domain restriction since this
       condition can be part of an OR clause

       so we post  (var == value) <=> expr_value
    */
    void add_equality( PyObject* desc, BoolVar& expr_value ) {
	long variable, value;
	
	variable = get_uint( desc, 1 );
	value = get_uint( desc, 2 );
	if (variable==1) {
	    debug("RQL:%ld == %ld ***\n", variable, value);
	} else {
	    debug("RQL:%ld == %ld\n", variable, value);
	}
	rel(SELF, variables[variable], IRT_EQ, value, expr_value);
    }

    /* post gecode condition for Var[i] == Var[j] ... == Var[k]

       there's no operator for assigning chained equality to boolean
      
       so we post for 1<=i<=N (var[0] == var[i]) <=> bool[i]
                  and    bool[1] & ... & bool[N] <=> expr_value
       that means if all vars are equal expr_value is true
       if all vars are different from var[0] expr_value is false
       if some are equals and some false, the constraint is unsatisfiable
    */
    void add_equivalence( PyObject* desc, BoolVar& expr_value ) {
	int len = PyList_Size(desc);
	int var0 = get_uint( desc, 1 );
	BoolVarArray terms(SELF, len-2,0,1);
	debug("RQL:EQV(%d",var0);
	for (int i=1;i<len-1;++i) {
	    int var1 = get_uint(desc, i+1);
	    debug(",%d",var1);
	    rel(SELF, variables[var0], IRT_EQ, variables[var1], terms[i-1] );
	}
	debug(")\n");
#if GE_VERSION<PM_VERSION(2,0,0)
    BoolVarArgs terms_args(terms);
    bool_and(SELF, terms_args, expr_value);
#else
	rel(SELF, BOT_AND, terms, expr_value);
#endif
    }

    /* simple and relation between nodes */
    void add_and( PyObject* desc, BoolVar& var ) {
	int len = PyList_Size(desc);
	BoolVarArray terms(SELF, len-1,0,1);

	debug("RQL:AND(\n");
	for(int i=0;i<len-1;++i) {
	    PyObject* expr = PyList_GetItem(desc, i+1);
	    add_constraints( expr, terms[i] );
	}
	debug("RQL:)\n");
#if GE_VERSION<PM_VERSION(2,0,0)
    BoolVarArgs terms_args(terms);
    bool_and(SELF, terms_args, var);
#else
    rel(SELF, BOT_AND, terms, var);
#endif
    }

    /* simple or relation between nodes */
    void add_or( PyObject* desc, BoolVar& var ) {
	int len = PyList_Size(desc);
	BoolVarArray terms(SELF, len-1,0,1);

	debug("RQL:OR(\n");
	for(int i=0;i<len-1;++i) {
	    PyObject* expr = PyList_GetItem(desc, i+1);
	    add_constraints( expr, terms[i] );
	}
	debug("RQL:)\n");
#if GE_VERSION<PM_VERSION(2,0,0)
    BoolVarArgs terms_args(terms);
    bool_or(SELF, terms_args, var);
#else
	rel(SELF, BOT_OR, terms, var);
#endif

    }

    template <template<class> class Engine>
    static void run( RqlContext& pb, Search::Stop* stop )
    {
	double t0 = 0;
	int i = pb.solutions;
	Timer t;
	RqlSolver* s = new RqlSolver( pb );
	t.start();
	unsigned int n_p = 0;
	unsigned int n_b = 0;
	if (s->status() != SS_FAILED) {
	    n_p = s->propagators();
#if GE_VERSION<PM_VERSION(3,2,0)
	    n_b = s->branchings();
#else
	    n_b = s->branchers();
#endif
	}
#if GE_VERSION<PM_VERSION(2,0,0)
    Engine<RqlSolver> e(s);
#else
    Search::Options opts;
	//opts.c_d = pb.c_d;
	//opts.a_d = pb.a_d;
	opts.stop = stop;
	Engine<RqlSolver> e(s, opts);
#endif
	delete s;
	do {
	    RqlSolver* ex = e.next();
	    if (ex == NULL)
		break;

	    ex->add_new_solution(pb);

	    delete ex;
	    t0 = t0 + t.stop();
	} while (--i != 0 && (pb.time<0 || t0 < pb.time));
	Search::Statistics stat = e.statistics();
	if (pb.verbosity) {
	    cout << endl;
	    cout << "Initial" << endl
		 << "\tpropagators:   " << n_p << endl
		 << "\tbranchings:    " << n_b << endl
		 << endl
		 << "Summary" << endl
		 << "\truntime:       " << t.stop() << endl
		 << "\tsolutions:     " << abs(static_cast<int>(pb.solutions) - i) << endl
		 << "\tpropagations:  " << stat.propagate << endl
		 << "\tfailures:      " << stat.fail << endl
#if GE_VERSION < PM_VERSION(3,0,0)
		 << "\tclones:        " << stat.clone << endl
		 << "\tcommits:       " << stat.commit << endl
#else
		 << "\tdepth:        " << stat.depth << endl
		 << "\tnode:       " << stat.node << endl
#endif
#if GE_VERSION < PM_VERSION(4,2,0)
		 << "\tpeak memory:   "
		 << static_cast<int>((stat.memory+1023) / 1024) << " KB"
		 << endl
#endif
		    ;
	}
    }

    /* We append each solutions to `sols` as a
       tuple `t` of the values assigned to each var
       that is t[i] = solution for var[i]
    */
    virtual void add_new_solution(RqlContext& pb) {
	PyObject *tuple, *ival;

	tuple = PyTuple_New( pb.nvars );

	for(int i=0;i<pb.nvars;++i) {
	    ival = PyInt_FromLong( variables[i].val() );
	    PyTuple_SetItem( tuple, i, ival );
	}
	PyList_Append( pb.sols, tuple );
    }

    /* another function need by gecode kernel */
    virtual Space* copy(bool share) {
	return new RqlSolver(share, *this);
    }

};

class FailTimeStop : public Search::Stop {
private:
    Search::TimeStop *ts;
    Search::FailStop *fs;
public:
    FailTimeStop(int fails, int time):ts(0L),fs(0L) {
	if (time>=0)
	    ts = new Search::TimeStop(time);
	if (fails>=0) {
	    fs = new Search::FailStop(fails);
	}
    }
#if GE_VERSION < PM_VERSION(3,1,0)
    bool stop(const Search::Statistics& s) {
	int sigs = PyErr_CheckSignals();
	bool fs_stop = false;
	bool ts_stop = false;
	if (fs) {
	    fs_stop = fs->stop(s);
	}
	if (ts) {
	    ts_stop = ts->stop(s);
	}
	return sigs || fs_stop || ts_stop;
    }
#else
    /* from gecode 3.1.0 */
    bool stop(const Search::Statistics& s, const Search::Options &o) {
	int sigs = PyErr_CheckSignals();
	bool fs_stop = false;
	bool ts_stop = false;
	if (fs) {
	    fs_stop = fs->stop(s,o);
	}
	if (ts) {
	    ts_stop = ts->stop(s,o);
	}
	return sigs || fs_stop || ts_stop;
    }
#endif

    /// Create appropriate stop-object
    static Search::Stop* create(int fails, int time) {
	return new FailTimeStop(fails, time);
    }
};

static void _solve( RqlContext& ctx )
{
    Search::Stop *stop = FailTimeStop::create(ctx.fails, ctx.time);

    RqlSolver::run<DFS>( ctx, stop );
}


static PyObject *
rql_solve(PyObject *self, PyObject *args)
{
    PyObject* sols = 0L;
    PyObject* constraints;
    PyObject* domains;
    long nvars, nvalues;
    if (!PyArg_ParseTuple(args, "OiO", &domains, &nvalues, &constraints))
        return NULL;
    sols = PyList_New(0);
    try {
	if (!PyList_Check(domains)) {
	    throw RqlError();
	}
	nvars = PyList_Size(domains);
	RqlContext ctx(nvars, domains, nvalues, constraints, sols );
	_solve( ctx );
    } catch(RqlError& e) {
	Py_DECREF(sols);
	PyErr_SetString(PyExc_RuntimeError, "Error parsing constraints");
	return NULL;
    };
    return sols;
}

static PyMethodDef SolveRqlMethods[] = {
    {"solve",  rql_solve, METH_VARARGS,
     "Solve RQL variable types problem."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};


PyMODINIT_FUNC
initrql_solve(void)
{
    PyObject* m;
    m = Py_InitModule("rql_solve", SolveRqlMethods);
    if (m == NULL)
        return;
}