C/C++ Bindings
When using any of the tools to auto-generate a solver it is directly supported to also generate C/C++ bindings for integrating the solver from any language which supports a C or C++ application binary interface (ABI). This is a powerful feature when packaging a solver for distribution, or including it into a larger project.
Generating bindings
Generating bindings is as easy as adding with_build_c_bindings() to your build configuration.
Here is a full example configuration for creating a solver. We will create an optimizer
for the Rosenbrock function with equality and bound constraints on the decision variables:
import casadi.casadi as cs
import opengen as og
u = cs.SX.sym("u", 5) # Decision variabels
p = cs.SX.sym("p", 2) # Parameters
phi = og.functions.rosenbrock(u, p) # Cost function
# Equality constraints: 1.5 * u[0] = u[1] and u[2] = u[3]
# Can also be inequality constratins using max{c(u, p), 0}, where c(u, p) < 0.
c = cs.vertcat(1.5*u[0] - u[1], u[2] - u[3])
# Bounds constraints on u
umin = [-2.0] * 5 # shorthand notation
umax = [ 2.0] * 5
# Bounds on u: (umin <= u <= umax)
bounds = og.constraints.Rectangle(umin, umax)
# Define the problem
problem = og.builder.Problem(u, p, phi) \
.with_penalty_constraints(c) \
.with_constraints(bounds)
# Meta information for the solver
meta = og.config.OptimizerMeta() \
.with_version("1.0.0") \
.with_authors(["P. Sopasakis", "E. Fresk"]) \
.with_optimizer_name("the_optimizer")
# Lets build in release mode with C bindings
build_config = og.config.BuildConfiguration() \
.with_build_mode("release") \
.with_build_c_bindings() # <--- The important setting
# Solver settings
solver_config = og.config.SolverConfiguration() \
.with_tolerance(1e-5) \
.with_constraints_tolerance(1e-4) \
.with_max_outer_iterations(15) \
.with_penalty_weight_update_factor(8.0) \
.with_initial_penalty_weights([20.0, 5.0])
# Create the solver!
builder = og.builder.OpEnOptimizerBuilder(problem,
metadata=meta,
build_configuration=build_config,
solver_configuration=solver_config)
builder.build()
The generated C/C++ bindings are in the auto-generated solver library. In particular
- The header files are at
the_optimizer/the_optimizer_bindings.{h,hpp} - The static and dynamic library files are located in
the_optimizer/target/{debug,release}(depending on whether it was a debug or release build)
Note that the_optimizer is the name given to the optimizer in the Python codegen above.
Matlab generation will come soon.
Bindings API
The generated API has the following functions available (for complete definitions see the generated header files) for a solver entitled "example":
/* Contents of header file: example_bindings.h */
#define EXAMPLE_N1 0
#define EXAMPLE_N2 2
#define EXAMPLE_NUM_DECISION_VARIABLES 5
#define EXAMPLE_NUM_PARAMETERS 2
typedef enum {
exampleConverged,
exampleNotConvergedIterations,
exampleNotConvergedOutOfTime,
exampleNotConvergedCost,
exampleNotConvergedNotFiniteComputation,
} exampleExitStatus;
typedef struct exampleCache exampleCache;
typedef struct {
exampleExitStatus exit_status;
int error_code;
char error_message[1024];
unsigned long num_outer_iterations;
unsigned long num_inner_iterations;
double last_problem_norm_fpr;
unsigned long long solve_time_ns;
double penalty;
double delta_y_norm_over_c;
double f2_norm;
double cost;
const double *lagrange;
} exampleSolverStatus;
void example_free(exampleCache *instance);
exampleCache *example_new(void);
exampleSolverStatus example_solve(exampleCache *instance,
double *u,
const double *params,
const double *y0,
const double *c0);
This is designed to follow a new-use-free pattern.
Function {optimizer-name}_new will allocate memory and setup a new solver instance and can be used to create as many solvers as necessary. Each solver instance can be used with {optimizer-name}_solve to solve the corresponding problem as many times as needed.
Parameter u is the starting guess and also the return of the decision variables and params is the array of static parameters. The size of u and params are {optimizer-name}_NUM_DECISION_VARIABLES and {optimizer-name}_NUM_PARAMETERS respectively. Arguments y0 and c0 are optional: pass 0 (or NULL) to use the default initial Lagrange multipliers and penalty parameter.
The returned exampleSolverStatus always contains a coarse solver outcome in
exit_status. On success it also contains error_code = 0 and an empty
error_message. If the solver fails internally, the bindings return a
structured error report with a nonzero error_code and a descriptive
error_message.
Finally, when done with the solver, use {optimizer-name}_free to release the memory allocated by {optimizer-name}_new.
Handling errors
The C bindings always return a value of type exampleSolverStatus. This means
that solver calls do not report failure by returning NULL or by using a
separate exception-like mechanism. Instead, callers should inspect both
exit_status and error_code.
error_code = 0means the solver call completed without an internal errorerror_code != 0means the solver failed anderror_messagecontains a descriptive explanationexit_statusgives the coarse outcome of the solve attempt, such as converged, reached the iteration limit, or failed because of a numerical issue
The recommended pattern is:
- Call
{optimizer-name}_solve(...) - Check whether
status.error_code != 0 - If so, report
status.error_messageand treat the call as failed - Otherwise, inspect
status.exit_statusto determine whether the solver converged or returned the best available non-converged iterate
For example:
exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty);
if (status.error_code != 0) {
fprintf(stderr, "Solver failed: [%d] %s\n",
status.error_code, status.error_message);
example_free(cache);
return EXIT_FAILURE;
}
if (status.exit_status != exampleConverged) {
fprintf(stderr, "Warning: solver did not converge fully\n");
}
The generated C example follows exactly this pattern.
At the ABI level, callers are still responsible for passing valid pointers and
correctly sized arrays for u, params, and optional arguments such as y0.
Those are contract violations, not recoverable solver errors.
Using the bindings in an app
Suppose that you have compiled your optimizer using the option
with_build_c_bindings() and you have generated C/C++ bindings.
OpEn will generate an example C file that you can use as a starting point
to build your application. This example file is stored in the directory
containing all other files of your optimizer and is called example_optimizer.c.
The auto-generated example has the following form:
/* File: the_optimizer/example_optimizer.c */
#include <stdio.h>
#include <stdlib.h>
#include "example_bindings.h"
int main(void) {
double p[EXAMPLE_NUM_PARAMETERS] = {0}; // parameter
double u[EXAMPLE_NUM_DECISION_VARIABLES] = {0}; // initial guess
double initial_penalty = 15.0;
exampleCache *cache = example_new();
if (cache == NULL) {
fprintf(stderr, "Could not allocate solver cache\n");
return EXIT_FAILURE;
}
exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty);
printf("exit status = %d\n", status.exit_status);
printf("error code = %d\n", status.error_code);
printf("error message = %s\n", status.error_message);
printf("iterations = %lu\n", status.num_inner_iterations);
printf("outer iterations = %lu\n", status.num_outer_iterations);
printf("solve time = %f ms\n", (double)status.solve_time_ns / 1e6);
if (status.error_code != 0) {
example_free(cache);
return EXIT_FAILURE;
}
for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) {
printf("u[%d] = %g\n", i, u[i]);
}
example_free(cache);
return EXIT_SUCCESS;
}
Compiling and linking
Using cmake
To compile your C program you need to link to the auto-generated
C bindings (see next section).
However, OpEn generates automatically a CMakeLists.txt file
to facilitate the compilation/linking procedure. A typical build is:
cmake -S . -B build
cmake --build build
Once you build your optimizer you can run the executable (optimizer)
with:
cmake --build build --target run
Compile your own code
When building and running an application based on the generated libraries it is good to know that GCC is a bit temperamental when linking libraries, so the following can be used as reference:
- Static linking
- Dynamic linking
- Direct linking
gcc example_optimizer.c -l:libthe_optimizer.a \
-L./target/release -pthread -lm -ldl -o optimizer
gcc example_optimizer.c -lthe_optimizer \
-L./target/release -pthread -lm -ldl -o optimizer
# Only works with clang
clang example_optimizer.c ./target/release/libthe_optimizer.a \
-pthread -lm -ldl -o optimizer
Running
Which will solve the problem and output the following when run:
- Static lib
- Dynamic lib
./optimizer
LD_LIBRARY_PATH=./target/release ./optimizer
The output looks like this:
exit status = 0
error code = 0
error message =
iterations = 69
outer iterations = 5
solve time = 0.140401 ms
u[0] = 0.654738
u[1] = 0.982045
u[2] = 0.98416
u[3] = 0.984188
u[4] = 0.969986
If error_code is nonzero, the solver failed to produce a valid result and
error_message contains the propagated reason from the generated Rust solver.