diff --git a/CMakeLists.txt b/CMakeLists.txt index b8cad38e5b..78ba3f956a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1170,6 +1170,13 @@ if(GECODE_ENABLE_FLATZINC) endif() endforeach() endif() + if(CMAKE_DL_LIBS) + foreach(kind shared static) + if(TARGET gecodeflatzinc_${kind}) + target_link_libraries(gecodeflatzinc_${kind} PUBLIC ${CMAKE_DL_LIBS}) + endif() + endforeach() + endif() endif() # Compatibility aggregate target for downstream projects expecting Gecode::gecode. diff --git a/Makefile.in b/Makefile.in index b9140bf348..819237324d 100755 --- a/Makefile.in +++ b/Makefile.in @@ -819,11 +819,11 @@ endif # FLATZINC # -FLATZINCSRC0 = flatzinc.cpp registry.cpp branch.cpp +FLATZINCSRC0 = flatzinc.cpp registry.cpp branch.cpp blackbox.cpp FLATZINC_GENSRC0 = parser.tab.cpp lexer.yy.cpp FLATZINCHDR0 = ast.hh conexpr.hh option.hh parser.hh \ plugin.hh registry.hh symboltable.hh varspec.hh \ - branch.hh branch.hpp lastval.hh complete.hh + branch.hh branch.hpp lastval.hh complete.hh blackbox.hh FLATZINCSRC = $(FLATZINCSRC0:%=gecode/flatzinc/%) FLATZINC_GENSRC = $(FLATZINC_GENSRC0:%=gecode/flatzinc/%) diff --git a/changelog.in b/changelog.in index 3b76bb1a95..cd3b6bc76c 100755 --- a/changelog.in +++ b/changelog.in @@ -75,6 +75,19 @@ This release modernizes the Gecode build infrastructure, adds a first-class CMake package for downstream consumers, refreshes the autoconf build path, and updates CI coverage for current platforms. +[ENTRY] +Module: flatzinc +What: new +Rank: minor +[DESCRIPTION] +Add support for the experimental MiniZinc black-box propagator interface. A +FlatZinc model can request propagation using an external function, implemented +either as a shared library or as a subprocess, through two generic propagators: +gecode_blackbox (value propagation, scheduled once all inputs are fixed) and +gecode_blackbox_bounds (bounds propagation, scheduled on bound changes).The +blackbox_exec and blackbox_dll annotations select the execution mode and pass +through extra arguments. + [ENTRY] Module: minimodel What: bug diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index eb1809eba8..affe090e31 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -232,6 +232,7 @@ set(GECODE_GIST_SOURCES ) set(GECODE_FLATZINC_SOURCES + gecode/flatzinc/blackbox.cpp gecode/flatzinc/branch.cpp gecode/flatzinc/flatzinc.cpp gecode/flatzinc/lexer.yy.cpp diff --git a/gecode/flatzinc/blackbox.cpp b/gecode/flatzinc/blackbox.cpp new file mode 100644 index 0000000000..7c662cd451 --- /dev/null +++ b/gecode/flatzinc/blackbox.cpp @@ -0,0 +1,562 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Jip J. Dekker + * + * Copyright: + * Jip J. Dekker, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#endif + +namespace Gecode { +namespace FlatZinc { + +BlackBoxDLL::BlackBoxDLL(const std::string &name, + const std::vector &args) { + std::string loadError; +#ifdef _WIN32 + library = LoadLibraryA(name.c_str()); + if (!library) { + loadError = std::string("unable to locate library `") + name + "'"; + library = LoadLibraryA((std::string(name) + ".dll").c_str()); + } + if (!library) { + library = LoadLibraryA((std::string("lib") + name + ".dll").c_str()); + } +#else + library = dlopen(name.c_str(), RTLD_LAZY); + if (!library) { + loadError = std::string(dlerror()); + library = dlopen((name + ".so").c_str(), RTLD_NOW); + } + if (!library) { + library = dlopen((std::string("lib") + name + ".so").c_str(), RTLD_NOW); + } +#endif + if (!library) { + throw Error("Blackbox", "Unable to open dynamic library: " + loadError); + } + + // find symbol for blacbox function +#ifdef _WIN32 + dll_fzn_blackbox = reinterpret_cast( + GetProcAddress((HMODULE)library, "fzn_blackbox")); + std::string symError = "."; +#else + *(void **)(&dll_fzn_blackbox) = dlsym(library, "fzn_blackbox"); + std::string symError(": "); + if (!dll_fzn_blackbox) { + symError += std::string(dlerror()); + } +#endif + if (!dll_fzn_blackbox) { + throw Error("Blackbox", + "Unable to find symbol `fzn_blackbox` in dynamic library" + + symError); + } + + // Optionally call the initialisation function with the given arguments. It is + // not an error for the library to omit `fzn_initialize`. + void(__stdcall *dll_fzn_initialize)(const char **, size_t) = nullptr; +#ifdef _WIN32 + dll_fzn_initialize = reinterpret_cast( + GetProcAddress((HMODULE)library, "fzn_initialize")); +#else + *(void **)(&dll_fzn_initialize) = dlsym(library, "fzn_initialize"); +#endif + if (dll_fzn_initialize != nullptr) { + std::vector argv; + argv.reserve(args.size()); + for (const std::string &a : args) { + argv.push_back(a.c_str()); + } + dll_fzn_initialize(argv.data(), argv.size()); + } +} + +BlackBoxDLL::~BlackBoxDLL() { + if (library) { +#ifdef _WIN32 + FreeLibrary((HMODULE)library); +#else + dlclose(library); +#endif + } +} + +BlackBoxExec::BlackBoxExec(const std::string &program, + const std::vector &args) { +#ifdef _WIN32 + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + HANDLE g_hChildStd_IN_Rd = NULL; + HANDLE g_hChildStd_IN_Wr = NULL; + HANDLE g_hChildStd_OUT_Rd = NULL; + HANDLE g_hChildStd_OUT_Wr = NULL; + + // Create a pipe for the child process's STDOUT. + if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0)) + std::cerr << "Stdout CreatePipe" << std::endl; + // Ensure the read handle to the pipe for STDOUT is not inherited. + if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) + std::cerr << "Stdout SetHandleInformation" << std::endl; + + // Create a pipe for the child process's STDIN + if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0)) + std::cerr << "Stdin CreatePipe" << std::endl; + // Ensure the write handle to the pipe for STDIN is not inherited. + if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0)) + std::cerr << "Stdin SetHandleInformation" << std::endl; + + PROCESS_INFORMATION piProcInfo; + STARTUPINFOA siStartInfo; + + // Set up members of the PROCESS_INFORMATION structure. + ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); + + // Set up members of the STARTUPINFO structure. + // This structure specifies the STDIN and STDOUT handles for redirection. + ZeroMemory(&siStartInfo, sizeof(STARTUPINFOA)); + siStartInfo.cb = sizeof(STARTUPINFOA); + siStartInfo.hStdOutput = g_hChildStd_OUT_Wr; + siStartInfo.hStdInput = g_hChildStd_IN_Rd; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + + // Build the command line: the program followed by the (quoted) arguments. + std::string prog = program; + for (const std::string &a : args) { + prog += " \""; + for (char ch : a) { + if (ch == '"' || ch == '\\') { + prog += '\\'; + } + prog += ch; + } + prog += '"'; + } + BOOL processStarted = + CreateProcessA(nullptr, + prog.data(), // command line + nullptr, // process security attributes + nullptr, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + nullptr, // use parent's environment + nullptr, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &piProcInfo); // receives PROCESS_INFORMATION + + if (!processStarted) { + throw Error("BlackBoxExec", "Unable to start program `" + program + "'"); + } + + CloseHandle(piProcInfo.hThread); + // Stop ReadFile from blocking + CloseHandle(g_hChildStd_OUT_Wr); + // Just close the child's in pipe here + CloseHandle(g_hChildStd_IN_Rd); + + pipe_send = g_hChildStd_IN_Wr; + pipe_receive = g_hChildStd_OUT_Rd; +#else + const int READ = 0; + const int WRITE = 1; + int child_in[2]; + int child_out[2]; + pipe(child_in); + pipe(child_out); + + if (fork() != 0) { + close(child_in[READ]); + close(child_out[WRITE]); + + pipe_send = child_in[WRITE]; + int pipe_receive = child_out[READ]; + file_receive = fdopen(pipe_receive, "r"); + return; + } + close(STDIN_FILENO); + close(STDOUT_FILENO); + dup2(child_in[READ], STDIN_FILENO); + dup2(child_out[WRITE], STDOUT_FILENO); + close(child_in[WRITE]); + close(child_out[READ]); + + // Launch the program directly (no shell), passing the annotation arguments as + // its command-line arguments. + std::vector argv; + argv.push_back(const_cast(program.c_str())); + for (const std::string &a : args) { + argv.push_back(const_cast(a.c_str())); + } + argv.push_back(nullptr); + execvp(program.c_str(), argv.data()); + // execvp only returns on failure. + std::exit(127); +#endif +}; + +BlackBoxExec::~BlackBoxExec() { +#ifdef _WIN32 + CloseHandle(pipe_send); + CloseHandle(pipe_receive); +#else + close(pipe_send); + fclose(file_receive); +#endif +} + +void BlackBoxExec::run(const std::vector &int_in, + const std::vector &float_in, + std::vector &int_out, + std::vector &float_out) { + // Construct program input: comma-separated integers, a semicolon, then + // comma-separated floats, terminated by a newline (e.g. "5,-7;2.5,1.125\n"). + std::stringstream out; + out.precision(std::numeric_limits::max_digits10); + for (size_t i = 0; i < int_in.size(); ++i) { + if (i != 0) { + out << ","; + } + out << int_in[i]; + } + out << ";"; + for (size_t i = 0; i < float_in.size(); ++i) { + if (i != 0) { + out << ","; + } + out << float_in[i]; + } + out << "\n"; + std::string out_buf = out.str(); +#ifdef _WIN32 + // Write to process input pipe + BOOL success = + WriteFile(pipe_send, out_buf.c_str(), out_buf.size(), nullptr, nullptr); + assert(success); + + // Read output from process by pipe + char c[2] = {0, 0}; + std::ostringstream oss; + while (c[0] != '\n') { + DWORD count = 0; + BOOL success = ReadFile(pipe_receive, c, sizeof(c) - 1, &count, NULL); + if (!success) { + throw Error( + "BlackBoxExec", + "Reading blackbox process output from pipe resulted did not succeed"); + } else if (count == 0) { + throw Error("BlackBoxExec", + "Blackbox process provided an incomplete response"); + } + assert(count == 1); + oss << c[0]; + } + std::string in_buffer(oss.str()); +#else + // Write to process input pipe + ssize_t bytes_written = write(pipe_send, out_buf.c_str(), out_buf.size()); + if (bytes_written != static_cast(out_buf.size())) { + throw Error("BlackBoxExec", + "Failed to write the full request to the blackbox process."); + } + + // Read from process output pipe + char *str = NULL; + size_t size = 0; + + if (getline(&str, &size, file_receive) == -1) { + throw Error( + "BlackBoxExec", + "Reading blackbox process output from pipe resulted in error no. " + + std::to_string(errno)); + } + std::string in_buffer(str); + free(str); +#endif + // Parse the response in a single left-to-right pass: comma-separated + // integers, a semicolon, then comma-separated floats (e.g. "5,-7;2.5,1.125\n"). + const char *p = in_buffer.c_str(); + char *end = nullptr; + auto skip_ws = [](const char *&q) { + while (*q == ' ' || *q == '\t' || *q == '\r') { + ++q; + } + }; + for (size_t i = 0; i < int_out.size(); ++i) { + long long v = std::strtoll(p, &end, 10); + if (end == p) { + throw Error("BlackBoxExec", "Failed to read output integer " + + std::to_string(i) + + " from blackbox process output, " + + std::to_string(int_out.size()) + + " integer values where expected."); + } + int_out[i] = static_cast(v); + p = end; + skip_ws(p); + if (*p == ',') { + ++p; + } + } + skip_ws(p); + if (*p != ';') { + throw Error("BlackBoxExec", + "Blackbox process response is missing the `;' separator between " + "the integer and floating point outputs."); + } + ++p; + for (size_t i = 0; i < float_out.size(); ++i) { + double v = std::strtod(p, &end); + if (end == p) { + throw Error("BlackBoxExec", "Failed to read output float " + + std::to_string(i) + + " from blackbox process output, " + + std::to_string(float_out.size()) + + " floating point values where expected."); + } + float_out[i] = v; + p = end; + skip_ws(p); + if (*p == ',') { + ++p; + } + } +} + +ExecStatus BlackBox::propagate(Space &home, const ModEventDelta &) { + if (int_input.assigned() +#ifdef GECODE_HAS_FLOAT_VARS + && float_input.assigned() +#endif + ) { + std::vector int_in(int_input.size()); + std::vector int_out(int_output.size()); + // std::cerr << "Black Box Fn input: "; + for (int i = 0; i < int_in.size(); i++) { + // std::cerr << int_input[i].val() << " "; + int_in[i] = int_input[i].val(); + } + std::vector float_in; + std::vector float_out; +#ifdef GECODE_HAS_FLOAT_VARS + float_in.resize(float_input.size()); + float_out.resize(float_output.size()); + for (int i = 0; i < float_in.size(); i++) { + // std::cerr << float_input[i].val() << " "; + float_in[i] = float_input[i].val().med(); + } +#endif + // std::cerr << std::endl; + + black_box()->run(int_in, float_in, int_out, float_out); + + // std::cerr << "Black Box Fn output: "; + for (int i = 0; i < int_out.size(); i++) { + // std::cerr << int_out[i] << " "; + GECODE_ME_CHECK(int_output[i].eq(home, static_cast(int_out[i]))); + } +#ifdef GECODE_HAS_FLOAT_VARS + for (int i = 0; i < float_out.size(); i++) { + // std::cerr << float_out[i] << " "; + GECODE_ME_CHECK(float_output[i].eq(home, float_out[i])); + } +#endif + // std::cerr << std::endl; + + return home.ES_SUBSUMED(*this); + } + return ES_FIX; +} + +ExecStatus BlackBoxBounds::propagate(Space &home, const ModEventDelta &) { + std::vector int_in(ivar.size() * 2); + std::vector int_out(ivar.size() * 2); + // std::cerr << "Black Box Bounds Fn input: "; + for (int i = 0; i < ivar.size(); i++) { + // std::cerr << ivar[i].min() << " " << ivar[i].max() << " "; + int_in[i*2] = ivar[i].min(); + int_in[i*2+1] = ivar[i].max(); + } + std::vector float_in; + std::vector float_out; +#ifdef GECODE_HAS_FLOAT_VARS + float_in.resize(fvar.size() * 2); + float_out.resize(fvar.size() * 2); + for (int i = 0; i < fvar.size(); i++) { + // std::cerr << fvar[i].min() << " " << fvar[i].max() << " "; + float_in[i*2] = fvar[i].min(); + float_in[i*2+1] = fvar[i].max(); + } +#endif + // std::cerr << std::endl; + + black_box()->run(int_in, float_in, int_out, float_out); + + // std::cerr << "Black Box Fn output: "; + for (int i = 0; i < ivar.size(); i++) { + // std::cerr << int_out[i*2] << ".." << int_out[i*2+1] << " "; + GECODE_ME_CHECK(ivar[i].gq(home, static_cast(int_out[i*2]))); + GECODE_ME_CHECK(ivar[i].lq(home, static_cast(int_out[i*2+1]))); + } +#ifdef GECODE_HAS_FLOAT_VARS + for (int i = 0; i < fvar.size(); i++) { + // std::cerr << float_out[i*2] << ".." << float_out[i*2+1] << " "; + GECODE_ME_CHECK(fvar[i].gq(home, float_out[i*2])); + GECODE_ME_CHECK(fvar[i].lq(home, float_out[i*2+1])); + } +#endif + // std::cerr << std::endl; + + return ES_NOFIX; +} + +void blackbox(Home home, const IntVarArgs &int_in, const IntVarArgs &int_out, +#ifdef GECODE_HAS_FLOAT_VARS + const FloatVarArgs &float_in, const FloatVarArgs &float_out, +#endif + const std::string &mode, const std::string &instantiation, + const std::vector &args) { + ViewArray int_input(home, int_in); + ViewArray int_output(home, int_out); +#ifdef GECODE_HAS_FLOAT_VARS + ViewArray float_input(home, float_in); + ViewArray float_output(home, float_out); +#endif + + if (home.failed()) + return; + PostInfo pi(home); + ExecStatus es = BlackBox::post(home, int_input, int_output, +#ifdef GECODE_HAS_FLOAT_VARS + float_input, float_output, +#endif + mode, instantiation, args); + GECODE_ES_FAIL(es); +} + +/// Parse the flat reason and mark, per channel, the variables whose bounds the +/// propagator depends on (the variables that appear as literals in any reason). +/// \a sub_int / \a sub_float are filled with one boolean per variable. Variable +/// indices in the reason are 1-based over the combined variable list, integer +/// variables first, then float variables. +/// +/// The flat reason is a concatenation of one entry per variable, each entry +/// being `[idx, |R_lb|, (var, bnd)..., |R_ub|, (var, bnd)...]`. An empty reason +/// falls back to subscribing to every variable. +static void reason_subscriptions(const std::vector &reason, int n_int, + int n_float, SharedArray &sub_int, + SharedArray &sub_float) { + const bool all = reason.empty(); + for (int i = 0; i < n_int; i++) { + sub_int[i] = all; + } + for (int i = 0; i < n_float; i++) { + sub_float[i] = all; + } + if (all) { + return; + } + + size_t pos = 0; + while (pos < reason.size()) { + pos++; // idx: the variable being explained (not needed for subscription) + for (int side = 0; side < 2; side++) { // lower- then upper-bound literals + int count = reason[pos++]; + for (int k = 0; k < count; k++) { + int var = reason[pos++]; // 1-based combined variable index + pos++; // bound code (per-variable granularity only) + if (var >= 1 && var <= n_int) { + sub_int[var - 1] = true; + } else if (var > n_int && var <= n_int + n_float) { + sub_float[var - 1 - n_int] = true; + } + } + } + } +} + +void blackbox_bounds(Home home, const IntVarArgs &ivar, +#ifdef GECODE_HAS_FLOAT_VARS + const FloatVarArgs &fvar, +#endif + const std::string &mode, const std::string &instantiation, + const std::vector &args, + const std::vector &reason) { + ViewArray int_var(home, ivar); +#ifdef GECODE_HAS_FLOAT_VARS + ViewArray float_var(home, fvar); + int n_float = fvar.size(); +#else + int n_float = 0; +#endif + + // Determine which variables the propagator depends on, so it is only + // subscribed (and thus scheduled) on the bounds mentioned in the reason. The + // marking is constant and shared between all copies of the propagator. + SharedArray sub_int(ivar.size()); + SharedArray sub_float(n_float); + reason_subscriptions(reason, ivar.size(), n_float, sub_int, sub_float); + + if (home.failed()) + return; + PostInfo pi(home); + ExecStatus es = BlackBoxBounds::post(home, int_var, +#ifdef GECODE_HAS_FLOAT_VARS + float_var, +#endif + sub_int, +#ifdef GECODE_HAS_FLOAT_VARS + sub_float, +#endif + mode, instantiation, args); + GECODE_ES_FAIL(es); +} + +} // namespace FlatZinc +} // namespace Gecode diff --git a/gecode/flatzinc/blackbox.hh b/gecode/flatzinc/blackbox.hh new file mode 100644 index 0000000000..5e7bf5edd7 --- /dev/null +++ b/gecode/flatzinc/blackbox.hh @@ -0,0 +1,400 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Jip J. Dekker + * + * Copyright: + * Jip J. Dekker, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef __FLATZINC_BLACKBOX_HH__ +#define __FLATZINC_BLACKBOX_HH__ + +#include +#include +#include +#include + +#include +#include +#ifdef GECODE_HAS_FLOAT_VARS +#include +#endif + +#ifdef _WIN32 +#define NOMINMAX // Ensure the words min/max remain available +#include +#else +// NOLINTNEXTLINE(bugprone-reserved-identifier) +#define __stdcall +#endif + +namespace Gecode { +namespace FlatZinc { + +/// Abstract class implemented by different methods to run blackbox functions +class BlackBoxFn : public SharedHandle::Object { +public: + virtual void run(const std::vector &int_in, + const std::vector &float_in, + std::vector &int_out, + std::vector &float_out) = 0; +}; + +/// Implementation of a black box function that dynamically loads a library and +/// run a contained function. +class BlackBoxDLL : public BlackBoxFn { +public: + BlackBoxDLL(const std::string &name, const std::vector &args); + ~BlackBoxDLL(); + void run(const std::vector &int_in, + const std::vector &float_in, std::vector &int_out, + std::vector &float_out) override { + dll_fzn_blackbox(int_in.data(), int_in.size(), float_in.data(), + float_in.size(), int_out.data(), int_out.size(), + float_out.data(), float_out.size()); + } + +protected: + void *library; + void(__stdcall *dll_fzn_blackbox)(const int64_t *, size_t, const double *, + size_t, int64_t *, size_t, double *, size_t); +}; + +/// Implementation of a black function that starts a seperate process to +/// repeatedly run a blackbox function, communication I/O over pipe. +class BlackBoxExec : public BlackBoxFn { +public: + BlackBoxExec(const std::string &program, const std::vector &args); + ~BlackBoxExec(); + void run(const std::vector &int_in, + const std::vector &float_in, std::vector &int_out, + std::vector &float_out) override; + +protected: +#ifdef _WIN32 + HANDLE pipe_send; + HANDLE pipe_receive; +#else + int pipe_send; + FILE *file_receive; +#endif +}; + +class BlackBoxHandle : public SharedHandle { +public: + BlackBoxHandle(BlackBoxFn *fn) : SharedHandle() { object(fn); } + BlackBoxHandle(const BlackBoxHandle &handle) : SharedHandle(handle) {} + BlackBoxHandle &operator=(const BlackBoxHandle &handle) { + return static_cast(SharedHandle::operator=(handle)); + } + BlackBoxFn *operator()() { return static_cast(object()); }; +}; + +class BlackBox : public Propagator { +protected: + /// Integer variables considered as the integer input to the blackbox function + ViewArray int_input; + /// Integer variables set to the integer output of the blackbox function + ViewArray int_output; + +#ifdef GECODE_HAS_FLOAT_VARS + /// Floating-point variables considered as the integer input to the blackbox function + ViewArray float_input; + /// Floating-point variables set to the integer output of the blackbox function + ViewArray float_output; +#endif + + /// Handle to the implementation of the blackbox function + /// + /// The handle ensures that the function implementation can be shared between + /// copies of the propagator. + BlackBoxHandle black_box; + + /// Constructor for cloning \a p + BlackBox(Space &home, BlackBox &p) + : Propagator(home, p), black_box(p.black_box) { + int_input.update(home, p.int_input); + int_output.update(home, p.int_output); +#ifdef GECODE_HAS_FLOAT_VARS + float_input.update(home, p.float_input); + float_output.update(home, p.float_output); +#endif + } + +public: + /// Constructor for creation + BlackBox(Home home, ViewArray &int_in, + ViewArray &int_out, +#ifdef GECODE_HAS_FLOAT_VARS + ViewArray &float_in, + ViewArray &float_out, +#endif + BlackBoxFn *black_box) + : Propagator(home), int_input(int_in), int_output(int_out), +#ifdef GECODE_HAS_FLOAT_VARS + float_input(float_in), float_output(float_out), +#endif + black_box(black_box) { + int_input.subscribe(home, *this, Int::PC_INT_VAL); +#ifdef GECODE_HAS_FLOAT_VARS + float_input.subscribe(home, *this, Float::PC_FLOAT_VAL); +#endif + } + /// Cost function (defined as exponential) + PropCost cost(const Space &home, const ModEventDelta &med) const override { + return PropCost::crazy(PropCost::HI, int_input.size() +#ifdef GECODE_HAS_FLOAT_VARS + + float_input.size() +#endif + ); + }; + /// Schedule function + void reschedule(Space &home) override { + int_input.cancel(home, *this, Int::PC_INT_VAL); +#ifdef GECODE_HAS_FLOAT_VARS + float_input.cancel(home, *this, Float::PC_FLOAT_VAL); +#endif + } + /// Delete propagator and return its size + size_t dispose(Space &home) override { + int_input.cancel(home, *this, Int::PC_INT_VAL); +#ifdef GECODE_HAS_FLOAT_VARS + float_input.cancel(home, *this, Float::PC_FLOAT_VAL); +#endif + (void)Propagator::dispose(home); + // destroy plugin container + return sizeof(*this); + }; + + ExecStatus propagate(Space &home, const ModEventDelta &) override; + + Propagator *copy(Space &home) override { + return new (home) BlackBox(home, *this); + } + + static ExecStatus post(Home home, ViewArray &int_input, + ViewArray &int_output, +#ifdef GECODE_HAS_FLOAT_VARS + ViewArray &float_input, + ViewArray &float_output, +#endif + const std::string &mode, + const std::string &instantiation, + const std::vector &args) { + BlackBoxFn *black_box(nullptr); + if (mode == "dll") { + black_box = new BlackBoxDLL(instantiation, args); + } else if (mode == "exec") { + black_box = new BlackBoxExec(instantiation, args); + } else { + throw Error("Blackbox", "Unknown blackbox protocol `" + mode + "'"); + } + + new (home) BlackBox(home, int_input, int_output, +#ifdef GECODE_HAS_FLOAT_VARS + float_input, float_output, +#endif + black_box); + return ES_OK; + } +}; + +class BlackBoxBounds : public Propagator { +protected: + /// Integer variables whose bounds are input and computed by the blackbox function (in order). + ViewArray ivar; + +#ifdef GECODE_HAS_FLOAT_VARS + /// Floating-point variables whose bounds are input and computed by the blackbox function (in order). + ViewArray fvar; +#endif + + /// For each variable in \a ivar, whether the propagator depends on its bounds + /// (derived from the reason). Only marked variables are subscribed, so the + /// propagator is scheduled precisely when one of the relevant bounds changes. + /// + /// The marking is constant during search and is shared between all copies of + /// the propagator. + SharedArray sub_int; +#ifdef GECODE_HAS_FLOAT_VARS + /// For each variable in \a fvar, whether the propagator depends on its bounds. + SharedArray sub_float; +#endif + + /// Handle to the implementation of the blackbox function + /// + /// The handle ensures that the function implementation can be shared between + /// copies of the propagator. + BlackBoxHandle black_box; + + /// Constructor for cloning \a p + BlackBoxBounds(Space &home, BlackBoxBounds &p) + : Propagator(home, p), sub_int(p.sub_int), +#ifdef GECODE_HAS_FLOAT_VARS + sub_float(p.sub_float), +#endif + black_box(p.black_box) { + ivar.update(home, p.ivar); +#ifdef GECODE_HAS_FLOAT_VARS + fvar.update(home, p.fvar); +#endif + } + +public: + /// Constructor for creation + BlackBoxBounds(Home home, ViewArray &ivar, +#ifdef GECODE_HAS_FLOAT_VARS + ViewArray &fvar, +#endif + SharedArray sub_int0, +#ifdef GECODE_HAS_FLOAT_VARS + SharedArray sub_float0, +#endif + BlackBoxFn *black_box) + : Propagator(home), ivar(ivar), +#ifdef GECODE_HAS_FLOAT_VARS + fvar(fvar), +#endif + sub_int(sub_int0), +#ifdef GECODE_HAS_FLOAT_VARS + sub_float(sub_float0), +#endif + black_box(black_box) { + for (int i = 0; i < ivar.size(); i++) { + if (sub_int[i]) { + ivar[i].subscribe(home, *this, Int::PC_INT_BND); + } + } +#ifdef GECODE_HAS_FLOAT_VARS + for (int i = 0; i < fvar.size(); i++) { + if (sub_float[i]) { + fvar[i].subscribe(home, *this, Float::PC_FLOAT_BND); + } + } +#endif + } + /// Cost function (defined as exponential) + PropCost cost(const Space &home, const ModEventDelta &med) const override { + return PropCost::crazy(PropCost::HI, ivar.size() +#ifdef GECODE_HAS_FLOAT_VARS + + fvar.size() +#endif + ); + }; + /// Schedule function + void reschedule(Space &home) override { + for (int i = 0; i < ivar.size(); i++) { + if (sub_int[i]) { + ivar[i].reschedule(home, *this, Int::PC_INT_BND); + } + } +#ifdef GECODE_HAS_FLOAT_VARS + for (int i = 0; i < fvar.size(); i++) { + if (sub_float[i]) { + fvar[i].reschedule(home, *this, Float::PC_FLOAT_BND); + } + } +#endif + } + /// Delete propagator and return its size + size_t dispose(Space &home) override { + for (int i = 0; i < ivar.size(); i++) { + if (sub_int[i]) { + ivar[i].cancel(home, *this, Int::PC_INT_BND); + } + } +#ifdef GECODE_HAS_FLOAT_VARS + for (int i = 0; i < fvar.size(); i++) { + if (sub_float[i]) { + fvar[i].cancel(home, *this, Float::PC_FLOAT_BND); + } + } +#endif + (void)Propagator::dispose(home); + // destroy plugin container + return sizeof(*this); + }; + + ExecStatus propagate(Space &home, const ModEventDelta &) override; + + Propagator *copy(Space &home) override { + return new (home) BlackBoxBounds(home, *this); + } + + static ExecStatus post(Home home, ViewArray &ivar, +#ifdef GECODE_HAS_FLOAT_VARS + ViewArray &fvar, +#endif + SharedArray sub_int, +#ifdef GECODE_HAS_FLOAT_VARS + SharedArray sub_float, +#endif + const std::string &mode, + const std::string &instantiation, + const std::vector &args) { + BlackBoxFn *black_box(nullptr); + if (mode == "dll") { + black_box = new BlackBoxDLL(instantiation, args); + } else if (mode == "exec") { + black_box = new BlackBoxExec(instantiation, args); + } else { + throw Error("Blackbox", "Unknown blackbox protocol `" + mode + "'"); + } + + new (home) BlackBoxBounds(home, ivar, +#ifdef GECODE_HAS_FLOAT_VARS + fvar, +#endif + sub_int, +#ifdef GECODE_HAS_FLOAT_VARS + sub_float, +#endif + black_box); + return ES_OK; + } +}; + +void blackbox(Home home, const IntVarArgs &int_in, const IntVarArgs &int_out, +#ifdef GECODE_HAS_FLOAT_VARS + const FloatVarArgs &float_in, const FloatVarArgs &float_out, +#endif + const std::string &mode, const std::string &instantiation, + const std::vector &args); + +void blackbox_bounds(Home home, const IntVarArgs &ivar, +#ifdef GECODE_HAS_FLOAT_VARS + const FloatVarArgs &fvar, +#endif + const std::string &mode, const std::string &instantiation, + const std::vector &args, + const std::vector &reason); + +} // namespace FlatZinc +} // namespace Gecode + +#endif //__FLATZINC_BLACKBOX_HH__ diff --git a/gecode/flatzinc/mznlib/experimental/blackbox/fzn_blackbox.mzn b/gecode/flatzinc/mznlib/experimental/blackbox/fzn_blackbox.mzn new file mode 100644 index 0000000000..c8d39f1c03 --- /dev/null +++ b/gecode/flatzinc/mznlib/experimental/blackbox/fzn_blackbox.mzn @@ -0,0 +1,13 @@ +predicate fzn_blackbox( + array[int] of var int: int_input, + array[int] of var float: float_input, + array[int] of var int: int_output, + array[int] of var float: float_output +) = gecode_blackbox(int_input, float_input, int_output, float_output); + +predicate gecode_blackbox( + array[int] of var int: int_input, + array[int] of var float: float_input, + array[int] of var int: int_output, + array[int] of var float: float_output +); diff --git a/gecode/flatzinc/mznlib/experimental/blackbox/fzn_blackbox_bounds.mzn b/gecode/flatzinc/mznlib/experimental/blackbox/fzn_blackbox_bounds.mzn new file mode 100644 index 0000000000..af24151a7c --- /dev/null +++ b/gecode/flatzinc/mznlib/experimental/blackbox/fzn_blackbox_bounds.mzn @@ -0,0 +1,11 @@ +predicate fzn_blackbox_bounds( + array[int] of var int: int_input, + array[int] of var float: float_input, + array[int] of int: flat_reason, +) = gecode_blackbox_bounds(int_input, float_input, flat_reason); + +predicate gecode_blackbox_bounds( + array[int] of var int: int_input, + array[int] of var float: float_input, + array[int] of int: flat_reason, +); diff --git a/gecode/flatzinc/registry.cpp b/gecode/flatzinc/registry.cpp index 028427ab1e..8655fae1e3 100755 --- a/gecode/flatzinc/registry.cpp +++ b/gecode/flatzinc/registry.cpp @@ -47,6 +47,7 @@ #include #endif #include +#include namespace Gecode { namespace FlatZinc { @@ -1660,6 +1661,88 @@ namespace Gecode { namespace FlatZinc { member(s,x,y,s.arg2BoolVar(ce[2]),s.ann2ipl(ann)); } + /// Read a `blackbox_exec` / `blackbox_dll` source annotation into \a mode, + /// \a instantiation (the executable/library) and \a args (its argument + /// list). Supports both the single-argument form (no arguments) and the + /// `(target, args)` form. + void blackbox_source(AST::Node* ann, std::string& mode, + std::string& instantiation, + std::vector& args) { + AST::Call* c = nullptr; + if (ann->hasCall("blackbox_dll")) { + c = ann->getCall("blackbox_dll"); + mode = "dll"; + } else if (ann->hasCall("blackbox_exec")) { + c = ann->getCall("blackbox_exec"); + mode = "exec"; + } else { + throw FlatZinc::Error("Registry", + "Blackbox constraint is missing a valid annotation specifying execution method."); + } + // For a single-argument call `args` is the bare argument node; for the + // `(target, args)` form it is an array of the two arguments. + if (AST::Array* arr = dynamic_cast(c->args)) { + instantiation = arr->a[0]->getString(); + if (arr->a.size() > 1) { + AST::Array* al = arr->a[1]->getArray(); + for (unsigned int i = 0; i < al->a.size(); i++) { + args.push_back(al->a[i]->getString()); + } + } + } else { + instantiation = c->args->getString(); + } + } + + void p_blackbox(FlatZincSpace& s, const ConExpr& ce, AST::Node* ann) { + std::string mode; + std::string instantiation; + std::vector args; + blackbox_source(ann, mode, instantiation, args); + IntVarArgs int_input = s.arg2intvarargs(ce[0]); + IntVarArgs int_output = s.arg2intvarargs(ce[2]); +#ifdef GECODE_HAS_FLOAT_VARS + FloatVarArgs float_input = s.arg2floatvarargs(ce[1]); + FloatVarArgs float_output = s.arg2floatvarargs(ce[3]); +#else + if (!ce[1]->getArray()->a.empty() || !ce[3]->getArray()->a.empty()) { + throw FlatZinc::Error("Registry", + "Blackbox propagator cannot use floating point values when Gecode is compiled without floating point decision variable support."); + } +#endif + FlatZinc::blackbox(s, int_input, int_output, +#ifdef GECODE_HAS_FLOAT_VARS +float_input, float_output, +#endif + mode, instantiation, args); + } + + void p_blackbox_bounds(FlatZincSpace& s, const ConExpr& ce, AST::Node* ann) { + std::string mode; + std::string instantiation; + std::vector args; + blackbox_source(ann, mode, instantiation, args); + IntVarArgs ivar = s.arg2intvarargs(ce[0]); +#ifdef GECODE_HAS_FLOAT_VARS + FloatVarArgs fvar = s.arg2floatvarargs(ce[1]); +#else + if (!ce[1]->getArray()->a.empty()) { + throw FlatZinc::Error("Registry", + "Blackbox propagator cannot use floating point values when Gecode is compiled without floating point decision variable support."); + } +#endif + IntArgs flat_reason = s.arg2intargs(ce[2]); + std::vector reason(flat_reason.size()); + for (int i = 0; i < flat_reason.size(); i++) { + reason[i] = flat_reason[i]; + } + FlatZinc::blackbox_bounds(s, ivar, +#ifdef GECODE_HAS_FLOAT_VARS +fvar, +#endif + mode, instantiation, args, reason); + } + class IntPoster { public: IntPoster(void) { @@ -1848,6 +1931,9 @@ namespace Gecode { namespace FlatZinc { registry().add("gecode_member_int_reif",&p_member_int_reif); registry().add("member_bool",&p_member_bool); registry().add("gecode_member_bool_reif",&p_member_bool_reif); + + registry().add("gecode_blackbox", &p_blackbox); + registry().add("gecode_blackbox_bounds", &p_blackbox_bounds); } }; IntPoster __int_poster;