Build and Validate PRIMA with PRIK¶
This example turns PRIMA, the modern Fortran implementation of Powell's derivative-free optimization solvers, into one Python extension. It compiles PRIMA once into a static library, asks PRIK to generate bindings only for the five solvers, and links those bindings against that library. Your objective function is an ordinary Python callable.
What you get¶
One extension, prik_prima, with exactly five solvers:
| Module | Solver | Problem type |
|---|---|---|
uobyqa_mod |
uobyqa |
Unconstrained |
newuoa_mod |
newuoa |
Unconstrained |
bobyqa_mod |
bobyqa |
Bound constraints |
lincoa_mod |
lincoa |
Linear constraints |
cobyla_mod |
cobyla |
Nonlinear constraints |
Each solver takes your objective as a Python callback and updates a NumPy x
in place. Every optional PRIMA argument stays optional in Python, including a
progress callback that can stop the solver early.
Quick start¶
From a PRIK checkout with PRIK installed, and with CMake and GNU Fortran on
PATH (see Set up a clean environment):
source examples/fortran/prima/build_all.sh
python3 -m pytest -q examples/fortran/prima/tests
The first command builds the extension and puts it on PYTHONPATH for this
shell; use source, not bash, so that setting survives. The second runs the
example's tests.
After this, you can import prik_prima in the same shell and call the solvers
as shown in Use the generated API.
Key files¶
Everything lives under
examples/fortran/prima/:
| File | What it does |
|---|---|
export_symbols.txt |
Names the five module::procedure solvers PRIK exposes. |
sources.txt |
Lists the 55 PRIMA sources, used by both CMake and PRIK. |
CMakeLists.txt |
Compiles those sources into the static library libprimaf.a. |
build_prik.sh |
Runs the three build steps below. |
build_all.sh |
Runs build_prik.sh and adds the extension to PYTHONPATH. |
tests/test_solvers.py |
Checks every solver and the callbacks. |
native/ |
The PRIMA source snapshot; the build downloads nothing. |
How the build works¶
PRIMA is compiled once. PRIK reads the same sources to learn the solvers'
interfaces, but it generates bindings only for the five names in
export_symbols.txt and links them to the library CMake already built. Use
this pattern for any Fortran library built as a static archive when Python
needs only a few of its entry points:
55 PRIMA sources ─┬─ cmake ──────────────────────> libprimaf.a ─┐
│ │
└─ prik generate --pyi ──> contract/ ─ prik ──┴─> prik_prima
build_prik.sh runs those three steps:
export EXAMPLE_WORKSPACE="$PWD"
export PRIMA_BUILD_ROOT="$(mktemp -d)"
mkdir -p "$PRIMA_BUILD_ROOT/prik/generated"
cmake \
-S "$EXAMPLE_WORKSPACE/examples/fortran/prima" \
-B "$PRIMA_BUILD_ROOT/native" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_Fortran_COMPILER="$(command -v gfortran)"
cmake --build "$PRIMA_BUILD_ROOT/native" --target primaf --parallel 2
PRIMA_SOURCES=()
while IFS= read -r source; do
PRIMA_SOURCES+=("$EXAMPLE_WORKSPACE/examples/fortran/prima/native/$source")
done < "$EXAMPLE_WORKSPACE/examples/fortran/prima/sources.txt"
python3 -m prik generate --pyi \
"${PRIMA_SOURCES[@]}" \
--export-symbols "$EXAMPLE_WORKSPACE/examples/fortran/prima/export_symbols.txt" \
--out "$PRIMA_BUILD_ROOT/contract" \
--compiler "$(command -v gfortran)" \
-I "$EXAMPLE_WORKSPACE/examples/fortran/prima/native/common" \
-D PRIMA_REAL_PRECISION=64 \
-D PRIMA_INTEGER_KIND=0
cd "$PRIMA_BUILD_ROOT/prik"
python3 -m prik "$PRIMA_BUILD_ROOT/contract/__init__.pyi" \
--out prik_prima \
--out-dir "$PRIMA_BUILD_ROOT/prik/generated" \
--compiler "$(command -v gfortran)" \
--native-link-item "archive:$PRIMA_BUILD_ROOT/native/libprimaf.a" \
--native-linker-language fortran \
-I "$PRIMA_BUILD_ROOT/native/mod" \
--jobs 2
| Step | Command | Result |
|---|---|---|
| 1. Compile PRIMA | cmake |
libprimaf.a and its Fortran module files |
| 2. Generate the contract | prik generate --pyi with --export-symbols |
One .pyi file per solver module, plus the callback interfaces they use |
| 3. Build the extension | prik with --native-link-item archive:… |
prik_prima, linked to libprimaf.a without recompiling PRIMA |
Steps 1 and 2 use the same PRIMA_REAL_PRECISION=64 and
PRIMA_INTEGER_KIND=0, so the contract describes the library that was
actually compiled. Everything is written to the temporary PRIMA_BUILD_ROOT
directory, not to the repository.
The generated contracts live in $PRIMA_BUILD_ROOT/contract/, one .pyi
file per solver module. Open them to see each solver's exact Python signature,
including every optional argument.
Use the generated API¶
Every solver follows the same pattern: the objective receives the current point
and writes its value into result; the solver updates x in place. Pass x
as a Fortran-ordered float64 array and integer options as np.int32.
UOBYQA minimizes a quadratic whose minimum is at (1, -2):
import numpy as np
import prik_prima
def objective(values, result):
result[...] = (values[0] - 1.0) ** 2 + (values[1] + 2.0) ** 2
x = np.asfortranarray(np.array([3.0, 0.0], dtype=np.float64))
prik_prima.uobyqa_mod.uobyqa(objective, x, maxfun=np.int32(100))
print(x) # [ 1. -2.]
newuoa, bobyqa, and lincoa take the same objective. cobyla handles
nonlinear constraints: its callback also fills a constraints array, each
entry meaning constraint <= 0, and its second argument m_nlcon is the
number of constraints. Requiring x[1] >= -1 moves the minimum to (1, -1):
def objective_and_constraints(values, result, constraints):
objective(values, result)
constraints[0] = -1.0 - values[1] # x[1] >= -1, written as -1 - x[1] <= 0
x = np.asfortranarray(np.array([3.0, 0.0], dtype=np.float64))
prik_prima.cobyla_mod.cobyla(objective_and_constraints, np.int32(1), x, maxfun=np.int32(200))
print(x.round(3)) # [ 1. -1.]
Progress callback. Pass callback_fcn to follow each iteration. Setting
terminate[...] = True stops the solver early; here UOBYQA stops after 8
evaluations instead of 22:
def progress(values, f, nf, tr, cstrv, nlconstr, terminate):
print(f"evaluation {nf}: f = {f:.3g}")
if f < 1e-6:
terminate[...] = True
x = np.asfortranarray(np.array([3.0, 0.0], dtype=np.float64))
prik_prima.uobyqa_mod.uobyqa(objective, x, maxfun=np.int32(100), callback_fcn=progress)
Arguments that a solver does not use arrive as None; for example, cstrv and
nlconstr are None in UOBYQA's callback.
Final value and evaluation count. f, nf, and info are optional
Fortran outputs. Pass rank-zero arrays to receive them; omitting them keeps them
absent to PRIMA:
f = np.zeros((), dtype=np.float64)
nf = np.zeros((), dtype=np.int32)
x = np.asfortranarray(np.array([3.0, 0.0], dtype=np.float64))
prik_prima.uobyqa_mod.uobyqa(objective, x, f=f, nf=nf, maxfun=np.int32(100))
print(float(f), int(nf))
Run the tests¶
python3 -m pytest -q examples/fortran/prima/tests
The suite checks each solver against the known minimum, that the extension exposes exactly the five solvers, and the progress callback with optional arguments present or omitted. It is not an exhaustive solver-option or constraint suite.
To compare COBYLA with SciPy's, which also comes from PRIMA:
python3 -m pip install "scipy==1.18.0"
python3 -m pytest -q examples/fortran/prima/tests/test_solvers.py::test_cobyla_agrees_with_scipy_on_a_quadratic
To test your own problem, add a case beside the checked-in tests.
Set up a clean environment¶
Clone PRIK, create a virtual environment, and install the Python tools used by the dedicated CI job:
git clone https://github.com/PyNumLab/prik.git
cd prik
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -e ".[qa]" "numpy==2.5.1"
Install CMake and GNU Fortran separately. On Ubuntu:
sudo apt-get update
sudo apt-get install --yes cmake gcc gfortran
gfortran --version
Run the example's commands from the repository root with the virtual environment active.
Versions used¶
| Component | Version / source |
|---|---|
| PRIK | current repository checkout |
| PRIMA | libprima/prima commit 1d76fb88 |
| Python | 3.12 in the dedicated CI job |
| NumPy | 2.5.1 |
| SciPy (optional comparison) | 1.18.0 in CI |
| Native compilers | GNU Fortran 13 + GCC 13 in CI; compatible local compilers work |
Tested platforms¶
The Real Libraries Portability workflow builds and runs the numerical suite with Python 3.12 on:
| Operating system | Architectures | Native toolchain |
|---|---|---|
| Linux | x86-64, ARM64 | GNU Fortran 13 + GCC 13 |
| macOS | Intel, ARM64 | GNU Fortran 13 + GNU GCC 13 |
Troubleshooting¶
- Confirm that
cmakeandgfortranare available onPATH. - Use
source examples/fortran/prima/build_all.sh; running it withbashstarts a child shell, so the exportedPYTHONPATHis lost. - SciPy is optional. The COBYLA comparison skips if it is not installed.
- Run one failing solver test with
-vv -sto see its output.
Source provenance¶
The files under examples/fortran/prima/native/
match libprima/prima commit 1d76fb88aeffb427cd17ed1e9d0d3b34f414913f.
They retain the upstream BSD 3-Clause license.