Skip to content
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
116 changes: 94 additions & 22 deletions docs/python-c.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ 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 dynamical library files are located in `the_optimizer/target/{debug,release}` (depending on whether it was a [*debug*] or [*release*] build)
* 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.

Expand Down Expand Up @@ -112,13 +112,16 @@ 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;
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;

Expand All @@ -137,10 +140,61 @@ 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.
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 = 0` means the solver call completed without an internal error
- `error_code != 0` means the solver failed and `error_message` contains a
descriptive explanation
- `exit_status` gives 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:

1. Call `{optimizer-name}_solve(...)`
2. Check whether `status.error_code != 0`
3. If so, report `status.error_message` and treat the call as failed
4. Otherwise, inspect `status.exit_status` to determine whether the solver
converged or returned the best available non-converged iterate

For example:

```c
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

Expand All @@ -155,26 +209,41 @@ 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() {
double p[EXAMPLE_NUM_PARAMETERS] = {1.0, 10.0}; // parameter
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();
exampleSolverStatus status = example_solve(cache, u, p);
example_free(cache);

for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) {
printf("u[%d] = %g\n", i, u[i]);
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);

return 0;
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;
}
```

Expand All @@ -185,19 +254,18 @@ int main() {
To compile your C program you need to link to the auto-generated
C bindings (see [next section](#compile-your-own-code)).
However, OpEn generates automatically a `CMakeLists.txt` file
to facilitate the compilation/linking procedure. To build the
auto-generated example run
to facilitate the compilation/linking procedure. A typical build is:

```bash
cmake .
make
cmake -S . -B build
cmake --build build
```

once you build your optimizer you can run the executable (`optimizer`)
with
Once you build your optimizer you can run the executable (`optimizer`)
with:

```bash
make run
cmake --build build --target run
```

#### Compile your own code
Expand Down Expand Up @@ -260,14 +328,18 @@ LD_LIBRARY_PATH=./target/release ./optimizer
The output looks like this:

```text
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
exit status = 0
iterations = 69
outer iterations = 5
solve time = 0.140401 ms
```

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.
2 changes: 1 addition & 1 deletion open-codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Note: This is the Changelog file of `opengen` - the Python interface of OpEn
- Extended `RosConfiguration` so it can be used for both ROS and ROS2 package generation
- Breaking change: the direct interface (Python bindings) now has an API which mirrors that of the TCP interface: the method `solve` returns either a solution or an error object. Website documentation is updated. New unit tests are implemented. Note that `solver.run()` does not return the solution object directly, but rather works in the same way as the TCP interface: it returns a response object (instance of `SolverResponse`), on which the method `.get()` returns either a `SolverStatus` or `SolverError`.
- Added helpful `__repr__` methods to generated Python binding response/status/error objects, TCP solver response/error objects, and `GeneratedOptimizer` for easier inspection and debugging
- Updated generated TCP server and C interface templates to work with the richer Rust solver error model and expose better failure information to clients
- Updated generated TCP server and C interface templates to work with the richer Rust solver error model and expose better failure information to clients. Updated auto-generated `CMakeLists.txt` file. Tighter unit tests.
- ROS2 generated packages now publish detailed `error_code` and `error_message` fields, plus `STATUS_INVALID_REQUEST`, so invalid requests and solver failures are reported explicitly instead of being silently ignored


Expand Down
37 changes: 30 additions & 7 deletions open-codegen/opengen/templates/c/example_cmakelists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,40 @@ cmake_minimum_required(VERSION 3.5)
# Project name
project({{meta.optimizer_name}})

# Build the generated example as C99.
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Resolve the generated static library name for the current platform.
if(WIN32)
set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/{{meta.optimizer_name}}.lib)
else()
set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a)
endif()

find_package(Threads REQUIRED)

# Add the executable
add_executable(optimizer example_optimizer.c)

# Add libraries to the executable
target_link_libraries(optimizer ${CMAKE_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a)
target_link_libraries(optimizer m)
target_link_libraries(optimizer dl)
target_link_libraries(optimizer pthread)
target_link_libraries(
optimizer
PRIVATE
${OPEN_STATIC_LIB}
Threads::Threads
)

if(UNIX)
target_link_libraries(optimizer PRIVATE m)
endif()

if(CMAKE_DL_LIBS)
target_link_libraries(optimizer PRIVATE ${CMAKE_DL_LIBS})
endif()

add_custom_target(run
COMMAND optimizer
COMMAND $<TARGET_FILE:optimizer>
DEPENDS optimizer
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
66 changes: 49 additions & 17 deletions open-codegen/opengen/templates/c/example_optimizer_c_bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,35 @@
*/

#include <stdio.h>
#include <stdlib.h>
#include "{{meta.optimizer_name}}_bindings.h"

/*
* Feel free to customize the following code...
*/

static const char *exit_status_to_string({{meta.optimizer_name}}ExitStatus exit_status) {
switch (exit_status) {
case {{meta.optimizer_name}}Converged:
return "Converged";
case {{meta.optimizer_name}}NotConvergedIterations:
return "NotConvergedIterations";
case {{meta.optimizer_name}}NotConvergedOutOfTime:
return "NotConvergedOutOfTime";
case {{meta.optimizer_name}}NotConvergedCost:
return "NotConvergedCost";
case {{meta.optimizer_name}}NotConvergedNotFiniteComputation:
return "NotConvergedNotFiniteComputation";
default:
return "Unknown";
}
}

int main(void) {
int i;

/* parameters */
double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {2.0, 10.0};
double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {0};

/* initial guess */
double u[{{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES] = {0};
Expand All @@ -45,28 +63,18 @@ int main(void) {

/* obtain cache */
{{meta.optimizer_name}}Cache *cache = {{meta.optimizer_name}}_new();
if (cache == NULL) {
fprintf(stderr, "Could not allocate solver cache\n");
return EXIT_FAILURE;
}

/* solve */
{{meta.optimizer_name}}SolverStatus status = {{meta.optimizer_name}}_solve(cache, u, p, {% if problem.dim_constraints_aug_lagrangian() > 0 %}y{% else %}0{% endif %}, &init_penalty);

/* print results */
printf("\n\n-------------------------------------------------\n");
printf(" Solution\n");
printf("-------------------------------------------------\n");

for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) {
printf("u[%d] = %g\n", i, u[i]);
}

printf("\n");
for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) {
printf("y[%d] = %g\n", i, status.lagrange[i]);
}

printf("\n\n-------------------------------------------------\n");
printf(" Solver Statistics\n");
printf("-------------------------------------------------\n");
printf("exit status : %d\n", status.exit_status);
printf("exit status : %d (%s)\n", status.exit_status, exit_status_to_string(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);
Expand All @@ -78,10 +86,34 @@ int main(void) {
printf("Cost : %f\n", status.cost);
printf("||FRP|| : %f\n\n", status.last_problem_norm_fpr);

if (status.error_code != 0) {
fprintf(stderr, "Solver returned an error; solution vector is not printed.\n");
{{meta.optimizer_name}}_free(cache);
return EXIT_FAILURE;
}

if (status.exit_status != {{meta.optimizer_name}}Converged) {
fprintf(stderr, "Warning: solver did not converge, printing best available iterate.\n");
}

/* print results */
printf("\n\n-------------------------------------------------\n");
printf(" Solution\n");
printf("-------------------------------------------------\n");

for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) {
printf("u[%d] = %g\n", i, u[i]);
}

printf("\n");
for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) {
printf("y[%d] = %g\n", i, status.lagrange[i]);
}



/* free memory */
{{meta.optimizer_name}}_free(cache);

return 0;
return EXIT_SUCCESS;
}
13 changes: 7 additions & 6 deletions open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% if activate_clib_generation -%}
// ---Export functionality from Rust to C/C++------------------------------------------------------------
{% set error_message_capacity = 1024 -%}
// ---Export functionality from Rust to C/C++ --------------

/// Solver cache (structure `{{meta.optimizer_name}}Cache`)
///
Expand All @@ -10,21 +11,21 @@ pub struct {{meta.optimizer_name}}Cache {

const {{meta.optimizer_name|upper}}_NO_ERROR_CODE: c_int = 0;
const {{meta.optimizer_name|upper}}_SOLVER_ERROR_CODE: c_int = 2000;
const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = 256;
const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = {{ error_message_capacity }};

impl {{meta.optimizer_name}}Cache {
pub fn new(cache: AlmCache) -> Self {
{{meta.optimizer_name}}Cache { cache }
}
}

fn empty_error_message() -> [c_char; 256] {
[0 as c_char; 256]
fn empty_error_message() -> [c_char; {{ error_message_capacity }}] {
[0 as c_char; {{ error_message_capacity }}]
}

fn error_message_to_c_array(
message: &str,
) -> [c_char; 256] {
) -> [c_char; {{ error_message_capacity }}] {
let mut buffer = empty_error_message();
let max_len = {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY - 1;
for (idx, byte) in message.as_bytes().iter().copied().take(max_len).enumerate() {
Expand Down Expand Up @@ -63,7 +64,7 @@ pub struct {{meta.optimizer_name}}SolverStatus {
/// Detailed error code (0 on success)
error_code: c_int,
/// Detailed error message (empty string on success)
error_message: [c_char; 256],
error_message: [c_char; {{ error_message_capacity }}],
/// Number of outer iterations
num_outer_iterations: c_ulong,
/// Total number of inner iterations
Expand Down
Loading