Skip to content

Commit f0f4d8a

Browse files
committed
Add C++ implementation of COBYLA
see #136
1 parent 1d76fb8 commit f0f4d8a

39 files changed

Lines changed: 5595 additions & 0 deletions

.github/workflows/test_cpp.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Test CPP
2+
3+
on:
4+
pull_request:
5+
schedule:
6+
- cron: '0 16 4-31/4 * *'
7+
workflow_dispatch:
8+
9+
jobs:
10+
test:
11+
name: Run CPP tests
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]
17+
steps:
18+
- uses: actions/checkout@v6
19+
20+
- name: Install dependencies (Ubuntu)
21+
if: runner.os == 'Linux'
22+
run: sudo apt-get update && sudo apt-get install -y libeigen3-dev
23+
24+
- name: Install dependencies (macOS)
25+
if: runner.os == 'macOS'
26+
run: brew install eigen
27+
28+
- name: Install dependencies (Windows)
29+
if: runner.os == 'Windows'
30+
run: vcpkg install eigen3 --triplet x64-windows
31+
32+
- name: Set vcpkg toolchain (Windows)
33+
if: runner.os == 'Windows'
34+
shell: pwsh
35+
run: echo "CMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" >> $env:GITHUB_ENV
36+
37+
- name: Build
38+
run: |
39+
cmake -DPRIMA_ENABLE_TESTING=ON -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_STANDARD_REQUIRED=TRUE -DCMAKE_BUILD_TYPE=Release -B build -S cpp
40+
cmake --build build -j3
41+
42+
- name: Test
43+
run: ctest --test-dir build --output-on-failure --timeout 100 --schedule-random -V -j3

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ if (PRIMA_ENABLE_C)
129129
set(primac_target "primac")
130130
endif ()
131131

132+
option (PRIMA_ENABLE_CPP "C++ binding" OFF)
133+
if (PRIMA_ENABLE_CPP)
134+
enable_language(CXX)
135+
add_subdirectory(cpp)
136+
endif ()
137+
132138
# Get the version number
133139
find_package(Git)
134140
set(IS_REPO FALSE)

cpp/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
cmake_minimum_required (VERSION 3.18)
2+
3+
project (primacpp VERSION 0.1.0 LANGUAGES CXX)
4+
5+
option (BUILD_SHARED_LIBS "shared/static" ON)
6+
7+
include (GNUInstallDirs)
8+
9+
find_package (Eigen3 REQUIRED)
10+
message (STATUS "Found Eigen3: ${Eigen3_DIR} (found version ${Eigen3_VERSION})")
11+
12+
add_subdirectory (src)
13+
enable_testing()
14+
add_subdirectory (tests)
15+
add_subdirectory (examples)

cpp/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
## About
2+
3+
This is a C++ translation of [Zaikun Zhang](https://www.zhangzk.net)'s [modern-Fortran reference implementation](https://github.com/libprima/prima/tree/main/fortran)
4+
for Powell's derivative-free optimization solvers, which is available at `fortran/` under the root directory.
5+
It is a faithful translation of the [Python translation](https://github.com/libprima/prima/tree/main/pyprima),
6+
following the same structure, variable names, and algorithm logic to keep maintenance across languages tractable.
7+
8+
Due to [bug-fixes](https://github.com/libprima/prima#bug-fixes) and [improvements](https://github.com/libprima/prima#improvements),
9+
the modern-Fortran reference implementation by [Zaikun Zhang](https://www.zhangzk.net)
10+
behaves differently from the original Fortran 77 implementation by [M. J. D. Powell](https://www.zhangzk.net/powell.html),
11+
even though the algorithms are essentially the same. Therefore, it is important to point out that you are using
12+
PRIMA rather than the original solvers if you want your results to be reproducible.
13+
14+
As of June 2026, only the COBYLA solver is available in this C++ translation.
15+
The other solvers will be translated from the Python/Fortran reference implementations in the future.
16+
17+
## Building
18+
19+
This is a header-only library requiring only Eigen3. To build the tests:
20+
21+
```bash
22+
cmake -S cpp -B build -DEigen3_DIR=/path/to/eigen3/cmake
23+
cmake --build build --target test_minimize_cpp_exe
24+
```
25+
26+
To install:
27+
28+
```bash
29+
cmake --install build --prefix /usr/local
30+
```
31+
32+
After installation, use from another project:
33+
34+
```cmake
35+
find_package(primacpp REQUIRED)
36+
target_link_libraries(myapp PRIVATE prima::primacpp)
37+
```
38+
39+
## Development notes
40+
41+
- Function names, variable names, and file layout follow the Fortran and Python implementations.
42+
Keep them in sync when making changes.
43+
- Comments are kept minimal compared to the Python/Fortran sources. When the intent is unclear,
44+
refer to the Python or Fortran reference.
45+
- The library is header-only. All implementation is in `.hpp` files under `src/prima/`.
46+
- The namespace is `prima`; internals go in `prima::detail`.

cpp/examples/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
add_executable (cobyla_example_exe EXCLUDE_FROM_ALL cobyla_example.cpp)
2+
add_executable (rosenbrock_example_exe EXCLUDE_FROM_ALL rosenbrock.cpp)
3+
4+
target_link_libraries (cobyla_example_exe PRIVATE primacpp)
5+
target_link_libraries (rosenbrock_example_exe PRIVATE primacpp)

cpp/examples/cobyla_example.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// This is an example to illustrate the usage of the COBYLA solver.
2+
//
3+
// Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
4+
//
5+
// Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
6+
7+
#include <iostream>
8+
#include <cmath>
9+
#include <Eigen/Core>
10+
#include "prima/prima.hpp"
11+
12+
using namespace prima;
13+
using namespace Eigen;
14+
15+
// Objective: f(x) = (x1-5)^2 + (x2-4)^2
16+
double objective(const VectorXd& x) {
17+
return std::pow(x(0) - 5.0, 2) + std::pow(x(1) - 4.0, 2);
18+
}
19+
20+
int main() {
21+
std::cout << "=== COBYLA Example ===" << std::endl;
22+
23+
// Simple constrained optimization:
24+
// min (x1-5)^2 + (x2-4)^2
25+
// s.t. x1^2 - 9 <= 0 (i.e., |x1| <= 3)
26+
27+
VectorXd x0(2);
28+
x0 << 0.0, 0.0;
29+
30+
// Nonlinear constraint: x1^2 - 9 <= 0
31+
auto cons_fun = [](const VectorXd& x) -> VectorXd {
32+
return VectorXd::Constant(1, x(0) * x(0) - 9.0);
33+
};
34+
VectorXd nlc_lb(1);
35+
nlc_lb << -std::numeric_limits<double>::infinity();
36+
VectorXd nlc_ub(1);
37+
nlc_ub << 0.0;
38+
NonlinearConstraint nlc(cons_fun, nlc_lb, nlc_ub);
39+
auto nlc_func = transform_constraint_function(nlc);
40+
41+
MinimizeOptions opts;
42+
opts.quiet = false; // Print progress
43+
opts.rhoend = 1e-6;
44+
opts.maxfun = 5000;
45+
46+
auto result = minimize(objective, x0, "cobyla", nullptr, nullptr, &nlc_func, opts);
47+
48+
std::cout << "\nResult:" << std::endl;
49+
std::cout << " x = [" << result.x(0) << ", " << result.x(1) << "]" << std::endl;
50+
std::cout << " f = " << result.fun << std::endl;
51+
std::cout << " constraint x1^2-9 = " << (result.x(0) * result.x(0) - 9.0) << std::endl;
52+
std::cout << " nfev = " << result.nfev << std::endl;
53+
54+
// Check: x1 should be near 3, x2 near 4, f near 4
55+
if (std::abs(result.x(0) - 3.0) < 1e-2 &&
56+
std::abs(result.x(1) - 4.0) < 1e-2 &&
57+
std::abs(result.fun - 4.0) < 1e-2) {
58+
std::cout << "\nExample PASSED." << std::endl;
59+
return 0;
60+
} else {
61+
std::cerr << "\nExample FAILED." << std::endl;
62+
return 1;
63+
}
64+
}

cpp/examples/rosenbrock.cpp

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Illustration of how to use prima with COBYLA.
2+
//
3+
// Minimize the chained Rosenbrock function subject to various constraints.
4+
//
5+
// Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
6+
//
7+
// Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
8+
9+
#include <iostream>
10+
#include <cmath>
11+
#include <iomanip>
12+
#include <Eigen/Core>
13+
#include "prima/prima.hpp"
14+
15+
using namespace prima;
16+
using namespace Eigen;
17+
18+
// Chained Rosenbrock function
19+
double chrosen(const VectorXd& x) {
20+
int n = x.size();
21+
double f = 0;
22+
for (int i = 0; i < n - 1; ++i) {
23+
f += std::pow(1 - x(i), 2) + 4 * std::pow(x(i + 1) - x(i) * x(i), 2);
24+
}
25+
return f;
26+
}
27+
28+
// Nonlinear inequality constraint: x(i)^2 >= x(i+1)
29+
VectorXd nlc_ineq(const VectorXd& x) {
30+
int n = x.size();
31+
VectorXd c(n - 1);
32+
for (int i = 0; i < n - 1; ++i) {
33+
c(i) = x(i) * x(i) - x(i + 1);
34+
}
35+
return c;
36+
}
37+
38+
// Nonlinear equality constraint: ||x||^2 = 1
39+
VectorXd nlc_eq(const VectorXd& x) {
40+
VectorXd c(1);
41+
c(0) = x.squaredNorm() - 1;
42+
return c;
43+
}
44+
45+
int main() {
46+
std::cout << std::setprecision(4);
47+
std::cout << "Minimize the chained Rosenbrock function with three variables "
48+
<< "subject to various constraints using COBYLA.\n" << std::endl;
49+
50+
VectorXd x0(3);
51+
x0 << 0, 0, 0;
52+
53+
// ---------------------------------------------------------------- //
54+
// 1. Nonlinear constraints
55+
// ||x||_2^2 = 1, x(i)^2 >= x(i+1) >= 0.5*x(i) >= 0 for i = 1, 2
56+
// ---------------------------------------------------------------- //
57+
std::cout << "1. Nonlinear constraints --- ||x||_2^2 = 1, "
58+
<< "x(i)^2 >= x(i+1) >= 0.5*x(i) >= 0 for i = 1, 2:\n" << std::endl;
59+
60+
VectorXd lb(3); lb << 0, 0, 0;
61+
VectorXd ub(3); ub << std::numeric_limits<double>::infinity(),
62+
std::numeric_limits<double>::infinity(),
63+
std::numeric_limits<double>::infinity();
64+
Bounds bounds(lb, ub);
65+
66+
// Linear constraints: 0.5*x(i) - x(i+1) <= 0
67+
MatrixXd A(2, 3);
68+
A << 0.5, -1, 0,
69+
0, 0.5, -1;
70+
LinearConstraint lin_con(A,
71+
VectorXd::Constant(2, -std::numeric_limits<double>::infinity()),
72+
VectorXd::Constant(2, 0.0));
73+
74+
// Nonlinear constraints
75+
NonlinearConstraint nlc_ineq_obj(nlc_ineq,
76+
VectorXd::Constant(2, 0.0),
77+
VectorXd::Constant(2, std::numeric_limits<double>::infinity()));
78+
NonlinearConstraint nlc_eq_obj(nlc_eq,
79+
VectorXd::Constant(1, 0.0),
80+
VectorXd::Constant(1, 0.0));
81+
82+
auto nlc_ineq_t = transform_constraint_function(nlc_ineq_obj);
83+
auto nlc_eq_t = transform_constraint_function(nlc_eq_obj);
84+
85+
MinimizeOptions opts;
86+
opts.quiet = true;
87+
88+
// COBYLA only handles one NonlinearConstraintFunction, so combine them
89+
NonlinearConstraintFunction combined_nlc = [nlc_ineq_t, nlc_eq_t](const VectorXd& x) -> VectorXd {
90+
VectorXd v1 = nlc_ineq_t(x);
91+
VectorXd v2 = nlc_eq_t(x);
92+
VectorXd r(v1.size() + v2.size());
93+
r.head(v1.size()) = v1;
94+
r.tail(v2.size()) = v2;
95+
return r;
96+
};
97+
98+
auto result = minimize(chrosen, x0, "cobyla", &bounds, &lin_con, &combined_nlc, opts);
99+
std::cout << " x = [" << result.x(0) << ", " << result.x(1) << ", " << result.x(2) << "]" << std::endl;
100+
std::cout << " f = " << result.fun << " nfev = " << result.nfev << std::endl;
101+
std::cout << " ||x||^2 = " << result.x.squaredNorm() << std::endl;
102+
103+
// ---------------------------------------------------------------- //
104+
// 2. Linear constraints
105+
// sum(x) = 1, x(i+1) <= x(i) <= 1 for i = 1, 2
106+
// ---------------------------------------------------------------- //
107+
std::cout << "\n2. Linear constraints --- sum(x) = 1, x(i+1) <= x(i) <= 1 for i = 1, 2:\n" << std::endl;
108+
109+
Bounds bounds2(
110+
VectorXd::Constant(3, -std::numeric_limits<double>::infinity()),
111+
VectorXd::Constant(3, 1.0));
112+
MatrixXd A2(3, 3);
113+
A2 << -1, 1, 0,
114+
0, -1, 1,
115+
1, 1, 1;
116+
LinearConstraint lin_con2(A2,
117+
Vector3d(-std::numeric_limits<double>::infinity(),
118+
-std::numeric_limits<double>::infinity(), 1.0),
119+
Vector3d(0.0, 0.0, 1.0));
120+
121+
auto result2 = minimize(chrosen, x0, "cobyla", &bounds2, &lin_con2, nullptr, opts);
122+
std::cout << " x = [" << result2.x(0) << ", " << result2.x(1) << ", " << result2.x(2) << "]" << std::endl;
123+
std::cout << " f = " << result2.fun << " nfev = " << result2.nfev << std::endl;
124+
std::cout << " sum(x) = " << result2.x.sum() << std::endl;
125+
126+
// ---------------------------------------------------------------- //
127+
// 3. Bound constraints: -0.5 <= x(1) <= 0.5, 0 <= x(2) <= 0.25
128+
// ---------------------------------------------------------------- //
129+
std::cout << "\n3. Bound constraints --- -0.5 <= x(1) <= 0.5, 0 <= x(2) <= 0.25:\n" << std::endl;
130+
131+
Bounds bounds3(
132+
Vector3d(-0.5, 0.0, -std::numeric_limits<double>::infinity()),
133+
Vector3d(0.5, 0.25, std::numeric_limits<double>::infinity()));
134+
135+
auto result3 = minimize(chrosen, x0, "cobyla", &bounds3, nullptr, nullptr, opts);
136+
std::cout << " x = [" << result3.x(0) << ", " << result3.x(1) << ", " << result3.x(2) << "]" << std::endl;
137+
std::cout << " f = " << result3.fun << " nfev = " << result3.nfev << std::endl;
138+
139+
// ---------------------------------------------------------------- //
140+
// 4. No constraints
141+
// ---------------------------------------------------------------- //
142+
std::cout << "\n4. No constraints:\n" << std::endl;
143+
auto result4 = minimize(chrosen, x0, "cobyla", nullptr, nullptr, nullptr, opts);
144+
std::cout << " x = [" << result4.x(0) << ", " << result4.x(1) << ", " << result4.x(2) << "]" << std::endl;
145+
std::cout << " f = " << result4.fun << " nfev = " << result4.nfev << std::endl;
146+
147+
return 0;
148+
}

cpp/src/CMakeLists.txt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
add_library (primacpp INTERFACE)
2+
target_include_directories (primacpp INTERFACE
3+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
4+
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
5+
)
6+
target_link_libraries (primacpp INTERFACE Eigen3::Eigen)
7+
target_compile_options (primacpp INTERFACE $<$<CXX_COMPILER_ID:MSVC>:/bigobj>)
8+
target_compile_features (primacpp INTERFACE cxx_std_17)
9+
10+
include (GNUInstallDirs)
11+
include (CMakePackageConfigHelpers)
12+
13+
install (
14+
TARGETS primacpp
15+
EXPORT primacpp-targets
16+
)
17+
18+
install (
19+
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/prima
20+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
21+
FILES_MATCHING PATTERN "*.hpp"
22+
)
23+
24+
install (
25+
EXPORT primacpp-targets
26+
FILE primacpp-targets.cmake
27+
NAMESPACE prima::
28+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/primacpp
29+
)
30+
31+
configure_package_config_file (
32+
${CMAKE_CURRENT_SOURCE_DIR}/primacpp-config.cmake.in
33+
${CMAKE_BINARY_DIR}/primacpp-config.cmake
34+
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/primacpp
35+
)
36+
37+
write_basic_package_version_file (
38+
${CMAKE_BINARY_DIR}/primacpp-config-version.cmake
39+
VERSION ${PROJECT_VERSION}
40+
COMPATIBILITY AnyNewerVersion
41+
)
42+
43+
install (
44+
FILES
45+
${CMAKE_BINARY_DIR}/primacpp-config.cmake
46+
${CMAKE_BINARY_DIR}/primacpp-config-version.cmake
47+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/primacpp
48+
)

0 commit comments

Comments
 (0)