Skip to content

Provide Python bindings for Minpack #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 150 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -24,6 +24,14 @@ jobs:
with:
submodules: recursive

- name: Cache GFortran install
if: ${{ contains(matrix.os, 'windows') }}
id: cache
uses: actions/cache@v2
with:
path: ./mingw-w64
key: gcc-${{ matrix.gcc }}-${{ matrix.os }}

- name: Install GFortran (MacOS)
if: ${{ contains(matrix.os, 'macos') }}
run: |
@@ -40,7 +48,7 @@ jobs:
--slave /usr/bin/gcov gcov /usr/bin/gcov-${{ matrix.gcc }}
- name: Install GFortran (Windows)
if: ${{ contains(matrix.os, 'windows') }}
if: ${{ contains(matrix.os, 'windows') && steps.cache.outputs.cache-hit != 'true' }}
run: |
Invoke-WebRequest -Uri ${{ env.DOWNLOAD }} -OutFile mingw-w64.zip
Expand-Archive mingw-w64.zip
@@ -70,6 +78,7 @@ jobs:
if: ${{ matrix.build == 'meson' }}
run: >-
meson setup _build
--libdir=lib
--prefix=${{ contains(matrix.os, 'windows') && '$pwd\_dist' || '$PWD/_dist' }}
- name: Compile project (meson)
@@ -84,6 +93,146 @@ jobs:
if: ${{ matrix.build == 'meson' }}
run: meson install -C _build --no-rebuild

- name: Create package (Unix)
if: ${{ matrix.build == 'meson' && ! contains(matrix.os, 'windows') }}
run: |
tar cvf ${{ env.OUTPUT }} _dist
xz -T0 ${{ env.OUTPUT }}
echo "MINPACK_OUTPUT=${{ env.OUTPUT }}.xz" >> $GITHUB_ENV
env:
OUTPUT: minpack-${{ matrix.os }}.tar

- name: Create package (Windows)
if: ${{ matrix.build == 'meson' && contains(matrix.os, 'windows') }}
run: |
tar cvf ${{ env.OUTPUT }} _dist
xz -T0 ${{ env.OUTPUT }}
echo "MINPACK_OUTPUT=${{ env.OUTPUT }}.xz" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
env:
OUTPUT: minpack-${{ matrix.os }}.tar

- name: Upload package
if: ${{ matrix.build == 'meson' }}
uses: actions/upload-artifact@v2
with:
name: ${{ env.MINPACK_OUTPUT }}
path: ${{ env.MINPACK_OUTPUT }}


Python:
needs:
- Build
runs-on: ${{ matrix.os }}
defaults:
run:
shell: ${{ contains(matrix.os, 'windows') && 'powershell' || 'bash -l {0}' }}
strategy:
fail-fast: false
matrix:
build: [meson]
os: [ubuntu-latest, macos-latest]
gcc: [10]
python: ['3.7', '3.8', '3.9']

# Additional test for setuptools build
include:
- build: setuptools
os: ubuntu-latest
gcc: 10
python: '3.9'

env:
FC: gfortran
CC: gcc
MINPACK_OUTPUT: minpack-${{ matrix.os }}.tar.xz

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Cache GFortran install
if: ${{ contains(matrix.os, 'windows') }}
id: cache
uses: actions/cache@v2
with:
path: ./mingw-w64
key: gcc-${{ matrix.gcc }}-${{ matrix.os }}

- name: Install dependencies
uses: mamba-org/provision-with-micromamba@main
with:
environment-file: config/ci/python-env.yaml
extra-specs: |
python=${{ matrix.python }}
- name: Install GFortran (MacOS)
if: ${{ contains(matrix.os, 'macos') }}
run: |
brew install gcc@${{ matrix.gcc }}
ln -s /usr/local/bin/gfortran-${{ matrix.gcc }} /usr/local/bin/gfortran
ln -s /usr/local/bin/gcc-${{ matrix.gcc }} /usr/local/bin/gcc
- name: Install GFortran (Linux)
if: ${{ contains(matrix.os, 'ubuntu') }}
run: |
sudo update-alternatives \
--install /usr/bin/gcc gcc /usr/bin/gcc-${{ matrix.gcc }} 100 \
--slave /usr/bin/gfortran gfortran /usr/bin/gfortran-${{ matrix.gcc }} \
--slave /usr/bin/gcov gcov /usr/bin/gcov-${{ matrix.gcc }}
- name: Install GFortran (Windows)
if: ${{ contains(matrix.os, 'windows') && steps.cache.outputs.cache-hit != 'true' }}
run: |
Invoke-WebRequest -Uri ${{ env.DOWNLOAD }} -OutFile mingw-w64.zip
Expand-Archive mingw-w64.zip
echo "$pwd\mingw-w64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
shell: pwsh
env:
DOWNLOAD: "https://github.com/brechtsanders/winlibs_mingw/releases/download/10.3.0-12.0.0-9.0.0-r2/winlibs-x86_64-posix-seh-gcc-10.3.0-mingw-w64-9.0.0-r2.zip"

- name: Download package
uses: actions/download-artifact@v2
with:
name: ${{ env.MINPACK_OUTPUT }}

- name: Unpack package (Unix)
if: ${{ ! contains(matrix.os, 'windows') }}
run: |
tar xvf ${{ env.MINPACK_OUTPUT }}
echo "MINPACK_PREFIX=$PWD/_dist" >> $GITHUB_ENV
- name: Unpack package (Windows)
if: ${{ contains(matrix.os, 'windows') }}
run: |
tar xvf ${{ env.MINPACK_OUTPUT }}
echo "MINPACK_OUTPUT=${{ env.OUTPUT }}.xz" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Install Python extension module (pip)
if: ${{ matrix.build == 'setuptools' }}
run: pip3 install . -vv
working-directory: python
env:
PKG_CONFIG_PATH: ${{ env.PKG_CONFIG_PATH }}:${{ env.MINPACK_PREFIX }}/lib/pkgconfig
LD_RUNPATH_SEARCH_PATH: ${{ env.MINPACK_PREFIX }}/lib

- name: Install Python extension module (meson)
if: ${{ matrix.build == 'meson' }}
run: |
set -ex
meson setup _build --prefix=$CONDA_PREFIX --libdir=lib
meson compile -C _build
meson install -C _build
working-directory: python
env:
PKG_CONFIG_PATH: ${{ env.PKG_CONFIG_PATH }}:${{ env.MINPACK_PREFIX }}/lib/pkgconfig

- name: Test Python API
run: pytest --doctest-modules --pyargs minpack --cov=minpack -vv
env:
LD_LIBRARY_PATH: ${{ env.LD_LIBRARY_PATH }}:${{ env.MINPACK_PREFIX }}/lib
DYLD_LIBRARY_PATH: ${{ env.DYLD_LIBRARY_PATH }}:${{ env.MINPACK_PREFIX }}/lib


Docs:
runs-on: ubuntu-latest
defaults:
14 changes: 14 additions & 0 deletions config/ci/python-env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: python
channels:
- conda-forge
dependencies:
- python
- pip
- pkgconfig
- pytest
- pytest-cov
- coverage
- cffi
- numpy
- meson
- ninja
58 changes: 58 additions & 0 deletions include/minpack.h
Original file line number Diff line number Diff line change
@@ -19,6 +19,16 @@ typedef void (*minpack_func)(
int* /* iflag */,
void* /* udata */);

#ifdef MINPACK_CFFI
extern "Python" void MINPACK_CALL
func(
int /* n */,
const double* /* x */,
double* /* fvec */,
int* /* iflag */,
void* /* udata */);
#endif

/*
* the purpose of hybrd is to find a zero of a system of
* n nonlinear functions in n variables by a modification
@@ -84,6 +94,18 @@ typedef void (*minpack_fcn_hybrj)(
int* /* iflag */,
void* /* udata */);

#ifdef MINPACK_CFFI
extern "Python" void MINPACK_CALL
fcn_hybrj(
int /* n */,
const double* /* x */,
double* /* fvec */,
double* /* fjac */,
int /* ldfjac */,
int* /* iflag */,
void* /* udata */);
#endif

/*
* the purpose of hybrj is to find a zero of a system of
* n nonlinear functions in n variables by a modification
@@ -148,6 +170,19 @@ typedef void (*minpack_fcn_lmder)(
int* /* iflag */,
void* /* udata */);

#ifdef MINPACK_CFFI
extern "Python" void MINPACK_CALL
fcn_lmder(
int /* m */,
int /* n */,
const double* /* x */,
double* /* fvec */,
double* /* fjac */,
int /* ldfjac */,
int* /* iflag */,
void* /* udata */);
#endif

/*
* the purpose of lmder is to minimize the sum of the squares of
* m nonlinear functions in n variables by a modification of
@@ -213,6 +248,17 @@ typedef void (*minpack_func2)(
int* /* iflag */,
void* /* udata */);

#ifdef MINPACK_CFFI
extern "Python" void MINPACK_CALL
func2(
int /* m */,
int /* n */,
const double* /* x */,
double* /* fvec */,
int* /* iflag */,
void* /* udata */);
#endif

/*
* the purpose of lmdif is to minimize the sum of the squares of
* m nonlinear functions in n variables by a modification of
@@ -279,6 +325,18 @@ typedef void (*minpack_fcn_lmstr)(
int* /* iflag */,
void* /* udata */);

#ifdef MINPACK_CFFI
extern "Python" void MINPACK_CALL
fcn_lmstr(
int /* m */,
int /* n */,
const double* /* x */,
double* /* fvec */,
double* /* fjrow */,
int* /* iflag */,
void* /* udata */);
#endif

/*
* the purpose of lmstr is to minimize the sum of the squares of
* m nonlinear functions in n variables by a modification of
5 changes: 5 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ project(
'buildtype=debugoptimized',
],
)
has_cc = add_languages('c', required: get_option('python'), native: false)

minpack_lib = library(
meson.project_name(),
@@ -59,3 +60,7 @@ subdir('examples')

# add the testsuite
subdir('test')

if get_option('python')
subdir('python/minpack')
endif
12 changes: 12 additions & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
option(
'python',
type: 'boolean',
value: false,
description: 'Build Python extension module',
)
option(
'python_version',
type: 'string',
value: 'python3',
description: 'Python version to link against.',
)
172 changes: 172 additions & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Prerequisites
*.d

# Compiled Object files
*.slo
*.lo
*.o
*.obj

# Precompiled Headers
*.gch
*.pch

# Compiled Dynamic libraries
*.so
*.dylib
*.dll

# Fortran module files
*.mod
*.smod

# Compiled Static libraries
*.lai
*.la
*.a
*.lib

# Executables
*.exe
*.out
*.app

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# Directories
/build*/
/_*/
85 changes: 85 additions & 0 deletions python/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Minpack Python bindings
=======================

Python bindings for Minpack.


Building the extension module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To perform a build some version of ``minpack`` has to be available on your system and preferably findable by ``pkg-config``.
Try to find a ``minpack`` installation you build against first with

.. code:: sh
pkg-config --modversion minpack
Adjust the ``PKG_CONFIG_PATH`` environment variable to include the correct directories to find the installation if necessary.


Using pip
^^^^^^^^^

This project support installation with pip as an easy way to build the Python API.

- C compiler to build the C-API and compile the extension module (the compiler name should be exported in the ``CC`` environment variable)
- Python 3.6 or newer
- The following Python packages are required additionally

- `cffi <https://cffi.readthedocs.io/>`_
- `numpy <https://numpy.org/>`_
- `pkgconfig <https://pypi.org/project/pkgconfig/>`_ (setup only)

Make sure to have your C compiler set to the ``CC`` environment variable

.. code:: sh
export CC=gcc
Install the project with pip

.. code:: sh
pip install .
If you already have a ``minpack`` installation, *e.g.* from conda-forge, you can build the Python extension module directly without cloning this repository

.. code:: sh
pip install "https://github.com/fortran-lang/minpack/archive/refs/heads/main.zip#egg=minpack&subdirectory=python"
Using meson
^^^^^^^^^^^

This directory contains a separate meson build file to allow the out-of-tree build of the CFFI extension module.
The out-of-tree build requires

- C compiler to build the C-API and compile the extension module
- `meson <https://mesonbuild.com>`_ version 0.53 or newer
- a build-system backend, *i.e.* `ninja <https://ninja-build.org>`_ version 1.7 or newer
- Python 3.6 or newer with the `CFFI <https://cffi.readthedocs.io/>`_ package installed

Setup a build with

.. code:: sh
meson setup _build -Dpython_version=$(which python3)
The Python version can be used to select a different Python version, it defaults to ``'python3'``.
Python 2 is not supported with this project, the Python version key is meant to select between several local Python 3 versions.

Compile the project with

.. code:: sh
meson compile -C _build
The extension module is now available in ``_build/minpack/_libminpack.*.so``.
You can install as usual with

.. code:: sh
meson configure _build --prefix=/path/to/install
meson install -C _build
83 changes: 83 additions & 0 deletions python/ffi-builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
FFI builder module for minpack for usage from meson and from setup.py.
Since meson has the full knowledge about the build, it will handle
the generation of the C definitions in the meson.build file rather
than in the FFI builder. This allows to correctly keep track of
dependencies and updates in the build process.
For setup.py we have to do the preprocessing ourselves here, this
requires us to use the C compiler to preprocess the header file
of minpack because the CFFI C parser cannot handle certain C
preprocessor constructs. Also, we cannot rely on an external build
system fixing dependencies for us and therefore have to find those
ourselves using pkg-config.
"""

import os
import cffi

library = "minpack"
include_header = '#include "minpack.h"'
prefix_var = "MINPACK_PREFIX"
if prefix_var not in os.environ:
prefix_var = "CONDA_PREFIX"

if __name__ == "__main__":
import sys

kwargs = dict(libraries=[library])

header_file = sys.argv[1]
module_name = sys.argv[2]

with open(header_file) as f:
cdefs = f.read()
else:
import subprocess

try:
import pkgconfig

if not pkgconfig.exists(library):
raise ModuleNotFoundError("Unable to find pkg-config package 'minpack'")
if pkgconfig.installed(library, "< 2.0.0"):
raise Exception(
"Installed 'minpack' version is too old, 2.0.0 or newer is required"
)

kwargs = pkgconfig.parse(library)
cflags = pkgconfig.cflags(library).split()

except ModuleNotFoundError:
kwargs = dict(libraries=[library])
cflags = []
if prefix_var in os.environ:
prefix = os.environ[prefix_var]
kwargs.update(
include_dirs=[os.path.join(prefix, "include")],
library_dirs=[os.path.join(prefix, "lib")],
runtime_library_dirs=[os.path.join(prefix, "lib")],
)
cflags.append("-I" + os.path.join(prefix, "include"))

cc = os.environ["CC"] if "CC" in os.environ else "cc"

module_name = "minpack._libminpack"

p = subprocess.Popen(
[cc, *cflags, "-DMINPACK_CFFI=1", "-E", "-"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = p.communicate(include_header.encode())

cdefs = out.decode()

ffibuilder = cffi.FFI()
ffibuilder.set_source(module_name, include_header, **kwargs)
ffibuilder.cdef(cdefs)

if __name__ == "__main__":
ffibuilder.distutils_extension(".")
14 changes: 14 additions & 0 deletions python/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
project(
'minpack',
'c',
meson_version: '>=0.53',
default_options: [
'buildtype=debugoptimized',
],
)
install = true

minpack_dep = dependency('minpack', version: '>=2.0.0')
minpack_header = files('../include/minpack.h')

subdir('minpack')
6 changes: 6 additions & 0 deletions python/meson_options.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
option(
'python_version',
type: 'string',
value: 'python3',
description: 'Python version to link against.',
)
18 changes: 18 additions & 0 deletions python/minpack/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""
Python bindings for the Minpack library.
"""

from .library import (
hybrd,
hybrj,
lmdif,
lmder,
lmstr,
hybrd1,
hybrj1,
lmdif1,
lmder1,
lmstr1,
chkder,
)
101 changes: 101 additions & 0 deletions python/minpack/exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
"""
Possible exceptions for the `minpack` package mapping the info codes produced
by the library to exceptions.
"""

from typing import Optional, Type


class MinpackError(Exception):
"""
Exception raised when Minpack returns an error.
"""

pass


class MinpackInputError(MinpackError):
"""
Exception raised when Minpack input is invalid.
"""

pass


class MinpackMaxIterations(MinpackError):
"""
The maximum number of calls to the objective function is reached.
"""

pass


class MinpackFunctionTolerance(MinpackError):
"""
`ftol` is too small. No further reduction in the sum of squares is possible.
"""

pass


class MinpackSolutionTolerance(MinpackError):
"""
`xtol` is too small. No further improvement in the approximate
solution x is possible.
"""

pass


class MinpackJacobianTolerance(MinpackError):
"""
`gtol` is too small. `fvec` is orthogonal to the columns of
the Jacobian to machine precision.
"""

pass


class MinpackSlowProgress(MinpackError):
"""
Iteration is not making good progress, as measured by the improvement
from the last ten iterations.
"""

pass


class MinpackSlowProgressJacobian(MinpackError):
"""
Iteration is not making good progress, as measured by the improvement
from the last five jacobian evaluations.
"""


def info_hy(info: int) -> Optional[Type[MinpackError]]:
"""
Get possible errors for `hybrd` and `hybrj` drivers.
"""

return {
0: MinpackInputError,
2: MinpackMaxIterations,
3: MinpackFunctionTolerance,
4: MinpackSlowProgressJacobian,
5: MinpackSlowProgress,
}.get(info)


def info_lm(info: int) -> Optional[Type[MinpackError]]:
"""
Get possible errors for `lmdif`, `lmder`, and `lmstr` drivers.
"""

return {
0: MinpackInputError,
5: MinpackMaxIterations,
6: MinpackFunctionTolerance,
7: MinpackSolutionTolerance,
8: MinpackJacobianTolerance,
}.get(info)
979 changes: 979 additions & 0 deletions python/minpack/library.py

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions python/minpack/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
cc = meson.get_compiler('c')
ext_module = '_libminpack'

pymod = import('python')
python = pymod.find_installation(
get_option('python_version'),
modules: [
'cffi',
],
)
python_dep = python.dependency(required: true)

# Python's CFFI is horrible in working with preprocessor statements,
# therefore, we have to preprocess the header before passing it to the ffibuilder
minpack_pp = configure_file(
command: [cc, '-DMINPACK_CFFI=1', '-E', '@INPUT@'],
input: minpack_header[0],
output: '@0@.h'.format(ext_module),
capture: true,
)

# This is the actual out-of-line API processing of the ffibuilder
minpack_cffi_srcs = configure_file(
command: [python, files('..'/'ffi-builder.py'), '@INPUT@', '@BASENAME@'],
input: minpack_pp,
output: '@BASENAME@.c',
)

# Actual generation of the Python extension
minpack_pyext = python.extension_module(
ext_module,
minpack_cffi_srcs,
dependencies: [minpack_dep, python_dep],
install: true,
subdir: 'minpack',
)

python.install_sources(
'__init__.py',
'exception.py',
'library.py',
'typing.py',
'test_library.py',
subdir: 'minpack',
)
272 changes: 272 additions & 0 deletions python/minpack/test_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
# -*- coding: utf-8 -*-

import pytest
import minpack.library
import numpy as np
from math import sqrt


@pytest.mark.parametrize("driver", [minpack.library.hybrd1, minpack.library.hybrd])
def test_hybrd(driver):
def fcn(x, fvec) -> None:
for k in range(x.size):
tmp = (3.0 - 2.0 * x[k]) * x[k]
tmp1 = x[k - 1] if k > 0 else 0.0
tmp2 = x[k + 1] if k < len(x) - 1 else 0.0
fvec[k] = tmp - tmp1 - 2.0 * tmp2 + 1.0

x = np.array(9 * [-1.0])
fvec = np.zeros(9, dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

assert driver(fcn, x, fvec, tol) == 1

assert pytest.approx(x, abs=10 * tol) == [
-0.5706545,
-0.6816283,
-0.7017325,
-0.7042129,
-0.7013690,
-0.6918656,
-0.6657920,
-0.5960342,
-0.4164121,
]


@pytest.mark.parametrize("driver", [minpack.library.hybrd1, minpack.library.hybrd])
def test_hybrd_exception(driver):
class DummyException(Exception):
...

def fcn(x, fvec) -> None:
raise DummyException()

x = np.array(9 * [-1.0])
fvec = np.zeros(9, dtype=np.float64)

with pytest.raises(DummyException):
driver(fcn, x, fvec)


@pytest.mark.parametrize("driver", [minpack.library.hybrj1, minpack.library.hybrj])
def test_hybrj(driver):
def fcn(x, fvec, fjac, jacobian: bool) -> None:

if jacobian:
for k in range(x.size):
for j in range(x.size):
fjac[k, j] = 0.0
fjac[k, k] = 3.0 - 4.0 * x[k]
if k > 0:
fjac[k, k - 1] = -1.0
if k < x.size - 1:
fjac[k, k + 1] = -2.0
else:
for k in range(x.size):
tmp = (3.0 - 2.0 * x[k]) * x[k]
tmp1 = x[k - 1] if k > 0 else 0.0
tmp2 = x[k + 1] if k < len(x) - 1 else 0.0
fvec[k] = tmp - tmp1 - 2.0 * tmp2 + 1.0

x = np.array(9 * [-1.0])
fvec = np.zeros(9, dtype=np.float64)
fjac = np.zeros((9, 9), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

assert driver(fcn, x, fvec, fjac, tol) == 1

assert pytest.approx(x, abs=10 * tol) == [
-0.5706545,
-0.6816283,
-0.7017325,
-0.7042129,
-0.7013690,
-0.6918656,
-0.6657920,
-0.5960342,
-0.4164121,
]


@pytest.mark.parametrize("driver", [minpack.library.hybrj1, minpack.library.hybrj])
def test_hybrj_exception(driver):
class DummyException(Exception):
...

def fcn(x, fvec, fjac, jacobian) -> None:
raise DummyException()

x = np.array(9 * [-1.0])
fvec = np.zeros(9, dtype=np.float64)
fjac = np.zeros((9, 9), dtype=np.float64)

with pytest.raises(DummyException):
driver(fcn, x, fvec, fjac)


@pytest.mark.parametrize("driver", [minpack.library.lmder1, minpack.library.lmder])
def test_lmder(driver):
y = np.array(
[
1.4e-1,
1.8e-1,
2.2e-1,
2.5e-1,
2.9e-1,
3.2e-1,
3.5e-1,
3.9e-1,
3.7e-1,
5.8e-1,
7.3e-1,
9.6e-1,
1.34e0,
2.1e0,
4.39e0,
]
)

def fcn(x, fvec, fjac, jacobian: bool) -> None:
if jacobian:
for i in range(fvec.size):
tmp1, tmp2 = i + 1, 16 - i - 1
tmp3 = tmp2 if i >= 8 else tmp1
tmp4 = (x[1] * tmp2 + x[2] * tmp3) ** 2
fjac[0, i] = -1.0
fjac[1, i] = tmp1 * tmp2 / tmp4
fjac[2, i] = tmp1 * tmp3 / tmp4
else:
for i in range(fvec.size):
tmp1, tmp2 = i + 1, 16 - i - 1
tmp3 = tmp2 if i >= 8 else tmp1
fvec[i] = y[i] - (x[0] + tmp1 / (x[1] * tmp2 + x[2] * tmp3))

x = np.array([1.0, 1.0, 1.0])
fvec = np.zeros(15, dtype=np.float64)
fjac = np.zeros((3, 15), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

xp = np.zeros(3, dtype=np.float64)
fvecp = np.zeros(15, dtype=np.float64)
err = np.zeros(15, dtype=np.float64)
minpack.library.chkder(x, fvec, fjac, xp, fvecp, False, err)
fcn(x, fvec, fjac, False)
fcn(x, fvec, fjac, True)
fcn(xp, fvecp, fjac, False)
minpack.library.chkder(x, fvec, fjac, xp, fvecp, True, err)

assert pytest.approx(err) == 15 * [1.0]

assert driver(fcn, x, fvec, fjac, tol) == 1

assert pytest.approx(x, abs=100 * tol) == [0.8241058e-1, 0.1133037e1, 0.2343695e1]


@pytest.mark.parametrize("driver", [minpack.library.lmder1, minpack.library.lmder])
def test_lmder_exception(driver):
class DummyException(Exception):
...

def fcn(x, fvec, fjac, jacobian: bool) -> None:
raise DummyException()

x = np.array([1.0, 1.0, 1.0])
fvec = np.zeros(15, dtype=np.float64)
fjac = np.zeros((3, 15), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

with pytest.raises(DummyException):
driver(fcn, x, fvec, fjac, tol)


@pytest.mark.parametrize("driver", [minpack.library.lmdif1, minpack.library.lmdif])
def test_lmdif(driver):
y = np.array(
[
1.4e-1,
1.8e-1,
2.2e-1,
2.5e-1,
2.9e-1,
3.2e-1,
3.5e-1,
3.9e-1,
3.7e-1,
5.8e-1,
7.3e-1,
9.6e-1,
1.34e0,
2.1e0,
4.39e0,
]
)

def fcn(x, fvec) -> None:
for i in range(fvec.size):
tmp1, tmp2 = i + 1, 16 - i - 1
tmp3 = tmp2 if i >= 8 else tmp1
fvec[i] = y[i] - (x[0] + tmp1 / (x[1] * tmp2 + x[2] * tmp3))

x = np.array([1.0, 1.0, 1.0])
fvec = np.zeros(15, dtype=np.float64)
fjac = np.zeros((3, 15), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

assert driver(fcn, x, fvec, tol) == 1

assert pytest.approx(x, abs=100 * tol) == [0.8241058e-1, 0.1133037e1, 0.2343695e1]


@pytest.mark.parametrize("driver", [minpack.library.lmdif1, minpack.library.lmdif])
def test_lmdif_exception(driver):
class DummyException(Exception):
...

def fcn(x, fvec) -> None:
raise DummyException()

x = np.array([1.0, 1.0, 1.0])
fvec = np.zeros(15, dtype=np.float64)
fjac = np.zeros((3, 15), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

with pytest.raises(DummyException):
driver(fcn, x, fvec, tol)


@pytest.mark.parametrize("driver", [minpack.library.lmstr1, minpack.library.lmstr])
def test_lmstr(driver):
def fcn(x, fvec, fjrow, row) -> None:
if row is None:
fvec[0] = 10.0 * (x[1] - x[0] ** 2)
fvec[1] = 1.0 - x[0]
else:
fjrow[0] = -20.0 * x[0] if row == 0 else -1.0
fjrow[1] = 10.0 if row == 0 else 0.0

x = np.array([-1.2, 1.0])
fvec = np.zeros(2, dtype=np.float64)
fjac = np.zeros((2, 2), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

assert driver(fcn, x, fvec, fjac, tol) == 4

assert pytest.approx(x, abs=100 * tol) == 2 * [1.0]


@pytest.mark.parametrize("driver", [minpack.library.lmstr1, minpack.library.lmstr])
def test_lmstr_exception(driver):
class DummyException(Exception):
...

def fcn(x, fvec, fjac, row) -> None:
raise DummyException()

x = np.array([-1.2, 1.0])
fvec = np.zeros(2, dtype=np.float64)
fjac = np.zeros((2, 2), dtype=np.float64)
tol = sqrt(np.finfo(np.float64).eps)

with pytest.raises(DummyException):
driver(fcn, x, fvec, fjac, tol)
13 changes: 13 additions & 0 deletions python/minpack/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
"""
Callback signatures for Minpack drivers.
"""

import numpy as np
from typing import Optional, Callable

CallableHybrd = Callable[[np.ndarray, np.ndarray], None]
CallableHybrj = Callable[[np.ndarray, np.ndarray, np.ndarray, bool], None]
CallableLmder = Callable[[np.ndarray, np.ndarray, np.ndarray, bool], None]
CallableLmdif = Callable[[np.ndarray, np.ndarray], None]
CallableLmstr = Callable[[np.ndarray, np.ndarray, np.ndarray, Optional[int]], None]
38 changes: 38 additions & 0 deletions python/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[metadata]
name = minpack
version = 2.0.0
desciption = Python bindings for MINPACK
long_desciption = file: README.rst
long_description_content_type = text/x-rst
url = https://github.com/fortran-lang/minpack
license = MIT
license_files =
../LICENSE.txt
classifiers =
Development Status :: 5 - Production
Intended Audience :: Science/Research
Programming Language :: Fortran
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10

[options]
packages = find:
install_requires =
cffi
numpy
tests_require =
pytest
pytest-cov
python_requires = >=3.6

[coverage:run]
omit =
*/test_*.py

[aliases]
test = pytest
7 changes: 7 additions & 0 deletions python/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from setuptools import setup

setup(
cffi_modules=["ffi-builder.py:ffibuilder"],
package_data={"minpack": ["_libminpack*.so"]},
)

2 changes: 1 addition & 1 deletion src/minpack.f90
Original file line number Diff line number Diff line change
@@ -1576,7 +1576,7 @@ subroutine lmder(fcn, m, n, x, Fvec, Fjac, Ldfjac, Ftol, Xtol, Gtol, Maxfev, &
real(wp), intent(inout) :: Wa1(n) !! work array of length n.
real(wp), intent(inout) :: Wa2(n) !! work array of length n.
real(wp), intent(inout) :: Wa3(n) !! work array of length n.
real(wp), intent(inout) :: Wa4(m) !! work array of length n.
real(wp), intent(inout) :: Wa4(m) !! work array of length m.

integer :: i, iflag, iter, j, l
real(wp) :: actred, delta, dirder, fnorm, fnorm1, gnorm, par, &
20 changes: 10 additions & 10 deletions test/api/tester.c
Original file line number Diff line number Diff line change
@@ -210,18 +210,18 @@ test_lmder1 (void)

minpack_chkder(m, n, x, fvec, fjac, m, xp, fvecp, 1, err);
info = 1;
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, y);
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, (void*)y);
info = 2;
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, y);
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, (void*)y);
info = 1;
trial_lmder_fcn(m, n, xp, fvecp, fjac, m, &info, y);
trial_lmder_fcn(m, n, xp, fvecp, fjac, m, &info, (void*)y);
minpack_chkder(m, n, x, fvec, fjac, m, xp, fvecp, 2, err);

for (int i = 0; i < 15; i++) {
if (!check(err[i], 1.0, tol, "Unexpected derivatives")) return 1;
}

minpack_lmder1(trial_lmder_fcn, m, n, x, fvec, fjac, m, tol, &info, ipvt, wa, 30, y);
minpack_lmder1(trial_lmder_fcn, m, n, x, fvec, fjac, m, tol, &info, ipvt, wa, 30, (void*)y);
if (!check(info, 1, "Unexpected info value")) return 1;
if (!check(x[0], 0.8241058e-1, 100*tol, "Unexpected x[0]")) return 1;
if (!check(x[1], 0.1133037e+1, 100*tol, "Unexpected x[1]")) return 1;
@@ -247,19 +247,19 @@ test_lmder (void)

minpack_chkder(m, n, x, fvec, fjac, m, xp, fvecp, 1, err);
info = 1;
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, y);
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, (void*)y);
info = 2;
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, y);
trial_lmder_fcn(m, n, x, fvec, fjac, m, &info, (void*)y);
info = 1;
trial_lmder_fcn(m, n, xp, fvecp, fjac, m, &info, y);
trial_lmder_fcn(m, n, xp, fvecp, fjac, m, &info, (void*)y);
minpack_chkder(m, n, x, fvec, fjac, m, xp, fvecp, 2, err);

for (int i = 0; i < 15; i++) {
if (!check(err[i], 1.0, tol, "Unexpected derivatives")) return 1;
}

minpack_lmder(trial_lmder_fcn, m, n, x, fvec, fjac, m, tol, tol, 0.0, 2000, diag, 1,
100.0, 0, &info, &nfev, &njev, ipvt, qtf, wa1, wa2, wa3, wa4, y);
100.0, 0, &info, &nfev, &njev, ipvt, qtf, wa1, wa2, wa3, wa4, (void*)y);
if (!check(info, 1, "Unexpected info value")) return 1;
if (!check(x[0], 0.8241058e-1, 100*tol, "Unexpected x[0]")) return 1;
if (!check(x[1], 0.1133037e+1, 100*tol, "Unexpected x[1]")) return 1;
@@ -298,7 +298,7 @@ test_lmdif1 (void)
int lwa = m*n + 5*n + m;
double wa[lwa];

minpack_lmdif1(trial_lmdif_fcn, 15, 3, x, fvec, tol, &info, ipvt, wa, lwa, y);
minpack_lmdif1(trial_lmdif_fcn, 15, 3, x, fvec, tol, &info, ipvt, wa, lwa, (void*)y);
if (!check(info, 1, "Unexpected info value")) return 1;
if (!check(x[0], 0.8241058e-1, 100*tol, "Unexpected x[0]")) return 1;
if (!check(x[1], 0.1133037e+1, 100*tol, "Unexpected x[1]")) return 1;
@@ -321,7 +321,7 @@ test_lmdif (void)
double fjac[m*n], diag[n], qtf[n], wa1[n], wa2[n], wa3[n], wa4[m];

minpack_lmdif(trial_lmdif_fcn, 15, 3, x, fvec, tol, tol, 0.0, 2000, 0.0, diag, 1,
100.0, 0, &info, &nfev, fjac, 15, ipvt, qtf, wa1, wa2, wa3, wa4, y);
100.0, 0, &info, &nfev, fjac, 15, ipvt, qtf, wa1, wa2, wa3, wa4, (void*)y);
if (!check(info, 1, "Unexpected info value")) return 1;
if (!check(x[0], 0.8241058e-1, 100*tol, "Unexpected x[0]")) return 1;
if (!check(x[1], 0.1133037e+1, 100*tol, "Unexpected x[1]")) return 1;
2 changes: 1 addition & 1 deletion test/meson.build
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ foreach t : tests
)
endforeach

if add_languages('c', required: false, native: false)
if has_cc
test(
'c-api',
executable(