diff --git a/.github/workflows/C-test-example.yml b/.github/workflows/C-test-example.yml index db5d3b400..22cddc85f 100644 --- a/.github/workflows/C-test-example.yml +++ b/.github/workflows/C-test-example.yml @@ -70,10 +70,10 @@ jobs: -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DUNO=/usr/local/lib/libuno.so . - - name: Build example + - name: Build C example working-directory: ${{github.workspace}}/interfaces/C/example run: cmake --build ${{github.workspace}}/interfaces/C/example/build --config ${{env.BUILD_TYPE}} - - name: Run example + - name: Run C example working-directory: ${{github.workspace}}/interfaces/C/example/build run: ./example_hs015 \ No newline at end of file diff --git a/.github/workflows/Rust-test-example.yml b/.github/workflows/Rust-test-example.yml new file mode 100644 index 000000000..b8d273ac0 --- /dev/null +++ b/.github/workflows/Rust-test-example.yml @@ -0,0 +1,77 @@ +name: Test Rust example + +on: + push: + branches: [ "main" ] + paths-ignore: + - '*.md' + - 'LICENSE' + - '*.cff' + - '*.yml' + - '*.yaml' + - 'docs/**' + pull_request: + branches: [ "main" ] + paths-ignore: + - '*.md' + - 'LICENSE' + - '*.cff' + - '*.yml' + - '*.yaml' + - 'docs/**' + +env: + BUILD_TYPE: Debug + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + architecture: [x64] + + steps: + - uses: actions/checkout@v4 + + - name: Download dependencies + run: bash dependencies/scripts/download_dependencies.sh + + - name: Configure (Linux) + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_FORTRAN_COMPILER=gfortran \ + -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ + -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ + -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ + -DMUMPS_COMMON_LIBRARY=${{github.workspace}}/dependencies/lib/libmumps_common.a \ + -DMUMPS_PORD_LIBRARY=${{github.workspace}}/dependencies/lib/libpord.a \ + -DMUMPS_MPISEQ_LIBRARY=${{github.workspace}}/dependencies/lib/libmpiseq.a \ + -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ + -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ + -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_SHARED_LIBS=ON + + - name: Build + run: cmake --build build --config ${{env.BUILD_TYPE}} -j4 + + - name: Install Uno + run: sudo cmake --install ${{github.workspace}}/build + + - name: Update dynamic linker cache + run: sudo ldconfig + + - name: Build Rust example + working-directory: ${{github.workspace}}/interfaces/Rust + run: cargo build --bin hs015 + + - name: Run Rust example + working-directory: ${{github.workspace}}/interfaces/Rust + run: cargo run --bin hs015 \ No newline at end of file diff --git a/.github/workflows/fortran-wrappers.yml b/.github/workflows/fortran-wrappers.yml index e54ba6a14..6dc3c361d 100644 --- a/.github/workflows/fortran-wrappers.yml +++ b/.github/workflows/fortran-wrappers.yml @@ -27,7 +27,7 @@ jobs: julia --color=no --project wrapper_fortran.jl cd ../../Fortran git diff --quiet uno_c.f90 uno_fortran.f90 || { - echo "❌ The Fortran wrappers in uno_c.f90 / uno_fortran.f90 are out of date. ❌"; + echo "❌ The Fortran wrappers in interfaces/Fortran/uno_c.f90 and interfaces/Fortran/uno_fortran.f90 are out of date. ❌"; echo "Please run wrapper_fortran.jl in interfaces/Julia/gen to regenerate them:"; echo ""; echo "cd interfaces/Julia/gen"; diff --git a/.github/workflows/julia-wrappers.yml b/.github/workflows/julia-wrappers.yml index d7c7bd7e9..6dc665799 100644 --- a/.github/workflows/julia-wrappers.yml +++ b/.github/workflows/julia-wrappers.yml @@ -27,7 +27,7 @@ jobs: julia --color=no --project wrapper_julia.jl cd ../src git diff --quiet libuno.jl || { - echo "❌ The Julia wrappers in libuno.jl are out of date. ❌"; + echo "❌ The Julia wrappers in interfaces/Julia/src/libuno.jl are out of date. ❌"; echo "Please run wrapper_julia.jl in interfaces/Julia/gen to regenerate them:"; echo ""; echo "cd interfaces/Julia/gen"; diff --git a/.github/workflows/rust-wrappers.yml b/.github/workflows/rust-wrappers.yml new file mode 100644 index 000000000..a554d3ea5 --- /dev/null +++ b/.github/workflows/rust-wrappers.yml @@ -0,0 +1,40 @@ +name: Rust wrappers +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Check for updates + runs-on: ubuntu-latest + steps: + - name: Checkout Uno + uses: actions/checkout@v4 + + - name: Install Julia + uses: julia-actions/setup-julia@v2 + with: + version: 1 + arch: x64 + + - name: Regenerate the Rust wrappers + shell: bash + run: | + cd interfaces/Julia/gen + julia --color=no --project -e 'using Pkg; Pkg.instantiate()' + julia --color=no --project wrapper_rust.jl + cd ../../Rust/src + git diff --quiet ffi.rs || { + echo "❌ The Rust wrappers in interfaces/Rust/src/ffi.rs are out of date. ❌"; + echo "Please run wrapper_rust.jl in interfaces/Julia/gen to regenerate them:"; + echo ""; + echo "cd interfaces/Julia/gen"; + echo "julia --project -e 'using Pkg; Pkg.instantiate()'"; + echo "julia --project wrapper_rust.jl"; + echo ""; + echo "You can also find the instructions at:"; + echo "https://github.com/cvanaret/Uno/blob/master/interfaces/Julia/README.md"; + exit 1; + } diff --git a/README.md b/README.md index a59677d23..7cc76ec3d 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,11 @@ Uno provides a native Fortran interface built on top of its C API using `iso_c_b It closely mirrors the C interface and is designed as a lightweight wrapper with minimal overhead, making it suitable for integration into existing Fortran codes while retaining full access to Uno's features. For more details, see its [README.md](interfaces/Fortran/README.md). +### Rust +Uno provides a Rust interface (`uno_rs`) built on top of its C API using Rust's FFI. +It exposes high-level wrappers over the raw bindings in `ffi.rs`. +For more details, see its [README.md](interfaces/Rust/README.md). + ## Latest results (August 13, 2025) Uno presets have been tested against state-of-the-art solvers on 429 small problems of the [CUTEst benchmark](https://arnold-neumaier.at/glopt/coconut/Benchmark/Library2_new_v1.html). diff --git a/interfaces/Julia/gen/README.md b/interfaces/Julia/gen/README.md index 40ff9ef34..b83ca0a0d 100644 --- a/interfaces/Julia/gen/README.md +++ b/interfaces/Julia/gen/README.md @@ -1,7 +1,7 @@ # Wrapping headers and generating wrappers -This directory contains scripts `wrapper_julia.jl` and `wrapper_fortran.jl` that can be used -to automatically generate Julia and Fortran wrappers from the C headers of Uno. +This directory contains scripts `wrapper_julia.jl`, `wrapper_fortran.jl` and `wrapper_rust.jl` +that can be used to automatically generate Julia, Fortran and Rust wrappers from the C headers of Uno. This is done using [Clang.jl](https://github.com/JuliaInterop/Clang.jl). # Usage @@ -17,7 +17,7 @@ julia> ] ## Julia -Regenerate the Julia wrappers with the following commands: +Regenerate the Julia wrappers (`interfaces/Julia/src/libuno.jl`) with the following commands: ```julia julia> include("wrapper_julia.jl") julia> main_julia() @@ -32,7 +32,7 @@ Note that if new constants are added to `Uno_C_API.h`, a manual update of `prolo ## Fortran -Regenerate the Fortran wrappers with the following commands: +Regenerate the Fortran wrappers (`interfaces/Fortran/src/uno_c.f90` and `interfaces/Fortran/src/uno_fortran.f90`) with the following commands: ```julia julia> include("wrapper_fortran.jl") julia> main_fortran() @@ -44,3 +44,18 @@ julia --project wrapper_fortran.jl ``` Note that if new constants or callbacks are added to `Uno_C_API.h`, a manual update of `prologue_fortran.f90` is required. + +## Rust + +Regenerate the Rust FFI bindings (`interfaces/Rust/src/ffi.rs`) with the following commands: +```julia +julia> include("wrapper_rust.jl") +julia> main_rust() +``` + +If you have already instantiated the environment, you can also run: +```bash +julia --project wrapper_rust.jl +``` + +Note that if new constants or callbacks are added to `Uno_C_API.h`, a manual update of `prologue_rust.rs` is required. diff --git a/interfaces/Julia/gen/prologue_rust.rs b/interfaces/Julia/gen/prologue_rust.rs new file mode 100644 index 000000000..887630ad2 --- /dev/null +++ b/interfaces/Julia/gen/prologue_rust.rs @@ -0,0 +1,187 @@ +// Copyright (c) 2026 Alexis Montoison and Charlie Vanaret +// Licensed under the MIT license. See LICENSE file in the project directory for details. + +#![allow(non_camel_case_types)] + +use std::os::raw::{c_char, c_int, c_void}; + +//--- UNO_INT_TYPE --- + +// ───────────────────────────────────────────── +// Optimization sense +// ───────────────────────────────────────────── +pub const UNO_MINIMIZE: uno_int = 1; +pub const UNO_MAXIMIZE: uno_int = -1; + +// ───────────────────────────────────────────── +// Lagrange multiplier sign convention +// ───────────────────────────────────────────── +pub const UNO_MULTIPLIER_POSITIVE: uno_int = 1; +pub const UNO_MULTIPLIER_NEGATIVE: uno_int = -1; + +// ───────────────────────────────────────────── +// Problem types +// ───────────────────────────────────────────── +pub const UNO_PROBLEM_LINEAR: &[u8] = b"LP\0"; +pub const UNO_PROBLEM_QUADRATIC: &[u8] = b"QP\0"; +pub const UNO_PROBLEM_NONLINEAR: &[u8] = b"NLP\0"; + +// ───────────────────────────────────────────── +// Base indexing style +// ───────────────────────────────────────────── +pub const UNO_ZERO_BASED_INDEXING: uno_int = 0; +pub const UNO_ONE_BASED_INDEXING: uno_int = 1; + +// ───────────────────────────────────────────── +// Triangular part +// ───────────────────────────────────────────── +pub const UNO_LOWER_TRIANGLE: c_char = b'L' as c_char; +pub const UNO_UPPER_TRIANGLE: c_char = b'U' as c_char; + +// ───────────────────────────────────────────── +// Option type +// ───────────────────────────────────────────── +pub const UNO_OPTION_TYPE_INTEGER: uno_int = 0; +pub const UNO_OPTION_TYPE_DOUBLE: uno_int = 1; +pub const UNO_OPTION_TYPE_BOOL: uno_int = 2; +pub const UNO_OPTION_TYPE_STRING: uno_int = 3; +pub const UNO_OPTION_TYPE_NOT_FOUND: uno_int = -1; + +// ───────────────────────────────────────────── +// Optimization status +// ───────────────────────────────────────────── +pub const UNO_SUCCESS: uno_int = 0; +pub const UNO_ITERATION_LIMIT: uno_int = 1; +pub const UNO_TIME_LIMIT: uno_int = 2; +pub const UNO_EVALUATION_ERROR: uno_int = 3; +pub const UNO_ALGORITHMIC_ERROR: uno_int = 4; +pub const UNO_USER_TERMINATION: uno_int = 5; + +// ───────────────────────────────────────────── +// Solution status +// ───────────────────────────────────────────── +pub const UNO_NOT_OPTIMAL: uno_int = 0; +pub const UNO_FEASIBLE_KKT_POINT: uno_int = 1; +pub const UNO_FEASIBLE_FJ_POINT: uno_int = 2; +pub const UNO_INFEASIBLE_STATIONARY_POINT: uno_int = 3; +pub const UNO_FEASIBLE_SMALL_STEP: uno_int = 4; +pub const UNO_INFEASIBLE_SMALL_STEP: uno_int = 5; +pub const UNO_UNBOUNDED: uno_int = 6; + +// ───────────────────────────────────────────── +// Version +// ───────────────────────────────────────────── +pub const UNO_VERSION_MAJOR: uno_int = 2; +pub const UNO_VERSION_MINOR: uno_int = 7; +pub const UNO_VERSION_PATCH: uno_int = 1; + +// ───────────────────────────────────────────── +// Callback type aliases +// ───────────────────────────────────────────── + +pub type uno_objective_callback = unsafe extern "C" fn( + number_variables: uno_int, + x: *const f64, + objective_value: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + constraint_values: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_objective_gradient_callback = unsafe extern "C" fn( + number_variables: uno_int, + x: *const f64, + gradient: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_jacobian_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_jacobian_nonzeros: uno_int, + x: *const f64, + jacobian_values: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_lagrangian_hessian_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + number_hessian_nonzeros: uno_int, + x: *const f64, + objective_multiplier: f64, + multipliers: *const f64, + hessian_values: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_jacobian_operator_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + evaluate_at_x: bool, + vector: *const f64, + result: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_jacobian_transposed_operator_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + evaluate_at_x: bool, + vector: *const f64, + result: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_lagrangian_hessian_operator_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + evaluate_at_x: bool, + objective_multiplier: f64, + multipliers: *const f64, + vector: *const f64, + result: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_notify_acceptable_iterate_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + primals: *const f64, + lower_bound_multipliers: *const f64, + upper_bound_multipliers: *const f64, + constraint_multipliers: *const f64, + objective_multiplier: f64, + primal_feasibility_residual: f64, + stationarity_residual: f64, + complementarity_residual: f64, + user_data: *mut c_void, +); + +pub type uno_termination_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + primals: *const f64, + lower_bound_multipliers: *const f64, + upper_bound_multipliers: *const f64, + constraint_multipliers: *const f64, + objective_multiplier: f64, + primal_feasibility_residual: f64, + stationarity_residual: f64, + complementarity_residual: f64, + user_data: *mut c_void, +) -> bool; + +pub type uno_logger_stream_callback = unsafe extern "C" fn( + buffer: *const c_char, + length: uno_int, + user_data: *mut c_void, +) -> uno_int; diff --git a/interfaces/Julia/gen/wrapper_rust.jl b/interfaces/Julia/gen/wrapper_rust.jl new file mode 100644 index 000000000..164fcd222 --- /dev/null +++ b/interfaces/Julia/gen/wrapper_rust.jl @@ -0,0 +1,297 @@ +# Script to parse Uno headers and generate Rust FFI bindings (ffi.rs). +using Clang +using Clang.Generators +using Clang.LibClang + +# Callback typedef names — excluded from extern "C" (defined in prologue_rust.rs) +const CALLBACKS = String[ + "uno_objective_callback", + "uno_objective_gradient_callback", + "uno_constraints_callback", + "uno_constraints_jacobian_callback", + "uno_lagrangian_hessian_callback", + "uno_constraints_jacobian_operator_callback", + "uno_constraints_jacobian_transposed_operator_callback", + "uno_lagrangian_hessian_operator_callback", + "uno_notify_acceptable_iterate_callback", + "uno_termination_callback", + "uno_logger_stream_callback", +] + +# Callbacks that may be passed as NULL (wrapped in Option<> in Rust) +const OPTIONAL_CALLBACKS = Set{String}([ + "uno_notify_acceptable_iterate_callback", + "uno_termination_callback", +]) + +const C_TO_RUST_INT = Dict( + "int8_t" => "i8", + "int16_t" => "i16", + "int32_t" => "i32", + "int64_t" => "i64", + "uint8_t" => "u8", + "uint16_t" => "u16", + "uint32_t" => "u32", + "uint64_t" => "u64", + "int" => "c_int", + "long" => "c_long", +) + +# ---------------------------------------------------------------- +# Read uno_int.h and return the Rust primitive type for uno_int +# ---------------------------------------------------------------- +function extract_uno_int_rust(include_dir) + uno_int_h = joinpath(include_dir, "uno_int.h") + text = read(uno_int_h, String) + m = match(r"typedef\s+(\w+)\s+uno_int\s*;", text) + m === nothing && error("Cannot find 'typedef ... uno_int' in $uno_int_h") + c_type = m[1] + haskey(C_TO_RUST_INT, c_type) || error("Unknown C type '$c_type' for uno_int") + return C_TO_RUST_INT[c_type] +end + +# ---------------------------------------------------------------- +# Map a Clang type → Rust type string for extern "C" declarations +# ---------------------------------------------------------------- +function cltype_to_rust(t, spell::String) + k = Clang.kind(t) + + # Elaborated types: uno_int or callback typedefs + if k == Clang.CXType_Elaborated + if spell in CALLBACKS + return spell in OPTIONAL_CALLBACKS ? "Option<$spell>" : spell + end + ct = Clang.getCanonicalType(t) + ck = Clang.kind(ct) + if ck in (Clang.CXType_Int, Clang.CXType_UInt, + Clang.CXType_Long, Clang.CXType_ULong, + Clang.CXType_LongLong, Clang.CXType_ULongLong) + return "uno_int" + end + if ck == Clang.CXType_Double; return "f64"; end + if ck == Clang.CXType_Bool; return "bool"; end + if ck == Clang.CXType_Pointer + pt = Clang.getPointeeType(ct) + Clang.kind(pt) == Clang.CXType_FunctionProto && return spell + return "*mut c_void" + end + return "*mut c_void" + end + + # Primitive by-value types + if k in (Clang.CXType_Int, Clang.CXType_UInt); return "c_int"; end + if k == Clang.CXType_Double; return "f64"; end + if k == Clang.CXType_Bool; return "bool"; end + if k in (Clang.CXType_Char_S, Clang.CXType_Char_U); return "c_char"; end + + # Pointer types + if k == Clang.CXType_Pointer + pt = Clang.getPointeeType(t) + pk = Clang.kind(pt) + is_const = occursin("const", spell) + + if pk == Clang.CXType_Void + return is_const ? "*const c_void" : "*mut c_void" + end + if pk == Clang.CXType_Double + return is_const ? "*const f64" : "*mut f64" + end + if pk in (Clang.CXType_Char_S, Clang.CXType_Char_U) + return is_const ? "*const c_char" : "*mut c_char" + end + if pk == Clang.CXType_Bool + return is_const ? "*const bool" : "*mut bool" + end + if pk in (Clang.CXType_Int, Clang.CXType_UInt) + return is_const ? "*const c_int" : "*mut c_int" + end + if pk == Clang.CXType_Elaborated + ptspell = Clang.spelling(pt) + if ptspell in CALLBACKS + return ptspell in OPTIONAL_CALLBACKS ? "Option<$ptspell>" : ptspell + end + ct = Clang.getCanonicalType(pt) + ck = Clang.kind(ct) + if ck in (Clang.CXType_Int, Clang.CXType_UInt, + Clang.CXType_Long, Clang.CXType_ULong, + Clang.CXType_LongLong, Clang.CXType_ULongLong) + return is_const ? "*const uno_int" : "*mut uno_int" + end + if ck == Clang.CXType_FunctionProto + return ptspell in OPTIONAL_CALLBACKS ? "Option<$ptspell>" : ptspell + end + end + if pk == Clang.CXType_FunctionProto + return "*mut c_void" # shouldn't happen for Uno + end + return is_const ? "*const c_void" : "*mut c_void" + end + + return "*mut c_void" +end + +# ---------------------------------------------------------------- +# Map a Clang return type → Rust return type string (nothing = void) +# ---------------------------------------------------------------- +function rettype_to_rust(t) + k = Clang.kind(t) + if k == Clang.CXType_Void; return nothing; end + if k == Clang.CXType_Bool; return "bool"; end + if k == Clang.CXType_Double; return "f64"; end + if k in (Clang.CXType_Int, Clang.CXType_UInt); return "c_int"; end + + if k == Clang.CXType_Elaborated + ct = Clang.getCanonicalType(t) + ck = Clang.kind(ct) + if ck in (Clang.CXType_Int, Clang.CXType_UInt, + Clang.CXType_Long, Clang.CXType_ULong, + Clang.CXType_LongLong, Clang.CXType_ULongLong) + return "uno_int" + end + if ck == Clang.CXType_Bool; return "bool"; end + if ck == Clang.CXType_Double; return "f64"; end + if ck == Clang.CXType_Pointer; return "*mut c_void"; end + end + + if k == Clang.CXType_Pointer + pt = Clang.getPointeeType(t) + pk = Clang.kind(pt) + spell = Clang.spelling(t) + is_const = occursin("const", spell) + if pk == Clang.CXType_Void + return is_const ? "*const c_void" : "*mut c_void" + end + if pk in (Clang.CXType_Char_S, Clang.CXType_Char_U) + return is_const ? "*const c_char" : "*mut c_char" + end + if pk == Clang.CXType_Double + return is_const ? "*const f64" : "*mut f64" + end + end + + return "*mut c_void" +end + +# ---------------------------------------------------------------- +# Pre-computed structs +# ---------------------------------------------------------------- +struct RustArgInfo + name :: String + rtype :: String +end + +struct RustFuncInfo + name :: String + ret :: Union{String, Nothing} # nothing = void + args :: Vector{RustArgInfo} +end + +# ---------------------------------------------------------------- +# Collect all uno_* function declarations in one pass +# ---------------------------------------------------------------- +function collect_funcs(root) + funcs = RustFuncInfo[] + for child in Clang.children(root) + k = Clang.kind(child) + name = Clang.spelling(child) + if k == Clang.CXCursor_FunctionDecl && startswith(name, "uno_") + ret_t = Clang.getCursorResultType(child) + ret = rettype_to_rust(ret_t) + clargs = Clang.get_function_args(child) + args = map(clargs) do a + aname = Clang.spelling(a) + at = Clang.getCursorType(a) + spell = Clang.spelling(at) + RustArgInfo(aname, cltype_to_rust(at, spell)) + end + push!(funcs, RustFuncInfo(name, ret, collect(args))) + end + end + return funcs +end + +# ---------------------------------------------------------------- +# Emit one extern "C" function declaration +# ---------------------------------------------------------------- +const RUST_MAX_COL = 100 + +function gen_one_fn(io, f::RustFuncInfo, trailing_blank::Bool) + println(io, " // $(f.name)") + ret_str = f.ret === nothing ? "" : " -> $(f.ret)" + + if isempty(f.args) + println(io, " pub fn $(f.name)()$ret_str;") + else + args_inline = join(["$(a.name): $(a.rtype)" for a in f.args], ", ") + oneliner = " pub fn $(f.name)($args_inline)$ret_str;" + if length(oneliner) <= RUST_MAX_COL + println(io, oneliner) + else + println(io, " pub fn $(f.name)(") + for a in f.args + println(io, " $(a.name): $(a.rtype),") + end + println(io, " )$ret_str;") + end + end + + trailing_blank && println(io, "") +end + +# ---------------------------------------------------------------- +# Generate the extern "C" block +# ---------------------------------------------------------------- +function gen_extern_c(io, funcs) + println(io, "// ─────────────────────────────────────────────") + println(io, "// extern \"C\" declarations") + println(io, "// ─────────────────────────────────────────────") + println(io, "") + println(io, "#[link(name = \"uno\")]") + println(io, "extern \"C\" {") + for (i, f) in enumerate(funcs) + gen_one_fn(io, f, i < length(funcs)) + end + println(io, "}") +end + +# ---------------------------------------------------------------- +# Entry point +# ---------------------------------------------------------------- +function main_rust() + include_dir = joinpath(@__DIR__, "..", "..", "C") + header_path = joinpath(include_dir, "Uno_C_API.h") + + uno_int_rust = extract_uno_int_rust(include_dir) + + args = get_default_args() + push!(args, "-I$include_dir") + + ctx = create_context([header_path], args) + tu = ctx.trans_units[1] + root = Clang.getTranslationUnitCursor(tu) + + funcs = GC.@preserve ctx tu collect_funcs(root) + + rust_dir = joinpath(@__DIR__, "..", "..", "Rust", "src") + out_path = joinpath(rust_dir, "ffi.rs") + + open(out_path, "w") do io + # Inject prologue with UnoInt type at the marker + prologue = read(joinpath(@__DIR__, "prologue_rust.rs"), String) + marker = "//--- UNO_INT_TYPE ---\n" + idx = findfirst(marker, prologue) + idx === nothing && error("Marker '$marker' not found in prologue_rust.rs") + print(io, prologue[1:first(idx)-1]) + println(io, "pub type uno_int = $uno_int_rust;") + print(io, prologue[last(idx)+1:end]) + println(io, "") + gen_extern_c(io, funcs) + end + + println("Generated: $out_path") +end + +# If run as a script with `julia wrapper_rust.jl` +if abspath(PROGRAM_FILE) == @__FILE__ + main_rust() +end diff --git a/interfaces/Rust/Cargo.toml b/interfaces/Rust/Cargo.toml new file mode 100644 index 000000000..4040119dc --- /dev/null +++ b/interfaces/Rust/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "uno_rs" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "hs015" +path = "example/example_hs015.rs" + +[[bin]] +name = "hs015_idiomatic" +path = "example/example_hs015_idiomatic.rs" + +[lib] +name = "uno_rs" +path = "src/lib.rs" + +[dependencies] \ No newline at end of file diff --git a/interfaces/Rust/README.md b/interfaces/Rust/README.md new file mode 100644 index 000000000..31f9d8323 --- /dev/null +++ b/interfaces/Rust/README.md @@ -0,0 +1,199 @@ +## Uno's Rust interface: how to use Uno from Rust + +Uno's Rust interface (`uno_rs`) allows you to solve an optimization model described by callback functions. + +There are two ways to use the crate: + +- **The idiomatic API (recommended)** lets you describe your problem with ordinary Rust closures or functions. The crate handles the FFI layer (function pointers, `user_data`, raw slices) for you. See [example_hs015_idiomatic.rs](example/example_hs015_idiomatic.rs). +- **The raw API** mirrors the C interface one-to-one: you pass `unsafe extern "C"` callbacks and raw pointers yourself. See [example_hs015.rs](example/example_hs015.rs). Use this only if you need full control over the FFI boundary. + +Both APIs share the same `Solver`, options, and `Solution` types. + +### Idiomatic API (closures) + +Import the safe builder: + +```rust +use uno_rs::{uno_int, Problem, Solver, UNO_MINIMIZE, UNO_ZERO_BASED_INDEXING}; +``` + +Build a [`Problem`] and attach an objective, constraints and (optionally) a Hessian using closures. Each closure takes safe `&[f64]` inputs and writes into a `&mut [f64]` (or `&mut f64`) output, returning `Ok(())` on success or `Err(())` to signal an evaluation error at the current point: + +```rust +let var_lb = [f64::NEG_INFINITY, f64::NEG_INFINITY]; +let var_ub = [0.5, f64::INFINITY]; +let mut problem = Problem::new("NLP", &var_lb, &var_ub, UNO_ZERO_BASED_INDEXING)?; + +// Objective and its gradient. +problem.set_objective( + UNO_MINIMIZE, + |x, obj| { *obj = 100.0 * (x[1] - x[0]*x[0]).powi(2) + (1.0 - x[0]).powi(2); Ok(()) }, + |x, g| { + g[0] = 400.0*x[0].powi(3) - 400.0*x[0]*x[1] + 2.0*x[0] - 2.0; + g[1] = 200.0 * (x[1] - x[0]*x[0]); + Ok(()) + }, +)?; + +// Constraints c0 = x0*x1, c1 = x0 + x1^2, plus their Jacobian (COO order). +let con_lb = [1.0, 0.0]; +let con_ub = [f64::INFINITY, f64::INFINITY]; +let jac_rows = [0, 1, 0, 1]; +let jac_cols = [0, 0, 1, 1]; +problem.set_constraints( + |x, c| { c[0] = x[0]*x[1]; c[1] = x[0] + x[1]*x[1]; Ok(()) }, + &con_lb, &con_ub, &jac_rows, &jac_cols, + |x, jv| { jv[0] = x[1]; jv[1] = 1.0; jv[2] = x[0]; jv[3] = 2.0*x[1]; Ok(()) }, +)?; + +problem.set_initial_primal_iterate(&[-2.0, 1.0])?; + +let solver = Solver::new(); +solver.set_preset("ipopt"); +let solution = solver.solve(&problem); // constraint count is taken from the Problem +``` + +Because these are real closures, they may capture state from their environment — something the raw `extern "C"` function-pointer callbacks cannot do. + +The builder methods return `Result<&mut Self, UnoError>`, so failures surface as ordinary Rust errors (and the `&mut Self` return makes chaining with `?` convenient). `Solver::solve` takes a `&Problem` and reads the constraint count from it, so dual vectors always come back with the right length. + +The rest of this document describes the **raw API**. + +Start by importing from the crate: + +```rust +use uno_rs::{uno_int, Model, Solver, UNO_MINIMIZE, UNO_ZERO_BASED_INDEXING}; +use std::os::raw::c_void; +``` + +### Building an optimization model + +Building an optimization model is incremental and starts with the information about the variables: + +```rust +let model = Model::new(problem_type, &variables_lower_bounds, &variables_upper_bounds, base_indexing); +``` + +The following optional elements can be added to the model separately: +- the objective function (and its gradient). It is 0 otherwise; +```rust +model.set_objective(optimization_sense, objective_function, objective_gradient); +``` +- constraint functions (and their Jacobian); +```rust +model.set_constraints(constraint_functions, &constraints_lower_bounds, &constraints_upper_bounds, + &jacobian_row_indices, &jacobian_column_indices, jacobian); +``` +- the Lagrangian Hessian; +```rust +model.set_lagrangian_hessian(hessian_triangular_part, &hessian_row_indices, &hessian_column_indices, lagrangian_hessian); +``` +- a Jacobian operator (performs Jacobian-vector products); +```rust +model.set_jacobian_operator(jacobian_operator); +``` +- a Jacobian-transposed operator (performs Jacobian-transposed-vector products); +```rust +model.set_jacobian_transposed_operator(jacobian_transposed_operator); +``` +- a Hessian operator (performs Hessian-vector products); +```rust +model.set_lagrangian_hessian_operator(lagrangian_hessian_operator); +``` +- a Lagrangian sign convention (default is `UNO_MULTIPLIER_NEGATIVE`); +```rust +model.set_lagrangian_sign_convention(lagrangian_sign_convention); +``` +- user data of an arbitrary type (`*mut c_void`); +```rust +model.set_user_data(user_data); +``` +- an initial primal point; +```rust +model.set_initial_primal_iterate(&initial_primal_iterate); +``` +- an initial dual point. +```rust +model.set_initial_dual_iterate(&initial_dual_iterate); +``` + +*Each of these functions returns a `bool` that is `true` upon success and `false` upon failure.* + +The memory for the model is allocated by Uno and freed automatically when the `Model` is dropped. + +### Creating an instance of the Uno solver + +Create an instance of the Uno solver with: +```rust +let solver = Solver::new(); +``` + +The memory for the solver is allocated by Uno and freed automatically when the `Solver` is dropped. + +### Passing options to the Uno solver + +Options can be passed to the Uno solver: +```rust +solver.set_integer_option("max_iterations", 1000); +solver.set_double_option("primal_tolerance", 1.0e-6); +solver.set_bool_option("print_solution", true); +solver.set_string_option("hessian_model", "exact"); +``` + +Loading options from a file (overwrites existing options): +```rust +solver.load_option_file("uno.opt"); +``` + +Getting typed value of an option from the Uno solver: +```rust +solver.get_string_option("hessian_model"); +solver.get_bool_option("print_solution"); +``` + +Setting a preset has Uno mimic an existing solver: +```rust +solver.set_preset("filtersqp"); +``` + +### Setting solver callbacks + +Setting the user callbacks to the Uno solver: +```rust +solver.set_callbacks(notify_acceptable_iterate_callback, user_termination_callback, user_data); +``` + +Setting the logger stream callback: +```rust +Solver::set_logger_stream_callback(logger_stream_callback, user_data); +``` + +and reset the logger stream to the standard output: +```rust +Solver::reset_logger_stream(); +``` + +### Solving the model + +The model can then be solved by Uno: +```rust +let solution = solver.optimize(&model); +// or, to also retrieve dual vectors: +let solution = solver.optimize_with_nc(&model, number_constraints); +``` + +### Inspecting the result + +A `Solution` struct allows you to inspect the result of the optimization: +- the optimization status (`UNO_SUCCESS`, `UNO_ITERATION_LIMIT`, `UNO_TIME_LIMIT`, `UNO_EVALUATION_ERROR`, `UNO_ALGORITHMIC_ERROR`): `solution.optimization_status` +- the solution status (`UNO_NOT_OPTIMAL`, `UNO_FEASIBLE_KKT_POINT`, `UNO_FEASIBLE_FJ_POINT`, `UNO_INFEASIBLE_STATIONARY_POINT`, `UNO_FEASIBLE_SMALL_STEP`, `UNO_INFEASIBLE_SMALL_STEP`, `UNO_UNBOUNDED`): `solution.solution_status` +- the objective value of the solution: `solution.objective` +- the primal solution: `solution.primals` +- the dual solution associated with the general constraints: `solution.constraint_duals` +- the dual solution associated with the lower bounds: `solution.lower_bound_duals` +- the dual solution associated with the upper bounds: `solution.upper_bound_duals` +- the primal feasibility residual at the solution: `solution.primal_feasibility` +- the stationarity residual at the solution: `solution.stationarity` +- the complementarity residual at the solution: `solution.complementarity` +- the number of (outer) iterations: `solution.iterations` +- the CPU time (in seconds): `solution.cpu_time` diff --git a/interfaces/Rust/build.rs b/interfaces/Rust/build.rs new file mode 100644 index 000000000..e8e6f2fb8 --- /dev/null +++ b/interfaces/Rust/build.rs @@ -0,0 +1,8 @@ +fn main() { + // Allow overriding the library directory via UNO_LIB_DIR environment variable + let lib_dir = std::env::var("UNO_LIB_DIR").unwrap_or_else(|_| "/usr/local/lib".to_string()); + println!("cargo:rustc-link-search=native={lib_dir}"); + println!("cargo:rustc-link-lib=dylib=uno"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=UNO_LIB_DIR"); +} diff --git a/interfaces/Rust/example/example_hs015.rs b/interfaces/Rust/example/example_hs015.rs new file mode 100644 index 000000000..03fff840c --- /dev/null +++ b/interfaces/Rust/example/example_hs015.rs @@ -0,0 +1,173 @@ +// Rust port of example_hs015.c +// HS015: min 100*(x1 - x0^2)^2 + (1-x0)^2 +// s.t. x0*x1 >= 1 +// x0 + x1^2 >= 0 +// x0 <= 0.5 +// Solution: x = (0.5, 2.0) + +use uno_rs::{ + get_version, uno_int, Model, Solver, + UNO_FEASIBLE_KKT_POINT, UNO_LOWER_TRIANGLE, UNO_MINIMIZE, + UNO_MULTIPLIER_NEGATIVE, UNO_SUCCESS, UNO_ZERO_BASED_INDEXING, +}; +use std::os::raw::c_void; + +// ── Objective: f(x) = 100*(x1 - x0^2)^2 + (1 - x0)^2 ────────────────────── + +unsafe extern "C" fn objective_function( + _n: uno_int, x: *const f64, obj: *mut f64, _data: *mut c_void, +) -> uno_int { + let x = std::slice::from_raw_parts(x, 2); + *obj = 100.0 * (x[1] - x[0] * x[0]).powi(2) + (1.0 - x[0]).powi(2); + 0 +} + +unsafe extern "C" fn objective_gradient( + _n: uno_int, x: *const f64, g: *mut f64, _data: *mut c_void, +) -> uno_int { + let x = std::slice::from_raw_parts(x, 2); + let g = std::slice::from_raw_parts_mut(g, 2); + g[0] = 400.0 * x[0].powi(3) - 400.0 * x[0] * x[1] + 2.0 * x[0] - 2.0; + g[1] = 200.0 * (x[1] - x[0] * x[0]); + 0 +} + +// ── Constraints: c0 = x0*x1, c1 = x0 + x1^2 ─────────────────────────────── + +unsafe extern "C" fn constraint_functions( + _n: uno_int, _nc: uno_int, x: *const f64, cv: *mut f64, _data: *mut c_void, +) -> uno_int { + let x = std::slice::from_raw_parts(x, 2); + let cv = std::slice::from_raw_parts_mut(cv, 2); + cv[0] = x[0] * x[1]; + cv[1] = x[0] + x[1] * x[1]; + 0 +} + +// ── Jacobian (COO, 4 nonzeros) ─────────────────────────────────────────────── +// Row 0 (c0): ∂/∂x0 = x1 (col 0), ∂/∂x1 = x0 (col 1) +// Row 1 (c1): ∂/∂x0 = 1 (col 0), ∂/∂x1 = 2*x1 (col 1) + +unsafe extern "C" fn jacobian_values( + _n: uno_int, _nnz: uno_int, x: *const f64, jv: *mut f64, _data: *mut c_void, +) -> uno_int { + let x = std::slice::from_raw_parts(x, 2); + let jv = std::slice::from_raw_parts_mut(jv, 4); + jv[0] = x[1]; // (row 0, col 0) + jv[1] = 1.0; // (row 1, col 0) + jv[2] = x[0]; // (row 0, col 1) + jv[3] = 2.0 * x[1]; // (row 1, col 1) + 0 +} + +// ── Lagrangian Hessian – lower triangle, 3 nonzeros ───────────────────────── +// L = rho*f(x) - y^T c(x) (UNO_MULTIPLIER_NEGATIVE convention) +// H00 = rho*(1200*x0^2 - 400*x1 + 2) +// H10 = -400*rho*x0 - y0 +// H11 = 200*rho - 2*y1 + +unsafe extern "C" fn lagrangian_hessian( + _n: uno_int, _nc: uno_int, _nnz: uno_int, + x: *const f64, rho: f64, y: *const f64, hv: *mut f64, + _data: *mut c_void, +) -> uno_int { + let x = std::slice::from_raw_parts(x, 2); + let y = std::slice::from_raw_parts(y, 2); + let hv = std::slice::from_raw_parts_mut(hv, 3); + hv[0] = rho * (1200.0 * x[0] * x[0] - 400.0 * x[1] + 2.0); + hv[1] = -400.0 * rho * x[0] - y[0]; + hv[2] = 200.0 * rho - 2.0 * y[1]; + 0 +} + +// ──────────────────────────────────────────────────────────────────────────── + +fn print_vec(label: &str, v: &[f64]) { + print!("{}: ", label); + for val in v { print!("{:.6} ", val); } + println!(); +} + +fn main() { + let (maj, min, pat) = get_version(); + println!("Uno v{}.{}.{}", maj, min, pat); + + // ── Build model ──────────────────────────────────────────────────────────── + let var_lb = [f64::NEG_INFINITY, f64::NEG_INFINITY]; + let var_ub = [0.5_f64, f64::INFINITY]; + let mut model = Model::new("NLP", &var_lb, &var_ub, UNO_ZERO_BASED_INDEXING); + + assert!(model.set_objective(UNO_MINIMIZE, objective_function, objective_gradient)); + + let con_lb = [1.0_f64, 0.0_f64]; + let con_ub = [f64::INFINITY, f64::INFINITY]; + // COO sparsity pattern: rows then cols, column-major ordering + let jac_rows: [uno_int; 4] = [0, 1, 0, 1]; + let jac_cols: [uno_int; 4] = [0, 0, 1, 1]; + assert!(model.set_constraints( + constraint_functions, + &con_lb, &con_ub, + &jac_rows, &jac_cols, + jacobian_values, + )); + + assert!(model.set_initial_primal_iterate(&[-2.0_f64, 1.0_f64])); + + // ── Build solver ─────────────────────────────────────────────────────────── + let solver = Solver::new(); + // "ipopt" preset: interior-point method — only needs MUMPS (no QP solver) + solver.set_preset("ipopt"); + solver.set_bool_option("print_solution", true); + + // ── Run 1: L-BFGS Hessian (default for NLP when no Hessian provided) ────── + println!("\n=== Run 1: L-BFGS Hessian ==="); + let sol1 = solver.optimize_with_nc(&model, 2); + println!("Optimization status : {}", sol1.optimization_status_str()); + println!("Solution status : {}", sol1.solution_status_str()); + println!("Objective : {:.10}", sol1.objective); + assert_eq!(sol1.optimization_status, UNO_SUCCESS, + "Run 1 optimization failed"); + assert_eq!(sol1.solution_status, UNO_FEASIBLE_KKT_POINT, + "Run 1 did not find a KKT point"); + + // ── Run 2: Exact Hessian ─────────────────────────────────────────────────── + println!("\n=== Run 2: Exact Hessian ==="); + let hess_rows: [uno_int; 3] = [0, 1, 1]; + let hess_cols: [uno_int; 3] = [0, 0, 1]; + assert!(model.set_lagrangian_hessian( + UNO_LOWER_TRIANGLE, + &hess_rows, &hess_cols, + lagrangian_hessian, + )); + assert!(model.set_lagrangian_sign_convention(UNO_MULTIPLIER_NEGATIVE)); + solver.set_string_option("hessian_model", "exact"); + + let sol2 = solver.optimize_with_nc(&model, 2); + println!("Optimization status : {}", sol2.optimization_status_str()); + println!("Solution status : {}", sol2.solution_status_str()); + assert_eq!(sol2.optimization_status, UNO_SUCCESS, + "Run 2 optimization failed"); + assert_eq!(sol2.solution_status, UNO_FEASIBLE_KKT_POINT, + "Run 2 did not find a KKT point"); + + println!("Objective : {:.10}", sol2.objective); + print_vec("Primal solution ", &sol2.primals); + print_vec("Constraint duals ", &sol2.constraint_duals); + print_vec("Lower-bound duals ", &sol2.lower_bound_duals); + print_vec("Upper-bound duals ", &sol2.upper_bound_duals); + println!("Primal feasibility : {:.2e}", sol2.primal_feasibility); + println!("Stationarity : {:.2e}", sol2.stationarity); + println!("Complementarity : {:.2e}", sol2.complementarity); + println!("Iterations : {}", sol2.iterations); + println!("CPU time : {:.4}s", sol2.cpu_time); + + // ── Verify x ≈ (0.5, 2.0) ───────────────────────────────────────────────── + let tol = 1e-4; + let x = &sol2.primals; + println!("\n=== Verification ==="); + println!("Expected : x0 = 0.5, x1 = 2.0"); + println!("Got : x0 = {:.8}, x1 = {:.8}", x[0], x[1]); + assert!((x[0] - 0.5).abs() < tol, "x[0] = {} not near 0.5", x[0]); + assert!((x[1] - 2.0).abs() < tol, "x[1] = {} not near 2.0", x[1]); + println!("✓ Solution x = (0.5, 2.0) confirmed within tolerance {}", tol); +} diff --git a/interfaces/Rust/example/example_hs015_idiomatic.rs b/interfaces/Rust/example/example_hs015_idiomatic.rs new file mode 100644 index 000000000..af4d2d679 --- /dev/null +++ b/interfaces/Rust/example/example_hs015_idiomatic.rs @@ -0,0 +1,119 @@ +// Rust port of example_hs015.c, using the idiomatic `Problem` API. +// HS015: min 100*(x1 - x0^2)^2 + (1-x0)^2 +// s.t. x0*x1 >= 1 +// x0 + x1^2 >= 0 +// x0 <= 0.5 +// Solution: x = (0.5, 2.0) +// +// Note how the objective, gradient, constraints, Jacobian and Hessian are +// ordinary Rust closures: no `unsafe extern "C"`, no raw pointers, no manual +// `from_raw_parts`. The crate installs the FFI trampolines internally. + +use uno_rs::{ + get_version, uno_int, Problem, Solver, + UNO_FEASIBLE_KKT_POINT, UNO_LOWER_TRIANGLE, UNO_MINIMIZE, + UNO_MULTIPLIER_NEGATIVE, UNO_SUCCESS, UNO_ZERO_BASED_INDEXING, +}; + +fn print_vec(label: &str, v: &[f64]) { + print!("{}: ", label); + for val in v { + print!("{:.6} ", val); + } + println!(); +} + +fn main() { + let (maj, min, pat) = get_version(); + println!("Uno v{}.{}.{}", maj, min, pat); + + // ── Build model ────────────────────────────────────────────────────────── + let var_lb = [f64::NEG_INFINITY, f64::NEG_INFINITY]; + let var_ub = [0.5_f64, f64::INFINITY]; + let mut problem = + Problem::new("NLP", &var_lb, &var_ub, UNO_ZERO_BASED_INDEXING).expect("create model"); + + // Objective f(x) = 100*(x1 - x0^2)^2 + (1 - x0)^2, plus its gradient. + problem + .set_objective( + UNO_MINIMIZE, + |x, obj| { + *obj = 100.0 * (x[1] - x[0] * x[0]).powi(2) + (1.0 - x[0]).powi(2); + Ok(()) + }, + |x, g| { + g[0] = 400.0 * x[0].powi(3) - 400.0 * x[0] * x[1] + 2.0 * x[0] - 2.0; + g[1] = 200.0 * (x[1] - x[0] * x[0]); + Ok(()) + }, + ) + .expect("set objective"); + + // Constraints c0 = x0*x1, c1 = x0 + x1^2, plus their Jacobian (COO). + let con_lb = [1.0_f64, 0.0_f64]; + let con_ub = [f64::INFINITY, f64::INFINITY]; + let jac_rows: [uno_int; 4] = [0, 1, 0, 1]; + let jac_cols: [uno_int; 4] = [0, 0, 1, 1]; + problem + .set_constraints( + |x, c| { + c[0] = x[0] * x[1]; + c[1] = x[0] + x[1] * x[1]; + Ok(()) + }, + &con_lb, + &con_ub, + &jac_rows, + &jac_cols, + |x, jv| { + jv[0] = x[1]; // (row 0, col 0) + jv[1] = 1.0; // (row 1, col 0) + jv[2] = x[0]; // (row 0, col 1) + jv[3] = 2.0 * x[1]; // (row 1, col 1) + Ok(()) + }, + ) + .expect("set constraints"); + + // Exact Lagrangian Hessian (lower triangle, 3 nonzeros). + // L = rho*f(x) - y^T c(x) (UNO_MULTIPLIER_NEGATIVE convention) + let hess_rows: [uno_int; 3] = [0, 1, 1]; + let hess_cols: [uno_int; 3] = [0, 0, 1]; + problem + .set_lagrangian_hessian(UNO_LOWER_TRIANGLE, &hess_rows, &hess_cols, |x, rho, y, hv| { + hv[0] = rho * (1200.0 * x[0] * x[0] - 400.0 * x[1] + 2.0); + hv[1] = -400.0 * rho * x[0] - y[0]; + hv[2] = 200.0 * rho - 2.0 * y[1]; + Ok(()) + }) + .expect("set hessian"); + problem + .set_lagrangian_sign_convention(UNO_MULTIPLIER_NEGATIVE) + .expect("set sign convention"); + + problem + .set_initial_primal_iterate(&[-2.0_f64, 1.0_f64]) + .expect("set initial iterate"); + + // ── Build solver ─────────────────────────────────────────────────────────── + let solver = Solver::new(); + solver.set_preset("ipopt"); + solver.set_bool_option("print_solution", true); + solver.set_string_option("hessian_model", "exact"); + + // ── Solve ───────────────────────────────────────────────────────────────── + let sol = solver.solve(&problem); + println!("Optimization status : {}", sol.optimization_status_str()); + println!("Solution status : {}", sol.solution_status_str()); + println!("Objective : {:.10}", sol.objective); + print_vec("Primal solution ", &sol.primals); + print_vec("Constraint duals ", &sol.constraint_duals); + + assert_eq!(sol.optimization_status, UNO_SUCCESS, "optimization failed"); + assert_eq!( + sol.solution_status, UNO_FEASIBLE_KKT_POINT, + "did not find a KKT point" + ); + + println!("\n✓ Solved with the idiomatic closure-based API"); +} diff --git a/interfaces/Rust/src/ffi.rs b/interfaces/Rust/src/ffi.rs new file mode 100644 index 000000000..0882cdbc5 --- /dev/null +++ b/interfaces/Rust/src/ffi.rs @@ -0,0 +1,511 @@ +// Copyright (c) 2026 Alexis Montoison and Charlie Vanaret +// Licensed under the MIT license. See LICENSE file in the project directory for details. + +#![allow(non_camel_case_types)] + +use std::os::raw::{c_char, c_int, c_void}; + +pub type uno_int = i32; + +// ───────────────────────────────────────────── +// Optimization sense +// ───────────────────────────────────────────── +pub const UNO_MINIMIZE: uno_int = 1; +pub const UNO_MAXIMIZE: uno_int = -1; + +// ───────────────────────────────────────────── +// Lagrange multiplier sign convention +// ───────────────────────────────────────────── +pub const UNO_MULTIPLIER_POSITIVE: uno_int = 1; +pub const UNO_MULTIPLIER_NEGATIVE: uno_int = -1; + +// ───────────────────────────────────────────── +// Problem types +// ───────────────────────────────────────────── +pub const UNO_PROBLEM_LINEAR: &[u8] = b"LP\0"; +pub const UNO_PROBLEM_QUADRATIC: &[u8] = b"QP\0"; +pub const UNO_PROBLEM_NONLINEAR: &[u8] = b"NLP\0"; + +// ───────────────────────────────────────────── +// Base indexing style +// ───────────────────────────────────────────── +pub const UNO_ZERO_BASED_INDEXING: uno_int = 0; +pub const UNO_ONE_BASED_INDEXING: uno_int = 1; + +// ───────────────────────────────────────────── +// Triangular part +// ───────────────────────────────────────────── +pub const UNO_LOWER_TRIANGLE: c_char = b'L' as c_char; +pub const UNO_UPPER_TRIANGLE: c_char = b'U' as c_char; + +// ───────────────────────────────────────────── +// Option type +// ───────────────────────────────────────────── +pub const UNO_OPTION_TYPE_INTEGER: uno_int = 0; +pub const UNO_OPTION_TYPE_DOUBLE: uno_int = 1; +pub const UNO_OPTION_TYPE_BOOL: uno_int = 2; +pub const UNO_OPTION_TYPE_STRING: uno_int = 3; +pub const UNO_OPTION_TYPE_NOT_FOUND: uno_int = -1; + +// ───────────────────────────────────────────── +// Optimization status +// ───────────────────────────────────────────── +pub const UNO_SUCCESS: uno_int = 0; +pub const UNO_ITERATION_LIMIT: uno_int = 1; +pub const UNO_TIME_LIMIT: uno_int = 2; +pub const UNO_EVALUATION_ERROR: uno_int = 3; +pub const UNO_ALGORITHMIC_ERROR: uno_int = 4; +pub const UNO_USER_TERMINATION: uno_int = 5; + +// ───────────────────────────────────────────── +// Solution status +// ───────────────────────────────────────────── +pub const UNO_NOT_OPTIMAL: uno_int = 0; +pub const UNO_FEASIBLE_KKT_POINT: uno_int = 1; +pub const UNO_FEASIBLE_FJ_POINT: uno_int = 2; +pub const UNO_INFEASIBLE_STATIONARY_POINT: uno_int = 3; +pub const UNO_FEASIBLE_SMALL_STEP: uno_int = 4; +pub const UNO_INFEASIBLE_SMALL_STEP: uno_int = 5; +pub const UNO_UNBOUNDED: uno_int = 6; + +// ───────────────────────────────────────────── +// Version +// ───────────────────────────────────────────── +pub const UNO_VERSION_MAJOR: uno_int = 2; +pub const UNO_VERSION_MINOR: uno_int = 7; +pub const UNO_VERSION_PATCH: uno_int = 1; + +// ───────────────────────────────────────────── +// Callback type aliases +// ───────────────────────────────────────────── + +pub type uno_objective_callback = unsafe extern "C" fn( + number_variables: uno_int, + x: *const f64, + objective_value: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + constraint_values: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_objective_gradient_callback = unsafe extern "C" fn( + number_variables: uno_int, + x: *const f64, + gradient: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_jacobian_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_jacobian_nonzeros: uno_int, + x: *const f64, + jacobian_values: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_lagrangian_hessian_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + number_hessian_nonzeros: uno_int, + x: *const f64, + objective_multiplier: f64, + multipliers: *const f64, + hessian_values: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_jacobian_operator_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + evaluate_at_x: bool, + vector: *const f64, + result: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_constraints_jacobian_transposed_operator_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + evaluate_at_x: bool, + vector: *const f64, + result: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_lagrangian_hessian_operator_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + x: *const f64, + evaluate_at_x: bool, + objective_multiplier: f64, + multipliers: *const f64, + vector: *const f64, + result: *mut f64, + user_data: *mut c_void, +) -> uno_int; + +pub type uno_notify_acceptable_iterate_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + primals: *const f64, + lower_bound_multipliers: *const f64, + upper_bound_multipliers: *const f64, + constraint_multipliers: *const f64, + objective_multiplier: f64, + primal_feasibility_residual: f64, + stationarity_residual: f64, + complementarity_residual: f64, + user_data: *mut c_void, +); + +pub type uno_termination_callback = unsafe extern "C" fn( + number_variables: uno_int, + number_constraints: uno_int, + primals: *const f64, + lower_bound_multipliers: *const f64, + upper_bound_multipliers: *const f64, + constraint_multipliers: *const f64, + objective_multiplier: f64, + primal_feasibility_residual: f64, + stationarity_residual: f64, + complementarity_residual: f64, + user_data: *mut c_void, +) -> bool; + +pub type uno_logger_stream_callback = unsafe extern "C" fn( + buffer: *const c_char, + length: uno_int, + user_data: *mut c_void, +) -> uno_int; + +// ───────────────────────────────────────────── +// extern "C" declarations +// ───────────────────────────────────────────── + +#[link(name = "uno")] +extern "C" { + // uno_get_version + pub fn uno_get_version(major: *mut uno_int, minor: *mut uno_int, patch: *mut uno_int); + + // uno_create_model + pub fn uno_create_model( + problem_type: *const c_char, + number_variables: uno_int, + variables_lower_bounds: *const f64, + variables_upper_bounds: *const f64, + base_indexing: uno_int, + ) -> *mut c_void; + + // uno_create_unconstrained_model + pub fn uno_create_unconstrained_model( + problem_type: *const c_char, + number_variables: uno_int, + base_indexing: uno_int, + ) -> *mut c_void; + + // uno_set_variables_lower_bounds + pub fn uno_set_variables_lower_bounds( + model: *mut c_void, + variables_lower_bounds: *const f64, + ) -> bool; + + // uno_set_variables_upper_bounds + pub fn uno_set_variables_upper_bounds( + model: *mut c_void, + variables_upper_bounds: *const f64, + ) -> bool; + + // uno_set_variable_lower_bound + pub fn uno_set_variable_lower_bound( + model: *mut c_void, + variable_index: uno_int, + lower_bound: f64, + ) -> bool; + + // uno_set_variable_upper_bound + pub fn uno_set_variable_upper_bound( + model: *mut c_void, + variable_index: uno_int, + upper_bound: f64, + ) -> bool; + + // uno_set_objective + pub fn uno_set_objective( + model: *mut c_void, + optimization_sense: uno_int, + objective_function: uno_objective_callback, + objective_gradient: uno_objective_gradient_callback, + ) -> bool; + + // uno_set_constraints + pub fn uno_set_constraints( + model: *mut c_void, + number_constraints: uno_int, + constraint_functions: uno_constraints_callback, + constraints_lower_bounds: *const f64, + constraints_upper_bounds: *const f64, + number_jacobian_nonzeros: uno_int, + jacobian_row_indices: *const uno_int, + jacobian_column_indices: *const uno_int, + jacobian: uno_constraints_jacobian_callback, + ) -> bool; + + // uno_set_constraints_lower_bounds + pub fn uno_set_constraints_lower_bounds( + model: *mut c_void, + constraints_lower_bounds: *const f64, + ) -> bool; + + // uno_set_constraints_upper_bounds + pub fn uno_set_constraints_upper_bounds( + model: *mut c_void, + constraints_upper_bounds: *const f64, + ) -> bool; + + // uno_set_constraint_lower_bound + pub fn uno_set_constraint_lower_bound( + model: *mut c_void, + constraint_index: uno_int, + lower_bound: f64, + ) -> bool; + + // uno_set_constraint_upper_bound + pub fn uno_set_constraint_upper_bound( + model: *mut c_void, + constraint_index: uno_int, + upper_bound: f64, + ) -> bool; + + // uno_set_jacobian_operator + pub fn uno_set_jacobian_operator( + model: *mut c_void, + jacobian_operator: uno_constraints_jacobian_operator_callback, + ) -> bool; + + // uno_set_jacobian_transposed_operator + pub fn uno_set_jacobian_transposed_operator( + model: *mut c_void, + jacobian_transposed_operator: uno_constraints_jacobian_transposed_operator_callback, + ) -> bool; + + // uno_set_lagrangian_hessian + pub fn uno_set_lagrangian_hessian( + model: *mut c_void, + number_hessian_nonzeros: uno_int, + hessian_triangular_part: c_char, + hessian_row_indices: *const uno_int, + hessian_column_indices: *const uno_int, + lagrangian_hessian: uno_lagrangian_hessian_callback, + ) -> bool; + + // uno_set_lagrangian_hessian_operator + pub fn uno_set_lagrangian_hessian_operator( + model: *mut c_void, + lagrangian_hessian_operator: uno_lagrangian_hessian_operator_callback, + ) -> bool; + + // uno_set_lagrangian_sign_convention + pub fn uno_set_lagrangian_sign_convention( + model: *mut c_void, + lagrangian_sign_convention: uno_int, + ) -> bool; + + // uno_set_user_data + pub fn uno_set_user_data(model: *mut c_void, user_data: *mut c_void) -> bool; + + // uno_set_initial_primal_iterate_component + pub fn uno_set_initial_primal_iterate_component( + model: *mut c_void, + index: uno_int, + initial_primal_component: f64, + ) -> bool; + + // uno_set_initial_dual_iterate_component + pub fn uno_set_initial_dual_iterate_component( + model: *mut c_void, + index: uno_int, + initial_dual_component: f64, + ) -> bool; + + // uno_set_initial_primal_iterate + pub fn uno_set_initial_primal_iterate( + model: *mut c_void, + initial_primal_iterate: *const f64, + ) -> bool; + + // uno_set_initial_dual_iterate + pub fn uno_set_initial_dual_iterate( + model: *mut c_void, + initial_dual_iterate: *const f64, + ) -> bool; + + // uno_create_solver + pub fn uno_create_solver() -> *mut c_void; + + // uno_set_solver_integer_option + pub fn uno_set_solver_integer_option( + solver: *mut c_void, + option_name: *const c_char, + option_value: uno_int, + ) -> bool; + + // uno_set_solver_double_option + pub fn uno_set_solver_double_option( + solver: *mut c_void, + option_name: *const c_char, + option_value: f64, + ) -> bool; + + // uno_set_solver_bool_option + pub fn uno_set_solver_bool_option( + solver: *mut c_void, + option_name: *const c_char, + option_value: bool, + ) -> bool; + + // uno_set_solver_string_option + pub fn uno_set_solver_string_option( + solver: *mut c_void, + option_name: *const c_char, + option_value: *const c_char, + ) -> bool; + + // uno_get_solver_option_type + pub fn uno_get_solver_option_type(solver: *mut c_void, option_name: *const c_char) -> uno_int; + + // uno_load_solver_option_file + pub fn uno_load_solver_option_file(solver: *mut c_void, file_name: *const c_char) -> bool; + + // uno_set_solver_preset + pub fn uno_set_solver_preset(solver: *mut c_void, preset_name: *const c_char) -> bool; + + // uno_set_solver_callbacks + pub fn uno_set_solver_callbacks( + solver: *mut c_void, + notify_acceptable_iterate_callback: Option, + termination_callback: Option, + user_data: *mut c_void, + ) -> bool; + + // uno_set_logger_stream_callback + pub fn uno_set_logger_stream_callback( + logger_stream_callback: uno_logger_stream_callback, + user_data: *mut c_void, + ) -> bool; + + // uno_reset_logger_stream + pub fn uno_reset_logger_stream() -> bool; + + // uno_optimize + pub fn uno_optimize(solver: *mut c_void, model: *mut c_void); + + // uno_get_method_description + pub fn uno_get_method_description(solver: *mut c_void) -> *const c_char; + + // uno_get_solver_integer_option + pub fn uno_get_solver_integer_option( + solver: *mut c_void, + option_name: *const c_char, + ) -> uno_int; + + // uno_get_solver_double_option + pub fn uno_get_solver_double_option(solver: *mut c_void, option_name: *const c_char) -> f64; + + // uno_get_solver_bool_option + pub fn uno_get_solver_bool_option(solver: *mut c_void, option_name: *const c_char) -> bool; + + // uno_get_solver_string_option + pub fn uno_get_solver_string_option( + solver: *mut c_void, + option_name: *const c_char, + ) -> *const c_char; + + // uno_get_optimization_status + pub fn uno_get_optimization_status(solver: *mut c_void) -> uno_int; + + // uno_get_solution_status + pub fn uno_get_solution_status(solver: *mut c_void) -> uno_int; + + // uno_get_solution_objective + pub fn uno_get_solution_objective(solver: *mut c_void) -> f64; + + // uno_get_primal_solution_component + pub fn uno_get_primal_solution_component(solver: *mut c_void, index: uno_int) -> f64; + + // uno_get_constraint_dual_solution_component + pub fn uno_get_constraint_dual_solution_component(solver: *mut c_void, index: uno_int) -> f64; + + // uno_get_lower_bound_dual_solution_component + pub fn uno_get_lower_bound_dual_solution_component(solver: *mut c_void, index: uno_int) -> f64; + + // uno_get_upper_bound_dual_solution_component + pub fn uno_get_upper_bound_dual_solution_component(solver: *mut c_void, index: uno_int) -> f64; + + // uno_get_primal_solution + pub fn uno_get_primal_solution(solver: *mut c_void, primal_solution: *mut f64); + + // uno_get_constraint_dual_solution + pub fn uno_get_constraint_dual_solution( + solver: *mut c_void, + constraint_dual_solution: *mut f64, + ); + + // uno_get_lower_bound_dual_solution + pub fn uno_get_lower_bound_dual_solution( + solver: *mut c_void, + lower_bound_dual_solution: *mut f64, + ); + + // uno_get_upper_bound_dual_solution + pub fn uno_get_upper_bound_dual_solution( + solver: *mut c_void, + upper_bound_dual_solution: *mut f64, + ); + + // uno_get_solution_primal_feasibility + pub fn uno_get_solution_primal_feasibility(solver: *mut c_void) -> f64; + + // uno_get_solution_stationarity + pub fn uno_get_solution_stationarity(solver: *mut c_void) -> f64; + + // uno_get_solution_complementarity + pub fn uno_get_solution_complementarity(solver: *mut c_void) -> f64; + + // uno_get_number_iterations + pub fn uno_get_number_iterations(solver: *mut c_void) -> uno_int; + + // uno_get_cpu_time + pub fn uno_get_cpu_time(solver: *mut c_void) -> f64; + + // uno_get_number_objective_evaluations + pub fn uno_get_number_objective_evaluations(solver: *mut c_void) -> uno_int; + + // uno_get_number_constraint_evaluations + pub fn uno_get_number_constraint_evaluations(solver: *mut c_void) -> uno_int; + + // uno_get_number_objective_gradient_evaluations + pub fn uno_get_number_objective_gradient_evaluations(solver: *mut c_void) -> uno_int; + + // uno_get_number_jacobian_evaluations + pub fn uno_get_number_jacobian_evaluations(solver: *mut c_void) -> uno_int; + + // uno_get_number_hessian_evaluations + pub fn uno_get_number_hessian_evaluations(solver: *mut c_void) -> uno_int; + + // uno_get_number_subproblem_solved_evaluations + pub fn uno_get_number_subproblem_solved_evaluations(solver: *mut c_void) -> uno_int; + + // uno_destroy_model + pub fn uno_destroy_model(model: *mut c_void); + + // uno_destroy_solver + pub fn uno_destroy_solver(solver: *mut c_void); +} diff --git a/interfaces/Rust/src/lib.rs b/interfaces/Rust/src/lib.rs new file mode 100644 index 000000000..812d85122 --- /dev/null +++ b/interfaces/Rust/src/lib.rs @@ -0,0 +1,422 @@ +// uno-rs: Rust interface to the Uno solver + +pub mod ffi; +pub mod safe; + +pub use safe::{Problem, UnoError}; + +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_void}; + +// re-export constants + +pub use ffi::{ + uno_int, + UNO_MINIMIZE, UNO_MAXIMIZE, + UNO_MULTIPLIER_POSITIVE, UNO_MULTIPLIER_NEGATIVE, + UNO_ZERO_BASED_INDEXING, UNO_ONE_BASED_INDEXING, + UNO_LOWER_TRIANGLE, UNO_UPPER_TRIANGLE, + UNO_SUCCESS, UNO_ITERATION_LIMIT, UNO_TIME_LIMIT, + UNO_EVALUATION_ERROR, UNO_ALGORITHMIC_ERROR, UNO_USER_TERMINATION, + UNO_NOT_OPTIMAL, UNO_FEASIBLE_KKT_POINT, UNO_FEASIBLE_FJ_POINT, + UNO_INFEASIBLE_STATIONARY_POINT, UNO_FEASIBLE_SMALL_STEP, + UNO_INFEASIBLE_SMALL_STEP, UNO_UNBOUNDED, +}; + +// version + +pub fn get_version() -> (i32, i32, i32) { + let mut major: uno_int = 0; + let mut minor: uno_int = 0; + let mut patch: uno_int = 0; + unsafe { ffi::uno_get_version(&mut major, &mut minor, &mut patch) } + (major, minor, patch) +} + +// callback type re-exports + +pub use ffi::{ + uno_objective_callback, + uno_objective_gradient_callback, + uno_constraints_callback, + uno_constraints_jacobian_callback, + uno_lagrangian_hessian_callback, + uno_constraints_jacobian_operator_callback, + uno_constraints_jacobian_transposed_operator_callback, + uno_lagrangian_hessian_operator_callback, + uno_notify_acceptable_iterate_callback, + uno_termination_callback, + uno_logger_stream_callback, +}; + +// Model + +/// An optimization model that can be passed to `Solver::optimize`. +/// +/// Owns the raw pointer returned by `uno_create_model` and destroys it on drop. +pub struct Model { + ptr: *mut c_void, + pub number_variables: uno_int, +} + +impl Drop for Model { + fn drop(&mut self) { + unsafe { ffi::uno_destroy_model(self.ptr) } + } +} + +impl Model { + /// Creates a new model. + /// + /// # Arguments + /// * `problem_type` – one of `"NLP"`, `"QP"`, or `"LP"`. + /// * `lower_bounds` / `upper_bounds` – per-variable bounds (`f64::NEG_INFINITY` / `f64::INFINITY` for unbounded). + /// * `base_indexing` – `UNO_ZERO_BASED_INDEXING` or `UNO_ONE_BASED_INDEXING`. + pub fn new( + problem_type: &str, + lower_bounds: &[f64], + upper_bounds: &[f64], + base_indexing: uno_int, + ) -> Self { + assert_eq!( + lower_bounds.len(), + upper_bounds.len(), + "lower and upper bound arrays must have the same length" + ); + let n = lower_bounds.len() as uno_int; + let pt = CString::new(problem_type).unwrap(); + let ptr = unsafe { + ffi::uno_create_model( + pt.as_ptr(), + n, + lower_bounds.as_ptr(), + upper_bounds.as_ptr(), + base_indexing, + ) + }; + assert!(!ptr.is_null(), "uno_create_model returned null"); + Self { ptr, number_variables: n } + } + + // objective + + pub fn set_objective( + &mut self, + sense: uno_int, + objective_fn: uno_objective_callback, + gradient_fn: uno_objective_gradient_callback, + ) -> bool { + unsafe { ffi::uno_set_objective(self.ptr, sense, objective_fn, gradient_fn) } + } + + // constraints + + pub fn set_constraints( + &mut self, + constraint_fn: uno_constraints_callback, + lower_bounds: &[f64], + upper_bounds: &[f64], + jacobian_row_indices: &[uno_int], + jacobian_col_indices: &[uno_int], + jacobian_fn: uno_constraints_jacobian_callback, + ) -> bool { + let nc = lower_bounds.len() as uno_int; + let nnz = jacobian_row_indices.len() as uno_int; + assert_eq!( + jacobian_row_indices.len(), + jacobian_col_indices.len(), + "jacobian row/col index arrays must have the same length" + ); + unsafe { + ffi::uno_set_constraints( + self.ptr, + nc, + constraint_fn, + lower_bounds.as_ptr(), + upper_bounds.as_ptr(), + nnz, + jacobian_row_indices.as_ptr(), + jacobian_col_indices.as_ptr(), + jacobian_fn, + ) + } + } + + pub fn set_jacobian_operator(&mut self, op: uno_constraints_jacobian_operator_callback) -> bool { + unsafe { ffi::uno_set_jacobian_operator(self.ptr, op) } + } + + pub fn set_jacobian_transposed_operator( + &mut self, + op: uno_constraints_jacobian_transposed_operator_callback, + ) -> bool { + unsafe { ffi::uno_set_jacobian_transposed_operator(self.ptr, op) } + } + + // Lagrangian Hessian + + pub fn set_lagrangian_hessian( + &mut self, + triangular_part: c_char, + row_indices: &[uno_int], + col_indices: &[uno_int], + hessian_fn: uno_lagrangian_hessian_callback, + ) -> bool { + let nnz = row_indices.len() as uno_int; + assert_eq!(row_indices.len(), col_indices.len()); + unsafe { + ffi::uno_set_lagrangian_hessian( + self.ptr, + nnz, + triangular_part, + row_indices.as_ptr(), + col_indices.as_ptr(), + hessian_fn, + ) + } + } + + pub fn set_lagrangian_hessian_operator( + &mut self, + op: uno_lagrangian_hessian_operator_callback, + ) -> bool { + unsafe { ffi::uno_set_lagrangian_hessian_operator(self.ptr, op) } + } + + pub fn set_lagrangian_sign_convention(&mut self, convention: uno_int) -> bool { + unsafe { ffi::uno_set_lagrangian_sign_convention(self.ptr, convention) } + } + + // initial iterates + + pub fn set_initial_primal_iterate(&mut self, x0: &[f64]) -> bool { + unsafe { ffi::uno_set_initial_primal_iterate(self.ptr, x0.as_ptr()) } + } + + pub fn set_initial_dual_iterate(&mut self, y0: &[f64]) -> bool { + unsafe { ffi::uno_set_initial_dual_iterate(self.ptr, y0.as_ptr()) } + } + + pub fn set_initial_primal_component(&mut self, index: uno_int, value: f64) -> bool { + unsafe { ffi::uno_set_initial_primal_iterate_component(self.ptr, index, value) } + } + + pub fn set_initial_dual_component(&mut self, index: uno_int, value: f64) -> bool { + unsafe { ffi::uno_set_initial_dual_iterate_component(self.ptr, index, value) } + } + + // user data + + pub fn set_user_data(&mut self, data: *mut c_void) -> bool { + unsafe { ffi::uno_set_user_data(self.ptr, data) } + } +} + +// Solution + +/// The result returned by `Solver::optimize`. +pub struct Solution { + pub optimization_status: uno_int, + pub solution_status: uno_int, + pub objective: f64, + pub primals: Vec, + pub constraint_duals: Vec, + pub lower_bound_duals: Vec, + pub upper_bound_duals: Vec, + pub primal_feasibility: f64, + pub stationarity: f64, + pub complementarity: f64, + pub iterations: uno_int, + pub cpu_time: f64, +} + +impl Solution { + pub fn optimization_status_str(&self) -> &'static str { + match self.optimization_status { + UNO_SUCCESS => "SUCCESS", + UNO_ITERATION_LIMIT => "ITERATION_LIMIT", + UNO_TIME_LIMIT => "TIME_LIMIT", + UNO_EVALUATION_ERROR => "EVALUATION_ERROR", + UNO_ALGORITHMIC_ERROR => "ALGORITHMIC_ERROR", + UNO_USER_TERMINATION => "USER_TERMINATION", + _ => "UNKNOWN", + } + } + + pub fn solution_status_str(&self) -> &'static str { + match self.solution_status { + UNO_NOT_OPTIMAL => "NOT_OPTIMAL", + UNO_FEASIBLE_KKT_POINT => "FEASIBLE_KKT_POINT", + UNO_FEASIBLE_FJ_POINT => "FEASIBLE_FJ_POINT", + UNO_INFEASIBLE_STATIONARY_POINT => "INFEASIBLE_STATIONARY_POINT", + UNO_FEASIBLE_SMALL_STEP => "FEASIBLE_SMALL_STEP", + UNO_INFEASIBLE_SMALL_STEP => "INFEASIBLE_SMALL_STEP", + UNO_UNBOUNDED => "UNBOUNDED", + _ => "UNKNOWN", + } + } +} + +// Solver + +/// Wraps the Uno solver pointer. +pub struct Solver { + ptr: *mut c_void, +} + +impl Drop for Solver { + fn drop(&mut self) { + unsafe { ffi::uno_destroy_solver(self.ptr) } + } +} + +impl Solver { + /// Creates a new solver. + pub fn new() -> Self { + let ptr = unsafe { ffi::uno_create_solver() }; + assert!(!ptr.is_null(), "uno_create_solver returned null"); + Self { ptr } + } + + // option setters + + pub fn set_integer_option(&self, name: &str, value: uno_int) -> bool { + let name = CString::new(name).unwrap(); + unsafe { ffi::uno_set_solver_integer_option(self.ptr, name.as_ptr(), value) } + } + + pub fn set_double_option(&self, name: &str, value: f64) -> bool { + let name = CString::new(name).unwrap(); + unsafe { ffi::uno_set_solver_double_option(self.ptr, name.as_ptr(), value) } + } + + pub fn set_bool_option(&self, name: &str, value: bool) -> bool { + let name = CString::new(name).unwrap(); + unsafe { ffi::uno_set_solver_bool_option(self.ptr, name.as_ptr(), value) } + } + + pub fn set_string_option(&self, name: &str, value: &str) -> bool { + let name = CString::new(name).unwrap(); + let value = CString::new(value).unwrap(); + unsafe { ffi::uno_set_solver_string_option(self.ptr, name.as_ptr(), value.as_ptr()) } + } + + pub fn set_preset(&self, preset: &str) -> bool { + let preset = CString::new(preset).unwrap(); + unsafe { ffi::uno_set_solver_preset(self.ptr, preset.as_ptr()) } + } + + pub fn load_option_file(&self, path: &str) -> bool { + let path = CString::new(path).unwrap(); + unsafe { ffi::uno_load_solver_option_file(self.ptr, path.as_ptr()) } + } + + // option getters + + pub fn get_string_option(&self, name: &str) -> String { + let name = CString::new(name).unwrap(); + let ptr = unsafe { ffi::uno_get_solver_string_option(self.ptr, name.as_ptr()) }; + if ptr.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() } + } + } + + pub fn get_bool_option(&self, name: &str) -> bool { + let name = CString::new(name).unwrap(); + unsafe { ffi::uno_get_solver_bool_option(self.ptr, name.as_ptr()) } + } + + // logger + + pub fn set_logger_stream_callback( + cb: uno_logger_stream_callback, + user_data: *mut c_void, + ) -> bool { + unsafe { ffi::uno_set_logger_stream_callback(cb, user_data) } + } + + pub fn reset_logger_stream() -> bool { + unsafe { ffi::uno_reset_logger_stream() } + } + + // callbacks + + pub fn set_callbacks( + &self, + notify_cb: Option, + termination_cb: Option, + user_data: *mut c_void, + ) -> bool { + unsafe { ffi::uno_set_solver_callbacks(self.ptr, notify_cb, termination_cb, user_data) } + } + + // solve + + /// Optimizes `model` and returns a `Solution`. + pub fn optimize(&self, model: &Model) -> Solution { + unsafe { ffi::uno_optimize(self.ptr, model.ptr) } + self.collect_solution(model.number_variables, 0) + } + + /// Like `optimize` but also collects dual vectors. Pass the number of + /// constraints in `number_constraints`. + pub fn optimize_with_nc(&self, model: &Model, number_constraints: usize) -> Solution { + unsafe { ffi::uno_optimize(self.ptr, model.ptr) } + self.collect_solution(model.number_variables, number_constraints as uno_int) + } + + /// Optimizes a safe [`Problem`] and returns a [`Solution`]. + /// + /// Unlike [`optimize_with_nc`](Self::optimize_with_nc), the number of + /// constraints is taken from the `Problem`, so dual vectors are always + /// collected with the correct length. + pub fn solve(&self, problem: &crate::safe::Problem) -> Solution { + unsafe { ffi::uno_optimize(self.ptr, problem.as_ptr()) } + self.collect_solution( + problem.number_variables() as uno_int, + problem.number_constraints() as uno_int, + ) + } + + fn collect_solution(&self, n: uno_int, nc: uno_int) -> Solution { + let n_usize = n as usize; + let nc_usize = nc as usize; + + let mut primals = vec![0.0f64; n_usize]; + let mut constraint_duals = vec![0.0f64; nc_usize]; + let mut lower_bound_duals = vec![0.0f64; n_usize]; + let mut upper_bound_duals = vec![0.0f64; n_usize]; + + unsafe { + ffi::uno_get_primal_solution(self.ptr, primals.as_mut_ptr()); + if nc_usize > 0 { + ffi::uno_get_constraint_dual_solution(self.ptr, constraint_duals.as_mut_ptr()); + } + ffi::uno_get_lower_bound_dual_solution(self.ptr, lower_bound_duals.as_mut_ptr()); + ffi::uno_get_upper_bound_dual_solution(self.ptr, upper_bound_duals.as_mut_ptr()); + } + + Solution { + optimization_status: unsafe { ffi::uno_get_optimization_status(self.ptr) }, + solution_status: unsafe { ffi::uno_get_solution_status(self.ptr) }, + objective: unsafe { ffi::uno_get_solution_objective(self.ptr) }, + primals, + constraint_duals, + lower_bound_duals, + upper_bound_duals, + primal_feasibility: unsafe { ffi::uno_get_solution_primal_feasibility(self.ptr) }, + stationarity: unsafe { ffi::uno_get_solution_stationarity(self.ptr) }, + complementarity: unsafe { ffi::uno_get_solution_complementarity(self.ptr) }, + iterations: unsafe { ffi::uno_get_number_iterations(self.ptr) }, + cpu_time: unsafe { ffi::uno_get_cpu_time(self.ptr) }, + } + } +} + +impl Default for Solver { + fn default() -> Self { + Self::new() + } +} diff --git a/interfaces/Rust/src/safe.rs b/interfaces/Rust/src/safe.rs new file mode 100644 index 000000000..665926a6c --- /dev/null +++ b/interfaces/Rust/src/safe.rs @@ -0,0 +1,439 @@ +// uno-rs: safe, idiomatic layer on top of the raw C callbacks. +// +// This module lets users provide ordinary Rust closures (or functions) +// instead of `unsafe extern "C"` callbacks. The crate stores the closures +// and installs generic trampolines that recover them from Uno's `user_data` +// pointer and call them with safe `&[f64]` / `&mut [f64]` slices. + +use std::os::raw::{c_char, c_void}; + +use crate::ffi; +use crate::uno_int; + +// ───────────────────────────────────────────────────────────────────────── +// Error type +// ───────────────────────────────────────────────────────────────────────── + +/// Error returned by the safe builder API when an underlying Uno call fails. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnoError { + /// Name of the underlying Uno C function that returned `false`. + pub operation: &'static str, +} + +impl std::fmt::Display for UnoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Uno operation `{}` failed", self.operation) + } +} + +impl std::error::Error for UnoError {} + +type Result = std::result::Result; + +fn check(ok: bool, operation: &'static str) -> Result<()> { + if ok { + Ok(()) + } else { + Err(UnoError { operation }) + } +} + +// ───────────────────────────────────────────────────────────────────────── +// User callback signatures (safe) +// +// Each callback returns `Result<(), ()>`: `Ok(())` maps to the Uno success +// code (0) and `Err(())` maps to a non-zero evaluation-error code, which is +// how the C API signals "could not evaluate at this point". +// ───────────────────────────────────────────────────────────────────────── + +/// `f(x) -> objective_value`. +/// +/// Inputs: `x` (length `n`). Output: write the scalar objective into `obj`. +pub type ObjectiveFn = dyn FnMut(&[f64], &mut f64) -> std::result::Result<(), ()>; + +/// `∇f(x)`. +/// +/// Inputs: `x` (length `n`). Output: fill `gradient` (length `n`). +pub type ObjectiveGradientFn = dyn FnMut(&[f64], &mut [f64]) -> std::result::Result<(), ()>; + +/// `c(x)`. +/// +/// Inputs: `x` (length `n`). Output: fill `constraints` (length `m`). +pub type ConstraintsFn = dyn FnMut(&[f64], &mut [f64]) -> std::result::Result<(), ()>; + +/// Constraint Jacobian values in the COO order fixed by the sparsity pattern. +/// +/// Inputs: `x` (length `n`). Output: fill `values` (length `nnz`). +pub type JacobianFn = dyn FnMut(&[f64], &mut [f64]) -> std::result::Result<(), ()>; + +/// Lagrangian Hessian values (lower/upper triangle as declared). +/// +/// Inputs: `x` (length `n`), `objective_multiplier` (ρ), `multipliers` +/// (length `m`). Output: fill `values` (length `nnz`). +pub type LagrangianHessianFn = + dyn FnMut(&[f64], f64, &[f64], &mut [f64]) -> std::result::Result<(), ()>; + +// ───────────────────────────────────────────────────────────────────────── +// Storage for the boxed closures. +// +// We keep every closure the user registers alive for as long as the model +// lives, in a single heap allocation whose address we hand to Uno as +// `user_data`. The trampolines below cast that pointer back to `&mut Callbacks`. +// ───────────────────────────────────────────────────────────────────────── + +#[derive(Default)] +struct Callbacks { + objective: Option>, + objective_gradient: Option>, + constraints: Option>, + jacobian: Option>, + hessian: Option>, +} + +#[inline] +fn ret_code(r: std::result::Result<(), ()>) -> uno_int { + match r { + Ok(()) => 0, + // Non-zero signals an evaluation error to Uno. + Err(()) => 1, + } +} + +// ───────────────────────────────────────────────────────────────────────── +// Trampolines: `extern "C"` functions matching the raw Uno signatures. +// +// SAFETY: Uno calls these with the `user_data` pointer we registered, which +// always points to the `Callbacks` box owned by the `Problem`. The box +// outlives every call because `optimize` borrows `&Problem`. The closures are +// only ever invoked from inside `uno_optimize`, single-threaded, so the +// `&mut` reborrow below is sound (no aliasing across calls). +// ───────────────────────────────────────────────────────────────────────── + +unsafe fn callbacks<'a>(user_data: *mut c_void) -> &'a mut Callbacks { + debug_assert!(!user_data.is_null(), "Uno called a trampoline with null user_data"); + &mut *(user_data as *mut Callbacks) +} + +unsafe extern "C" fn objective_trampoline( + n: uno_int, + x: *const f64, + obj: *mut f64, + user_data: *mut c_void, +) -> uno_int { + let cb = callbacks(user_data); + let f = cb.objective.as_mut().expect("objective callback missing"); + let x = std::slice::from_raw_parts(x, n as usize); + ret_code(f(x, &mut *obj)) +} + +unsafe extern "C" fn objective_gradient_trampoline( + n: uno_int, + x: *const f64, + gradient: *mut f64, + user_data: *mut c_void, +) -> uno_int { + let cb = callbacks(user_data); + let f = cb + .objective_gradient + .as_mut() + .expect("objective-gradient callback missing"); + let x = std::slice::from_raw_parts(x, n as usize); + let g = std::slice::from_raw_parts_mut(gradient, n as usize); + ret_code(f(x, g)) +} + +unsafe extern "C" fn constraints_trampoline( + n: uno_int, + m: uno_int, + x: *const f64, + values: *mut f64, + user_data: *mut c_void, +) -> uno_int { + let cb = callbacks(user_data); + let f = cb.constraints.as_mut().expect("constraints callback missing"); + let x = std::slice::from_raw_parts(x, n as usize); + let c = std::slice::from_raw_parts_mut(values, m as usize); + ret_code(f(x, c)) +} + +unsafe extern "C" fn jacobian_trampoline( + n: uno_int, + nnz: uno_int, + x: *const f64, + values: *mut f64, + user_data: *mut c_void, +) -> uno_int { + let cb = callbacks(user_data); + let f = cb.jacobian.as_mut().expect("jacobian callback missing"); + let x = std::slice::from_raw_parts(x, n as usize); + let v = std::slice::from_raw_parts_mut(values, nnz as usize); + ret_code(f(x, v)) +} + +unsafe extern "C" fn hessian_trampoline( + n: uno_int, + m: uno_int, + nnz: uno_int, + x: *const f64, + objective_multiplier: f64, + multipliers: *const f64, + values: *mut f64, + user_data: *mut c_void, +) -> uno_int { + let cb = callbacks(user_data); + let f = cb.hessian.as_mut().expect("hessian callback missing"); + let x = std::slice::from_raw_parts(x, n as usize); + let y = std::slice::from_raw_parts(multipliers, m as usize); + let h = std::slice::from_raw_parts_mut(values, nnz as usize); + ret_code(f(x, objective_multiplier, y, h)) +} + +// ───────────────────────────────────────────────────────────────────────── +// Problem: the safe model builder. +// ───────────────────────────────────────────────────────────────────────── + +/// A safe, idiomatic optimization model. +/// +/// Build it with [`Problem::new`], attach an objective / constraints / Hessian +/// using ordinary Rust closures, then hand it to [`crate::Solver::solve`]. +/// +/// The closures are stored on the heap inside the `Problem` and kept alive +/// until the `Problem` is dropped. The FFI plumbing (function pointers, +/// `user_data`, raw slices) is handled internally. +pub struct Problem { + ptr: *mut c_void, + number_variables: uno_int, + number_constraints: uno_int, + // Boxed twice: the outer Box gives a stable address to register as + // `user_data`; the inner struct owns the user's closures. Heap-stable for + // the lifetime of the Problem. + callbacks: Box, +} + +impl Drop for Problem { + fn drop(&mut self) { + unsafe { ffi::uno_destroy_model(self.ptr) } + // `self.callbacks` is freed here, after the model that referenced it. + } +} + +impl Problem { + /// Creates a new model with per-variable bounds. + /// + /// Use `f64::NEG_INFINITY` / `f64::INFINITY` for unbounded variables. + /// `problem_type` is one of `"NLP"`, `"QP"`, `"LP"`. + pub fn new( + problem_type: &str, + lower_bounds: &[f64], + upper_bounds: &[f64], + base_indexing: uno_int, + ) -> Result { + assert_eq!( + lower_bounds.len(), + upper_bounds.len(), + "lower and upper bound arrays must have the same length" + ); + let n = lower_bounds.len() as uno_int; + let pt = std::ffi::CString::new(problem_type).expect("problem_type contained a NUL byte"); + let ptr = unsafe { + ffi::uno_create_model( + pt.as_ptr(), + n, + lower_bounds.as_ptr(), + upper_bounds.as_ptr(), + base_indexing, + ) + }; + if ptr.is_null() { + return Err(UnoError { operation: "uno_create_model" }); + } + + let mut problem = Self { + ptr, + number_variables: n, + number_constraints: 0, + callbacks: Box::new(Callbacks::default()), + }; + // Register the (stable) address of our callbacks box as Uno's user_data. + let ud = problem.callbacks.as_mut() as *mut Callbacks as *mut c_void; + check( + unsafe { ffi::uno_set_user_data(problem.ptr, ud) }, + "uno_set_user_data", + )?; + Ok(problem) + } + + /// Number of variables in the model. + pub fn number_variables(&self) -> usize { + self.number_variables as usize + } + + /// Number of constraints registered so far. + pub fn number_constraints(&self) -> usize { + self.number_constraints as usize + } + + /// Sets the objective from Rust closures. + /// + /// * `objective` receives `x` and writes the scalar value. + /// * `gradient` receives `x` and fills the length-`n` gradient slice. + /// + /// Either closure may return `Err(())` to signal an evaluation error. + pub fn set_objective( + &mut self, + sense: uno_int, + objective: F, + gradient: G, + ) -> Result<&mut Self> + where + F: FnMut(&[f64], &mut f64) -> std::result::Result<(), ()> + 'static, + G: FnMut(&[f64], &mut [f64]) -> std::result::Result<(), ()> + 'static, + { + self.callbacks.objective = Some(Box::new(objective)); + self.callbacks.objective_gradient = Some(Box::new(gradient)); + check( + unsafe { + ffi::uno_set_objective( + self.ptr, + sense, + objective_trampoline, + objective_gradient_trampoline, + ) + }, + "uno_set_objective", + )?; + Ok(self) + } + + /// Sets the constraints and their Jacobian from Rust closures. + /// + /// The Jacobian sparsity is given in COO form by `jacobian_row_indices` + /// and `jacobian_col_indices`; `jacobian` must fill its `values` slice in + /// that same order. + pub fn set_constraints( + &mut self, + constraints: C, + lower_bounds: &[f64], + upper_bounds: &[f64], + jacobian_row_indices: &[uno_int], + jacobian_col_indices: &[uno_int], + jacobian: J, + ) -> Result<&mut Self> + where + C: FnMut(&[f64], &mut [f64]) -> std::result::Result<(), ()> + 'static, + J: FnMut(&[f64], &mut [f64]) -> std::result::Result<(), ()> + 'static, + { + assert_eq!( + lower_bounds.len(), + upper_bounds.len(), + "constraint lower/upper bound arrays must have the same length" + ); + assert_eq!( + jacobian_row_indices.len(), + jacobian_col_indices.len(), + "jacobian row/col index arrays must have the same length" + ); + let nc = lower_bounds.len() as uno_int; + let nnz = jacobian_row_indices.len() as uno_int; + + self.callbacks.constraints = Some(Box::new(constraints)); + self.callbacks.jacobian = Some(Box::new(jacobian)); + + check( + unsafe { + ffi::uno_set_constraints( + self.ptr, + nc, + constraints_trampoline, + lower_bounds.as_ptr(), + upper_bounds.as_ptr(), + nnz, + jacobian_row_indices.as_ptr(), + jacobian_col_indices.as_ptr(), + jacobian_trampoline, + ) + }, + "uno_set_constraints", + )?; + self.number_constraints = nc; + Ok(self) + } + + /// Sets the Lagrangian Hessian from a Rust closure. + /// + /// The sparsity is given in COO form by `row_indices` / `col_indices` over + /// the declared triangle; the closure fills `values` in that order. + /// Signature: `|x, objective_multiplier, multipliers, values|`. + pub fn set_lagrangian_hessian( + &mut self, + triangular_part: c_char, + row_indices: &[uno_int], + col_indices: &[uno_int], + hessian: H, + ) -> Result<&mut Self> + where + H: FnMut(&[f64], f64, &[f64], &mut [f64]) -> std::result::Result<(), ()> + 'static, + { + assert_eq!( + row_indices.len(), + col_indices.len(), + "hessian row/col index arrays must have the same length" + ); + let nnz = row_indices.len() as uno_int; + + self.callbacks.hessian = Some(Box::new(hessian)); + + check( + unsafe { + ffi::uno_set_lagrangian_hessian( + self.ptr, + nnz, + triangular_part, + row_indices.as_ptr(), + col_indices.as_ptr(), + hessian_trampoline, + ) + }, + "uno_set_lagrangian_hessian", + )?; + Ok(self) + } + + /// Sets the Lagrangian sign convention. + pub fn set_lagrangian_sign_convention(&mut self, convention: uno_int) -> Result<&mut Self> { + check( + unsafe { ffi::uno_set_lagrangian_sign_convention(self.ptr, convention) }, + "uno_set_lagrangian_sign_convention", + )?; + Ok(self) + } + + /// Sets the initial primal iterate (length `n`). + pub fn set_initial_primal_iterate(&mut self, x0: &[f64]) -> Result<&mut Self> { + assert_eq!( + x0.len(), + self.number_variables as usize, + "initial primal iterate has wrong length" + ); + check( + unsafe { ffi::uno_set_initial_primal_iterate(self.ptr, x0.as_ptr()) }, + "uno_set_initial_primal_iterate", + )?; + Ok(self) + } + + /// Sets the initial dual iterate. + pub fn set_initial_dual_iterate(&mut self, y0: &[f64]) -> Result<&mut Self> { + check( + unsafe { ffi::uno_set_initial_dual_iterate(self.ptr, y0.as_ptr()) }, + "uno_set_initial_dual_iterate", + )?; + Ok(self) + } + + /// Raw model pointer, for use by the solver. Internal. + pub(crate) fn as_ptr(&self) -> *mut c_void { + self.ptr + } +}