up follow livre

This commit is contained in:
Tykayn 2025-08-30 18:14:14 +02:00 committed by tykayn
parent b4b4398bb0
commit 3a7a3849ae
12242 changed files with 2564461 additions and 6914 deletions

View file

@ -0,0 +1,43 @@
#!/usr/bin/env python3
#cython: language_level=3
#cython: boundscheck=False
#cython: wraparound=False
"""
Taken from docstring for scipy.optimize.cython_optimize module.
"""
from scipy.optimize.cython_optimize cimport brentq
# import math from Cython
from libc cimport math
myargs = {'C0': 1.0, 'C1': 0.7} # a dictionary of extra arguments
XLO, XHI = 0.5, 1.0 # lower and upper search boundaries
XTOL, RTOL, MITR = 1e-3, 1e-3, 10 # other solver parameters
# user-defined struct for extra parameters
ctypedef struct test_params:
double C0
double C1
# user-defined callback
cdef double f(double x, void *args) noexcept:
cdef test_params *myargs = <test_params *> args
return myargs.C0 - math.exp(-(x - myargs.C1))
# Cython wrapper function
cdef double brentq_wrapper_example(dict args, double xa, double xb,
double xtol, double rtol, int mitr):
# Cython automatically casts dictionary to struct
cdef test_params myargs = args
return brentq(
f, xa, xb, <test_params *> &myargs, xtol, rtol, mitr, NULL)
# Python function
def brentq_example(args=myargs, xa=XLO, xb=XHI, xtol=XTOL, rtol=RTOL,
mitr=MITR):
'''Calls Cython wrapper from Python.'''
return brentq_wrapper_example(args, xa, xb, xtol, rtol, mitr)

View file

@ -0,0 +1,32 @@
project('random-build-examples', 'c', 'cpp', 'cython')
fs = import('fs')
py3 = import('python').find_installation(pure: false)
cy = meson.get_compiler('cython')
if not cy.version().version_compare('>=3.0.8')
error('tests requires Cython >= 3.0.8')
endif
cython_args = []
if cy.version().version_compare('>=3.1.0')
cython_args += ['-Xfreethreading_compatible=True']
endif
py3.extension_module(
'extending',
'extending.pyx',
cython_args: cython_args,
install: false,
)
extending_cpp = fs.copyfile('extending.pyx', 'extending_cpp.pyx')
py3.extension_module(
'extending_cpp',
extending_cpp,
cython_args: cython_args,
install: false,
override_options : ['cython_language=cpp']
)

View file

@ -0,0 +1,535 @@
"""
Unit tests for the basin hopping global minimization algorithm.
"""
import copy
from numpy.testing import (assert_almost_equal, assert_equal, assert_,
assert_allclose)
import pytest
from pytest import raises as assert_raises
import numpy as np
from numpy import cos, sin
from scipy.optimize import basinhopping, OptimizeResult
from scipy.optimize._basinhopping import (
Storage, RandomDisplacement, Metropolis, AdaptiveStepsize)
def func1d(x):
f = cos(14.5 * x - 0.3) + (x + 0.2) * x
df = np.array(-14.5 * sin(14.5 * x - 0.3) + 2. * x + 0.2)
return f, df
def func2d_nograd(x):
f = cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0]
return f
def func2d(x):
f = cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0]
df = np.zeros(2)
df[0] = -14.5 * sin(14.5 * x[0] - 0.3) + 2. * x[0] + 0.2
df[1] = 2. * x[1] + 0.2
return f, df
def func2d_easyderiv(x):
f = 2.0*x[0]**2 + 2.0*x[0]*x[1] + 2.0*x[1]**2 - 6.0*x[0]
df = np.zeros(2)
df[0] = 4.0*x[0] + 2.0*x[1] - 6.0
df[1] = 2.0*x[0] + 4.0*x[1]
return f, df
class MyTakeStep1(RandomDisplacement):
"""use a copy of displace, but have it set a special parameter to
make sure it's actually being used."""
def __init__(self):
self.been_called = False
super().__init__()
def __call__(self, x):
self.been_called = True
return super().__call__(x)
def myTakeStep2(x):
"""redo RandomDisplacement in function form without the attribute stepsize
to make sure everything still works ok
"""
s = 0.5
x += np.random.uniform(-s, s, np.shape(x))
return x
class MyAcceptTest:
"""pass a custom accept test
This does nothing but make sure it's being used and ensure all the
possible return values are accepted
"""
def __init__(self):
self.been_called = False
self.ncalls = 0
self.testres = [False, 'force accept', True, np.bool_(True),
np.bool_(False), [], {}, 0, 1]
def __call__(self, **kwargs):
self.been_called = True
self.ncalls += 1
if self.ncalls - 1 < len(self.testres):
return self.testres[self.ncalls - 1]
else:
return True
class MyCallBack:
"""pass a custom callback function
This makes sure it's being used. It also returns True after 10
steps to ensure that it's stopping early.
"""
def __init__(self):
self.been_called = False
self.ncalls = 0
def __call__(self, x, f, accepted):
self.been_called = True
self.ncalls += 1
if self.ncalls == 10:
return True
class TestBasinHopping:
def setup_method(self):
""" Tests setup.
Run tests based on the 1-D and 2-D functions described above.
"""
self.x0 = (1.0, [1.0, 1.0])
self.sol = (-0.195, np.array([-0.195, -0.1]))
self.tol = 3 # number of decimal places
self.niter = 100
self.disp = False
self.kwargs = {"method": "L-BFGS-B", "jac": True}
self.kwargs_nograd = {"method": "L-BFGS-B"}
def test_TypeError(self):
# test the TypeErrors are raised on bad input
i = 1
# if take_step is passed, it must be callable
assert_raises(TypeError, basinhopping, func2d, self.x0[i],
take_step=1)
# if accept_test is passed, it must be callable
assert_raises(TypeError, basinhopping, func2d, self.x0[i],
accept_test=1)
def test_input_validation(self):
msg = 'target_accept_rate has to be in range \\(0, 1\\)'
with assert_raises(ValueError, match=msg):
basinhopping(func1d, self.x0[0], target_accept_rate=0.)
with assert_raises(ValueError, match=msg):
basinhopping(func1d, self.x0[0], target_accept_rate=1.)
msg = 'stepwise_factor has to be in range \\(0, 1\\)'
with assert_raises(ValueError, match=msg):
basinhopping(func1d, self.x0[0], stepwise_factor=0.)
with assert_raises(ValueError, match=msg):
basinhopping(func1d, self.x0[0], stepwise_factor=1.)
def test_1d_grad(self):
# test 1-D minimizations with gradient
i = 0
res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=self.niter, disp=self.disp)
assert_almost_equal(res.x, self.sol[i], self.tol)
def test_2d(self):
# test 2d minimizations with gradient
i = 1
res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=self.niter, disp=self.disp)
assert_almost_equal(res.x, self.sol[i], self.tol)
assert_(res.nfev > 0)
def test_njev(self):
# test njev is returned correctly
i = 1
minimizer_kwargs = self.kwargs.copy()
# L-BFGS-B doesn't use njev, but BFGS does
minimizer_kwargs["method"] = "BFGS"
res = basinhopping(func2d, self.x0[i],
minimizer_kwargs=minimizer_kwargs, niter=self.niter,
disp=self.disp)
assert_(res.nfev > 0)
assert_equal(res.nfev, res.njev)
def test_jac(self):
# test Jacobian returned
minimizer_kwargs = self.kwargs.copy()
# BFGS returns a Jacobian
minimizer_kwargs["method"] = "BFGS"
res = basinhopping(func2d_easyderiv, [0.0, 0.0],
minimizer_kwargs=minimizer_kwargs, niter=self.niter,
disp=self.disp)
assert_(hasattr(res.lowest_optimization_result, "jac"))
# in this case, the Jacobian is just [df/dx, df/dy]
_, jacobian = func2d_easyderiv(res.x)
assert_almost_equal(res.lowest_optimization_result.jac, jacobian,
self.tol)
def test_2d_nograd(self):
# test 2-D minimizations without gradient
i = 1
res = basinhopping(func2d_nograd, self.x0[i],
minimizer_kwargs=self.kwargs_nograd,
niter=self.niter, disp=self.disp)
assert_almost_equal(res.x, self.sol[i], self.tol)
@pytest.mark.fail_slow(10)
def test_all_minimizers(self):
# Test 2-D minimizations with gradient. Nelder-Mead, Powell, COBYLA, and
# COBYQA don't accept jac=True, so aren't included here.
i = 1
methods = ['CG', 'BFGS', 'Newton-CG', 'L-BFGS-B', 'TNC', 'SLSQP']
minimizer_kwargs = copy.copy(self.kwargs)
for method in methods:
minimizer_kwargs["method"] = method
res = basinhopping(func2d, self.x0[i],
minimizer_kwargs=minimizer_kwargs,
niter=self.niter, disp=self.disp)
assert_almost_equal(res.x, self.sol[i], self.tol)
@pytest.mark.fail_slow(40)
def test_all_nograd_minimizers(self):
# Test 2-D minimizations without gradient. Newton-CG requires jac=True,
# so not included here.
i = 1
methods = ['CG', 'BFGS', 'L-BFGS-B', 'TNC', 'SLSQP',
'Nelder-Mead', 'Powell', 'COBYLA', 'COBYQA']
minimizer_kwargs = copy.copy(self.kwargs_nograd)
for method in methods:
# COBYQA takes extensive amount of time on this problem
niter = 10 if method == 'COBYQA' else self.niter
minimizer_kwargs["method"] = method
res = basinhopping(func2d_nograd, self.x0[i],
minimizer_kwargs=minimizer_kwargs,
niter=niter, disp=self.disp, seed=1234)
tol = self.tol
if method == 'COBYLA':
tol = 2
assert_almost_equal(res.x, self.sol[i], decimal=tol)
def test_pass_takestep(self):
# test that passing a custom takestep works
# also test that the stepsize is being adjusted
takestep = MyTakeStep1()
initial_step_size = takestep.stepsize
i = 1
res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=self.niter, disp=self.disp,
take_step=takestep)
assert_almost_equal(res.x, self.sol[i], self.tol)
assert_(takestep.been_called)
# make sure that the build in adaptive step size has been used
assert_(initial_step_size != takestep.stepsize)
def test_pass_simple_takestep(self):
# test that passing a custom takestep without attribute stepsize
takestep = myTakeStep2
i = 1
res = basinhopping(func2d_nograd, self.x0[i],
minimizer_kwargs=self.kwargs_nograd,
niter=self.niter, disp=self.disp,
take_step=takestep)
assert_almost_equal(res.x, self.sol[i], self.tol)
def test_pass_accept_test(self):
# test passing a custom accept test
# makes sure it's being used and ensures all the possible return values
# are accepted.
accept_test = MyAcceptTest()
i = 1
# there's no point in running it more than a few steps.
basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=10, disp=self.disp, accept_test=accept_test)
assert_(accept_test.been_called)
def test_pass_callback(self):
# test passing a custom callback function
# This makes sure it's being used. It also returns True after 10 steps
# to ensure that it's stopping early.
callback = MyCallBack()
i = 1
# there's no point in running it more than a few steps.
res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=30, disp=self.disp, callback=callback)
assert_(callback.been_called)
assert_("callback" in res.message[0])
# One of the calls of MyCallBack is during BasinHoppingRunner
# construction, so there are only 9 remaining before MyCallBack stops
# the minimization.
assert_equal(res.nit, 9)
def test_minimizer_fail(self):
# test if a minimizer fails
i = 1
self.kwargs["options"] = dict(maxiter=0)
self.niter = 10
res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=self.niter, disp=self.disp)
# the number of failed minimizations should be the number of
# iterations + 1
assert_equal(res.nit + 1, res.minimization_failures)
def test_niter_zero(self):
# gh5915, what happens if you call basinhopping with niter=0
i = 0
basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=0, disp=self.disp)
def test_rng_reproducibility(self):
# rng should ensure reproducibility between runs
minimizer_kwargs = {"method": "L-BFGS-B", "jac": True}
f_1 = []
def callback(x, f, accepted):
f_1.append(f)
basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs,
niter=10, callback=callback, rng=10)
f_2 = []
def callback2(x, f, accepted):
f_2.append(f)
basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs,
niter=10, callback=callback2, rng=10)
assert_equal(np.array(f_1), np.array(f_2))
def test_random_gen(self):
# check that np.random.Generator can be used (numpy >= 1.17)
rng = np.random.default_rng(1)
minimizer_kwargs = {"method": "L-BFGS-B", "jac": True}
res1 = basinhopping(func2d, [1.0, 1.0],
minimizer_kwargs=minimizer_kwargs,
niter=10, rng=rng)
rng = np.random.default_rng(1)
res2 = basinhopping(func2d, [1.0, 1.0],
minimizer_kwargs=minimizer_kwargs,
niter=10, rng=rng)
assert_equal(res1.x, res2.x)
def test_monotonic_basin_hopping(self):
# test 1-D minimizations with gradient and T=0
i = 0
res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
niter=self.niter, disp=self.disp, T=0)
assert_almost_equal(res.x, self.sol[i], self.tol)
@pytest.mark.thread_unsafe
class Test_Storage:
def setup_method(self):
self.x0 = np.array(1)
self.f0 = 0
minres = OptimizeResult(success=True)
minres.x = self.x0
minres.fun = self.f0
self.storage = Storage(minres)
def test_higher_f_rejected(self):
new_minres = OptimizeResult(success=True)
new_minres.x = self.x0 + 1
new_minres.fun = self.f0 + 1
ret = self.storage.update(new_minres)
minres = self.storage.get_lowest()
assert_equal(self.x0, minres.x)
assert_equal(self.f0, minres.fun)
assert_(not ret)
@pytest.mark.parametrize('success', [True, False])
def test_lower_f_accepted(self, success):
new_minres = OptimizeResult(success=success)
new_minres.x = self.x0 + 1
new_minres.fun = self.f0 - 1
ret = self.storage.update(new_minres)
minres = self.storage.get_lowest()
assert (self.x0 != minres.x) == success # can't use `is`
assert (self.f0 != minres.fun) == success # left side is NumPy bool
assert ret is success
class Test_RandomDisplacement:
def setup_method(self):
self.stepsize = 1.0
self.N = 300000
def test_random(self):
# the mean should be 0
# the variance should be (2*stepsize)**2 / 12
# note these tests are random, they will fail from time to time
rng = np.random.RandomState(0)
x0 = np.zeros([self.N])
displace = RandomDisplacement(stepsize=self.stepsize, rng=rng)
x = displace(x0)
v = (2. * self.stepsize) ** 2 / 12
assert_almost_equal(np.mean(x), 0., 1)
assert_almost_equal(np.var(x), v, 1)
class Test_Metropolis:
def setup_method(self):
self.T = 2.
self.met = Metropolis(self.T)
self.res_new = OptimizeResult(success=True, fun=0.)
self.res_old = OptimizeResult(success=True, fun=1.)
def test_boolean_return(self):
# the return must be a bool, else an error will be raised in
# basinhopping
ret = self.met(res_new=self.res_new, res_old=self.res_old)
assert isinstance(ret, bool)
def test_lower_f_accepted(self):
assert_(self.met(res_new=self.res_new, res_old=self.res_old))
def test_accept(self):
# test that steps are randomly accepted for f_new > f_old
one_accept = False
one_reject = False
for i in range(1000):
if one_accept and one_reject:
break
res_new = OptimizeResult(success=True, fun=1.)
res_old = OptimizeResult(success=True, fun=0.5)
ret = self.met(res_new=res_new, res_old=res_old)
if ret:
one_accept = True
else:
one_reject = True
assert_(one_accept)
assert_(one_reject)
def test_GH7495(self):
# an overflow in exp was producing a RuntimeWarning
# create own object here in case someone changes self.T
met = Metropolis(2)
res_new = OptimizeResult(success=True, fun=0.)
res_old = OptimizeResult(success=True, fun=2000)
with np.errstate(over='raise'):
met.accept_reject(res_new=res_new, res_old=res_old)
def test_gh7799(self):
# gh-7799 reported a problem in which local search was successful but
# basinhopping returned an invalid solution. Show that this is fixed.
def func(x):
return (x**2-8)**2+(x+2)**2
x0 = -4
limit = 50 # Constrain to func value >= 50
con = {'type': 'ineq', 'fun': lambda x: func(x) - limit},
res = basinhopping(
func,
x0,
30,
seed=np.random.RandomState(1234),
minimizer_kwargs={'constraints': con}
)
assert res.success
assert_allclose(res.fun, limit, rtol=1e-6)
def test_accept_gh7799(self):
# Metropolis should not accept the result of an unsuccessful new local
# search if the old local search was successful
met = Metropolis(0) # monotonic basin hopping
res_new = OptimizeResult(success=True, fun=0.)
res_old = OptimizeResult(success=True, fun=1.)
# if new local search was successful and energy is lower, accept
assert met(res_new=res_new, res_old=res_old)
# if new res is unsuccessful, don't accept - even if energy is lower
res_new.success = False
assert not met(res_new=res_new, res_old=res_old)
# ...unless the old res was unsuccessful, too. In that case, why not?
res_old.success = False
assert met(res_new=res_new, res_old=res_old)
def test_reject_all_gh7799(self):
# Test the behavior when there is no feasible solution
def fun(x):
return x@x
def constraint(x):
return x + 1
kwargs = {'constraints': {'type': 'eq', 'fun': constraint},
'bounds': [(0, 1), (0, 1)], 'method': 'slsqp'}
res = basinhopping(fun, x0=[2, 3], niter=10, minimizer_kwargs=kwargs)
assert not res.success
class Test_AdaptiveStepsize:
def setup_method(self):
self.stepsize = 1.
self.ts = RandomDisplacement(stepsize=self.stepsize)
self.target_accept_rate = 0.5
self.takestep = AdaptiveStepsize(takestep=self.ts, verbose=False,
accept_rate=self.target_accept_rate)
def test_adaptive_increase(self):
# if few steps are rejected, the stepsize should increase
x = 0.
self.takestep(x)
self.takestep.report(False)
for i in range(self.takestep.interval):
self.takestep(x)
self.takestep.report(True)
assert_(self.ts.stepsize > self.stepsize)
def test_adaptive_decrease(self):
# if few steps are rejected, the stepsize should increase
x = 0.
self.takestep(x)
self.takestep.report(True)
for i in range(self.takestep.interval):
self.takestep(x)
self.takestep.report(False)
assert_(self.ts.stepsize < self.stepsize)
def test_all_accepted(self):
# test that everything works OK if all steps were accepted
x = 0.
for i in range(self.takestep.interval + 1):
self.takestep(x)
self.takestep.report(True)
assert_(self.ts.stepsize > self.stepsize)
def test_all_rejected(self):
# test that everything works OK if all steps were rejected
x = 0.
for i in range(self.takestep.interval + 1):
self.takestep(x)
self.takestep.report(False)
assert_(self.ts.stepsize < self.stepsize)

View file

@ -0,0 +1,416 @@
# Dual annealing unit tests implementation.
# Copyright (c) 2018 Sylvain Gubian <sylvain.gubian@pmi.com>,
# Yang Xiang <yang.xiang@pmi.com>
# Author: Sylvain Gubian, PMP S.A.
"""
Unit tests for the dual annealing global optimizer
"""
from scipy.optimize import dual_annealing, Bounds
from scipy.optimize._dual_annealing import EnergyState
from scipy.optimize._dual_annealing import LocalSearchWrapper
from scipy.optimize._dual_annealing import ObjectiveFunWrapper
from scipy.optimize._dual_annealing import StrategyChain
from scipy.optimize._dual_annealing import VisitingDistribution
from scipy.optimize import rosen, rosen_der
import pytest
import numpy as np
from numpy.testing import assert_equal, assert_allclose, assert_array_less
from pytest import raises as assert_raises
from scipy._lib._util import check_random_state
import threading
class TestDualAnnealing:
def setup_method(self):
# A function that returns always infinity for initialization tests
self.weirdfunc = lambda x: np.inf
# 2-D bounds for testing function
self.ld_bounds = [(-5.12, 5.12)] * 2
# 4-D bounds for testing function
self.hd_bounds = self.ld_bounds * 4
# Number of values to be generated for testing visit function
self.nbtestvalues = 5000
self.high_temperature = 5230
self.low_temperature = 0.1
self.qv = 2.62
self.seed = 1234
self.rng = check_random_state(self.seed)
self.nb_fun_call = threading.local()
self.ngev = threading.local()
def callback(self, x, f, context):
# For testing callback mechanism. Should stop for e <= 1 as
# the callback function returns True
if f <= 1.0:
return True
def func(self, x, args=()):
# Using Rastrigin function for performing tests
if args:
shift = args
else:
shift = 0
y = np.sum((x - shift) ** 2 - 10 * np.cos(2 * np.pi * (
x - shift))) + 10 * np.size(x) + shift
if not hasattr(self.nb_fun_call, 'c'):
self.nb_fun_call.c = 0
self.nb_fun_call.c += 1
return y
def rosen_der_wrapper(self, x, args=()):
if not hasattr(self.ngev, 'c'):
self.ngev.c = 0
self.ngev.c += 1
return rosen_der(x, *args)
# FIXME: there are some discontinuities in behaviour as a function of `qv`,
# this needs investigating - see gh-12384
@pytest.mark.parametrize('qv', [1.1, 1.41, 2, 2.62, 2.9])
def test_visiting_stepping(self, qv):
lu = list(zip(*self.ld_bounds))
lower = np.array(lu[0])
upper = np.array(lu[1])
dim = lower.size
vd = VisitingDistribution(lower, upper, qv, self.rng)
values = np.zeros(dim)
x_step_low = vd.visiting(values, 0, self.high_temperature)
# Make sure that only the first component is changed
assert_equal(np.not_equal(x_step_low, 0), True)
values = np.zeros(dim)
x_step_high = vd.visiting(values, dim, self.high_temperature)
# Make sure that component other than at dim has changed
assert_equal(np.not_equal(x_step_high[0], 0), True)
@pytest.mark.parametrize('qv', [2.25, 2.62, 2.9])
def test_visiting_dist_high_temperature(self, qv):
lu = list(zip(*self.ld_bounds))
lower = np.array(lu[0])
upper = np.array(lu[1])
vd = VisitingDistribution(lower, upper, qv, self.rng)
# values = np.zeros(self.nbtestvalues)
# for i in np.arange(self.nbtestvalues):
# values[i] = vd.visit_fn(self.high_temperature)
values = vd.visit_fn(self.high_temperature, self.nbtestvalues)
# Visiting distribution is a distorted version of Cauchy-Lorentz
# distribution, and as no 1st and higher moments (no mean defined,
# no variance defined).
# Check that big tails values are generated
assert_array_less(np.min(values), 1e-10)
assert_array_less(1e+10, np.max(values))
def test_reset(self):
owf = ObjectiveFunWrapper(self.weirdfunc)
lu = list(zip(*self.ld_bounds))
lower = np.array(lu[0])
upper = np.array(lu[1])
es = EnergyState(lower, upper)
assert_raises(ValueError, es.reset, owf, check_random_state(None))
def test_low_dim(self):
ret = dual_annealing(
self.func, self.ld_bounds, rng=self.seed)
assert_allclose(ret.fun, 0., atol=1e-12)
assert ret.success
@pytest.mark.fail_slow(10)
def test_high_dim(self):
ret = dual_annealing(self.func, self.hd_bounds, rng=self.seed)
assert_allclose(ret.fun, 0., atol=1e-12)
assert ret.success
def test_low_dim_no_ls(self):
ret = dual_annealing(self.func, self.ld_bounds,
no_local_search=True, seed=self.seed)
assert_allclose(ret.fun, 0., atol=1e-4)
@pytest.mark.fail_slow(10)
def test_high_dim_no_ls(self):
ret = dual_annealing(self.func, self.hd_bounds,
no_local_search=True, rng=self.seed)
assert_allclose(ret.fun, 0., atol=1.2e-4)
def test_nb_fun_call(self):
self.nb_fun_call.c = 0
ret = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
assert_equal(self.nb_fun_call.c, ret.nfev)
def test_nb_fun_call_no_ls(self):
self.nb_fun_call.c = 0
ret = dual_annealing(self.func, self.ld_bounds,
no_local_search=True, rng=self.seed)
assert_equal(self.nb_fun_call.c, ret.nfev)
def test_max_reinit(self):
assert_raises(ValueError, dual_annealing, self.weirdfunc,
self.ld_bounds)
@pytest.mark.fail_slow(10)
def test_reproduce(self):
res1 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
res2 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
res3 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
# If we have reproducible results, x components found has to
# be exactly the same, which is not the case with no seeding
assert_equal(res1.x, res2.x)
assert_equal(res1.x, res3.x)
def test_rand_gen(self):
# check that np.random.Generator can be used (numpy >= 1.17)
# obtain a np.random.Generator object
rng = np.random.default_rng(1)
res1 = dual_annealing(self.func, self.ld_bounds, rng=rng)
# seed again
rng = np.random.default_rng(1)
res2 = dual_annealing(self.func, self.ld_bounds, rng=rng)
# If we have reproducible results, x components found has to
# be exactly the same, which is not the case with no seeding
assert_equal(res1.x, res2.x)
def test_bounds_integrity(self):
wrong_bounds = [(-5.12, 5.12), (1, 0), (5.12, 5.12)]
assert_raises(ValueError, dual_annealing, self.func,
wrong_bounds)
def test_bound_validity(self):
invalid_bounds = [(-5, 5), (-np.inf, 0), (-5, 5)]
assert_raises(ValueError, dual_annealing, self.func,
invalid_bounds)
invalid_bounds = [(-5, 5), (0, np.inf), (-5, 5)]
assert_raises(ValueError, dual_annealing, self.func,
invalid_bounds)
invalid_bounds = [(-5, 5), (0, np.nan), (-5, 5)]
assert_raises(ValueError, dual_annealing, self.func,
invalid_bounds)
@pytest.mark.thread_unsafe
def test_deprecated_local_search_options_bounds(self):
def func(x):
return np.sum((x - 5) * (x - 1))
bounds = list(zip([-6, -5], [6, 5]))
# Test bounds can be passed (see gh-10831)
with pytest.warns(RuntimeWarning, match=r"Method CG cannot handle "):
dual_annealing(
func,
bounds=bounds,
minimizer_kwargs={"method": "CG", "bounds": bounds})
@pytest.mark.thread_unsafe
def test_minimizer_kwargs_bounds(self):
def func(x):
return np.sum((x - 5) * (x - 1))
bounds = list(zip([-6, -5], [6, 5]))
# Test bounds can be passed (see gh-10831)
dual_annealing(
func,
bounds=bounds,
minimizer_kwargs={"method": "SLSQP", "bounds": bounds})
with pytest.warns(RuntimeWarning, match=r"Method CG cannot handle "):
dual_annealing(
func,
bounds=bounds,
minimizer_kwargs={"method": "CG", "bounds": bounds})
def test_max_fun_ls(self):
ret = dual_annealing(self.func, self.ld_bounds, maxfun=100,
rng=self.seed)
ls_max_iter = min(max(
len(self.ld_bounds) * LocalSearchWrapper.LS_MAXITER_RATIO,
LocalSearchWrapper.LS_MAXITER_MIN),
LocalSearchWrapper.LS_MAXITER_MAX)
assert ret.nfev <= 100 + ls_max_iter
assert not ret.success
def test_max_fun_no_ls(self):
ret = dual_annealing(self.func, self.ld_bounds,
no_local_search=True, maxfun=500, rng=self.seed)
assert ret.nfev <= 500
assert not ret.success
def test_maxiter(self):
ret = dual_annealing(self.func, self.ld_bounds, maxiter=700,
rng=self.seed)
assert ret.nit <= 700
# Testing that args are passed correctly for dual_annealing
def test_fun_args_ls(self):
ret = dual_annealing(self.func, self.ld_bounds,
args=((3.14159,)), rng=self.seed)
assert_allclose(ret.fun, 3.14159, atol=1e-6)
# Testing that args are passed correctly for pure simulated annealing
def test_fun_args_no_ls(self):
ret = dual_annealing(self.func, self.ld_bounds,
args=((3.14159, )), no_local_search=True,
rng=self.seed)
assert_allclose(ret.fun, 3.14159, atol=1e-4)
def test_callback_stop(self):
# Testing that callback make the algorithm stop for
# fun value <= 1.0 (see callback method)
ret = dual_annealing(self.func, self.ld_bounds,
callback=self.callback, rng=self.seed)
assert ret.fun <= 1.0
assert 'stop early' in ret.message[0]
assert not ret.success
@pytest.mark.parametrize('method, atol', [
('Nelder-Mead', 2e-5),
('COBYLA', 1e-5),
('COBYQA', 1e-8),
('Powell', 1e-8),
('CG', 1e-8),
('BFGS', 1e-8),
('TNC', 1e-8),
('SLSQP', 2e-7),
])
def test_multi_ls_minimizer(self, method, atol):
ret = dual_annealing(self.func, self.ld_bounds,
minimizer_kwargs=dict(method=method),
rng=self.seed)
assert_allclose(ret.fun, 0., atol=atol)
def test_wrong_restart_temp(self):
assert_raises(ValueError, dual_annealing, self.func,
self.ld_bounds, restart_temp_ratio=1)
assert_raises(ValueError, dual_annealing, self.func,
self.ld_bounds, restart_temp_ratio=0)
def test_gradient_gnev(self):
minimizer_opts = {
'jac': self.rosen_der_wrapper,
}
ret = dual_annealing(rosen, self.ld_bounds,
minimizer_kwargs=minimizer_opts,
rng=self.seed)
assert ret.njev == self.ngev.c
@pytest.mark.fail_slow(10)
def test_from_docstring(self):
def func(x):
return np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x)
lw = [-5.12] * 10
up = [5.12] * 10
ret = dual_annealing(func, bounds=list(zip(lw, up)), rng=1234)
assert_allclose(ret.x,
[-4.26437714e-09, -3.91699361e-09, -1.86149218e-09,
-3.97165720e-09, -6.29151648e-09, -6.53145322e-09,
-3.93616815e-09, -6.55623025e-09, -6.05775280e-09,
-5.00668935e-09], atol=4e-8)
assert_allclose(ret.fun, 0.000000, atol=5e-13)
@pytest.mark.parametrize('new_e, temp_step, accepted, accept_rate', [
(0, 100, 1000, 1.0097587941791923),
(0, 2, 1000, 1.2599210498948732),
(10, 100, 878, 0.8786035869128718),
(10, 60, 695, 0.6812920690579612),
(2, 100, 990, 0.9897404249173424),
])
def test_accept_reject_probabilistic(
self, new_e, temp_step, accepted, accept_rate):
# Test accepts unconditionally with e < current_energy and
# probabilistically with e > current_energy
rs = check_random_state(123)
count_accepted = 0
iterations = 1000
accept_param = -5
current_energy = 1
for _ in range(iterations):
energy_state = EnergyState(lower=None, upper=None)
# Set energy state with current_energy, any location.
energy_state.update_current(current_energy, [0])
chain = StrategyChain(
accept_param, None, None, None, rs, energy_state)
# Normally this is set in run()
chain.temperature_step = temp_step
# Check if update is accepted.
chain.accept_reject(j=1, e=new_e, x_visit=[2])
if energy_state.current_energy == new_e:
count_accepted += 1
assert count_accepted == accepted
# Check accept rate
pqv = 1 - (1 - accept_param) * (new_e - current_energy) / temp_step
rate = 0 if pqv <= 0 else np.exp(np.log(pqv) / (1 - accept_param))
assert_allclose(rate, accept_rate)
@pytest.mark.fail_slow(10)
def test_bounds_class(self):
# test that result does not depend on the bounds type
def func(x):
f = np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x)
return f
lw = [-5.12] * 5
up = [5.12] * 5
# Unbounded global minimum is all zeros. Most bounds below will force
# a DV away from unbounded minimum and be active at solution.
up[0] = -2.0
up[1] = -1.0
lw[3] = 1.0
lw[4] = 2.0
# run optimizations
bounds = Bounds(lw, up)
ret_bounds_class = dual_annealing(func, bounds=bounds, rng=1234)
bounds_old = list(zip(lw, up))
ret_bounds_list = dual_annealing(func, bounds=bounds_old, rng=1234)
# test that found minima, function evaluations and iterations match
assert_allclose(ret_bounds_class.x, ret_bounds_list.x, atol=1e-8)
assert_allclose(ret_bounds_class.x, np.arange(-2, 3), atol=1e-7)
assert_allclose(ret_bounds_list.fun, ret_bounds_class.fun, atol=1e-9)
assert ret_bounds_list.nfev == ret_bounds_class.nfev
@pytest.mark.fail_slow(10)
def test_callable_jac_hess_with_args_gh11052(self):
# dual_annealing used to fail when `jac` was callable and `args` were
# used; check that this is resolved. Example is from gh-11052.
# extended to hess as part of closing gh20614
rng = np.random.default_rng(94253637693657847462)
def f(x, power):
return np.sum(np.exp(x ** power))
def jac(x, power):
return np.exp(x ** power) * power * x ** (power - 1)
def hess(x, power):
# calculated using WolframAlpha as d^2/dx^2 e^(x^p)
return np.diag(
power * np.exp(x ** power) * x ** (power - 2) *
(power * x ** power + power - 1)
)
def hessp(x, p, power):
return hess(x, power) @ p
res1 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
minimizer_kwargs=dict(method='L-BFGS-B'))
res2 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
minimizer_kwargs=dict(method='L-BFGS-B',
jac=jac))
res3 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
minimizer_kwargs=dict(method='newton-cg',
jac=jac, hess=hess))
res4 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
minimizer_kwargs=dict(method='newton-cg',
jac=jac, hessp=hessp))
assert_allclose(res1.fun, res2.fun, rtol=1e-6)
assert_allclose(res3.fun, res2.fun, rtol=1e-6)
assert_allclose(res4.fun, res2.fun, rtol=1e-6)

View file

@ -0,0 +1,312 @@
"""
Unit test for Linear Programming via Simplex Algorithm.
"""
from copy import deepcopy
from datetime import date
import numpy as np
from numpy.testing import assert_, assert_allclose, assert_equal
from numpy.exceptions import VisibleDeprecationWarning
from pytest import raises as assert_raises
from scipy.optimize._linprog_util import _clean_inputs, _LPProblem
def test_aliasing():
"""
Test for ensuring that no objects referred to by `lp` attributes,
`c`, `A_ub`, `b_ub`, `A_eq`, `b_eq`, `bounds`, have been modified
by `_clean_inputs` as a side effect.
"""
lp = _LPProblem(
c=1,
A_ub=[[1]],
b_ub=[1],
A_eq=[[1]],
b_eq=[1],
bounds=(-np.inf, np.inf)
)
lp_copy = deepcopy(lp)
_clean_inputs(lp)
assert_(lp.c == lp_copy.c, "c modified by _clean_inputs")
assert_(lp.A_ub == lp_copy.A_ub, "A_ub modified by _clean_inputs")
assert_(lp.b_ub == lp_copy.b_ub, "b_ub modified by _clean_inputs")
assert_(lp.A_eq == lp_copy.A_eq, "A_eq modified by _clean_inputs")
assert_(lp.b_eq == lp_copy.b_eq, "b_eq modified by _clean_inputs")
assert_(lp.bounds == lp_copy.bounds, "bounds modified by _clean_inputs")
def test_aliasing2():
"""
Similar purpose as `test_aliasing` above.
"""
lp = _LPProblem(
c=np.array([1, 1]),
A_ub=np.array([[1, 1], [2, 2]]),
b_ub=np.array([[1], [1]]),
A_eq=np.array([[1, 1]]),
b_eq=np.array([1]),
bounds=[(-np.inf, np.inf), (None, 1)]
)
lp_copy = deepcopy(lp)
_clean_inputs(lp)
assert_allclose(lp.c, lp_copy.c, err_msg="c modified by _clean_inputs")
assert_allclose(lp.A_ub, lp_copy.A_ub, err_msg="A_ub modified by _clean_inputs")
assert_allclose(lp.b_ub, lp_copy.b_ub, err_msg="b_ub modified by _clean_inputs")
assert_allclose(lp.A_eq, lp_copy.A_eq, err_msg="A_eq modified by _clean_inputs")
assert_allclose(lp.b_eq, lp_copy.b_eq, err_msg="b_eq modified by _clean_inputs")
assert_(lp.bounds == lp_copy.bounds, "bounds modified by _clean_inputs")
def test_missing_inputs():
c = [1, 2]
A_ub = np.array([[1, 1], [2, 2]])
b_ub = np.array([1, 1])
A_eq = np.array([[1, 1], [2, 2]])
b_eq = np.array([1, 1])
assert_raises(TypeError, _clean_inputs)
assert_raises(TypeError, _clean_inputs, _LPProblem(c=None))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_ub=A_ub))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_ub=A_ub, b_ub=None))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, b_ub=b_ub))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_ub=None, b_ub=b_ub))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_eq=A_eq))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_eq=A_eq, b_eq=None))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, b_eq=b_eq))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_eq=None, b_eq=b_eq))
def test_too_many_dimensions():
cb = [1, 2, 3, 4]
A = np.random.rand(4, 4)
bad2D = [[1, 2], [3, 4]]
bad3D = np.random.rand(4, 4, 4)
assert_raises(ValueError, _clean_inputs, _LPProblem(c=bad2D, A_ub=A, b_ub=cb))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=cb, A_ub=bad3D, b_ub=cb))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=cb, A_ub=A, b_ub=bad2D))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=cb, A_eq=bad3D, b_eq=cb))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=cb, A_eq=A, b_eq=bad2D))
def test_too_few_dimensions():
bad = np.random.rand(4, 4).ravel()
cb = np.random.rand(4)
assert_raises(ValueError, _clean_inputs, _LPProblem(c=cb, A_ub=bad, b_ub=cb))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=cb, A_eq=bad, b_eq=cb))
def test_inconsistent_dimensions():
m = 2
n = 4
c = [1, 2, 3, 4]
Agood = np.random.rand(m, n)
Abad = np.random.rand(m, n + 1)
bgood = np.random.rand(m)
bbad = np.random.rand(m + 1)
boundsbad = [(0, 1)] * (n + 1)
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_ub=Abad, b_ub=bgood))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_ub=Agood, b_ub=bbad))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_eq=Abad, b_eq=bgood))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, A_eq=Agood, b_eq=bbad))
assert_raises(ValueError, _clean_inputs, _LPProblem(c=c, bounds=boundsbad))
with np.testing.suppress_warnings() as sup:
sup.filter(VisibleDeprecationWarning, "Creating an ndarray from ragged")
assert_raises(ValueError, _clean_inputs,
_LPProblem(c=c, bounds=[[1, 2], [2, 3], [3, 4], [4, 5, 6]]))
def test_type_errors():
lp = _LPProblem(
c=[1, 2],
A_ub=np.array([[1, 1], [2, 2]]),
b_ub=np.array([1, 1]),
A_eq=np.array([[1, 1], [2, 2]]),
b_eq=np.array([1, 1]),
bounds=[(0, 1)]
)
bad = "hello"
assert_raises(TypeError, _clean_inputs, lp._replace(c=bad))
assert_raises(TypeError, _clean_inputs, lp._replace(A_ub=bad))
assert_raises(TypeError, _clean_inputs, lp._replace(b_ub=bad))
assert_raises(TypeError, _clean_inputs, lp._replace(A_eq=bad))
assert_raises(TypeError, _clean_inputs, lp._replace(b_eq=bad))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=bad))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds="hi"))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=["hi"]))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=[("hi")]))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=[(1, "")]))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=[(1, 2), (1, "")]))
assert_raises(TypeError, _clean_inputs,
lp._replace(bounds=[(1, date(2020, 2, 29))]))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=[[[1, 2]]]))
def test_non_finite_errors():
lp = _LPProblem(
c=[1, 2],
A_ub=np.array([[1, 1], [2, 2]]),
b_ub=np.array([1, 1]),
A_eq=np.array([[1, 1], [2, 2]]),
b_eq=np.array([1, 1]),
bounds=[(0, 1)]
)
assert_raises(ValueError, _clean_inputs, lp._replace(c=[0, None]))
assert_raises(ValueError, _clean_inputs, lp._replace(c=[np.inf, 0]))
assert_raises(ValueError, _clean_inputs, lp._replace(c=[0, -np.inf]))
assert_raises(ValueError, _clean_inputs, lp._replace(c=[np.nan, 0]))
assert_raises(ValueError, _clean_inputs, lp._replace(A_ub=[[1, 2], [None, 1]]))
assert_raises(ValueError, _clean_inputs, lp._replace(b_ub=[np.inf, 1]))
assert_raises(ValueError, _clean_inputs, lp._replace(A_eq=[[1, 2], [1, -np.inf]]))
assert_raises(ValueError, _clean_inputs, lp._replace(b_eq=[1, np.nan]))
def test__clean_inputs1():
lp = _LPProblem(
c=[1, 2],
A_ub=[[1, 1], [2, 2]],
b_ub=[1, 1],
A_eq=[[1, 1], [2, 2]],
b_eq=[1, 1],
bounds=None
)
lp_cleaned = _clean_inputs(lp)
assert_allclose(lp_cleaned.c, np.array(lp.c))
assert_allclose(lp_cleaned.A_ub, np.array(lp.A_ub))
assert_allclose(lp_cleaned.b_ub, np.array(lp.b_ub))
assert_allclose(lp_cleaned.A_eq, np.array(lp.A_eq))
assert_allclose(lp_cleaned.b_eq, np.array(lp.b_eq))
assert_equal(lp_cleaned.bounds, [(0, np.inf)] * 2)
assert_(lp_cleaned.c.shape == (2,), "")
assert_(lp_cleaned.A_ub.shape == (2, 2), "")
assert_(lp_cleaned.b_ub.shape == (2,), "")
assert_(lp_cleaned.A_eq.shape == (2, 2), "")
assert_(lp_cleaned.b_eq.shape == (2,), "")
def test__clean_inputs2():
lp = _LPProblem(
c=1,
A_ub=[[1]],
b_ub=1,
A_eq=[[1]],
b_eq=1,
bounds=(0, 1)
)
lp_cleaned = _clean_inputs(lp)
assert_allclose(lp_cleaned.c, np.array(lp.c))
assert_allclose(lp_cleaned.A_ub, np.array(lp.A_ub))
assert_allclose(lp_cleaned.b_ub, np.array(lp.b_ub))
assert_allclose(lp_cleaned.A_eq, np.array(lp.A_eq))
assert_allclose(lp_cleaned.b_eq, np.array(lp.b_eq))
assert_equal(lp_cleaned.bounds, [(0, 1)])
assert_(lp_cleaned.c.shape == (1,), "")
assert_(lp_cleaned.A_ub.shape == (1, 1), "")
assert_(lp_cleaned.b_ub.shape == (1,), "")
assert_(lp_cleaned.A_eq.shape == (1, 1), "")
assert_(lp_cleaned.b_eq.shape == (1,), "")
def test__clean_inputs3():
lp = _LPProblem(
c=[[1, 2]],
A_ub=np.random.rand(2, 2),
b_ub=[[1], [2]],
A_eq=np.random.rand(2, 2),
b_eq=[[1], [2]],
bounds=[(0, 1)]
)
lp_cleaned = _clean_inputs(lp)
assert_allclose(lp_cleaned.c, np.array([1, 2]))
assert_allclose(lp_cleaned.b_ub, np.array([1, 2]))
assert_allclose(lp_cleaned.b_eq, np.array([1, 2]))
assert_equal(lp_cleaned.bounds, [(0, 1)] * 2)
assert_(lp_cleaned.c.shape == (2,), "")
assert_(lp_cleaned.b_ub.shape == (2,), "")
assert_(lp_cleaned.b_eq.shape == (2,), "")
def test_bad_bounds():
lp = _LPProblem(c=[1, 2])
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=(1, 2, 2)))
assert_raises(ValueError, _clean_inputs, lp._replace(bounds=[(1, 2, 2)]))
with np.testing.suppress_warnings() as sup:
sup.filter(VisibleDeprecationWarning, "Creating an ndarray from ragged")
assert_raises(ValueError, _clean_inputs,
lp._replace(bounds=[(1, 2), (1, 2, 2)]))
assert_raises(ValueError, _clean_inputs,
lp._replace(bounds=[(1, 2), (1, 2), (1, 2)]))
lp = _LPProblem(c=[1, 2, 3, 4])
assert_raises(ValueError, _clean_inputs,
lp._replace(bounds=[(1, 2, 3, 4), (1, 2, 3, 4)]))
def test_good_bounds():
lp = _LPProblem(c=[1, 2])
lp_cleaned = _clean_inputs(lp) # lp.bounds is None by default
assert_equal(lp_cleaned.bounds, [(0, np.inf)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=[]))
assert_equal(lp_cleaned.bounds, [(0, np.inf)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=[[]]))
assert_equal(lp_cleaned.bounds, [(0, np.inf)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=(1, 2)))
assert_equal(lp_cleaned.bounds, [(1, 2)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(1, 2)]))
assert_equal(lp_cleaned.bounds, [(1, 2)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(1, None)]))
assert_equal(lp_cleaned.bounds, [(1, np.inf)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(None, 1)]))
assert_equal(lp_cleaned.bounds, [(-np.inf, 1)] * 2)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(None, None), (-np.inf, None)]))
assert_equal(lp_cleaned.bounds, [(-np.inf, np.inf)] * 2)
lp = _LPProblem(c=[1, 2, 3, 4])
lp_cleaned = _clean_inputs(lp) # lp.bounds is None by default
assert_equal(lp_cleaned.bounds, [(0, np.inf)] * 4)
lp_cleaned = _clean_inputs(lp._replace(bounds=(1, 2)))
assert_equal(lp_cleaned.bounds, [(1, 2)] * 4)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(1, 2)]))
assert_equal(lp_cleaned.bounds, [(1, 2)] * 4)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(1, None)]))
assert_equal(lp_cleaned.bounds, [(1, np.inf)] * 4)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(None, 1)]))
assert_equal(lp_cleaned.bounds, [(-np.inf, 1)] * 4)
lp_cleaned = _clean_inputs(lp._replace(bounds=[(None, None),
(-np.inf, None),
(None, np.inf),
(-np.inf, np.inf)]))
assert_equal(lp_cleaned.bounds, [(-np.inf, np.inf)] * 4)

View file

@ -0,0 +1,885 @@
import math
from itertools import product
import numpy as np
from numpy.testing import assert_allclose, assert_equal, assert_
import pytest
from pytest import raises as assert_raises
from scipy._lib._util import MapWrapper, _ScalarFunctionWrapper
from scipy.sparse import csr_array, csc_array, lil_array
from scipy.optimize._numdiff import (
_adjust_scheme_to_bounds, approx_derivative, check_derivative,
group_columns, _eps_for_method, _compute_absolute_step)
from scipy.optimize import rosen
def test_group_columns():
structure = [
[1, 1, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0]
]
for transform in [np.asarray, csr_array, csc_array, lil_array]:
A = transform(structure)
order = np.arange(6)
groups_true = np.array([0, 1, 2, 0, 1, 2])
groups = group_columns(A, order)
assert_equal(groups, groups_true)
order = [1, 2, 4, 3, 5, 0]
groups_true = np.array([2, 0, 1, 2, 0, 1])
groups = group_columns(A, order)
assert_equal(groups, groups_true)
# Test repeatability.
groups_1 = group_columns(A)
groups_2 = group_columns(A)
assert_equal(groups_1, groups_2)
def test_correct_fp_eps():
# check that relative step size is correct for FP size
EPS = np.finfo(np.float64).eps
relative_step = {"2-point": EPS**0.5,
"3-point": EPS**(1/3),
"cs": EPS**0.5}
for method in ['2-point', '3-point', 'cs']:
assert_allclose(
_eps_for_method(np.float64, np.float64, method),
relative_step[method])
assert_allclose(
_eps_for_method(np.complex128, np.complex128, method),
relative_step[method]
)
# check another FP size
EPS = np.finfo(np.float32).eps
relative_step = {"2-point": EPS**0.5,
"3-point": EPS**(1/3),
"cs": EPS**0.5}
for method in ['2-point', '3-point', 'cs']:
assert_allclose(
_eps_for_method(np.float64, np.float32, method),
relative_step[method]
)
assert_allclose(
_eps_for_method(np.float32, np.float64, method),
relative_step[method]
)
assert_allclose(
_eps_for_method(np.float32, np.float32, method),
relative_step[method]
)
class TestAdjustSchemeToBounds:
def test_no_bounds(self):
x0 = np.zeros(3)
h = np.full(3, 1e-2)
inf_lower = np.empty_like(x0)
inf_upper = np.empty_like(x0)
inf_lower.fill(-np.inf)
inf_upper.fill(np.inf)
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 1, '1-sided', inf_lower, inf_upper)
assert_allclose(h_adjusted, h)
assert_(np.all(one_sided))
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 2, '1-sided', inf_lower, inf_upper)
assert_allclose(h_adjusted, h)
assert_(np.all(one_sided))
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 1, '2-sided', inf_lower, inf_upper)
assert_allclose(h_adjusted, h)
assert_(np.all(~one_sided))
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 2, '2-sided', inf_lower, inf_upper)
assert_allclose(h_adjusted, h)
assert_(np.all(~one_sided))
def test_with_bound(self):
x0 = np.array([0.0, 0.85, -0.85])
lb = -np.ones(3)
ub = np.ones(3)
h = np.array([1, 1, -1]) * 1e-1
h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 1, '1-sided', lb, ub)
assert_allclose(h_adjusted, h)
h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 2, '1-sided', lb, ub)
assert_allclose(h_adjusted, np.array([1, -1, 1]) * 1e-1)
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 1, '2-sided', lb, ub)
assert_allclose(h_adjusted, np.abs(h))
assert_(np.all(~one_sided))
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 2, '2-sided', lb, ub)
assert_allclose(h_adjusted, np.array([1, -1, 1]) * 1e-1)
assert_equal(one_sided, np.array([False, True, True]))
def test_tight_bounds(self):
lb = np.array([-0.03, -0.03])
ub = np.array([0.05, 0.05])
x0 = np.array([0.0, 0.03])
h = np.array([-0.1, -0.1])
h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 1, '1-sided', lb, ub)
assert_allclose(h_adjusted, np.array([0.05, -0.06]))
h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 2, '1-sided', lb, ub)
assert_allclose(h_adjusted, np.array([0.025, -0.03]))
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 1, '2-sided', lb, ub)
assert_allclose(h_adjusted, np.array([0.03, -0.03]))
assert_equal(one_sided, np.array([False, True]))
h_adjusted, one_sided = _adjust_scheme_to_bounds(
x0, h, 2, '2-sided', lb, ub)
assert_allclose(h_adjusted, np.array([0.015, -0.015]))
assert_equal(one_sided, np.array([False, True]))
class TestApproxDerivativesDense:
def fun_scalar_scalar(self, x):
return np.sinh(x)
def jac_scalar_scalar(self, x):
return np.cosh(x)
def fun_scalar_vector(self, x):
return np.array([x[0]**2, np.tan(x[0]), np.exp(x[0])])
def jac_scalar_vector(self, x):
return np.array(
[2 * x[0], np.cos(x[0]) ** -2, np.exp(x[0])]).reshape(-1, 1)
def fun_vector_scalar(self, x):
return np.sin(x[0] * x[1]) * np.log(x[0])
def wrong_dimensions_fun(self, x):
return np.array([x**2, np.tan(x), np.exp(x)])
def jac_vector_scalar(self, x):
return np.array([
x[1] * np.cos(x[0] * x[1]) * np.log(x[0]) +
np.sin(x[0] * x[1]) / x[0],
x[0] * np.cos(x[0] * x[1]) * np.log(x[0])
])
def fun_vector_vector(self, x):
return np.array([
x[0] * np.sin(x[1]),
x[1] * np.cos(x[0]),
x[0] ** 3 * x[1] ** -0.5
])
def fun_vector_vector_with_arg(self, x, arg):
"""Used to test passing custom arguments with check_derivative()"""
assert arg == 42
return np.array([
x[0] * np.sin(x[1]),
x[1] * np.cos(x[0]),
x[0] ** 3 * x[1] ** -0.5
])
def jac_vector_vector(self, x):
return np.array([
[np.sin(x[1]), x[0] * np.cos(x[1])],
[-x[1] * np.sin(x[0]), np.cos(x[0])],
[3 * x[0] ** 2 * x[1] ** -0.5, -0.5 * x[0] ** 3 * x[1] ** -1.5]
])
def jac_vector_vector_with_arg(self, x, arg):
"""Used to test passing custom arguments with check_derivative()"""
assert arg == 42
return np.array([
[np.sin(x[1]), x[0] * np.cos(x[1])],
[-x[1] * np.sin(x[0]), np.cos(x[0])],
[3 * x[0] ** 2 * x[1] ** -0.5, -0.5 * x[0] ** 3 * x[1] ** -1.5]
])
def fun_parametrized(self, x, c0, c1=1.0):
return np.array([np.exp(c0 * x[0]), np.exp(c1 * x[1])])
def jac_parametrized(self, x, c0, c1=0.1):
return np.array([
[c0 * np.exp(c0 * x[0]), 0],
[0, c1 * np.exp(c1 * x[1])]
])
def fun_with_nan(self, x):
return x if np.abs(x) <= 1e-8 else np.nan
def jac_with_nan(self, x):
return 1.0 if np.abs(x) <= 1e-8 else np.nan
def fun_zero_jacobian(self, x):
return np.array([x[0] * x[1], np.cos(x[0] * x[1])])
def jac_zero_jacobian(self, x):
return np.array([
[x[1], x[0]],
[-x[1] * np.sin(x[0] * x[1]), -x[0] * np.sin(x[0] * x[1])]
])
def jac_non_numpy(self, x):
# x can be a scalar or an array [val].
# Cast to true scalar before handing over to math.exp
xp = np.asarray(x).item()
return math.exp(xp)
def test_scalar_scalar(self):
x0 = 1.0
jac_diff_2 = approx_derivative(self.fun_scalar_scalar, x0,
method='2-point')
jac_diff_3 = approx_derivative(self.fun_scalar_scalar, x0)
jac_diff_4 = approx_derivative(self.fun_scalar_scalar, x0,
method='cs')
jac_true = self.jac_scalar_scalar(x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
def test_scalar_scalar_abs_step(self):
# can approx_derivative use abs_step?
x0 = 1.0
jac_diff_2 = approx_derivative(self.fun_scalar_scalar, x0,
method='2-point', abs_step=1.49e-8)
jac_diff_3 = approx_derivative(self.fun_scalar_scalar, x0,
abs_step=1.49e-8)
jac_diff_4 = approx_derivative(self.fun_scalar_scalar, x0,
method='cs', abs_step=1.49e-8)
jac_true = self.jac_scalar_scalar(x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
def test_scalar_vector(self):
x0 = 0.5
with MapWrapper(2) as mapper:
jac_diff_2 = approx_derivative(self.fun_scalar_vector, x0,
method='2-point', workers=mapper)
jac_diff_3 = approx_derivative(self.fun_scalar_vector, x0, workers=map)
jac_diff_4 = approx_derivative(self.fun_scalar_vector, x0,
method='cs', workers=None)
jac_true = self.jac_scalar_vector(np.atleast_1d(x0))
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
@pytest.mark.fail_slow(5.0)
def test_workers_evaluations_and_nfev(self):
# check that nfev consumed by approx_derivative is tracked properly
# and that parallel evaluation is same as series
x0 = [0.5, 1.5, 2.0]
with MapWrapper(2) as mapper:
md2, mdct2 = approx_derivative(rosen, x0,
method='2-point', workers=mapper,
full_output=True)
md3, mdct3 = approx_derivative(rosen, x0,
workers=mapper, full_output=True)
# supply a number for workers. This is not normally recommended
# for upstream workers as setting up processes incurs a large overhead
md4, mdct4 = approx_derivative(rosen, x0,
method='cs', workers=2,
full_output=True)
sfr = _ScalarFunctionWrapper(rosen)
d2, dct2 = approx_derivative(sfr, x0, method='2-point', full_output=True)
assert_equal(dct2['nfev'], sfr.nfev)
sfr.nfev = 0
d3, dct3 = approx_derivative(sfr, x0, full_output=True)
assert_equal(dct3['nfev'], sfr.nfev)
sfr.nfev = 0
d4, dct4 = approx_derivative(sfr, x0, method='cs', full_output=True)
assert_equal(dct4['nfev'], sfr.nfev)
assert_equal(mdct2['nfev'], dct2['nfev'])
assert_equal(mdct3['nfev'], dct3['nfev'])
assert_equal(mdct4['nfev'], dct4['nfev'])
# also check that gradients are equivalent
assert_equal(md2, d2)
assert_equal(md3, d3)
assert_equal(md4, d4)
def test_vector_scalar(self):
x0 = np.array([100.0, -0.5])
jac_diff_2 = approx_derivative(self.fun_vector_scalar, x0,
method='2-point')
jac_diff_3 = approx_derivative(self.fun_vector_scalar, x0)
jac_diff_4 = approx_derivative(self.fun_vector_scalar, x0,
method='cs')
jac_true = self.jac_vector_scalar(x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-7)
assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
def test_vector_scalar_abs_step(self):
# can approx_derivative use abs_step?
x0 = np.array([100.0, -0.5])
jac_diff_2 = approx_derivative(self.fun_vector_scalar, x0,
method='2-point', abs_step=1.49e-8)
jac_diff_3 = approx_derivative(self.fun_vector_scalar, x0,
abs_step=1.49e-8, rel_step=np.inf)
jac_diff_4 = approx_derivative(self.fun_vector_scalar, x0,
method='cs', abs_step=1.49e-8)
jac_true = self.jac_vector_scalar(x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=3e-9)
assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
def test_vector_vector(self):
x0 = np.array([-100.0, 0.2])
jac_diff_2 = approx_derivative(self.fun_vector_vector, x0,
method='2-point')
jac_diff_3 = approx_derivative(self.fun_vector_vector, x0)
with MapWrapper(2) as mapper:
jac_diff_4 = approx_derivative(self.fun_vector_vector, x0,
method='cs', workers=mapper)
jac_true = self.jac_vector_vector(x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-5)
assert_allclose(jac_diff_3, jac_true, rtol=1e-6)
assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
def test_wrong_dimensions(self):
x0 = 1.0
assert_raises(RuntimeError, approx_derivative,
self.wrong_dimensions_fun, x0)
f0 = self.wrong_dimensions_fun(np.atleast_1d(x0))
assert_raises(ValueError, approx_derivative,
self.wrong_dimensions_fun, x0, f0=f0)
def test_custom_rel_step(self):
x0 = np.array([-0.1, 0.1])
jac_diff_2 = approx_derivative(self.fun_vector_vector, x0,
method='2-point', rel_step=1e-4)
jac_diff_3 = approx_derivative(self.fun_vector_vector, x0,
rel_step=1e-4)
jac_true = self.jac_vector_vector(x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-2)
assert_allclose(jac_diff_3, jac_true, rtol=1e-4)
def test_options(self):
x0 = np.array([1.0, 1.0])
c0 = -1.0
c1 = 1.0
lb = 0.0
ub = 2.0
f0 = self.fun_parametrized(x0, c0, c1=c1)
rel_step = np.array([-1e-6, 1e-7])
jac_true = self.jac_parametrized(x0, c0, c1)
jac_diff_2 = approx_derivative(
self.fun_parametrized, x0, method='2-point', rel_step=rel_step,
f0=f0, args=(c0,), kwargs=dict(c1=c1), bounds=(lb, ub))
jac_diff_3 = approx_derivative(
self.fun_parametrized, x0, rel_step=rel_step,
f0=f0, args=(c0,), kwargs=dict(c1=c1), bounds=(lb, ub))
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
def test_with_bounds_2_point(self):
lb = -np.ones(2)
ub = np.ones(2)
x0 = np.array([-2.0, 0.2])
assert_raises(ValueError, approx_derivative,
self.fun_vector_vector, x0, bounds=(lb, ub))
x0 = np.array([-1.0, 1.0])
jac_diff = approx_derivative(self.fun_vector_vector, x0,
method='2-point', bounds=(lb, ub))
jac_true = self.jac_vector_vector(x0)
assert_allclose(jac_diff, jac_true, rtol=1e-6)
def test_with_bounds_3_point(self):
lb = np.array([1.0, 1.0])
ub = np.array([2.0, 2.0])
x0 = np.array([1.0, 2.0])
jac_true = self.jac_vector_vector(x0)
jac_diff = approx_derivative(self.fun_vector_vector, x0)
assert_allclose(jac_diff, jac_true, rtol=1e-9)
jac_diff = approx_derivative(self.fun_vector_vector, x0,
bounds=(lb, np.inf))
assert_allclose(jac_diff, jac_true, rtol=1e-9)
jac_diff = approx_derivative(self.fun_vector_vector, x0,
bounds=(-np.inf, ub))
assert_allclose(jac_diff, jac_true, rtol=1e-9)
jac_diff = approx_derivative(self.fun_vector_vector, x0,
bounds=(lb, ub))
assert_allclose(jac_diff, jac_true, rtol=1e-9)
def test_tight_bounds(self):
x0 = np.array([10.0, 10.0])
lb = x0 - 3e-9
ub = x0 + 2e-9
jac_true = self.jac_vector_vector(x0)
jac_diff = approx_derivative(
self.fun_vector_vector, x0, method='2-point', bounds=(lb, ub))
assert_allclose(jac_diff, jac_true, rtol=1e-6)
jac_diff = approx_derivative(
self.fun_vector_vector, x0, method='2-point',
rel_step=1e-6, bounds=(lb, ub))
assert_allclose(jac_diff, jac_true, rtol=1e-6)
jac_diff = approx_derivative(
self.fun_vector_vector, x0, bounds=(lb, ub))
assert_allclose(jac_diff, jac_true, rtol=1e-6)
jac_diff = approx_derivative(
self.fun_vector_vector, x0, rel_step=1e-6, bounds=(lb, ub))
assert_allclose(jac_true, jac_diff, rtol=1e-6)
def test_bound_switches(self):
lb = -1e-8
ub = 1e-8
x0 = 0.0
jac_true = self.jac_with_nan(x0)
jac_diff_2 = approx_derivative(
self.fun_with_nan, x0, method='2-point', rel_step=1e-6,
bounds=(lb, ub))
jac_diff_3 = approx_derivative(
self.fun_with_nan, x0, rel_step=1e-6, bounds=(lb, ub))
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
x0 = 1e-8
jac_true = self.jac_with_nan(x0)
jac_diff_2 = approx_derivative(
self.fun_with_nan, x0, method='2-point', rel_step=1e-6,
bounds=(lb, ub))
jac_diff_3 = approx_derivative(
self.fun_with_nan, x0, rel_step=1e-6, bounds=(lb, ub))
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
def test_non_numpy(self):
x0 = 1.0
jac_true = self.jac_non_numpy(x0)
jac_diff_2 = approx_derivative(self.jac_non_numpy, x0,
method='2-point')
jac_diff_3 = approx_derivative(self.jac_non_numpy, x0)
assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
assert_allclose(jac_diff_3, jac_true, rtol=1e-8)
# math.exp cannot handle complex arguments, hence this raises
assert_raises(TypeError, approx_derivative, self.jac_non_numpy, x0,
**dict(method='cs'))
def test_fp(self):
# checks that approx_derivative works for FP size other than 64.
# Example is derived from the minimal working example in gh12991.
np.random.seed(1)
def func(p, x):
return p[0] + p[1] * x
def err(p, x, y):
return func(p, x) - y
x = np.linspace(0, 1, 100, dtype=np.float64)
y = np.random.random(100).astype(np.float64)
p0 = np.array([-1.0, -1.0])
jac_fp64 = approx_derivative(err, p0, method='2-point', args=(x, y))
# parameter vector is float32, func output is float64
jac_fp = approx_derivative(err, p0.astype(np.float32),
method='2-point', args=(x, y))
assert err(p0, x, y).dtype == np.float64
assert_allclose(jac_fp, jac_fp64, atol=1e-3)
# parameter vector is float64, func output is float32
def err_fp32(p):
assert p.dtype == np.float32
return err(p, x, y).astype(np.float32)
jac_fp = approx_derivative(err_fp32, p0.astype(np.float32),
method='2-point')
assert_allclose(jac_fp, jac_fp64, atol=1e-3)
# check upper bound of error on the derivative for 2-point
def f(x):
return np.sin(x)
def g(x):
return np.cos(x)
def hess(x):
return -np.sin(x)
def calc_atol(h, x0, f, hess, EPS):
# truncation error
t0 = h / 2 * max(np.abs(hess(x0)), np.abs(hess(x0 + h)))
# roundoff error. There may be a divisor (>1) missing from
# the following line, so this contribution is possibly
# overestimated
t1 = EPS / h * max(np.abs(f(x0)), np.abs(f(x0 + h)))
return t0 + t1
for dtype in [np.float16, np.float32, np.float64]:
EPS = np.finfo(dtype).eps
x0 = np.array(1.0).astype(dtype)
h = _compute_absolute_step(None, x0, f(x0), '2-point')
atol = calc_atol(h, x0, f, hess, EPS)
err = approx_derivative(f, x0, method='2-point',
abs_step=h) - g(x0)
assert abs(err) < atol
def test_check_derivative(self):
x0 = np.array([-10.0, 10])
accuracy = check_derivative(self.fun_vector_vector,
self.jac_vector_vector, x0)
assert_(accuracy < 1e-9)
accuracy = check_derivative(self.fun_vector_vector,
self.jac_vector_vector, x0)
assert_(accuracy < 1e-6)
x0 = np.array([0.0, 0.0])
accuracy = check_derivative(self.fun_zero_jacobian,
self.jac_zero_jacobian, x0)
assert_(accuracy == 0)
accuracy = check_derivative(self.fun_zero_jacobian,
self.jac_zero_jacobian, x0)
assert_(accuracy == 0)
def test_check_derivative_with_kwargs(self):
x0 = np.array([-10.0, 10])
accuracy = check_derivative(self.fun_vector_vector_with_arg,
self.jac_vector_vector_with_arg,
x0,
kwargs={'arg': 42})
assert_(accuracy < 1e-9)
class TestApproxDerivativeSparse:
# Example from Numerical Optimization 2nd edition, p. 198.
def setup_method(self):
np.random.seed(0)
self.n = 50
self.lb = -0.1 * (1 + np.arange(self.n))
self.ub = 0.1 * (1 + np.arange(self.n))
self.x0 = np.empty(self.n)
self.x0[::2] = (1 - 1e-7) * self.lb[::2]
self.x0[1::2] = (1 - 1e-7) * self.ub[1::2]
self.J_true = self.jac(self.x0)
def fun(self, x):
e = x[1:]**3 - x[:-1]**2
return np.hstack((0, 3 * e)) + np.hstack((2 * e, 0))
def jac(self, x):
n = x.size
J = np.zeros((n, n))
J[0, 0] = -4 * x[0]
J[0, 1] = 6 * x[1]**2
for i in range(1, n - 1):
J[i, i - 1] = -6 * x[i-1]
J[i, i] = 9 * x[i]**2 - 4 * x[i]
J[i, i + 1] = 6 * x[i+1]**2
J[-1, -1] = 9 * x[-1]**2
J[-1, -2] = -6 * x[-2]
return J
def structure(self, n):
A = np.zeros((n, n), dtype=int)
A[0, 0] = 1
A[0, 1] = 1
for i in range(1, n - 1):
A[i, i - 1: i + 2] = 1
A[-1, -1] = 1
A[-1, -2] = 1
return A
@pytest.mark.fail_slow(5)
def test_all(self):
A = self.structure(self.n)
order = np.arange(self.n)
groups_1 = group_columns(A, order)
np.random.shuffle(order)
groups_2 = group_columns(A, order)
with MapWrapper(2) as mapper:
for method, groups, l, u, mf in product(
['2-point', '3-point', 'cs'], [groups_1, groups_2],
[-np.inf, self.lb], [np.inf, self.ub], [map, mapper]):
J = approx_derivative(self.fun, self.x0, method=method,
bounds=(l, u), sparsity=(A, groups),
workers=mf)
assert_(isinstance(J, csr_array))
assert_allclose(J.toarray(), self.J_true, rtol=1e-6)
rel_step = np.full_like(self.x0, 1e-8)
rel_step[::2] *= -1
J = approx_derivative(self.fun, self.x0, method=method,
rel_step=rel_step, sparsity=(A, groups),
workers=mf)
assert_allclose(J.toarray(), self.J_true, rtol=1e-5)
def test_no_precomputed_groups(self):
A = self.structure(self.n)
J = approx_derivative(self.fun, self.x0, sparsity=A)
assert_allclose(J.toarray(), self.J_true, rtol=1e-6)
def test_equivalence(self):
structure = np.ones((self.n, self.n), dtype=int)
groups = np.arange(self.n)
for method in ['2-point', '3-point', 'cs']:
J_dense = approx_derivative(self.fun, self.x0, method=method)
J_sparse = approx_derivative(
self.fun, self.x0, sparsity=(structure, groups), method=method)
assert_allclose(J_dense, J_sparse.toarray(),
rtol=5e-16, atol=7e-15)
def test_check_derivative(self):
def jac(x):
return csr_array(self.jac(x))
accuracy = check_derivative(self.fun, jac, self.x0,
bounds=(self.lb, self.ub))
assert_(accuracy < 1e-9)
accuracy = check_derivative(self.fun, jac, self.x0,
bounds=(self.lb, self.ub))
assert_(accuracy < 1e-9)
class TestApproxDerivativeLinearOperator:
def fun_scalar_scalar(self, x):
return np.sinh(x)
def jac_scalar_scalar(self, x):
return np.cosh(x)
def fun_scalar_vector(self, x):
return np.array([x[0]**2, np.tan(x[0]), np.exp(x[0])])
def jac_scalar_vector(self, x):
return np.array(
[2 * x[0], np.cos(x[0]) ** -2, np.exp(x[0])]).reshape(-1, 1)
def fun_vector_scalar(self, x):
return np.sin(x[0] * x[1]) * np.log(x[0])
def jac_vector_scalar(self, x):
return np.array([
x[1] * np.cos(x[0] * x[1]) * np.log(x[0]) +
np.sin(x[0] * x[1]) / x[0],
x[0] * np.cos(x[0] * x[1]) * np.log(x[0])
])
def fun_vector_vector(self, x):
return np.array([
x[0] * np.sin(x[1]),
x[1] * np.cos(x[0]),
x[0] ** 3 * x[1] ** -0.5
])
def jac_vector_vector(self, x):
return np.array([
[np.sin(x[1]), x[0] * np.cos(x[1])],
[-x[1] * np.sin(x[0]), np.cos(x[0])],
[3 * x[0] ** 2 * x[1] ** -0.5, -0.5 * x[0] ** 3 * x[1] ** -1.5]
])
def test_scalar_scalar(self):
x0 = 1.0
jac_diff_2 = approx_derivative(self.fun_scalar_scalar, x0,
method='2-point',
as_linear_operator=True)
jac_diff_3 = approx_derivative(self.fun_scalar_scalar, x0,
as_linear_operator=True)
jac_diff_4 = approx_derivative(self.fun_scalar_scalar, x0,
method='cs',
as_linear_operator=True)
jac_true = self.jac_scalar_scalar(x0)
np.random.seed(1)
for i in range(10):
p = np.random.uniform(-10, 10, size=(1,))
assert_allclose(jac_diff_2.dot(p), jac_true*p,
rtol=1e-5)
assert_allclose(jac_diff_3.dot(p), jac_true*p,
rtol=5e-6)
assert_allclose(jac_diff_4.dot(p), jac_true*p,
rtol=5e-6)
def test_scalar_vector(self):
x0 = 0.5
jac_diff_2 = approx_derivative(self.fun_scalar_vector, x0,
method='2-point',
as_linear_operator=True)
jac_diff_3 = approx_derivative(self.fun_scalar_vector, x0,
as_linear_operator=True)
jac_diff_4 = approx_derivative(self.fun_scalar_vector, x0,
method='cs',
as_linear_operator=True)
jac_true = self.jac_scalar_vector(np.atleast_1d(x0))
np.random.seed(1)
for i in range(10):
p = np.random.uniform(-10, 10, size=(1,))
assert_allclose(jac_diff_2.dot(p), jac_true.dot(p),
rtol=1e-5)
assert_allclose(jac_diff_3.dot(p), jac_true.dot(p),
rtol=5e-6)
assert_allclose(jac_diff_4.dot(p), jac_true.dot(p),
rtol=5e-6)
def test_vector_scalar(self):
x0 = np.array([100.0, -0.5])
jac_diff_2 = approx_derivative(self.fun_vector_scalar, x0,
method='2-point',
as_linear_operator=True)
jac_diff_3 = approx_derivative(self.fun_vector_scalar, x0,
as_linear_operator=True)
jac_diff_4 = approx_derivative(self.fun_vector_scalar, x0,
method='cs',
as_linear_operator=True)
jac_true = self.jac_vector_scalar(x0)
np.random.seed(1)
for i in range(10):
p = np.random.uniform(-10, 10, size=x0.shape)
assert_allclose(jac_diff_2.dot(p), np.atleast_1d(jac_true.dot(p)),
rtol=1e-5)
assert_allclose(jac_diff_3.dot(p), np.atleast_1d(jac_true.dot(p)),
rtol=5e-6)
assert_allclose(jac_diff_4.dot(p), np.atleast_1d(jac_true.dot(p)),
rtol=1e-7)
def test_vector_vector(self):
x0 = np.array([-100.0, 0.2])
jac_diff_2 = approx_derivative(self.fun_vector_vector, x0,
method='2-point',
as_linear_operator=True)
jac_diff_3 = approx_derivative(self.fun_vector_vector, x0,
as_linear_operator=True)
jac_diff_4 = approx_derivative(self.fun_vector_vector, x0,
method='cs',
as_linear_operator=True)
jac_true = self.jac_vector_vector(x0)
np.random.seed(1)
for i in range(10):
p = np.random.uniform(-10, 10, size=x0.shape)
assert_allclose(jac_diff_2.dot(p), jac_true.dot(p), rtol=1e-5)
assert_allclose(jac_diff_3.dot(p), jac_true.dot(p), rtol=1e-6)
assert_allclose(jac_diff_4.dot(p), jac_true.dot(p), rtol=1e-7)
def test_exception(self):
x0 = np.array([-100.0, 0.2])
assert_raises(ValueError, approx_derivative,
self.fun_vector_vector, x0,
method='2-point', bounds=(1, np.inf))
def test_absolute_step_sign():
# test for gh12487
# if an absolute step is specified for 2-point differences make sure that
# the side corresponds to the step. i.e. if step is positive then forward
# differences should be used, if step is negative then backwards
# differences should be used.
# function has double discontinuity at x = [-1, -1]
# first component is \/, second component is /\
def f(x):
return -np.abs(x[0] + 1) + np.abs(x[1] + 1)
# check that the forward difference is used
grad = approx_derivative(f, [-1, -1], method='2-point', abs_step=1e-8)
assert_allclose(grad, [-1.0, 1.0])
# check that the backwards difference is used
grad = approx_derivative(f, [-1, -1], method='2-point', abs_step=-1e-8)
assert_allclose(grad, [1.0, -1.0])
# check that the forwards difference is used with a step for both
# parameters
grad = approx_derivative(
f, [-1, -1], method='2-point', abs_step=[1e-8, 1e-8]
)
assert_allclose(grad, [-1.0, 1.0])
# check that we can mix forward/backwards steps.
grad = approx_derivative(
f, [-1, -1], method='2-point', abs_step=[1e-8, -1e-8]
)
assert_allclose(grad, [-1.0, -1.0])
grad = approx_derivative(
f, [-1, -1], method='2-point', abs_step=[-1e-8, 1e-8]
)
assert_allclose(grad, [1.0, 1.0])
# the forward step should reverse to a backwards step if it runs into a
# bound
# This is kind of tested in TestAdjustSchemeToBounds, but only for a lower level
# function.
grad = approx_derivative(
f, [-1, -1], method='2-point', abs_step=1e-8,
bounds=(-np.inf, -1)
)
assert_allclose(grad, [1.0, -1.0])
grad = approx_derivative(
f, [-1, -1], method='2-point', abs_step=-1e-8, bounds=(-1, np.inf)
)
assert_allclose(grad, [-1.0, 1.0])
def test__compute_absolute_step():
# tests calculation of absolute step from rel_step
methods = ['2-point', '3-point', 'cs']
x0 = np.array([1e-5, 0, 1, 1e5])
EPS = np.finfo(np.float64).eps
relative_step = {
"2-point": EPS**0.5,
"3-point": EPS**(1/3),
"cs": EPS**0.5
}
f0 = np.array(1.0)
for method in methods:
rel_step = relative_step[method]
correct_step = np.array([rel_step,
rel_step * 1.,
rel_step * 1.,
rel_step * np.abs(x0[3])])
abs_step = _compute_absolute_step(None, x0, f0, method)
assert_allclose(abs_step, correct_step)
sign_x0 = (-x0 >= 0).astype(float) * 2 - 1
abs_step = _compute_absolute_step(None, -x0, f0, method)
assert_allclose(abs_step, sign_x0 * correct_step)
# if a relative step is provided it should be used
rel_step = np.array([0.1, 1, 10, 100])
correct_step = np.array([rel_step[0] * x0[0],
relative_step['2-point'],
rel_step[2] * 1.,
rel_step[3] * np.abs(x0[3])])
abs_step = _compute_absolute_step(rel_step, x0, f0, '2-point')
assert_allclose(abs_step, correct_step)
sign_x0 = (-x0 >= 0).astype(float) * 2 - 1
abs_step = _compute_absolute_step(rel_step, -x0, f0, '2-point')
assert_allclose(abs_step, sign_x0 * correct_step)

View file

@ -0,0 +1,228 @@
"""
Unit test for Linear Programming via Simplex Algorithm.
"""
# TODO: add tests for:
# https://github.com/scipy/scipy/issues/5400
# https://github.com/scipy/scipy/issues/6690
import numpy as np
from numpy.testing import (
assert_,
assert_allclose,
assert_equal)
from .test_linprog import magic_square
from scipy.optimize._remove_redundancy import _remove_redundancy_svd
from scipy.optimize._remove_redundancy import _remove_redundancy_pivot_dense
from scipy.optimize._remove_redundancy import _remove_redundancy_pivot_sparse
from scipy.optimize._remove_redundancy import _remove_redundancy_id
from scipy.sparse import csc_array
def setup_module():
np.random.seed(2017)
def redundancy_removed(A, B):
"""Checks whether a matrix contains only independent rows of another"""
for rowA in A:
# `rowA in B` is not a reliable check
for rowB in B:
if np.all(rowA == rowB):
break
else:
return False
return A.shape[0] == np.linalg.matrix_rank(A) == np.linalg.matrix_rank(B)
class RRCommonTests:
def test_no_redundancy(self):
m, n = 10, 10
A0 = np.random.rand(m, n)
b0 = np.random.rand(m)
A1, b1, status, message = self.rr(A0, b0)
assert_allclose(A0, A1)
assert_allclose(b0, b1)
assert_equal(status, 0)
def test_infeasible_zero_row(self):
A = np.eye(3)
A[1, :] = 0
b = np.random.rand(3)
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 2)
def test_remove_zero_row(self):
A = np.eye(3)
A[1, :] = 0
b = np.random.rand(3)
b[1] = 0
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 0)
assert_allclose(A1, A[[0, 2], :])
assert_allclose(b1, b[[0, 2]])
def test_infeasible_m_gt_n(self):
m, n = 20, 10
A0 = np.random.rand(m, n)
b0 = np.random.rand(m)
A1, b1, status, message = self.rr(A0, b0)
assert_equal(status, 2)
def test_infeasible_m_eq_n(self):
m, n = 10, 10
A0 = np.random.rand(m, n)
b0 = np.random.rand(m)
A0[-1, :] = 2 * A0[-2, :]
A1, b1, status, message = self.rr(A0, b0)
assert_equal(status, 2)
def test_infeasible_m_lt_n(self):
m, n = 9, 10
A0 = np.random.rand(m, n)
b0 = np.random.rand(m)
A0[-1, :] = np.arange(m - 1).dot(A0[:-1])
A1, b1, status, message = self.rr(A0, b0)
assert_equal(status, 2)
def test_m_gt_n(self):
np.random.seed(2032)
m, n = 20, 10
A0 = np.random.rand(m, n)
b0 = np.random.rand(m)
x = np.linalg.solve(A0[:n, :], b0[:n])
b0[n:] = A0[n:, :].dot(x)
A1, b1, status, message = self.rr(A0, b0)
assert_equal(status, 0)
assert_equal(A1.shape[0], n)
assert_equal(np.linalg.matrix_rank(A1), n)
def test_m_gt_n_rank_deficient(self):
m, n = 20, 10
A0 = np.zeros((m, n))
A0[:, 0] = 1
b0 = np.ones(m)
A1, b1, status, message = self.rr(A0, b0)
assert_equal(status, 0)
assert_allclose(A1, A0[0:1, :])
assert_allclose(b1, b0[0])
def test_m_lt_n_rank_deficient(self):
m, n = 9, 10
A0 = np.random.rand(m, n)
b0 = np.random.rand(m)
A0[-1, :] = np.arange(m - 1).dot(A0[:-1])
b0[-1] = np.arange(m - 1).dot(b0[:-1])
A1, b1, status, message = self.rr(A0, b0)
assert_equal(status, 0)
assert_equal(A1.shape[0], 8)
assert_equal(np.linalg.matrix_rank(A1), 8)
def test_dense1(self):
A = np.ones((6, 6))
A[0, :3] = 0
A[1, 3:] = 0
A[3:, ::2] = -1
A[3, :2] = 0
A[4, 2:] = 0
b = np.zeros(A.shape[0])
A1, b1, status, message = self.rr(A, b)
assert_(redundancy_removed(A1, A))
assert_equal(status, 0)
def test_dense2(self):
A = np.eye(6)
A[-2, -1] = 1
A[-1, :] = 1
b = np.zeros(A.shape[0])
A1, b1, status, message = self.rr(A, b)
assert_(redundancy_removed(A1, A))
assert_equal(status, 0)
def test_dense3(self):
A = np.eye(6)
A[-2, -1] = 1
A[-1, :] = 1
b = np.random.rand(A.shape[0])
b[-1] = np.sum(b[:-1])
A1, b1, status, message = self.rr(A, b)
assert_(redundancy_removed(A1, A))
assert_equal(status, 0)
def test_m_gt_n_sparse(self):
np.random.seed(2013)
m, n = 20, 5
p = 0.1
A = np.random.rand(m, n)
A[np.random.rand(m, n) > p] = 0
rank = np.linalg.matrix_rank(A)
b = np.zeros(A.shape[0])
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 0)
assert_equal(A1.shape[0], rank)
assert_equal(np.linalg.matrix_rank(A1), rank)
def test_m_lt_n_sparse(self):
np.random.seed(2017)
m, n = 20, 50
p = 0.05
A = np.random.rand(m, n)
A[np.random.rand(m, n) > p] = 0
rank = np.linalg.matrix_rank(A)
b = np.zeros(A.shape[0])
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 0)
assert_equal(A1.shape[0], rank)
assert_equal(np.linalg.matrix_rank(A1), rank)
def test_m_eq_n_sparse(self):
np.random.seed(2017)
m, n = 100, 100
p = 0.01
A = np.random.rand(m, n)
A[np.random.rand(m, n) > p] = 0
rank = np.linalg.matrix_rank(A)
b = np.zeros(A.shape[0])
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 0)
assert_equal(A1.shape[0], rank)
assert_equal(np.linalg.matrix_rank(A1), rank)
def test_magic_square(self):
A, b, c, numbers, _ = magic_square(3)
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 0)
assert_equal(A1.shape[0], 23)
assert_equal(np.linalg.matrix_rank(A1), 23)
def test_magic_square2(self):
A, b, c, numbers, _ = magic_square(4)
A1, b1, status, message = self.rr(A, b)
assert_equal(status, 0)
assert_equal(A1.shape[0], 39)
assert_equal(np.linalg.matrix_rank(A1), 39)
class TestRRSVD(RRCommonTests):
def rr(self, A, b):
return _remove_redundancy_svd(A, b)
class TestRRPivotDense(RRCommonTests):
def rr(self, A, b):
return _remove_redundancy_pivot_dense(A, b)
class TestRRID(RRCommonTests):
def rr(self, A, b):
return _remove_redundancy_id(A, b)
class TestRRPivotSparse(RRCommonTests):
def rr(self, A, b):
rr_res = _remove_redundancy_pivot_sparse(csc_array(A), b)
A1, b1, status, message = rr_res
return A1.toarray(), b1, status, message

View file

@ -0,0 +1,124 @@
"""
Unit tests for optimization routines from _root.py.
"""
from numpy.testing import assert_, assert_equal
import pytest
from pytest import raises as assert_raises, warns as assert_warns
import numpy as np
from scipy.optimize import root
class TestRoot:
def test_tol_parameter(self):
# Check that the minimize() tol= argument does something
def func(z):
x, y = z
return np.array([x**3 - 1, y**3 - 1])
def dfunc(z):
x, y = z
return np.array([[3*x**2, 0], [0, 3*y**2]])
for method in ['hybr', 'lm', 'broyden1', 'broyden2', 'anderson',
'diagbroyden', 'krylov']:
if method in ('linearmixing', 'excitingmixing'):
# doesn't converge
continue
if method in ('hybr', 'lm'):
jac = dfunc
else:
jac = None
sol1 = root(func, [1.1,1.1], jac=jac, tol=1e-4, method=method)
sol2 = root(func, [1.1,1.1], jac=jac, tol=0.5, method=method)
msg = f"{method}: {func(sol1.x)} vs. {func(sol2.x)}"
assert_(sol1.success, msg)
assert_(sol2.success, msg)
assert_(abs(func(sol1.x)).max() < abs(func(sol2.x)).max(),
msg)
def test_tol_norm(self):
def norm(x):
return abs(x[0])
for method in ['excitingmixing',
'diagbroyden',
'linearmixing',
'anderson',
'broyden1',
'broyden2',
'krylov']:
root(np.zeros_like, np.zeros(2), method=method,
options={"tol_norm": norm})
def test_minimize_scalar_coerce_args_param(self):
# GitHub issue #3503
def func(z, f=1):
x, y = z
return np.array([x**3 - 1, y**3 - f])
root(func, [1.1, 1.1], args=1.5)
def test_f_size(self):
# gh8320
# check that decreasing the size of the returned array raises an error
# and doesn't segfault
class fun:
def __init__(self):
self.count = 0
def __call__(self, x):
self.count += 1
if not (self.count % 5):
ret = x[0] + 0.5 * (x[0] - x[1]) ** 3 - 1.0
else:
ret = ([x[0] + 0.5 * (x[0] - x[1]) ** 3 - 1.0,
0.5 * (x[1] - x[0]) ** 3 + x[1]])
return ret
F = fun()
with assert_raises(ValueError):
root(F, [0.1, 0.0], method='lm')
@pytest.mark.thread_unsafe
def test_gh_10370(self):
# gh-10370 reported that passing both `args` and `jac` to `root` with
# `method='krylov'` caused a failure. Ensure that this is fixed whether
# the gradient is passed via `jac` or as a second output of `fun`.
def fun(x, ignored):
return [3*x[0] - 0.25*x[1]**2 + 10, 0.1*x[0]**2 + 5*x[1] - 2]
def grad(x, ignored):
return [[3, 0.5 * x[1]], [0.2 * x[0], 5]]
def fun_grad(x, ignored):
return fun(x, ignored), grad(x, ignored)
x0 = np.zeros(2)
ref = root(fun, x0, args=(1,), method='krylov')
message = 'Method krylov does not use the jacobian'
with assert_warns(RuntimeWarning, match=message):
res1 = root(fun, x0, args=(1,), method='krylov', jac=grad)
with assert_warns(RuntimeWarning, match=message):
res2 = root(fun_grad, x0, args=(1,), method='krylov', jac=True)
assert_equal(res1.x, ref.x)
assert_equal(res2.x, ref.x)
assert res1.success is res2.success is ref.success is True
@pytest.mark.parametrize("method", ["hybr", "lm", "broyden1", "broyden2",
"anderson", "linearmixing",
"diagbroyden", "excitingmixing",
"krylov", "df-sane"])
def test_method_in_result(self, method):
def func(x):
return x - 1
res = root(func, x0=[1], method=method)
assert res.method == method

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,226 @@
import itertools
import numpy as np
from numpy import exp
from numpy.testing import assert_, assert_equal
from scipy.optimize import root
def test_performance():
# Compare performance results to those listed in
# [Cheng & Li, IMA J. Num. An. 29, 814 (2008)]
# and
# [W. La Cruz, J.M. Martinez, M. Raydan, Math. Comp. 75, 1429 (2006)].
# and those produced by dfsane.f from M. Raydan's website.
#
# Where the results disagree, the largest limits are taken.
e_a = 1e-5
e_r = 1e-4
table_1 = [
dict(F=F_1, x0=x0_1, n=1000, nit=5, nfev=5),
dict(F=F_1, x0=x0_1, n=10000, nit=2, nfev=2),
dict(F=F_2, x0=x0_2, n=500, nit=11, nfev=11),
dict(F=F_2, x0=x0_2, n=2000, nit=11, nfev=11),
# dict(F=F_4, x0=x0_4, n=999, nit=243, nfev=1188) removed:
# too sensitive to rounding errors
# Results from dfsane.f; papers list nit=3, nfev=3
dict(F=F_6, x0=x0_6, n=100, nit=6, nfev=6),
# Must have n%3==0, typo in papers?
dict(F=F_7, x0=x0_7, n=99, nit=23, nfev=29),
# Must have n%3==0, typo in papers?
dict(F=F_7, x0=x0_7, n=999, nit=23, nfev=29),
# Results from dfsane.f; papers list nit=nfev=6?
dict(F=F_9, x0=x0_9, n=100, nit=12, nfev=18),
dict(F=F_9, x0=x0_9, n=1000, nit=12, nfev=18),
# Results from dfsane.f; papers list nit=2, nfev=12
dict(F=F_10, x0=x0_10, n=1000, nit=5, nfev=5),
]
# Check also scaling invariance
for xscale, yscale, line_search in itertools.product(
[1.0, 1e-10, 1e10], [1.0, 1e-10, 1e10], ['cruz', 'cheng']
):
for problem in table_1:
n = problem['n']
def func(x, n):
return yscale * problem['F'](x / xscale, n)
args = (n,)
x0 = problem['x0'](n) * xscale
fatol = np.sqrt(n) * e_a * yscale + e_r * np.linalg.norm(func(x0, n))
sigma_eps = 1e-10 * min(yscale/xscale, xscale/yscale)
sigma_0 = xscale/yscale
with np.errstate(over='ignore'):
sol = root(func, x0, args=args,
options=dict(ftol=0, fatol=fatol, maxfev=problem['nfev'] + 1,
sigma_0=sigma_0, sigma_eps=sigma_eps,
line_search=line_search),
method='DF-SANE')
err_msg = repr(
[xscale, yscale, line_search, problem, np.linalg.norm(func(sol.x, n)),
fatol, sol.success, sol.nit, sol.nfev]
)
assert sol.success, err_msg
# nfev+1: dfsane.f doesn't count first eval
assert sol.nfev <= problem['nfev'] + 1, err_msg
assert sol.nit <= problem['nit'], err_msg
assert np.linalg.norm(func(sol.x, n)) <= fatol, err_msg
def test_complex():
def func(z):
return z**2 - 1 + 2j
x0 = 2.0j
ftol = 1e-4
sol = root(func, x0, tol=ftol, method='DF-SANE')
assert_(sol.success)
f0 = np.linalg.norm(func(x0))
fx = np.linalg.norm(func(sol.x))
assert_(fx <= ftol*f0)
def test_linear_definite():
# The DF-SANE paper proves convergence for "strongly isolated"
# solutions.
#
# For linear systems F(x) = A x - b = 0, with A positive or
# negative definite, the solution is strongly isolated.
def check_solvability(A, b, line_search='cruz'):
def func(x):
return A.dot(x) - b
xp = np.linalg.solve(A, b)
eps = np.linalg.norm(func(xp)) * 1e3
sol = root(
func, b,
options=dict(fatol=eps, ftol=0, maxfev=17523, line_search=line_search),
method='DF-SANE',
)
assert_(sol.success)
assert_(np.linalg.norm(func(sol.x)) <= eps)
n = 90
# Test linear pos.def. system
np.random.seed(1234)
A = np.arange(n*n).reshape(n, n)
A = A + n*n * np.diag(1 + np.arange(n))
assert_(np.linalg.eigvals(A).min() > 0)
b = np.arange(n) * 1.0
check_solvability(A, b, 'cruz')
check_solvability(A, b, 'cheng')
# Test linear neg.def. system
check_solvability(-A, b, 'cruz')
check_solvability(-A, b, 'cheng')
def test_shape():
def f(x, arg):
return x - arg
for dt in [float, complex]:
x = np.zeros([2,2])
arg = np.ones([2,2], dtype=dt)
sol = root(f, x, args=(arg,), method='DF-SANE')
assert_(sol.success)
assert_equal(sol.x.shape, x.shape)
# Some of the test functions and initial guesses listed in
# [W. La Cruz, M. Raydan. Optimization Methods and Software, 18, 583 (2003)]
def F_1(x, n):
g = np.zeros([n])
i = np.arange(2, n+1)
g[0] = exp(x[0] - 1) - 1
g[1:] = i*(exp(x[1:] - 1) - x[1:])
return g
def x0_1(n):
x0 = np.empty([n])
x0.fill(n/(n-1))
return x0
def F_2(x, n):
g = np.zeros([n])
i = np.arange(2, n+1)
g[0] = exp(x[0]) - 1
g[1:] = 0.1*i*(exp(x[1:]) + x[:-1] - 1)
return g
def x0_2(n):
x0 = np.empty([n])
x0.fill(1/n**2)
return x0
def F_4(x, n): # skip name check
assert_equal(n % 3, 0)
g = np.zeros([n])
# Note: the first line is typoed in some of the references;
# correct in original [Gasparo, Optimization Meth. 13, 79 (2000)]
g[::3] = 0.6 * x[::3] + 1.6 * x[1::3]**3 - 7.2 * x[1::3]**2 + 9.6 * x[1::3] - 4.8
g[1::3] = (0.48 * x[::3] - 0.72 * x[1::3]**3 + 3.24 * x[1::3]**2 - 4.32 * x[1::3]
- x[2::3] + 0.2 * x[2::3]**3 + 2.16)
g[2::3] = 1.25 * x[2::3] - 0.25*x[2::3]**3
return g
def x0_4(n): # skip name check
assert_equal(n % 3, 0)
x0 = np.array([-1, 1/2, -1] * (n//3))
return x0
def F_6(x, n):
c = 0.9
mu = (np.arange(1, n+1) - 0.5)/n
return x - 1/(1 - c/(2*n) * (mu[:,None]*x / (mu[:,None] + mu)).sum(axis=1))
def x0_6(n):
return np.ones([n])
def F_7(x, n):
assert_equal(n % 3, 0)
def phi(t):
v = 0.5*t - 2
v[t > -1] = ((-592*t**3 + 888*t**2 + 4551*t - 1924)/1998)[t > -1]
v[t >= 2] = (0.5*t + 2)[t >= 2]
return v
g = np.zeros([n])
g[::3] = 1e4 * x[1::3]**2 - 1
g[1::3] = exp(-x[::3]) + exp(-x[1::3]) - 1.0001
g[2::3] = phi(x[2::3])
return g
def x0_7(n):
assert_equal(n % 3, 0)
return np.array([1e-3, 18, 1] * (n//3))
def F_9(x, n):
g = np.zeros([n])
i = np.arange(2, n)
g[0] = x[0]**3/3 + x[1]**2/2
g[1:-1] = -x[1:-1]**2/2 + i*x[1:-1]**3/3 + x[2:]**2/2
g[-1] = -x[-1]**2/2 + n*x[-1]**3/3
return g
def x0_9(n):
return np.ones([n])
def F_10(x, n):
return np.log(1 + x) - x/n
def x0_10(n):
return np.ones([n])

View file

@ -0,0 +1,896 @@
import pytest
import numpy as np
from scipy.optimize._bracket import _ELIMITS
from scipy.optimize.elementwise import bracket_root, bracket_minimum
import scipy._lib._elementwise_iterative_method as eim
from scipy import stats
from scipy._lib._array_api_no_0d import (xp_assert_close, xp_assert_equal,
xp_assert_less)
from scipy._lib._array_api import xp_ravel
# These tests were originally written for the private `optimize._bracket`
# interfaces, but now we want the tests to check the behavior of the public
# `optimize.elementwise` interfaces. Therefore, rather than importing
# `_bracket_root`/`_bracket_minimum` from `_bracket.py`, we import
# `bracket_root`/`bracket_minimum` from `optimize.elementwise` and wrap those
# functions to conform to the private interface. This may look a little strange,
# since it effectively just inverts the interface transformation done within the
# `bracket_root`/`bracket_minimum` functions, but it allows us to run the original,
# unmodified tests on the public interfaces, simplifying the PR that adds
# the public interfaces. We'll refactor this when we want to @parametrize the
# tests over multiple `method`s.
def _bracket_root(*args, **kwargs):
res = bracket_root(*args, **kwargs)
res.xl, res.xr = res.bracket
res.fl, res.fr = res.f_bracket
del res.bracket
del res.f_bracket
return res
def _bracket_minimum(*args, **kwargs):
res = bracket_minimum(*args, **kwargs)
res.xl, res.xm, res.xr = res.bracket
res.fl, res.fm, res.fr = res.f_bracket
del res.bracket
del res.f_bracket
return res
array_api_strict_skip_reason = 'Array API does not support fancy indexing assignment.'
boolean_index_skip_reason = 'JAX/Dask arrays do not support boolean assignment.'
@pytest.mark.skip_xp_backends('array_api_strict', reason=array_api_strict_skip_reason)
@pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
@pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
class TestBracketRoot:
@pytest.mark.parametrize("seed", (615655101, 3141866013, 238075752))
@pytest.mark.parametrize("use_xmin", (False, True))
@pytest.mark.parametrize("other_side", (False, True))
@pytest.mark.parametrize("fix_one_side", (False, True))
def test_nfev_expected(self, seed, use_xmin, other_side, fix_one_side, xp):
# Property-based test to confirm that _bracket_root is behaving as
# expected. The basic case is when root < a < b.
# The number of times bracket expands (per side) can be found by
# setting the expression for the left endpoint of the bracket to the
# root of f (x=0), solving for i, and rounding up. The corresponding
# lower and upper ends of the bracket are found by plugging this back
# into the expression for the ends of the bracket.
# `other_side=True` is the case that a < b < root
# Special cases like a < root < b are tested separately
rng = np.random.default_rng(seed)
xl0, d, factor = xp.asarray(rng.random(size=3) * [1e5, 10, 5])
factor = 1 + factor # factor must be greater than 1
xr0 = xl0 + d # xr0 must be greater than a in basic case
def f(x):
f.count += 1
return x # root is 0
if use_xmin:
xmin = xp.asarray(-rng.random())
n = xp.ceil(xp.log(-(xl0 - xmin) / xmin) / xp.log(factor))
l, u = xmin + (xl0 - xmin)*factor**-n, xmin + (xl0 - xmin)*factor**-(n - 1)
kwargs = dict(xl0=xl0, xr0=xr0, factor=factor, xmin=xmin)
else:
n = xp.ceil(xp.log(xr0/d) / xp.log(factor))
l, u = xr0 - d*factor**n, xr0 - d*factor**(n-1)
kwargs = dict(xl0=xl0, xr0=xr0, factor=factor)
if other_side:
kwargs['xl0'], kwargs['xr0'] = -kwargs['xr0'], -kwargs['xl0']
l, u = -u, -l
if 'xmin' in kwargs:
kwargs['xmax'] = -kwargs.pop('xmin')
if fix_one_side:
if other_side:
kwargs['xmin'] = -xr0
else:
kwargs['xmax'] = xr0
f.count = 0
res = _bracket_root(f, **kwargs)
# Compare reported number of function evaluations `nfev` against
# reported `nit`, actual function call count `f.count`, and theoretical
# number of expansions `n`.
# When both sides are free, these get multiplied by 2 because function
# is evaluated on the left and the right each iteration.
# When one side is fixed, however, we add one: on the right side, the
# function gets evaluated once at b.
# Add 1 to `n` and `res.nit` because function evaluations occur at
# iterations *0*, 1, ..., `n`. Subtract 1 from `f.count` because
# function is called separately for left and right in iteration 0.
if not fix_one_side:
assert res.nfev == 2*(res.nit+1) == 2*(f.count-1) == 2*(n + 1)
else:
assert res.nfev == (res.nit+1)+1 == (f.count-1)+1 == (n+1)+1
# Compare reported bracket to theoretical bracket and reported function
# values to function evaluated at bracket.
bracket = xp.asarray([res.xl, res.xr])
xp_assert_close(bracket, xp.asarray([l, u]))
f_bracket = xp.asarray([res.fl, res.fr])
xp_assert_close(f_bracket, f(bracket))
# Check that bracket is valid and that status and success are correct
assert res.xr > res.xl
signs = xp.sign(f_bracket)
assert signs[0] == -signs[1]
assert res.status == 0
assert res.success
def f(self, q, p):
return stats._stats_py._SimpleNormal().cdf(q) - p
@pytest.mark.parametrize('p', [0.6, np.linspace(0.05, 0.95, 10)])
@pytest.mark.parametrize('xmin', [-5, None])
@pytest.mark.parametrize('xmax', [5, None])
@pytest.mark.parametrize('factor', [1.2, 2])
def test_basic(self, p, xmin, xmax, factor, xp):
# Test basic functionality to bracket root (distribution PPF)
res = _bracket_root(self.f, xp.asarray(-0.01), 0.01, xmin=xmin, xmax=xmax,
factor=factor, args=(xp.asarray(p),))
xp_assert_equal(-xp.sign(res.fl), xp.sign(res.fr))
@pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
def test_vectorization(self, shape, xp):
# Test for correct functionality, output shapes, and dtypes for various
# input shapes.
p = np.linspace(-0.05, 1.05, 12).reshape(shape) if shape else np.float64(0.6)
args = (p,)
maxiter = 10
@np.vectorize
def bracket_root_single(xl0, xr0, xmin, xmax, factor, p):
return _bracket_root(self.f, xl0, xr0, xmin=xmin, xmax=xmax,
factor=factor, args=(p,),
maxiter=maxiter)
def f(*args, **kwargs):
f.f_evals += 1
return self.f(*args, **kwargs)
f.f_evals = 0
rng = np.random.default_rng(2348234)
xl0 = -rng.random(size=shape)
xr0 = rng.random(size=shape)
xmin, xmax = 1e3*xl0, 1e3*xr0
if shape: # make some elements un
i = rng.random(size=shape) > 0.5
xmin[i], xmax[i] = -np.inf, np.inf
factor = rng.random(size=shape) + 1.5
refs = bracket_root_single(xl0, xr0, xmin, xmax, factor, p).ravel()
xl0, xr0, xmin, xmax, factor = (xp.asarray(xl0), xp.asarray(xr0),
xp.asarray(xmin), xp.asarray(xmax),
xp.asarray(factor))
args = tuple(map(xp.asarray, args))
res = _bracket_root(f, xl0, xr0, xmin=xmin, xmax=xmax, factor=factor,
args=args, maxiter=maxiter)
attrs = ['xl', 'xr', 'fl', 'fr', 'success', 'nfev', 'nit']
for attr in attrs:
ref_attr = [xp.asarray(getattr(ref, attr)) for ref in refs]
res_attr = getattr(res, attr)
xp_assert_close(xp_ravel(res_attr, xp=xp), xp.stack(ref_attr))
assert res_attr.shape == shape
assert res.success.dtype == xp.bool
if shape:
assert xp.all(res.success[1:-1])
assert res.status.dtype == xp.int32
assert res.nfev.dtype == xp.int32
assert res.nit.dtype == xp.int32
assert xp.max(res.nit) == f.f_evals - 2
xp_assert_less(res.xl, res.xr)
xp_assert_close(res.fl, xp.asarray(self.f(res.xl, *args)))
xp_assert_close(res.fr, xp.asarray(self.f(res.xr, *args)))
def test_flags(self, xp):
# Test cases that should produce different status flags; show that all
# can be produced simultaneously.
def f(xs, js):
funcs = [lambda x: x - 1.5,
lambda x: x - 1000,
lambda x: x - 1000,
lambda x: x * xp.nan,
lambda x: x]
return [funcs[int(j)](x) for x, j in zip(xs, js)]
args = (xp.arange(5, dtype=xp.int64),)
res = _bracket_root(f,
xl0=xp.asarray([-1., -1., -1., -1., 4.]),
xr0=xp.asarray([1, 1, 1, 1, -4]),
xmin=xp.asarray([-xp.inf, -1, -xp.inf, -xp.inf, 6]),
xmax=xp.asarray([xp.inf, 1, xp.inf, xp.inf, 2]),
args=args, maxiter=3)
ref_flags = xp.asarray([eim._ECONVERGED,
_ELIMITS,
eim._ECONVERR,
eim._EVALUEERR,
eim._EINPUTERR],
dtype=xp.int32)
xp_assert_equal(res.status, ref_flags)
@pytest.mark.parametrize("root", (0.622, [0.622, 0.623]))
@pytest.mark.parametrize('xmin', [-5, None])
@pytest.mark.parametrize('xmax', [5, None])
@pytest.mark.parametrize("dtype", ("float16", "float32", "float64"))
def test_dtype(self, root, xmin, xmax, dtype, xp):
# Test that dtypes are preserved
dtype = getattr(xp, dtype)
xmin = xmin if xmin is None else xp.asarray(xmin, dtype=dtype)
xmax = xmax if xmax is None else xp.asarray(xmax, dtype=dtype)
root = xp.asarray(root, dtype=dtype)
def f(x, root):
return xp.astype((x - root) ** 3, dtype)
bracket = xp.asarray([-0.01, 0.01], dtype=dtype)
res = _bracket_root(f, *bracket, xmin=xmin, xmax=xmax, args=(root,))
assert xp.all(res.success)
assert res.xl.dtype == res.xr.dtype == dtype
assert res.fl.dtype == res.fr.dtype == dtype
def test_input_validation(self, xp):
# Test input validation for appropriate error messages
message = '`func` must be callable.'
with pytest.raises(ValueError, match=message):
_bracket_root(None, -4, 4)
message = '...must be numeric and real.'
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4+1j, 4)
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4+1j)
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, xmin=4+1j)
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, xmax=4+1j)
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, factor=4+1j)
message = "All elements of `factor` must be greater than 1."
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, factor=0.5)
message = "broadcast"
# raised by `xp.broadcast, but the traceback is readable IMO
with pytest.raises(Exception, match=message):
_bracket_root(lambda x: x, xp.asarray([-2, -3]), xp.asarray([3, 4, 5]))
# Consider making this give a more readable error message
# with pytest.raises(ValueError, match=message):
# _bracket_root(lambda x: [x[0], x[1], x[1]], [-3, -3], [5, 5])
message = '`maxiter` must be a non-negative integer.'
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, maxiter=1.5)
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, maxiter=-1)
with pytest.raises(ValueError, match=message):
_bracket_root(lambda x: x, -4, 4, maxiter="shrubbery")
def test_special_cases(self, xp):
# Test edge cases and other special cases
# Test that integers are not passed to `f`
# (otherwise this would overflow)
def f(x):
assert xp.isdtype(x.dtype, "real floating")
return x ** 99 - 1
res = _bracket_root(f, xp.asarray(-7.), xp.asarray(5.))
assert res.success
# Test maxiter = 0. Should do nothing to bracket.
def f(x):
return x - 10
bracket = (xp.asarray(-3.), xp.asarray(5.))
res = _bracket_root(f, *bracket, maxiter=0)
assert res.xl, res.xr == bracket
assert res.nit == 0
assert res.nfev == 2
assert res.status == -2
# Test scalar `args` (not in tuple)
def f(x, c):
return c*x - 1
res = _bracket_root(f, xp.asarray(-1.), xp.asarray(1.),
args=xp.asarray(3.))
assert res.success
xp_assert_close(res.fl, f(res.xl, 3))
# Test other edge cases
def f(x):
f.count += 1
return x
# 1. root lies within guess of bracket
f.count = 0
_bracket_root(f, xp.asarray(-10), xp.asarray(20))
assert f.count == 2
# 2. bracket endpoint hits root exactly
f.count = 0
res = _bracket_root(f, xp.asarray(5.), xp.asarray(10.),
factor=2)
assert res.nfev == 4
xp_assert_close(res.xl, xp.asarray(0.), atol=1e-15)
xp_assert_close(res.xr, xp.asarray(5.), atol=1e-15)
# 3. bracket limit hits root exactly
with np.errstate(over='ignore'):
res = _bracket_root(f, xp.asarray(5.), xp.asarray(10.),
xmin=0)
xp_assert_close(res.xl, xp.asarray(0.), atol=1e-15)
with np.errstate(over='ignore'):
res = _bracket_root(f, xp.asarray(-10.), xp.asarray(-5.),
xmax=0)
xp_assert_close(res.xr, xp.asarray(0.), atol=1e-15)
# 4. bracket not within min, max
with np.errstate(over='ignore'):
res = _bracket_root(f, xp.asarray(5.), xp.asarray(10.),
xmin=1)
assert not res.success
def test_bug_fixes(self):
# 1. Bug in double sided bracket search.
# Happened in some cases where there are terminations on one side
# after corresponding searches on other side failed due to reaching the
# boundary.
# https://github.com/scipy/scipy/pull/22560#discussion_r1962853839
def f(x, p):
return np.exp(x) - p
p = np.asarray([0.29, 0.35])
res = _bracket_root(f, xl0=-1, xmin=-np.inf, xmax=0, args=(p, ))
# https://github.com/scipy/scipy/pull/22560/files#r1962952517
def f(x, p, c):
return np.exp(x*c) - p
p = [0.32061201, 0.39175242, 0.40047535, 0.50527218, 0.55654373,
0.11911647, 0.37507896, 0.66554191]
c = [1., -1., 1., 1., -1., 1., 1., 1.]
xl0 = [-7.63108551, 3.27840947, -8.36968526, -1.78124372,
0.92201295, -2.48930123, -0.66733533, -0.44606749]
xr0 = [-6.63108551, 4.27840947, -7.36968526, -0.78124372,
1.92201295, -1.48930123, 0., 0.]
xmin = [-np.inf, 0., -np.inf, -np.inf, 0., -np.inf, -np.inf,
-np.inf]
xmax = [0., np.inf, 0., 0., np.inf, 0., 0., 0.]
res = _bracket_root(f, xl0=xl0, xr0=xr0, xmin=xmin, xmax=xmax, args=(p, c))
# 2. Default xl0 + 1 for xr0 exceeds xmax.
# https://github.com/scipy/scipy/pull/22560#discussion_r1962947434
res = _bracket_root(lambda x: x + 0.25, xl0=-0.5, xmin=-np.inf, xmax=0)
assert res.success
@pytest.mark.skip_xp_backends('torch', reason='data-apis/array-api-compat#271')
@pytest.mark.skip_xp_backends('array_api_strict', reason=array_api_strict_skip_reason)
@pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
@pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
class TestBracketMinimum:
def init_f(self):
def f(x, a, b):
f.count += 1
return (x - a)**2 + b
f.count = 0
return f
def assert_valid_bracket(self, result, xp):
assert xp.all(
(result.xl < result.xm) & (result.xm < result.xr)
)
assert xp.all(
(result.fl >= result.fm) & (result.fr > result.fm)
| (result.fl > result.fm) & (result.fr > result.fm)
)
def get_kwargs(
self, *, xl0=None, xr0=None, factor=None, xmin=None, xmax=None, args=None
):
names = ("xl0", "xr0", "xmin", "xmax", "factor", "args")
return {
name: val for name, val in zip(names, (xl0, xr0, xmin, xmax, factor, args))
if val is not None
}
@pytest.mark.parametrize(
"seed",
(
307448016549685229886351382450158984917,
11650702770735516532954347931959000479,
113767103358505514764278732330028568336,
)
)
@pytest.mark.parametrize("use_xmin", (False, True))
@pytest.mark.parametrize("other_side", (False, True))
def test_nfev_expected(self, seed, use_xmin, other_side, xp):
rng = np.random.default_rng(seed)
args = (xp.asarray(0.), xp.asarray(0.)) # f(x) = x^2 with minimum at 0
# xl0, xm0, xr0 are chosen such that the initial bracket is to
# the right of the minimum, and the bracket will expand
# downhill towards zero.
xl0, d1, d2, factor = xp.asarray(rng.random(size=4) * [1e5, 10, 10, 5])
xm0 = xl0 + d1
xr0 = xm0 + d2
# Factor should be greater than one.
factor += 1
if use_xmin:
xmin = xp.asarray(-rng.random() * 5, dtype=xp.float64)
n = int(xp.ceil(xp.log(-(xl0 - xmin) / xmin) / xp.log(factor)))
lower = xmin + (xl0 - xmin)*factor**-n
middle = xmin + (xl0 - xmin)*factor**-(n-1)
upper = xmin + (xl0 - xmin)*factor**-(n-2) if n > 1 else xm0
# It may be the case the lower is below the minimum, but we still
# don't have a valid bracket.
if middle**2 > lower**2:
n += 1
lower, middle, upper = (
xmin + (xl0 - xmin)*factor**-n, lower, middle
)
else:
xmin = None
n = int(xp.ceil(xp.log(xl0 / d1) / xp.log(factor)))
lower = xl0 - d1*factor**n
middle = xl0 - d1*factor**(n-1) if n > 1 else xl0
upper = xl0 - d1*factor**(n-2) if n > 1 else xm0
# It may be the case the lower is below the minimum, but we still
# don't have a valid bracket.
if middle**2 > lower**2:
n += 1
lower, middle, upper = (
xl0 - d1*factor**n, lower, middle
)
f = self.init_f()
xmax = None
if other_side:
xl0, xm0, xr0 = -xr0, -xm0, -xl0
xmin, xmax = None, -xmin if xmin is not None else None
lower, middle, upper = -upper, -middle, -lower
kwargs = self.get_kwargs(
xl0=xl0, xr0=xr0, xmin=xmin, xmax=xmax, factor=factor, args=args
)
result = _bracket_minimum(f, xp.asarray(xm0), **kwargs)
# Check that `nfev` and `nit` have the correct relationship
assert result.nfev == result.nit + 3
# Check that `nfev` reports the correct number of function evaluations.
assert result.nfev == f.count
# Check that the number of iterations matches the theoretical value.
assert result.nit == n
# Compare reported bracket to theoretical bracket and reported function
# values to function evaluated at bracket.
xp_assert_close(result.xl, lower)
xp_assert_close(result.xm, middle)
xp_assert_close(result.xr, upper)
xp_assert_close(result.fl, f(lower, *args))
xp_assert_close(result.fm, f(middle, *args))
xp_assert_close(result.fr, f(upper, *args))
self.assert_valid_bracket(result, xp)
assert result.status == 0
assert result.success
def test_flags(self, xp):
# Test cases that should produce different status flags; show that all
# can be produced simultaneously
def f(xs, js):
funcs = [lambda x: (x - 1.5)**2,
lambda x: x,
lambda x: x,
lambda x: xp.asarray(xp.nan),
lambda x: x**2]
return [funcs[int(j)](x) for x, j in zip(xs, js)]
args = (xp.arange(5, dtype=xp.int64),)
xl0 = xp.asarray([-1.0, -1.0, -1.0, -1.0, 6.0])
xm0 = xp.asarray([0.0, 0.0, 0.0, 0.0, 4.0])
xr0 = xp.asarray([1.0, 1.0, 1.0, 1.0, 2.0])
xmin = xp.asarray([-xp.inf, -1.0, -xp.inf, -xp.inf, 8.0])
result = _bracket_minimum(f, xm0, xl0=xl0, xr0=xr0, xmin=xmin,
args=args, maxiter=3)
reference_flags = xp.asarray([eim._ECONVERGED, _ELIMITS,
eim._ECONVERR, eim._EVALUEERR,
eim._EINPUTERR], dtype=xp.int32)
xp_assert_equal(result.status, reference_flags)
@pytest.mark.parametrize("minimum", (0.622, [0.622, 0.623]))
@pytest.mark.parametrize("dtype", ("float16", "float32", "float64"))
@pytest.mark.parametrize("xmin", [-5, None])
@pytest.mark.parametrize("xmax", [5, None])
def test_dtypes(self, minimum, xmin, xmax, dtype, xp):
dtype = getattr(xp, dtype)
xmin = xmin if xmin is None else xp.asarray(xmin, dtype=dtype)
xmax = xmax if xmax is None else xp.asarray(xmax, dtype=dtype)
minimum = xp.asarray(minimum, dtype=dtype)
def f(x, minimum):
return xp.astype((x - minimum)**2, dtype)
xl0, xm0, xr0 = [-0.01, 0.0, 0.01]
result = _bracket_minimum(
f, xp.asarray(xm0, dtype=dtype), xl0=xp.asarray(xl0, dtype=dtype),
xr0=xp.asarray(xr0, dtype=dtype), xmin=xmin, xmax=xmax, args=(minimum, )
)
assert xp.all(result.success)
assert result.xl.dtype == result.xm.dtype == result.xr.dtype == dtype
assert result.fl.dtype == result.fm.dtype == result.fr.dtype == dtype
@pytest.mark.skip_xp_backends(np_only=True, reason="str/object arrays")
def test_input_validation(self, xp):
# Test input validation for appropriate error messages
message = '`func` must be callable.'
with pytest.raises(ValueError, match=message):
_bracket_minimum(None, -4, xl0=4)
message = '...must be numeric and real.'
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(4+1j))
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xl0=4+1j)
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xr0=4+1j)
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xmin=4+1j)
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xmax=4+1j)
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), factor=4+1j)
message = "All elements of `factor` must be greater than 1."
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x, xp.asarray(-4), factor=0.5)
message = "Array shapes are incompatible for broadcasting."
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray([-2, -3]), xl0=[-3, -4, -5])
message = '`maxiter` must be a non-negative integer.'
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xr0=4, maxiter=1.5)
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xr0=4, maxiter=-1)
with pytest.raises(ValueError, match=message):
_bracket_minimum(lambda x: x**2, xp.asarray(-4), xr0=4, maxiter="ekki")
@pytest.mark.parametrize("xl0", [0.0, None])
@pytest.mark.parametrize("xm0", (0.05, 0.1, 0.15))
@pytest.mark.parametrize("xr0", (0.2, 0.4, 0.6, None))
# Minimum is ``a`` for each tuple ``(a, b)`` below. Tests cases where minimum
# is within, or at varying distances to the left or right of the initial
# bracket.
@pytest.mark.parametrize(
"args",
(
(1.2, 0), (-0.5, 0), (0.1, 0), (0.2, 0), (3.6, 0), (21.4, 0),
(121.6, 0), (5764.1, 0), (-6.4, 0), (-12.9, 0), (-146.2, 0)
)
)
def test_scalar_no_limits(self, xl0, xm0, xr0, args, xp):
f = self.init_f()
kwargs = self.get_kwargs(xl0=xl0, xr0=xr0, args=tuple(map(xp.asarray, args)))
result = _bracket_minimum(f, xp.asarray(xm0, dtype=xp.float64), **kwargs)
self.assert_valid_bracket(result, xp)
assert result.status == 0
assert result.success
assert result.nfev == f.count
@pytest.mark.parametrize(
# xmin is set at 0.0 in all cases.
"xl0,xm0,xr0,xmin",
(
# Initial bracket at varying distances from the xmin.
(0.5, 0.75, 1.0, 0.0),
(1.0, 2.5, 4.0, 0.0),
(2.0, 4.0, 6.0, 0.0),
(12.0, 16.0, 20.0, 0.0),
# Test default initial left endpoint selection. It should not
# be below xmin.
(None, 0.75, 1.0, 0.0),
(None, 2.5, 4.0, 0.0),
(None, 4.0, 6.0, 0.0),
(None, 16.0, 20.0, 0.0),
)
)
@pytest.mark.parametrize(
"args", (
(0.0, 0.0), # Minimum is directly at xmin.
(1e-300, 0.0), # Minimum is extremely close to xmin.
(1e-20, 0.0), # Minimum is very close to xmin.
# Minimum at varying distances from xmin.
(0.1, 0.0),
(0.2, 0.0),
(0.4, 0.0)
)
)
def test_scalar_with_limit_left(self, xl0, xm0, xr0, xmin, args, xp):
f = self.init_f()
kwargs = self.get_kwargs(xl0=xl0, xr0=xr0, xmin=xmin,
args=tuple(map(xp.asarray, args)))
result = _bracket_minimum(f, xp.asarray(xm0), **kwargs)
self.assert_valid_bracket(result, xp)
assert result.status == 0
assert result.success
assert result.nfev == f.count
@pytest.mark.parametrize(
#xmax is set to 1.0 in all cases.
"xl0,xm0,xr0,xmax",
(
# Bracket at varying distances from xmax.
(0.2, 0.3, 0.4, 1.0),
(0.05, 0.075, 0.1, 1.0),
(-0.2, -0.1, 0.0, 1.0),
(-21.2, -17.7, -14.2, 1.0),
# Test default right endpoint selection. It should not exceed xmax.
(0.2, 0.3, None, 1.0),
(0.05, 0.075, None, 1.0),
(-0.2, -0.1, None, 1.0),
(-21.2, -17.7, None, 1.0),
)
)
@pytest.mark.parametrize(
"args", (
(0.9999999999999999, 0.0), # Minimum very close to xmax.
# Minimum at varying distances from xmax.
(0.9, 0.0),
(0.7, 0.0),
(0.5, 0.0)
)
)
def test_scalar_with_limit_right(self, xl0, xm0, xr0, xmax, args, xp):
f = self.init_f()
args = tuple(xp.asarray(arg, dtype=xp.float64) for arg in args)
kwargs = self.get_kwargs(xl0=xl0, xr0=xr0, xmax=xmax, args=args)
result = _bracket_minimum(f, xp.asarray(xm0, dtype=xp.float64), **kwargs)
self.assert_valid_bracket(result, xp)
assert result.status == 0
assert result.success
assert result.nfev == f.count
@pytest.mark.parametrize(
"xl0,xm0,xr0,xmin,xmax,args",
(
( # Case 1:
# Initial bracket.
0.2,
0.3,
0.4,
# Function slopes down to the right from the bracket to a minimum
# at 1.0. xmax is also at 1.0
None,
1.0,
(1.0, 0.0)
),
( # Case 2:
# Initial bracket.
1.4,
1.95,
2.5,
# Function slopes down to the left from the bracket to a minimum at
# 0.3 with xmin set to 0.3.
0.3,
None,
(0.3, 0.0)
),
(
# Case 3:
# Initial bracket.
2.6,
3.25,
3.9,
# Function slopes down and to the right to a minimum at 99.4 with xmax
# at 99.4. Tests case where minimum is at xmax relatively further from
# the bracket.
None,
99.4,
(99.4, 0)
),
(
# Case 4:
# Initial bracket.
4,
4.5,
5,
# Function slopes down and to the left away from the bracket with a
# minimum at -26.3 with xmin set to -26.3. Tests case where minimum is
# at xmin relatively far from the bracket.
-26.3,
None,
(-26.3, 0)
),
(
# Case 5:
# Similar to Case 1 above, but tests default values of xl0 and xr0.
None,
0.3,
None,
None,
1.0,
(1.0, 0.0)
),
( # Case 6:
# Similar to Case 2 above, but tests default values of xl0 and xr0.
None,
1.95,
None,
0.3,
None,
(0.3, 0.0)
),
(
# Case 7:
# Similar to Case 3 above, but tests default values of xl0 and xr0.
None,
3.25,
None,
None,
99.4,
(99.4, 0)
),
(
# Case 8:
# Similar to Case 4 above, but tests default values of xl0 and xr0.
None,
4.5,
None,
-26.3,
None,
(-26.3, 0)
),
)
)
def test_minimum_at_boundary_point(self, xl0, xm0, xr0, xmin, xmax, args, xp):
f = self.init_f()
kwargs = self.get_kwargs(xr0=xr0, xmin=xmin, xmax=xmax,
args=tuple(map(xp.asarray, args)))
result = _bracket_minimum(f, xp.asarray(xm0), **kwargs)
assert result.status == -1
assert args[0] in (result.xl, result.xr)
assert result.nfev == f.count
@pytest.mark.parametrize('shape', [tuple(), (12, ), (3, 4), (3, 2, 2)])
def test_vectorization(self, shape, xp):
# Test for correct functionality, output shapes, and dtypes for
# various input shapes.
a = np.linspace(-0.05, 1.05, 12).reshape(shape) if shape else 0.6
args = (a, 0.)
maxiter = 10
@np.vectorize
def bracket_minimum_single(xm0, xl0, xr0, xmin, xmax, factor, a):
return _bracket_minimum(self.init_f(), xm0, xl0=xl0, xr0=xr0, xmin=xmin,
xmax=xmax, factor=factor, maxiter=maxiter,
args=(a, 0.0))
f = self.init_f()
rng = np.random.default_rng(2348234)
xl0 = -rng.random(size=shape)
xr0 = rng.random(size=shape)
xm0 = xl0 + rng.random(size=shape) * (xr0 - xl0)
xmin, xmax = 1e3*xl0, 1e3*xr0
if shape: # make some elements un
i = rng.random(size=shape) > 0.5
xmin[i], xmax[i] = -np.inf, np.inf
factor = rng.random(size=shape) + 1.5
refs = bracket_minimum_single(xm0, xl0, xr0, xmin, xmax, factor, a).ravel()
args = tuple(xp.asarray(arg, dtype=xp.float64) for arg in args)
res = _bracket_minimum(f, xp.asarray(xm0), xl0=xp.asarray(xl0),
xr0=xp.asarray(xr0), xmin=xp.asarray(xmin),
xmax=xp.asarray(xmax), factor=xp.asarray(factor),
args=args, maxiter=maxiter)
attrs = ['xl', 'xm', 'xr', 'fl', 'fm', 'fr', 'success', 'nfev', 'nit']
for attr in attrs:
ref_attr = [xp.asarray(getattr(ref, attr)) for ref in refs]
res_attr = getattr(res, attr)
xp_assert_close(xp_ravel(res_attr, xp=xp), xp.stack(ref_attr))
assert res_attr.shape == shape
assert res.success.dtype == xp.bool
if shape:
assert xp.all(res.success[1:-1])
assert res.status.dtype == xp.int32
assert res.nfev.dtype == xp.int32
assert res.nit.dtype == xp.int32
assert xp.max(res.nit) == f.count - 3
self.assert_valid_bracket(res, xp)
xp_assert_close(res.fl, f(res.xl, *args))
xp_assert_close(res.fm, f(res.xm, *args))
xp_assert_close(res.fr, f(res.xr, *args))
def test_special_cases(self, xp):
# Test edge cases and other special cases.
# Test that integers are not passed to `f`
# (otherwise this would overflow)
def f(x):
assert xp.isdtype(x.dtype, "numeric")
return x ** 98 - 1
result = _bracket_minimum(f, xp.asarray(-7., dtype=xp.float64), xr0=5)
assert result.success
# Test maxiter = 0. Should do nothing to bracket.
def f(x):
return x**2 - 10
xl0, xm0, xr0 = xp.asarray(-3.), xp.asarray(-1.), xp.asarray(2.)
result = _bracket_minimum(f, xm0, xl0=xl0, xr0=xr0, maxiter=0)
xp_assert_equal(result.xl, xl0)
xp_assert_equal(result.xm, xm0)
xp_assert_equal(result.xr, xr0)
# Test scalar `args` (not in tuple)
def f(x, c):
return c*x**2 - 1
result = _bracket_minimum(f, xp.asarray(-1.), args=xp.asarray(3.))
assert result.success
xp_assert_close(result.fl, f(result.xl, 3))
# Initial bracket is valid.
f = self.init_f()
xl0, xm0, xr0 = xp.asarray(-1.0), xp.asarray(-0.2), xp.asarray(1.0)
args = (xp.asarray(0.), xp.asarray(0.))
result = _bracket_minimum(f, xm0, xl0=xl0, xr0=xr0, args=args)
assert f.count == 3
xp_assert_equal(result.xl, xl0)
xp_assert_equal(result.xm , xm0)
xp_assert_equal(result.xr, xr0)
xp_assert_equal(result.fl, f(xl0, *args))
xp_assert_equal(result.fm, f(xm0, *args))
xp_assert_equal(result.fr, f(xr0, *args))
def test_gh_20562_left(self, xp):
# Regression test for https://github.com/scipy/scipy/issues/20562
# minimum of f in [xmin, xmax] is at xmin.
xmin, xmax = xp.asarray(0.21933608), xp.asarray(1.39713606)
def f(x):
log_a, log_b = xp.log(xmin), xp.log(xmax)
return -((log_b - log_a)*x)**-1
result = _bracket_minimum(f, xp.asarray(0.5535723499480897), xmin=xmin,
xmax=xmax)
xp_assert_close(result.xl, xmin)
def test_gh_20562_right(self, xp):
# Regression test for https://github.com/scipy/scipy/issues/20562
# minimum of f in [xmin, xmax] is at xmax.
xmin, xmax = xp.asarray(-1.39713606), xp.asarray(-0.21933608)
def f(x):
log_a, log_b = xp.log(-xmax), xp.log(-xmin)
return ((log_b - log_a)*x)**-1
result = _bracket_minimum(f, xp.asarray(-0.5535723499480897),
xmin=xmin, xmax=xmax)
xp_assert_close(result.xr, xmax)

View file

@ -0,0 +1,982 @@
import math
import pytest
import numpy as np
from copy import deepcopy
from scipy import stats, special
import scipy._lib._elementwise_iterative_method as eim
import scipy._lib.array_api_extra as xpx
from scipy._lib._array_api import array_namespace, is_cupy, is_numpy, xp_ravel, xp_size
from scipy._lib._array_api_no_0d import (xp_assert_close, xp_assert_equal,
xp_assert_less)
from scipy.optimize.elementwise import find_minimum, find_root
from scipy.optimize._tstutils import _CHANDRUPATLA_TESTS
from itertools import permutations
def _vectorize(xp):
# xp-compatible version of np.vectorize
# assumes arguments are all arrays of the same shape
def decorator(f):
def wrapped(*arg_arrays):
shape = arg_arrays[0].shape
arg_arrays = [xp_ravel(arg_array, xp=xp) for arg_array in arg_arrays]
res = []
for i in range(math.prod(shape)):
arg_scalars = [arg_array[i] for arg_array in arg_arrays]
res.append(f(*arg_scalars))
return res
return wrapped
return decorator
# These tests were originally written for the private `optimize._chandrupatla`
# interfaces, but now we want the tests to check the behavior of the public
# `optimize.elementwise` interfaces. Therefore, rather than importing
# `_chandrupatla`/`_chandrupatla_minimize` from `_chandrupatla.py`, we import
# `find_root`/`find_minimum` from `optimize.elementwise` and wrap those
# functions to conform to the private interface. This may look a little strange,
# since it effectively just inverts the interface transformation done within the
# `find_root`/`find_minimum` functions, but it allows us to run the original,
# unmodified tests on the public interfaces, simplifying the PR that adds
# the public interfaces. We'll refactor this when we want to @parametrize the
# tests over multiple `method`s.
def _wrap_chandrupatla(func):
def _chandrupatla_wrapper(f, *bracket, **kwargs):
# avoid passing arguments to `find_minimum` to this function
tol_keys = {'xatol', 'xrtol', 'fatol', 'frtol'}
tolerances = {key: kwargs.pop(key) for key in tol_keys if key in kwargs}
_callback = kwargs.pop('callback', None)
if callable(_callback):
def callback(res):
if func == find_root:
res.xl, res.xr = res.bracket
res.fl, res.fr = res.f_bracket
else:
res.xl, res.xm, res.xr = res.bracket
res.fl, res.fm, res.fr = res.f_bracket
res.fun = res.f_x
del res.bracket
del res.f_bracket
del res.f_x
return _callback(res)
else:
callback = _callback
res = func(f, bracket, tolerances=tolerances, callback=callback, **kwargs)
if func == find_root:
res.xl, res.xr = res.bracket
res.fl, res.fr = res.f_bracket
else:
res.xl, res.xm, res.xr = res.bracket
res.fl, res.fm, res.fr = res.f_bracket
res.fun = res.f_x
del res.bracket
del res.f_bracket
del res.f_x
return res
return _chandrupatla_wrapper
_chandrupatla_minimize = _wrap_chandrupatla(find_minimum)
def f1(x):
return 100*(1 - x**3.)**2 + (1-x**2.) + 2*(1-x)**2.
def f2(x):
return 5 + (x - 2.)**6
def f3(x):
xp = array_namespace(x)
return xp.exp(x) - 5*x
def f4(x):
return x**5. - 5*x**3. - 20.*x + 5.
def f5(x):
return 8*x**3 - 2*x**2 - 7*x + 3
def _bracket_minimum(func, x1, x2):
phi = 1.61803398875
maxiter = 100
f1 = func(x1)
f2 = func(x2)
step = x2 - x1
x1, x2, f1, f2, step = ((x2, x1, f2, f1, -step) if f2 > f1
else (x1, x2, f1, f2, step))
for i in range(maxiter):
step *= phi
x3 = x2 + step
f3 = func(x3)
if f3 < f2:
x1, x2, f1, f2 = x2, x3, f2, f3
else:
break
return x1, x2, x3, f1, f2, f3
cases = [
(f1, -1, 11),
(f1, -2, 13),
(f1, -4, 13),
(f1, -8, 15),
(f1, -16, 16),
(f1, -32, 19),
(f1, -64, 20),
(f1, -128, 21),
(f1, -256, 21),
(f1, -512, 19),
(f1, -1024, 24),
(f2, -1, 8),
(f2, -2, 6),
(f2, -4, 6),
(f2, -8, 7),
(f2, -16, 8),
(f2, -32, 8),
(f2, -64, 9),
(f2, -128, 11),
(f2, -256, 13),
(f2, -512, 12),
(f2, -1024, 13),
(f3, -1, 11),
(f3, -2, 11),
(f3, -4, 11),
(f3, -8, 10),
(f3, -16, 14),
(f3, -32, 12),
(f3, -64, 15),
(f3, -128, 18),
(f3, -256, 18),
(f3, -512, 19),
(f3, -1024, 19),
(f4, -0.05, 9),
(f4, -0.10, 11),
(f4, -0.15, 11),
(f4, -0.20, 11),
(f4, -0.25, 11),
(f4, -0.30, 9),
(f4, -0.35, 9),
(f4, -0.40, 9),
(f4, -0.45, 10),
(f4, -0.50, 10),
(f4, -0.55, 10),
(f5, -0.05, 6),
(f5, -0.10, 7),
(f5, -0.15, 8),
(f5, -0.20, 10),
(f5, -0.25, 9),
(f5, -0.30, 8),
(f5, -0.35, 7),
(f5, -0.40, 7),
(f5, -0.45, 9),
(f5, -0.50, 9),
(f5, -0.55, 8)
]
@pytest.mark.skip_xp_backends('dask.array', reason='no take_along_axis')
@pytest.mark.skip_xp_backends('jax.numpy',
reason='JAX arrays do not support item assignment.')
@pytest.mark.skip_xp_backends('array_api_strict',
reason='Currently uses fancy indexing assignment.')
class TestChandrupatlaMinimize:
def f(self, x, loc):
xp = array_namespace(x, loc)
res = -xp.exp(-1/2 * (x-loc)**2) / (2*xp.pi)**0.5
return xp.asarray(res, dtype=x.dtype)[()]
@pytest.mark.parametrize('dtype', ('float32', 'float64'))
@pytest.mark.parametrize('loc', [0.6, np.linspace(-1.05, 1.05, 10)])
def test_basic(self, loc, xp, dtype):
# Find mode of normal distribution. Compare mode against location
# parameter and value of pdf at mode against expected pdf.
rtol = {'float32': 5e-3, 'float64': 5e-7}[dtype]
dtype = getattr(xp, dtype)
bracket = (xp.asarray(xi, dtype=dtype) for xi in (-5, 0, 5))
loc = xp.asarray(loc, dtype=dtype)
fun = xp.broadcast_to(xp.asarray(-stats.norm.pdf(0), dtype=dtype), loc.shape)
res = _chandrupatla_minimize(self.f, *bracket, args=(loc,))
xp_assert_close(res.x, loc, rtol=rtol)
xp_assert_equal(res.fun, fun)
@pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
def test_vectorization(self, shape, xp):
# Test for correct functionality, output shapes, and dtypes for various
# input shapes.
loc = xp.linspace(-0.05, 1.05, 12).reshape(shape) if shape else xp.asarray(0.6)
args = (loc,)
bracket = xp.asarray(-5.), xp.asarray(0.), xp.asarray(5.)
@_vectorize(xp)
def chandrupatla_single(loc_single):
return _chandrupatla_minimize(self.f, *bracket, args=(loc_single,))
def f(*args, **kwargs):
f.f_evals += 1
return self.f(*args, **kwargs)
f.f_evals = 0
res = _chandrupatla_minimize(f, *bracket, args=args)
refs = chandrupatla_single(loc)
attrs = ['x', 'fun', 'success', 'status', 'nfev', 'nit',
'xl', 'xm', 'xr', 'fl', 'fm', 'fr']
for attr in attrs:
ref_attr = xp.stack([getattr(ref, attr) for ref in refs])
res_attr = xp_ravel(getattr(res, attr))
xp_assert_equal(res_attr, ref_attr)
assert getattr(res, attr).shape == shape
xp_assert_equal(res.fun, self.f(res.x, *args))
xp_assert_equal(res.fl, self.f(res.xl, *args))
xp_assert_equal(res.fm, self.f(res.xm, *args))
xp_assert_equal(res.fr, self.f(res.xr, *args))
assert xp.max(res.nfev) == f.f_evals
assert xp.max(res.nit) == f.f_evals - 3
assert xp.isdtype(res.success.dtype, 'bool')
assert xp.isdtype(res.status.dtype, 'integral')
assert xp.isdtype(res.nfev.dtype, 'integral')
assert xp.isdtype(res.nit.dtype, 'integral')
def test_flags(self, xp):
# Test cases that should produce different status flags; show that all
# can be produced simultaneously.
def f(xs, js):
funcs = [lambda x: (x - 2.5) ** 2,
lambda x: x - 10,
lambda x: (x - 2.5) ** 4,
lambda x: xp.full_like(x, xp.asarray(xp.nan))]
res = []
for i in range(xp_size(js)):
x = xs[i, ...]
j = int(xp_ravel(js)[i])
res.append(funcs[j](x))
return xp.stack(res)
args = (xp.arange(4, dtype=xp.int64),)
bracket = (xp.asarray([0]*4, dtype=xp.float64),
xp.asarray([2]*4, dtype=xp.float64),
xp.asarray([np.pi]*4, dtype=xp.float64))
res = _chandrupatla_minimize(f, *bracket, args=args, maxiter=10)
ref_flags = xp.asarray([eim._ECONVERGED, eim._ESIGNERR, eim._ECONVERR,
eim._EVALUEERR], dtype=xp.int32)
xp_assert_equal(res.status, ref_flags)
def test_convergence(self, xp):
# Test that the convergence tolerances behave as expected
rng = np.random.default_rng(2585255913088665241)
p = xp.asarray(rng.random(size=3))
bracket = (xp.asarray(-5, dtype=xp.float64), xp.asarray(0), xp.asarray(5))
args = (p,)
kwargs0 = dict(args=args, xatol=0, xrtol=0, fatol=0, frtol=0)
kwargs = kwargs0.copy()
kwargs['xatol'] = 1e-3
res1 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
j1 = xp.abs(res1.xr - res1.xl)
tol = xp.asarray(4*kwargs['xatol'], dtype=p.dtype)
xp_assert_less(j1, xp.full((3,), tol, dtype=p.dtype))
kwargs['xatol'] = 1e-6
res2 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
j2 = xp.abs(res2.xr - res2.xl)
tol = xp.asarray(4*kwargs['xatol'], dtype=p.dtype)
xp_assert_less(j2, xp.full((3,), tol, dtype=p.dtype))
xp_assert_less(j2, j1)
kwargs = kwargs0.copy()
kwargs['xrtol'] = 1e-3
res1 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
j1 = xp.abs(res1.xr - res1.xl)
tol = xp.asarray(4*kwargs['xrtol']*xp.abs(res1.x), dtype=p.dtype)
xp_assert_less(j1, tol)
kwargs['xrtol'] = 1e-6
res2 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
j2 = xp.abs(res2.xr - res2.xl)
tol = xp.asarray(4*kwargs['xrtol']*xp.abs(res2.x), dtype=p.dtype)
xp_assert_less(j2, tol)
xp_assert_less(j2, j1)
kwargs = kwargs0.copy()
kwargs['fatol'] = 1e-3
res1 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
h1 = xp.abs(res1.fl - 2 * res1.fm + res1.fr)
tol = xp.asarray(2*kwargs['fatol'], dtype=p.dtype)
xp_assert_less(h1, xp.full((3,), tol, dtype=p.dtype))
kwargs['fatol'] = 1e-6
res2 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
h2 = xp.abs(res2.fl - 2 * res2.fm + res2.fr)
tol = xp.asarray(2*kwargs['fatol'], dtype=p.dtype)
xp_assert_less(h2, xp.full((3,), tol, dtype=p.dtype))
xp_assert_less(h2, h1)
kwargs = kwargs0.copy()
kwargs['frtol'] = 1e-3
res1 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
h1 = xp.abs(res1.fl - 2 * res1.fm + res1.fr)
tol = xp.asarray(2*kwargs['frtol']*xp.abs(res1.fun), dtype=p.dtype)
xp_assert_less(h1, tol)
kwargs['frtol'] = 1e-6
res2 = _chandrupatla_minimize(self.f, *bracket, **kwargs)
h2 = xp.abs(res2.fl - 2 * res2.fm + res2.fr)
tol = xp.asarray(2*kwargs['frtol']*abs(res2.fun), dtype=p.dtype)
xp_assert_less(h2, tol)
xp_assert_less(h2, h1)
def test_maxiter_callback(self, xp):
# Test behavior of `maxiter` parameter and `callback` interface
loc = xp.asarray(0.612814)
bracket = (xp.asarray(-5), xp.asarray(0), xp.asarray(5))
maxiter = 5
res = _chandrupatla_minimize(self.f, *bracket, args=(loc,),
maxiter=maxiter)
assert not xp.any(res.success)
assert xp.all(res.nfev == maxiter+3)
assert xp.all(res.nit == maxiter)
def callback(res):
callback.iter += 1
callback.res = res
assert hasattr(res, 'x')
if callback.iter == 0:
# callback is called once with initial bracket
assert (res.xl, res.xm, res.xr) == bracket
else:
changed_xr = (res.xl == callback.xl) & (res.xr != callback.xr)
changed_xl = (res.xl != callback.xl) & (res.xr == callback.xr)
assert xp.all(changed_xr | changed_xl)
callback.xl = res.xl
callback.xr = res.xr
assert res.status == eim._EINPROGRESS
xp_assert_equal(self.f(res.xl, loc), res.fl)
xp_assert_equal(self.f(res.xm, loc), res.fm)
xp_assert_equal(self.f(res.xr, loc), res.fr)
xp_assert_equal(self.f(res.x, loc), res.fun)
if callback.iter == maxiter:
raise StopIteration
callback.xl = xp.nan
callback.xr = xp.nan
callback.iter = -1 # callback called once before first iteration
callback.res = None
res2 = _chandrupatla_minimize(self.f, *bracket, args=(loc,),
callback=callback)
# terminating with callback is identical to terminating due to maxiter
# (except for `status`)
for key in res.keys():
if key == 'status':
assert res[key] == eim._ECONVERR
# assert callback.res[key] == eim._EINPROGRESS
assert res2[key] == eim._ECALLBACK
else:
assert res2[key] == callback.res[key] == res[key]
@pytest.mark.parametrize('case', cases)
def test_nit_expected(self, case, xp):
# Test that `_chandrupatla` implements Chandrupatla's algorithm:
# in all 55 test cases, the number of iterations performed
# matches the number reported in the original paper.
func, x1, nit = case
# Find bracket using the algorithm in the paper
step = 0.2
x2 = x1 + step
x1, x2, x3, f1, f2, f3 = _bracket_minimum(func, x1, x2)
# Use tolerances from original paper
xatol = 0.0001
fatol = 0.000001
xrtol = 1e-16
frtol = 1e-16
bracket = xp.asarray(x1), xp.asarray(x2), xp.asarray(x3, dtype=xp.float64)
res = _chandrupatla_minimize(func, *bracket, xatol=xatol,
fatol=fatol, xrtol=xrtol, frtol=frtol)
xp_assert_equal(res.nit, xp.asarray(nit, dtype=xp.int32))
@pytest.mark.parametrize("loc", (0.65, [0.65, 0.7]))
@pytest.mark.parametrize("dtype", ('float16', 'float32', 'float64'))
def test_dtype(self, loc, dtype, xp):
# Test that dtypes are preserved
dtype = getattr(xp, dtype)
loc = xp.asarray(loc, dtype=dtype)
bracket = (xp.asarray(-3, dtype=dtype),
xp.asarray(1, dtype=dtype),
xp.asarray(5, dtype=dtype))
def f(x, loc):
assert x.dtype == dtype
return xp.astype((x - loc)**2, dtype)
res = _chandrupatla_minimize(f, *bracket, args=(loc,))
assert res.x.dtype == dtype
xp_assert_close(res.x, loc, rtol=math.sqrt(xp.finfo(dtype).eps))
def test_input_validation(self, xp):
# Test input validation for appropriate error messages
message = '`func` must be callable.'
bracket = xp.asarray(-4), xp.asarray(0), xp.asarray(4)
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(None, *bracket)
message = 'Abscissae and function output must be real numbers.'
bracket = xp.asarray(-4 + 1j), xp.asarray(0), xp.asarray(4)
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket)
message = "...be broadcast..."
bracket = xp.asarray([-2, -3]), xp.asarray([0, 0]), xp.asarray([3, 4, 5])
# raised by `np.broadcast, but the traceback is readable IMO
with pytest.raises((ValueError, RuntimeError), match=message):
_chandrupatla_minimize(lambda x: x, *bracket)
message = "The shape of the array returned by `func` must be the same"
bracket = xp.asarray([-3, -3]), xp.asarray([0, 0]), xp.asarray([5, 5])
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: [x[0, ...], x[1, ...], x[1, ...]],
*bracket)
message = 'Tolerances must be non-negative scalars.'
bracket = xp.asarray(-4), xp.asarray(0), xp.asarray(4)
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, xatol=-1)
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, xrtol=xp.nan)
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, fatol='ekki')
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, frtol=xp.nan)
message = '`maxiter` must be a non-negative integer.'
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, maxiter=1.5)
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, maxiter=-1)
message = '`callback` must be callable.'
with pytest.raises(ValueError, match=message):
_chandrupatla_minimize(lambda x: x, *bracket, callback='shrubbery')
def test_bracket_order(self, xp):
# Confirm that order of points in bracket doesn't
loc = xp.linspace(-1, 1, 6)[:, xp.newaxis]
brackets = xp.asarray(list(permutations([-5, 0, 5]))).T
res = _chandrupatla_minimize(self.f, *brackets, args=(loc,))
assert xp.all(xpx.isclose(res.x, loc) | (res.fun == self.f(loc, loc)))
ref = res.x[:, 0] # all columns should be the same
xp_assert_close(*xp.broadcast_arrays(res.x.T, ref), rtol=1e-15)
def test_special_cases(self, xp):
# Test edge cases and other special cases
# Test that integers are not passed to `f`
def f(x):
assert xp.isdtype(x.dtype, "real floating")
return (x - 1)**2
bracket = xp.asarray(-7), xp.asarray(0), xp.asarray(8)
with np.errstate(invalid='ignore'):
res = _chandrupatla_minimize(f, *bracket, fatol=0, frtol=0)
assert res.success
xp_assert_close(res.x, xp.asarray(1.), rtol=1e-3)
xp_assert_close(res.fun, xp.asarray(0.), atol=1e-200)
# Test that if all elements of bracket equal minimizer, algorithm
# reports convergence
def f(x):
return (x-1)**2
bracket = xp.asarray(1), xp.asarray(1), xp.asarray(1)
res = _chandrupatla_minimize(f, *bracket)
assert res.success
xp_assert_equal(res.x, xp.asarray(1.))
# Test maxiter = 0. Should do nothing to bracket.
def f(x):
return (x-1)**2
bracket = xp.asarray(-3), xp.asarray(1.1), xp.asarray(5)
res = _chandrupatla_minimize(f, *bracket, maxiter=0)
assert res.xl, res.xr == bracket
assert res.nit == 0
assert res.nfev == 3
assert res.status == -2
assert res.x == 1.1 # best so far
# Test scalar `args` (not in tuple)
def f(x, c):
return (x-c)**2 - 1
bracket = xp.asarray(-1), xp.asarray(0), xp.asarray(1)
c = xp.asarray(1/3)
res = _chandrupatla_minimize(f, *bracket, args=(c,))
xp_assert_close(res.x, c)
# Test zero tolerances
def f(x):
return -xp.sin(x)
bracket = xp.asarray(0), xp.asarray(1), xp.asarray(xp.pi)
res = _chandrupatla_minimize(f, *bracket, xatol=0, xrtol=0, fatol=0, frtol=0)
assert res.success
# found a minimum exactly (according to floating point arithmetic)
assert res.xl < res.xm < res.xr
assert f(res.xl) == f(res.xm) == f(res.xr)
@pytest.mark.skip_xp_backends('dask.array', reason='boolean indexing assignment')
@pytest.mark.skip_xp_backends('array_api_strict',
reason='Currently uses fancy indexing assignment.')
@pytest.mark.skip_xp_backends('jax.numpy',
reason='JAX arrays do not support item assignment.')
class TestFindRoot:
def f(self, q, p):
return special.ndtr(q) - p
@pytest.mark.parametrize('p', [0.6, np.linspace(-0.05, 1.05, 10)])
def test_basic(self, p, xp):
# Invert distribution CDF and compare against distribution `ppf`
a, b = xp.asarray(-5.), xp.asarray(5.)
res = find_root(self.f, (a, b), args=(xp.asarray(p),))
ref = xp.asarray(stats.norm().ppf(p), dtype=xp.asarray(p).dtype)
xp_assert_close(res.x, ref)
@pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
def test_vectorization(self, shape, xp):
# Test for correct functionality, output shapes, and dtypes for various
# input shapes.
p = (np.linspace(-0.05, 1.05, 12).reshape(shape) if shape
else np.float64(0.6))
p_xp = xp.asarray(p)
args_xp = (p_xp,)
dtype = p_xp.dtype
@np.vectorize
def find_root_single(p):
return find_root(self.f, (-5, 5), args=(p,))
def f(*args, **kwargs):
f.f_evals += 1
return self.f(*args, **kwargs)
f.f_evals = 0
bracket = xp.asarray(-5., dtype=xp.float64), xp.asarray(5., dtype=xp.float64)
res = find_root(f, bracket, args=args_xp)
refs = find_root_single(p).ravel()
ref_x = [ref.x for ref in refs]
ref_x = xp.reshape(xp.asarray(ref_x, dtype=dtype), shape)
xp_assert_close(res.x, ref_x)
ref_f = [ref.f_x for ref in refs]
ref_f = xp.reshape(xp.asarray(ref_f, dtype=dtype), shape)
xp_assert_close(res.f_x, ref_f, atol=1e-15)
xp_assert_equal(res.f_x, self.f(res.x, *args_xp))
ref_success = [bool(ref.success) for ref in refs]
ref_success = xp.reshape(xp.asarray(ref_success, dtype=xp.bool), shape)
xp_assert_equal(res.success, ref_success)
ref_status = [ref.status for ref in refs]
ref_status = xp.reshape(xp.asarray(ref_status, dtype=xp.int32), shape)
xp_assert_equal(res.status, ref_status)
ref_nfev = [ref.nfev for ref in refs]
ref_nfev = xp.reshape(xp.asarray(ref_nfev, dtype=xp.int32), shape)
if is_numpy(xp):
xp_assert_equal(res.nfev, ref_nfev)
assert xp.max(res.nfev) == f.f_evals
else: # different backend may lead to different nfev
assert res.nfev.shape == shape
assert res.nfev.dtype == xp.int32
ref_nit = [ref.nit for ref in refs]
ref_nit = xp.reshape(xp.asarray(ref_nit, dtype=xp.int32), shape)
if is_numpy(xp):
xp_assert_equal(res.nit, ref_nit)
assert xp.max(res.nit) == f.f_evals-2
else:
assert res.nit.shape == shape
assert res.nit.dtype == xp.int32
ref_xl = [ref.bracket[0] for ref in refs]
ref_xl = xp.reshape(xp.asarray(ref_xl, dtype=dtype), shape)
xp_assert_close(res.bracket[0], ref_xl)
ref_xr = [ref.bracket[1] for ref in refs]
ref_xr = xp.reshape(xp.asarray(ref_xr, dtype=dtype), shape)
xp_assert_close(res.bracket[1], ref_xr)
xp_assert_less(res.bracket[0], res.bracket[1])
finite = xp.isfinite(res.x)
assert xp.all((res.x[finite] == res.bracket[0][finite])
| (res.x[finite] == res.bracket[1][finite]))
# PyTorch and CuPy don't solve to the same accuracy as NumPy - that's OK.
atol = 1e-15 if is_numpy(xp) else 1e-9
ref_fl = [ref.f_bracket[0] for ref in refs]
ref_fl = xp.reshape(xp.asarray(ref_fl, dtype=dtype), shape)
xp_assert_close(res.f_bracket[0], ref_fl, atol=atol)
xp_assert_equal(res.f_bracket[0], self.f(res.bracket[0], *args_xp))
ref_fr = [ref.f_bracket[1] for ref in refs]
ref_fr = xp.reshape(xp.asarray(ref_fr, dtype=dtype), shape)
xp_assert_close(res.f_bracket[1], ref_fr, atol=atol)
xp_assert_equal(res.f_bracket[1], self.f(res.bracket[1], *args_xp))
assert xp.all(xp.abs(res.f_x[finite]) ==
xp.minimum(xp.abs(res.f_bracket[0][finite]),
xp.abs(res.f_bracket[1][finite])))
def test_flags(self, xp):
# Test cases that should produce different status flags; show that all
# can be produced simultaneously.
def f(xs, js):
# Note that full_like and int(j) shouldn't really be required. CuPy
# is just really picky here, so I'm making it a special case to
# make sure the other backends work when the user is less careful.
assert js.dtype == xp.int64
if is_cupy(xp):
funcs = [lambda x: x - 2.5,
lambda x: x - 10,
lambda x: (x - 0.1)**3,
lambda x: xp.full_like(x, xp.asarray(xp.nan))]
return [funcs[int(j)](x) for x, j in zip(xs, js)]
funcs = [lambda x: x - 2.5,
lambda x: x - 10,
lambda x: (x - 0.1) ** 3,
lambda x: xp.nan]
return [funcs[j](x) for x, j in zip(xs, js)]
args = (xp.arange(4, dtype=xp.int64),)
a, b = xp.asarray([0.]*4), xp.asarray([xp.pi]*4)
res = find_root(f, (a, b), args=args, maxiter=2)
ref_flags = xp.asarray([eim._ECONVERGED,
eim._ESIGNERR,
eim._ECONVERR,
eim._EVALUEERR], dtype=xp.int32)
xp_assert_equal(res.status, ref_flags)
def test_convergence(self, xp):
# Test that the convergence tolerances behave as expected
rng = np.random.default_rng(2585255913088665241)
p = xp.asarray(rng.random(size=3))
bracket = (-xp.asarray(5.), xp.asarray(5.))
args = (p,)
kwargs0 = dict(args=args, tolerances=dict(xatol=0, xrtol=0, fatol=0, frtol=0))
kwargs = deepcopy(kwargs0)
kwargs['tolerances']['xatol'] = 1e-3
res1 = find_root(self.f, bracket, **kwargs)
xp_assert_less(res1.bracket[1] - res1.bracket[0],
xp.full_like(p, xp.asarray(1e-3)))
kwargs['tolerances']['xatol'] = 1e-6
res2 = find_root(self.f, bracket, **kwargs)
xp_assert_less(res2.bracket[1] - res2.bracket[0],
xp.full_like(p, xp.asarray(1e-6)))
xp_assert_less(res2.bracket[1] - res2.bracket[0],
res1.bracket[1] - res1.bracket[0])
kwargs = deepcopy(kwargs0)
kwargs['tolerances']['xrtol'] = 1e-3
res1 = find_root(self.f, bracket, **kwargs)
xp_assert_less(res1.bracket[1] - res1.bracket[0], 1e-3 * xp.abs(res1.x))
kwargs['tolerances']['xrtol'] = 1e-6
res2 = find_root(self.f, bracket, **kwargs)
xp_assert_less(res2.bracket[1] - res2.bracket[0],
1e-6 * xp.abs(res2.x))
xp_assert_less(res2.bracket[1] - res2.bracket[0],
res1.bracket[1] - res1.bracket[0])
kwargs = deepcopy(kwargs0)
kwargs['tolerances']['fatol'] = 1e-3
res1 = find_root(self.f, bracket, **kwargs)
xp_assert_less(xp.abs(res1.f_x), xp.full_like(p, xp.asarray(1e-3)))
kwargs['tolerances']['fatol'] = 1e-6
res2 = find_root(self.f, bracket, **kwargs)
xp_assert_less(xp.abs(res2.f_x), xp.full_like(p, xp.asarray(1e-6)))
xp_assert_less(xp.abs(res2.f_x), xp.abs(res1.f_x))
kwargs = deepcopy(kwargs0)
kwargs['tolerances']['frtol'] = 1e-3
x1, x2 = bracket
f0 = xp.minimum(xp.abs(self.f(x1, *args)), xp.abs(self.f(x2, *args)))
res1 = find_root(self.f, bracket, **kwargs)
xp_assert_less(xp.abs(res1.f_x), 1e-3*f0)
kwargs['tolerances']['frtol'] = 1e-6
res2 = find_root(self.f, bracket, **kwargs)
xp_assert_less(xp.abs(res2.f_x), 1e-6*f0)
xp_assert_less(xp.abs(res2.f_x), xp.abs(res1.f_x))
def test_maxiter_callback(self, xp):
# Test behavior of `maxiter` parameter and `callback` interface
p = xp.asarray(0.612814)
bracket = (xp.asarray(-5.), xp.asarray(5.))
maxiter = 5
def f(q, p):
res = special.ndtr(q) - p
f.x = q
f.f_x = res
return res
f.x = None
f.f_x = None
res = find_root(f, bracket, args=(p,), maxiter=maxiter)
assert not xp.any(res.success)
assert xp.all(res.nfev == maxiter+2)
assert xp.all(res.nit == maxiter)
def callback(res):
callback.iter += 1
callback.res = res
assert hasattr(res, 'x')
if callback.iter == 0:
# callback is called once with initial bracket
assert (res.bracket[0], res.bracket[1]) == bracket
else:
changed = (((res.bracket[0] == callback.bracket[0])
& (res.bracket[1] != callback.bracket[1]))
| ((res.bracket[0] != callback.bracket[0])
& (res.bracket[1] == callback.bracket[1])))
assert xp.all(changed)
callback.bracket[0] = res.bracket[0]
callback.bracket[1] = res.bracket[1]
assert res.status == eim._EINPROGRESS
xp_assert_equal(self.f(res.bracket[0], p), res.f_bracket[0])
xp_assert_equal(self.f(res.bracket[1], p), res.f_bracket[1])
xp_assert_equal(self.f(res.x, p), res.f_x)
if callback.iter == maxiter:
raise StopIteration
callback.iter = -1 # callback called once before first iteration
callback.res = None
callback.bracket = [None, None]
res2 = find_root(f, bracket, args=(p,), callback=callback)
# terminating with callback is identical to terminating due to maxiter
# (except for `status`)
for key in res.keys():
if key == 'status':
xp_assert_equal(res[key], xp.asarray(eim._ECONVERR, dtype=xp.int32))
xp_assert_equal(res2[key], xp.asarray(eim._ECALLBACK, dtype=xp.int32))
elif key in {'bracket', 'f_bracket'}:
xp_assert_equal(res2[key][0], res[key][0])
xp_assert_equal(res2[key][1], res[key][1])
elif key.startswith('_'):
continue
else:
xp_assert_equal(res2[key], res[key])
@pytest.mark.parametrize('case', _CHANDRUPATLA_TESTS)
def test_nit_expected(self, case, xp):
# Test that `_chandrupatla` implements Chandrupatla's algorithm:
# in all 40 test cases, the number of iterations performed
# matches the number reported in the original paper.
f, bracket, root, nfeval, id = case
# Chandrupatla's criterion is equivalent to
# abs(x2-x1) < 4*abs(xmin)*xrtol + xatol, but we use the more standard
# abs(x2-x1) < abs(xmin)*xrtol + xatol. Therefore, set xrtol to 4x
# that used by Chandrupatla in tests.
bracket = (xp.asarray(bracket[0], dtype=xp.float64),
xp.asarray(bracket[1], dtype=xp.float64))
root = xp.asarray(root, dtype=xp.float64)
res = find_root(f, bracket, tolerances=dict(xrtol=4e-10, xatol=1e-5))
xp_assert_close(res.f_x, xp.asarray(f(root), dtype=xp.float64),
rtol=1e-8, atol=2e-3)
xp_assert_equal(res.nfev, xp.asarray(nfeval, dtype=xp.int32))
@pytest.mark.parametrize("root", (0.622, [0.622, 0.623]))
@pytest.mark.parametrize("dtype", ('float16', 'float32', 'float64'))
def test_dtype(self, root, dtype, xp):
# Test that dtypes are preserved
not_numpy = not is_numpy(xp)
if not_numpy and dtype == 'float16':
pytest.skip("`float16` dtype only supported for NumPy arrays.")
dtype = getattr(xp, dtype, None)
if dtype is None:
pytest.skip(f"{xp} does not support {dtype}")
def f(x, root):
res = (x - root) ** 3.
if is_numpy(xp): # NumPy does not preserve dtype
return xp.asarray(res, dtype=dtype)
return res
a, b = xp.asarray(-3, dtype=dtype), xp.asarray(3, dtype=dtype)
root = xp.asarray(root, dtype=dtype)
res = find_root(f, (a, b), args=(root,), tolerances={'xatol': 1e-3})
try:
xp_assert_close(res.x, root, atol=1e-3)
except AssertionError:
assert res.x.dtype == dtype
xp.all(res.f_x == 0)
def test_input_validation(self, xp):
# Test input validation for appropriate error messages
def func(x):
return x
message = '`func` must be callable.'
with pytest.raises(ValueError, match=message):
bracket = xp.asarray(-4), xp.asarray(4)
find_root(None, bracket)
message = 'Abscissae and function output must be real numbers.'
with pytest.raises(ValueError, match=message):
bracket = xp.asarray(-4+1j), xp.asarray(4)
find_root(func, bracket)
# raised by `np.broadcast, but the traceback is readable IMO
message = "...not be broadcast..." # all messages include this part
with pytest.raises((ValueError, RuntimeError), match=message):
bracket = xp.asarray([-2, -3]), xp.asarray([3, 4, 5])
find_root(func, bracket)
message = "The shape of the array returned by `func`..."
with pytest.raises(ValueError, match=message):
bracket = xp.asarray([-3, -3]), xp.asarray([5, 5])
find_root(lambda x: [x[0], x[1], x[1]], bracket)
message = 'Tolerances must be non-negative scalars.'
bracket = xp.asarray(-4), xp.asarray(4)
with pytest.raises(ValueError, match=message):
find_root(func, bracket, tolerances=dict(xatol=-1))
with pytest.raises(ValueError, match=message):
find_root(func, bracket, tolerances=dict(xrtol=xp.nan))
with pytest.raises(ValueError, match=message):
find_root(func, bracket, tolerances=dict(fatol='ekki'))
with pytest.raises(ValueError, match=message):
find_root(func, bracket, tolerances=dict(frtol=xp.nan))
message = '`maxiter` must be a non-negative integer.'
with pytest.raises(ValueError, match=message):
find_root(func, bracket, maxiter=1.5)
with pytest.raises(ValueError, match=message):
find_root(func, bracket, maxiter=-1)
message = '`callback` must be callable.'
with pytest.raises(ValueError, match=message):
find_root(func, bracket, callback='shrubbery')
def test_special_cases(self, xp):
# Test edge cases and other special cases
# Test infinite function values
def f(x):
return 1 / x + 1 - 1 / (-x + 1)
a, b = xp.asarray([0.1, 0., 0., 0.1]), xp.asarray([0.9, 1.0, 0.9, 1.0])
with np.errstate(divide='ignore', invalid='ignore'):
res = find_root(f, (a, b))
assert xp.all(res.success)
xp_assert_close(res.x[1:], xp.full((3,), res.x[0]))
# Test that integers are not passed to `f`
# (otherwise this would overflow)
def f(x):
assert xp.isdtype(x.dtype, "real floating")
# this would overflow if x were an xp integer dtype
return x ** 31 - 1
# note that all inputs are integer type; result is automatically default float
res = find_root(f, (xp.asarray(-7), xp.asarray(5)))
assert res.success
xp_assert_close(res.x, xp.asarray(1.))
# Test that if both ends of bracket equal root, algorithm reports
# convergence.
def f(x, root):
return x**2 - root
root = xp.asarray([0, 1])
res = find_root(f, (xp.asarray(1), xp.asarray(1)), args=(root,))
xp_assert_equal(res.success, xp.asarray([False, True]))
xp_assert_equal(res.x, xp.asarray([xp.nan, 1.]))
def f(x):
return 1/x
with np.errstate(invalid='ignore'):
inf = xp.asarray(xp.inf)
res = find_root(f, (inf, inf))
assert res.success
xp_assert_equal(res.x, xp.asarray(xp.inf))
# Test maxiter = 0. Should do nothing to bracket.
def f(x):
return x**3 - 1
a, b = xp.asarray(-3.), xp.asarray(5.)
res = find_root(f, (a, b), maxiter=0)
xp_assert_equal(res.success, xp.asarray(False))
xp_assert_equal(res.status, xp.asarray(-2, dtype=xp.int32))
xp_assert_equal(res.nit, xp.asarray(0, dtype=xp.int32))
xp_assert_equal(res.nfev, xp.asarray(2, dtype=xp.int32))
xp_assert_equal(res.bracket[0], a)
xp_assert_equal(res.bracket[1], b)
# The `x` attribute is the one with the smaller function value
xp_assert_equal(res.x, a)
# Reverse bracket; check that this is still true
res = find_root(f, (-b, -a), maxiter=0)
xp_assert_equal(res.x, -a)
# Test maxiter = 1
res = find_root(f, (a, b), maxiter=1)
xp_assert_equal(res.success, xp.asarray(True))
xp_assert_equal(res.status, xp.asarray(0, dtype=xp.int32))
xp_assert_equal(res.nit, xp.asarray(1, dtype=xp.int32))
xp_assert_equal(res.nfev, xp.asarray(3, dtype=xp.int32))
xp_assert_close(res.x, xp.asarray(1.))
# Test scalar `args` (not in tuple)
def f(x, c):
return c*x - 1
res = find_root(f, (xp.asarray(-1), xp.asarray(1)), args=xp.asarray(3))
xp_assert_close(res.x, xp.asarray(1/3))
# # TODO: Test zero tolerance
# # ~~What's going on here - why are iterations repeated?~~
# # tl goes to zero when xatol=xrtol=0. When function is nearly linear,
# # this causes convergence issues.
# def f(x):
# return np.cos(x)
#
# res = _chandrupatla_root(f, 0, np.pi, xatol=0, xrtol=0)
# assert res.nit < 100
# xp = np.nextafter(res.x, np.inf)
# xm = np.nextafter(res.x, -np.inf)
# assert np.abs(res.fun) < np.abs(f(xp))
# assert np.abs(res.fun) < np.abs(f(xm))

View file

@ -0,0 +1,195 @@
import math
import numpy as np
from numpy.testing import assert_allclose, assert_array_almost_equal
from scipy.optimize import (
fmin_cobyla, minimize, Bounds, NonlinearConstraint, LinearConstraint,
OptimizeResult
)
class TestCobyla:
def setup_method(self):
# The algorithm is very fragile on 32 bit, so unfortunately we need to start
# very near the solution in order for the test to pass.
self.x0 = [np.sqrt(25 - (2.0/3)**2), 2.0/3 + 1e-4]
self.solution = [math.sqrt(25 - (2.0/3)**2), 2.0/3]
self.opts = {'disp': 0, 'rhobeg': 1, 'tol': 1e-6,
'maxiter': 100}
def fun(self, x):
return x[0]**2 + abs(x[1])**3
def con1(self, x):
return x[0]**2 + x[1]**2 - 25
def con2(self, x):
return -self.con1(x)
def test_simple(self):
# use disp=True as smoke test for gh-8118
x = fmin_cobyla(self.fun, self.x0, [self.con1, self.con2], rhobeg=1,
rhoend=1e-5, maxfun=100, disp=1)
assert_allclose(x, self.solution, atol=1e-4)
def test_minimize_simple(self):
class Callback:
def __init__(self):
self.n_calls = 0
self.last_x = None
def __call__(self, x):
self.n_calls += 1
self.last_x = x
class CallbackNewSyntax:
def __init__(self):
self.n_calls = 0
def __call__(self, intermediate_result):
assert isinstance(intermediate_result, OptimizeResult)
self.n_calls += 1
callback = Callback()
callback_new_syntax = CallbackNewSyntax()
# Minimize with method='COBYLA'
cons = (NonlinearConstraint(self.con1, 0, np.inf),
{'type': 'ineq', 'fun': self.con2})
sol = minimize(self.fun, self.x0, method='cobyla', constraints=cons,
callback=callback, options=self.opts)
sol_new = minimize(self.fun, self.x0, method='cobyla', constraints=cons,
callback=callback_new_syntax, options=self.opts)
assert_allclose(sol.x, self.solution, atol=1e-4)
assert sol.success, sol.message
assert sol.maxcv < 1e-5, sol
assert sol.nfev < 70, sol
assert sol.fun < self.fun(self.solution) + 1e-3, sol
assert_array_almost_equal(
sol.x,
callback.last_x,
decimal=5,
err_msg="Last design vector sent to the callback is not equal to"
" returned value.",
)
assert sol_new.success, sol_new.message
assert sol.fun == sol_new.fun
assert sol.maxcv == sol_new.maxcv
assert sol.nfev == sol_new.nfev
assert callback.n_calls == callback_new_syntax.n_calls, \
"Callback is not called the same amount of times for old and new syntax."
def test_minimize_constraint_violation(self):
# We set up conflicting constraints so that the algorithm will be
# guaranteed to end up with maxcv > 0.
cons = ({'type': 'ineq', 'fun': lambda x: 4 - x},
{'type': 'ineq', 'fun': lambda x: x - 5})
sol = minimize(lambda x: x, [0], method='cobyla', constraints=cons,
options={'catol': 0.6})
assert sol.maxcv > 0.1
assert sol.success
sol = minimize(lambda x: x, [0], method='cobyla', constraints=cons,
options={'catol': 0.4})
assert sol.maxcv > 0.1
assert not sol.success
def test_f_target(self):
f_target = 250
sol = minimize(lambda x: x**2, [500], method='cobyla',
options={'f_target': f_target})
assert sol.status == 1
assert sol.success
assert sol.fun <= f_target
def test_minimize_linear_constraints(self):
constraints = LinearConstraint([1.0, 1.0], 1.0, 1.0)
sol = minimize(
self.fun,
self.x0,
method='cobyla',
constraints=constraints,
options=self.opts,
)
solution = [(4 - np.sqrt(7)) / 3, (np.sqrt(7) - 1) / 3]
assert_allclose(sol.x, solution, atol=1e-4)
assert sol.success, sol.message
assert sol.maxcv < 1e-8, sol
assert sol.nfev <= 100, sol
assert sol.fun < self.fun(solution) + 1e-3, sol
def test_vector_constraints():
# test that fmin_cobyla and minimize can take a combination
# of constraints, some returning a number and others an array
def fun(x):
return (x[0] - 1)**2 + (x[1] - 2.5)**2
def fmin(x):
return fun(x) - 1
def cons1(x):
a = np.array([[1, -2, 2], [-1, -2, 6], [-1, 2, 2]])
return np.array([a[i, 0] * x[0] + a[i, 1] * x[1] +
a[i, 2] for i in range(len(a))])
def cons2(x):
return x # identity, acts as bounds x > 0
x0 = np.array([2, 0])
cons_list = [fun, cons1, cons2]
xsol = [1.4, 1.7]
fsol = 0.8
# testing fmin_cobyla
sol = fmin_cobyla(fun, x0, cons_list, rhoend=1e-5)
assert_allclose(sol, xsol, atol=1e-4)
sol = fmin_cobyla(fun, x0, fmin, rhoend=1e-5)
assert_allclose(fun(sol), 1, atol=1e-4)
# testing minimize
constraints = [{'type': 'ineq', 'fun': cons} for cons in cons_list]
sol = minimize(fun, x0, constraints=constraints, tol=1e-5)
assert_allclose(sol.x, xsol, atol=1e-4)
assert sol.success, sol.message
assert_allclose(sol.fun, fsol, atol=1e-4)
constraints = {'type': 'ineq', 'fun': fmin}
sol = minimize(fun, x0, constraints=constraints, tol=1e-5)
assert_allclose(sol.fun, 1, atol=1e-4)
class TestBounds:
# Test cobyla support for bounds (only when used via `minimize`)
# Invalid bounds is tested in
# test_optimize.TestOptimizeSimple.test_minimize_invalid_bounds
def test_basic(self):
def f(x):
return np.sum(x**2)
lb = [-1, None, 1, None, -0.5]
ub = [-0.5, -0.5, None, None, -0.5]
bounds = [(a, b) for a, b in zip(lb, ub)]
# these are converted to Bounds internally
res = minimize(f, x0=[1, 2, 3, 4, 5], method='cobyla', bounds=bounds)
ref = [-0.5, -0.5, 1, 0, -0.5]
assert res.success
assert_allclose(res.x, ref, atol=1e-3)
def test_unbounded(self):
def f(x):
return np.sum(x**2)
bounds = Bounds([-np.inf, -np.inf], [np.inf, np.inf])
res = minimize(f, x0=[1, 2], method='cobyla', bounds=bounds)
assert res.success
assert_allclose(res.x, 0, atol=1e-3)
bounds = Bounds([1, -np.inf], [np.inf, np.inf])
res = minimize(f, x0=[1, 2], method='cobyla', bounds=bounds)
assert res.success
assert_allclose(res.x, [1, 0], atol=1e-3)

View file

@ -0,0 +1,252 @@
import numpy as np
import pytest
import threading
from numpy.testing import assert_allclose, assert_equal
from scipy.optimize import (
Bounds,
LinearConstraint,
NonlinearConstraint,
OptimizeResult,
minimize,
)
class TestCOBYQA:
def setup_method(self):
self.x0 = [4.95, 0.66]
self.options = {'maxfev': 100}
@staticmethod
def fun(x, c=1.0):
return x[0]**2 + c * abs(x[1])**3
@staticmethod
def con(x):
return x[0]**2 + x[1]**2 - 25.0
def test_minimize_simple(self):
class Callback:
def __init__(self):
self.lock = threading.Lock()
self.n_calls = 0
def __call__(self, x):
assert isinstance(x, np.ndarray)
with self.lock:
self.n_calls += 1
class CallbackNewSyntax:
def __init__(self):
self.lock = threading.Lock()
self.n_calls = 0
def __call__(self, intermediate_result):
assert isinstance(intermediate_result, OptimizeResult)
with self.lock:
self.n_calls += 1
x0 = [4.95, 0.66]
callback = Callback()
callback_new_syntax = CallbackNewSyntax()
# Minimize with method='cobyqa'.
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
sol = minimize(
self.fun,
x0,
method='cobyqa',
constraints=constraints,
callback=callback,
options=self.options,
)
sol_new = minimize(
self.fun,
x0,
method='cobyqa',
constraints=constraints,
callback=callback_new_syntax,
options=self.options,
)
solution = [np.sqrt(25.0 - 4.0 / 9.0), 2.0 / 3.0]
assert_allclose(sol.x, solution, atol=1e-4)
assert sol.success, sol.message
assert sol.maxcv < 1e-8, sol
assert sol.nfev <= 100, sol
assert sol.fun < self.fun(solution) + 1e-3, sol
assert sol.nfev == callback.n_calls, \
"Callback is not called exactly once for every function eval."
assert_equal(sol.x, sol_new.x)
assert sol_new.success, sol_new.message
assert sol.fun == sol_new.fun
assert sol.maxcv == sol_new.maxcv
assert sol.nfev == sol_new.nfev
assert sol.nit == sol_new.nit
assert sol_new.nfev == callback_new_syntax.n_calls, \
"Callback is not called exactly once for every function eval."
def test_minimize_bounds(self):
def fun_check_bounds(x):
assert np.all(bounds.lb <= x) and np.all(x <= bounds.ub)
return self.fun(x)
# Case where the bounds are not active at the solution.
bounds = Bounds([4.5, 0.6], [5.0, 0.7])
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
sol = minimize(
fun_check_bounds,
self.x0,
method='cobyqa',
bounds=bounds,
constraints=constraints,
options=self.options,
)
solution = [np.sqrt(25.0 - 4.0 / 9.0), 2.0 / 3.0]
assert_allclose(sol.x, solution, atol=1e-4)
assert sol.success, sol.message
assert sol.maxcv < 1e-8, sol
assert np.all(bounds.lb <= sol.x) and np.all(sol.x <= bounds.ub), sol
assert sol.nfev <= 100, sol
assert sol.fun < self.fun(solution) + 1e-3, sol
# Case where the bounds are active at the solution.
bounds = Bounds([5.0, 0.6], [5.5, 0.65])
sol = minimize(
fun_check_bounds,
self.x0,
method='cobyqa',
bounds=bounds,
constraints=constraints,
options=self.options,
)
assert not sol.success, sol.message
assert sol.maxcv > 0.35, sol
assert np.all(bounds.lb <= sol.x) and np.all(sol.x <= bounds.ub), sol
assert sol.nfev <= 100, sol
def test_minimize_linear_constraints(self):
constraints = LinearConstraint([1.0, 1.0], 1.0, 1.0)
sol = minimize(
self.fun,
self.x0,
method='cobyqa',
constraints=constraints,
options=self.options,
)
solution = [(4 - np.sqrt(7)) / 3, (np.sqrt(7) - 1) / 3]
assert_allclose(sol.x, solution, atol=1e-4)
assert sol.success, sol.message
assert sol.maxcv < 1e-8, sol
assert sol.nfev <= 100, sol
assert sol.fun < self.fun(solution) + 1e-3, sol
def test_minimize_args(self):
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
sol = minimize(
self.fun,
self.x0,
args=(2.0,),
method='cobyqa',
constraints=constraints,
options=self.options,
)
solution = [np.sqrt(25.0 - 4.0 / 36.0), 2.0 / 6.0]
assert_allclose(sol.x, solution, atol=1e-4)
assert sol.success, sol.message
assert sol.maxcv < 1e-8, sol
assert sol.nfev <= 100, sol
assert sol.fun < self.fun(solution, 2.0) + 1e-3, sol
def test_minimize_array(self):
def fun_array(x, dim):
f = np.array(self.fun(x))
return np.reshape(f, (1,) * dim)
# The argument fun can return an array with a single element.
bounds = Bounds([4.5, 0.6], [5.0, 0.7])
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
sol = minimize(
self.fun,
self.x0,
method='cobyqa',
bounds=bounds,
constraints=constraints,
options=self.options,
)
for dim in [0, 1, 2]:
sol_array = minimize(
fun_array,
self.x0,
args=(dim,),
method='cobyqa',
bounds=bounds,
constraints=constraints,
options=self.options,
)
assert_equal(sol.x, sol_array.x)
assert sol_array.success, sol_array.message
assert sol.fun == sol_array.fun
assert sol.maxcv == sol_array.maxcv
assert sol.nfev == sol_array.nfev
assert sol.nit == sol_array.nit
# The argument fun cannot return an array with more than one element.
with pytest.raises(TypeError):
minimize(
lambda x: np.array([self.fun(x), self.fun(x)]),
self.x0,
method='cobyqa',
bounds=bounds,
constraints=constraints,
options=self.options,
)
def test_minimize_maxfev(self):
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
options = {'maxfev': 2}
sol = minimize(
self.fun,
self.x0,
method='cobyqa',
constraints=constraints,
options=options,
)
assert not sol.success, sol.message
assert sol.nfev <= 2, sol
def test_minimize_maxiter(self):
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
options = {'maxiter': 2}
sol = minimize(
self.fun,
self.x0,
method='cobyqa',
constraints=constraints,
options=options,
)
assert not sol.success, sol.message
assert sol.nit <= 2, sol
def test_minimize_f_target(self):
constraints = NonlinearConstraint(self.con, 0.0, 0.0)
sol_ref = minimize(
self.fun,
self.x0,
method='cobyqa',
constraints=constraints,
options=self.options,
)
options = dict(self.options)
options['f_target'] = sol_ref.fun
sol = minimize(
self.fun,
self.x0,
method='cobyqa',
constraints=constraints,
options=options,
)
assert sol.success, sol.message
assert sol.maxcv < 1e-8, sol
assert sol.nfev <= sol_ref.nfev, sol
assert sol.fun <= sol_ref.fun, sol

View file

@ -0,0 +1,286 @@
"""
Unit test for constraint conversion
"""
import numpy as np
from numpy.testing import (assert_array_almost_equal,
assert_allclose, assert_warns, suppress_warnings)
import pytest
from scipy.optimize import (NonlinearConstraint, LinearConstraint,
OptimizeWarning, minimize, BFGS)
from .test_minimize_constrained import (Maratos, HyperbolicIneq, Rosenbrock,
IneqRosenbrock, EqIneqRosenbrock,
BoundedRosenbrock, Elec)
class TestOldToNew:
x0 = (2, 0)
bnds = ((0, None), (0, None))
method = "trust-constr"
def test_constraint_dictionary_1(self):
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2
cons = ({'type': 'ineq', 'fun': lambda x: x[0] - 2 * x[1] + 2},
{'type': 'ineq', 'fun': lambda x: -x[0] - 2 * x[1] + 6},
{'type': 'ineq', 'fun': lambda x: -x[0] + 2 * x[1] + 2})
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
res = minimize(fun, self.x0, method=self.method,
bounds=self.bnds, constraints=cons)
assert_allclose(res.x, [1.4, 1.7], rtol=1e-4)
assert_allclose(res.fun, 0.8, rtol=1e-4)
def test_constraint_dictionary_2(self):
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2
cons = {'type': 'eq',
'fun': lambda x, p1, p2: p1*x[0] - p2*x[1],
'args': (1, 1.1),
'jac': lambda x, p1, p2: np.array([[p1, -p2]])}
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
res = minimize(fun, self.x0, method=self.method,
bounds=self.bnds, constraints=cons)
assert_allclose(res.x, [1.7918552, 1.62895927])
assert_allclose(res.fun, 1.3857466063348418)
def test_constraint_dictionary_3(self):
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2
cons = [{'type': 'ineq', 'fun': lambda x: x[0] - 2 * x[1] + 2},
NonlinearConstraint(lambda x: x[0] - x[1], 0, 0)]
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
res = minimize(fun, self.x0, method=self.method,
bounds=self.bnds, constraints=cons)
assert_allclose(res.x, [1.75, 1.75], rtol=1e-4)
assert_allclose(res.fun, 1.125, rtol=1e-4)
class TestNewToOld:
@pytest.mark.fail_slow(2)
def test_multiple_constraint_objects(self, num_parallel_threads):
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2 + (x[2] - 0.75) ** 2
x0 = [2, 0, 1]
coni = [] # only inequality constraints (can use cobyla)
methods = ["slsqp", "cobyla", "cobyqa", "trust-constr"]
# mixed old and new
coni.append([{'type': 'ineq', 'fun': lambda x: x[0] - 2 * x[1] + 2},
NonlinearConstraint(lambda x: x[0] - x[1], -1, 1)])
coni.append([LinearConstraint([1, -2, 0], -2, np.inf),
NonlinearConstraint(lambda x: x[0] - x[1], -1, 1)])
coni.append([NonlinearConstraint(lambda x: x[0] - 2 * x[1] + 2, 0, np.inf),
NonlinearConstraint(lambda x: x[0] - x[1], -1, 1)])
for con in coni:
funs = {}
for method in methods:
with suppress_warnings() as sup:
sup.filter(UserWarning)
result = minimize(fun, x0, method=method, constraints=con)
funs[method] = result.fun
assert_allclose(funs['slsqp'], funs['trust-constr'], rtol=1e-4)
assert_allclose(funs['cobyla'], funs['trust-constr'], rtol=1e-4)
if num_parallel_threads == 1:
assert_allclose(funs['cobyqa'], funs['trust-constr'],
rtol=1e-4)
@pytest.mark.fail_slow(20)
def test_individual_constraint_objects(self, num_parallel_threads):
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2 + (x[2] - 0.75) ** 2
x0 = [2, 0, 1]
cone = [] # with equality constraints (can't use cobyla)
coni = [] # only inequality constraints (can use cobyla)
methods = ["slsqp", "cobyla", "cobyqa", "trust-constr"]
# nonstandard data types for constraint equality bounds
cone.append(NonlinearConstraint(lambda x: x[0] - x[1], 1, 1))
cone.append(NonlinearConstraint(lambda x: x[0] - x[1], [1.21], [1.21]))
cone.append(NonlinearConstraint(lambda x: x[0] - x[1],
1.21, np.array([1.21])))
# multiple equalities
cone.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
1.21, 1.21)) # two same equalities
cone.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
[1.21, 1.4], [1.21, 1.4])) # two different equalities
cone.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
[1.21, 1.21], 1.21)) # equality specified two ways
cone.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
[1.21, -np.inf], [1.21, np.inf])) # equality + unbounded
# nonstandard data types for constraint inequality bounds
coni.append(NonlinearConstraint(lambda x: x[0] - x[1], 1.21, np.inf))
coni.append(NonlinearConstraint(lambda x: x[0] - x[1], [1.21], np.inf))
coni.append(NonlinearConstraint(lambda x: x[0] - x[1],
1.21, np.array([np.inf])))
coni.append(NonlinearConstraint(lambda x: x[0] - x[1], -np.inf, -3))
coni.append(NonlinearConstraint(lambda x: x[0] - x[1],
np.array(-np.inf), -3))
# multiple inequalities/equalities
coni.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
1.21, np.inf)) # two same inequalities
cone.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
[1.21, -np.inf], [1.21, 1.4])) # mixed equality/inequality
coni.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
[1.1, .8], [1.2, 1.4])) # bounded above and below
coni.append(NonlinearConstraint(
lambda x: [x[0] - x[1], x[1] - x[2]],
[-1.2, -1.4], [-1.1, -.8])) # - bounded above and below
# quick check of LinearConstraint class (very little new code to test)
cone.append(LinearConstraint([1, -1, 0], 1.21, 1.21))
cone.append(LinearConstraint([[1, -1, 0], [0, 1, -1]], 1.21, 1.21))
cone.append(LinearConstraint([[1, -1, 0], [0, 1, -1]],
[1.21, -np.inf], [1.21, 1.4]))
for con in coni:
funs = {}
for method in methods:
with suppress_warnings() as sup:
sup.filter(UserWarning)
result = minimize(fun, x0, method=method, constraints=con)
funs[method] = result.fun
assert_allclose(funs['slsqp'], funs['trust-constr'], rtol=1e-3)
assert_allclose(funs['cobyla'], funs['trust-constr'], rtol=1e-3)
if num_parallel_threads == 1:
assert_allclose(funs['cobyqa'], funs['trust-constr'],
rtol=1e-3)
for con in cone:
funs = {}
for method in [method for method in methods if method != 'cobyla']:
with suppress_warnings() as sup:
sup.filter(UserWarning)
result = minimize(fun, x0, method=method, constraints=con)
funs[method] = result.fun
assert_allclose(funs['slsqp'], funs['trust-constr'], rtol=1e-3)
if num_parallel_threads == 1:
assert_allclose(funs['cobyqa'], funs['trust-constr'],
rtol=1e-3)
class TestNewToOldSLSQP:
method = 'slsqp'
elec = Elec(n_electrons=2)
elec.x_opt = np.array([-0.58438468, 0.58438466, 0.73597047,
-0.73597044, 0.34180668, -0.34180667])
brock = BoundedRosenbrock()
brock.x_opt = [0, 0]
list_of_problems = [Maratos(),
HyperbolicIneq(),
Rosenbrock(),
IneqRosenbrock(),
EqIneqRosenbrock(),
elec,
brock
]
def test_list_of_problems(self):
for prob in self.list_of_problems:
with suppress_warnings() as sup:
sup.filter(UserWarning)
result = minimize(prob.fun, prob.x0,
method=self.method,
bounds=prob.bounds,
constraints=prob.constr)
assert_array_almost_equal(result.x, prob.x_opt, decimal=3)
@pytest.mark.thread_unsafe
def test_warn_mixed_constraints(self):
# warns about inefficiency of mixed equality/inequality constraints
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2 + (x[2] - 0.75) ** 2
cons = NonlinearConstraint(lambda x: [x[0]**2 - x[1], x[1] - x[2]],
[1.1, .8], [1.1, 1.4])
bnds = ((0, None), (0, None), (0, None))
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
assert_warns(OptimizeWarning, minimize, fun, (2, 0, 1),
method=self.method, bounds=bnds, constraints=cons)
@pytest.mark.thread_unsafe
def test_warn_ignored_options(self):
# warns about constraint options being ignored
def fun(x):
return (x[0] - 1) ** 2 + (x[1] - 2.5) ** 2 + (x[2] - 0.75) ** 2
x0 = (2, 0, 1)
if self.method == "slsqp":
bnds = ((0, None), (0, None), (0, None))
else:
bnds = None
cons = NonlinearConstraint(lambda x: x[0], 2, np.inf)
res = minimize(fun, x0, method=self.method,
bounds=bnds, constraints=cons)
# no warnings without constraint options
assert_allclose(res.fun, 1)
cons = LinearConstraint([1, 0, 0], 2, np.inf)
res = minimize(fun, x0, method=self.method,
bounds=bnds, constraints=cons)
# no warnings without constraint options
assert_allclose(res.fun, 1)
cons = []
cons.append(NonlinearConstraint(lambda x: x[0]**2, 2, np.inf,
keep_feasible=True))
cons.append(NonlinearConstraint(lambda x: x[0]**2, 2, np.inf,
hess=BFGS()))
cons.append(NonlinearConstraint(lambda x: x[0]**2, 2, np.inf,
finite_diff_jac_sparsity=42))
cons.append(NonlinearConstraint(lambda x: x[0]**2, 2, np.inf,
finite_diff_rel_step=42))
cons.append(LinearConstraint([1, 0, 0], 2, np.inf,
keep_feasible=True))
for con in cons:
assert_warns(OptimizeWarning, minimize, fun, x0,
method=self.method, bounds=bnds, constraints=cons)
class TestNewToOldCobyla:
method = 'cobyla'
list_of_problems = [
Elec(n_electrons=2),
Elec(n_electrons=4),
]
@pytest.mark.slow
def test_list_of_problems(self):
for prob in self.list_of_problems:
with suppress_warnings() as sup:
sup.filter(UserWarning)
truth = minimize(prob.fun, prob.x0,
method='trust-constr',
bounds=prob.bounds,
constraints=prob.constr)
result = minimize(prob.fun, prob.x0,
method=self.method,
bounds=prob.bounds,
constraints=prob.constr)
assert_allclose(result.fun, truth.fun, rtol=1e-3)

View file

@ -0,0 +1,255 @@
import pytest
import numpy as np
from numpy.testing import TestCase, assert_array_equal
import scipy.sparse as sps
from scipy.optimize._constraints import (
Bounds, LinearConstraint, NonlinearConstraint, PreparedConstraint,
new_bounds_to_old, old_bound_to_new, strict_bounds)
class TestStrictBounds(TestCase):
def test_scalarvalue_unique_enforce_feasibility(self):
m = 3
lb = 2
ub = 4
enforce_feasibility = False
strict_lb, strict_ub = strict_bounds(lb, ub,
enforce_feasibility,
m)
assert_array_equal(strict_lb, [-np.inf, -np.inf, -np.inf])
assert_array_equal(strict_ub, [np.inf, np.inf, np.inf])
enforce_feasibility = True
strict_lb, strict_ub = strict_bounds(lb, ub,
enforce_feasibility,
m)
assert_array_equal(strict_lb, [2, 2, 2])
assert_array_equal(strict_ub, [4, 4, 4])
def test_vectorvalue_unique_enforce_feasibility(self):
m = 3
lb = [1, 2, 3]
ub = [4, 5, 6]
enforce_feasibility = False
strict_lb, strict_ub = strict_bounds(lb, ub,
enforce_feasibility,
m)
assert_array_equal(strict_lb, [-np.inf, -np.inf, -np.inf])
assert_array_equal(strict_ub, [np.inf, np.inf, np.inf])
enforce_feasibility = True
strict_lb, strict_ub = strict_bounds(lb, ub,
enforce_feasibility,
m)
assert_array_equal(strict_lb, [1, 2, 3])
assert_array_equal(strict_ub, [4, 5, 6])
def test_scalarvalue_vector_enforce_feasibility(self):
m = 3
lb = 2
ub = 4
enforce_feasibility = [False, True, False]
strict_lb, strict_ub = strict_bounds(lb, ub,
enforce_feasibility,
m)
assert_array_equal(strict_lb, [-np.inf, 2, -np.inf])
assert_array_equal(strict_ub, [np.inf, 4, np.inf])
def test_vectorvalue_vector_enforce_feasibility(self):
m = 3
lb = [1, 2, 3]
ub = [4, 6, np.inf]
enforce_feasibility = [True, False, True]
strict_lb, strict_ub = strict_bounds(lb, ub,
enforce_feasibility,
m)
assert_array_equal(strict_lb, [1, -np.inf, 3])
assert_array_equal(strict_ub, [4, np.inf, np.inf])
def test_prepare_constraint_infeasible_x0():
lb = np.array([0, 20, 30])
ub = np.array([0.5, np.inf, 70])
x0 = np.array([1, 2, 3])
enforce_feasibility = np.array([False, True, True], dtype=bool)
bounds = Bounds(lb, ub, enforce_feasibility)
pytest.raises(ValueError, PreparedConstraint, bounds, x0)
pc = PreparedConstraint(Bounds(lb, ub), [1, 2, 3])
assert (pc.violation([1, 2, 3]) > 0).any()
assert (pc.violation([0.25, 21, 31]) == 0).all()
x0 = np.array([1, 2, 3, 4])
A = np.array([[1, 2, 3, 4], [5, 0, 0, 6], [7, 0, 8, 0]])
enforce_feasibility = np.array([True, True, True], dtype=bool)
linear = LinearConstraint(A, -np.inf, 0, enforce_feasibility)
pytest.raises(ValueError, PreparedConstraint, linear, x0)
pc = PreparedConstraint(LinearConstraint(A, -np.inf, 0),
[1, 2, 3, 4])
assert (pc.violation([1, 2, 3, 4]) > 0).any()
assert (pc.violation([-10, 2, -10, 4]) == 0).all()
def fun(x):
return A.dot(x)
def jac(x):
return A
def hess(x, v):
return sps.csr_array((4, 4))
nonlinear = NonlinearConstraint(fun, -np.inf, 0, jac, hess,
enforce_feasibility)
pytest.raises(ValueError, PreparedConstraint, nonlinear, x0)
pc = PreparedConstraint(nonlinear, [-10, 2, -10, 4])
assert (pc.violation([1, 2, 3, 4]) > 0).any()
assert (pc.violation([-10, 2, -10, 4]) == 0).all()
def test_violation():
def cons_f(x):
return np.array([x[0] ** 2 + x[1], x[0] ** 2 - x[1]])
nlc = NonlinearConstraint(cons_f, [-1, -0.8500], [2, 2])
pc = PreparedConstraint(nlc, [0.5, 1])
assert_array_equal(pc.violation([0.5, 1]), [0., 0.])
np.testing.assert_almost_equal(pc.violation([0.5, 1.2]), [0., 0.1])
np.testing.assert_almost_equal(pc.violation([1.2, 1.2]), [0.64, 0])
np.testing.assert_almost_equal(pc.violation([0.1, -1.2]), [0.19, 0])
np.testing.assert_almost_equal(pc.violation([0.1, 2]), [0.01, 1.14])
def test_new_bounds_to_old():
lb = np.array([-np.inf, 2, 3])
ub = np.array([3, np.inf, 10])
bounds = [(None, 3), (2, None), (3, 10)]
assert_array_equal(new_bounds_to_old(lb, ub, 3), bounds)
bounds_single_lb = [(-1, 3), (-1, None), (-1, 10)]
assert_array_equal(new_bounds_to_old(-1, ub, 3), bounds_single_lb)
bounds_no_lb = [(None, 3), (None, None), (None, 10)]
assert_array_equal(new_bounds_to_old(-np.inf, ub, 3), bounds_no_lb)
bounds_single_ub = [(None, 20), (2, 20), (3, 20)]
assert_array_equal(new_bounds_to_old(lb, 20, 3), bounds_single_ub)
bounds_no_ub = [(None, None), (2, None), (3, None)]
assert_array_equal(new_bounds_to_old(lb, np.inf, 3), bounds_no_ub)
bounds_single_both = [(1, 2), (1, 2), (1, 2)]
assert_array_equal(new_bounds_to_old(1, 2, 3), bounds_single_both)
bounds_no_both = [(None, None), (None, None), (None, None)]
assert_array_equal(new_bounds_to_old(-np.inf, np.inf, 3), bounds_no_both)
def test_old_bounds_to_new():
bounds = ([1, 2], (None, 3), (-1, None))
lb_true = np.array([1, -np.inf, -1])
ub_true = np.array([2, 3, np.inf])
lb, ub = old_bound_to_new(bounds)
assert_array_equal(lb, lb_true)
assert_array_equal(ub, ub_true)
bounds = [(-np.inf, np.inf), (np.array([1]), np.array([1]))]
lb, ub = old_bound_to_new(bounds)
assert_array_equal(lb, [-np.inf, 1])
assert_array_equal(ub, [np.inf, 1])
class TestBounds:
def test_repr(self):
# so that eval works
from numpy import array, inf # noqa: F401
for args in (
(-1.0, 5.0),
(-1.0, np.inf, True),
(np.array([1.0, -np.inf]), np.array([2.0, np.inf])),
(np.array([1.0, -np.inf]), np.array([2.0, np.inf]),
np.array([True, False])),
):
bounds = Bounds(*args)
bounds2 = eval(repr(Bounds(*args)))
assert_array_equal(bounds.lb, bounds2.lb)
assert_array_equal(bounds.ub, bounds2.ub)
assert_array_equal(bounds.keep_feasible, bounds2.keep_feasible)
def test_array(self):
# gh13501
b = Bounds(lb=[0.0, 0.0], ub=[1.0, 1.0])
assert isinstance(b.lb, np.ndarray)
assert isinstance(b.ub, np.ndarray)
def test_defaults(self):
b1 = Bounds()
b2 = Bounds(np.asarray(-np.inf), np.asarray(np.inf))
assert b1.lb == b2.lb
assert b1.ub == b2.ub
def test_input_validation(self):
message = "Lower and upper bounds must be dense arrays."
with pytest.raises(ValueError, match=message):
Bounds(sps.coo_array([1, 2]), [1, 2])
with pytest.raises(ValueError, match=message):
Bounds([1, 2], sps.coo_array([1, 2]))
message = "`keep_feasible` must be a dense array."
with pytest.raises(ValueError, match=message):
Bounds([1, 2], [1, 2], keep_feasible=sps.coo_array([True, True]))
message = "`lb`, `ub`, and `keep_feasible` must be broadcastable."
with pytest.raises(ValueError, match=message):
Bounds([1, 2], [1, 2, 3])
def test_residual(self):
bounds = Bounds(-2, 4)
x0 = [-1, 2]
np.testing.assert_allclose(bounds.residual(x0), ([1, 4], [5, 2]))
class TestLinearConstraint:
def test_defaults(self):
A = np.eye(4)
lc = LinearConstraint(A)
lc2 = LinearConstraint(A, -np.inf, np.inf)
assert_array_equal(lc.lb, lc2.lb)
assert_array_equal(lc.ub, lc2.ub)
def test_input_validation(self):
A = np.eye(4)
message = "`lb`, `ub`, and `keep_feasible` must be broadcastable"
with pytest.raises(ValueError, match=message):
LinearConstraint(A, [1, 2], [1, 2, 3])
message = "Constraint limits must be dense arrays"
with pytest.raises(ValueError, match=message):
LinearConstraint(A, sps.coo_array([1, 2]), [2, 3])
with pytest.raises(ValueError, match=message):
LinearConstraint(A, [1, 2], sps.coo_array([2, 3]))
message = "`keep_feasible` must be a dense array"
with pytest.raises(ValueError, match=message):
keep_feasible = sps.coo_array([True, True])
LinearConstraint(A, [1, 2], [2, 3], keep_feasible=keep_feasible)
A = np.empty((4, 3, 5))
message = "`A` must have exactly two dimensions."
with pytest.raises(ValueError, match=message):
LinearConstraint(A)
def test_residual(self):
A = np.eye(2)
lc = LinearConstraint(A, -2, 4)
x0 = [-1, 2]
np.testing.assert_allclose(lc.residual(x0), ([1, 4], [5, 2]))

View file

@ -0,0 +1,92 @@
"""
Test Cython optimize zeros API functions: ``bisect``, ``ridder``, ``brenth``,
and ``brentq`` in `scipy.optimize.cython_optimize`, by finding the roots of a
3rd order polynomial given a sequence of constant terms, ``a0``, and fixed 1st,
2nd, and 3rd order terms in ``args``.
.. math::
f(x, a0, args) = ((args[2]*x + args[1])*x + args[0])*x + a0
The 3rd order polynomial function is written in Cython and called in a Python
wrapper named after the zero function. See the private ``_zeros`` Cython module
in `scipy.optimize.cython_optimze` for more information.
"""
import numpy.testing as npt
from scipy.optimize.cython_optimize import _zeros
# CONSTANTS
# Solve x**3 - A0 = 0 for A0 = [2.0, 2.1, ..., 2.9].
# The ARGS have 3 elements just to show how this could be done for any cubic
# polynomial.
A0 = tuple(-2.0 - x/10.0 for x in range(10)) # constant term
ARGS = (0.0, 0.0, 1.0) # 1st, 2nd, and 3rd order terms
XLO, XHI = 0.0, 2.0 # first and second bounds of zeros functions
# absolute and relative tolerances and max iterations for zeros functions
XTOL, RTOL, MITR = 0.001, 0.001, 10
EXPECTED = [(-a0) ** (1.0/3.0) for a0 in A0]
# = [1.2599210498948732,
# 1.2805791649874942,
# 1.300591446851387,
# 1.3200061217959123,
# 1.338865900164339,
# 1.3572088082974532,
# 1.375068867074141,
# 1.3924766500838337,
# 1.4094597464129783,
# 1.4260431471424087]
# test bisect
def test_bisect():
npt.assert_allclose(
EXPECTED,
list(
_zeros.loop_example('bisect', A0, ARGS, XLO, XHI, XTOL, RTOL, MITR)
),
rtol=RTOL, atol=XTOL
)
# test ridder
def test_ridder():
npt.assert_allclose(
EXPECTED,
list(
_zeros.loop_example('ridder', A0, ARGS, XLO, XHI, XTOL, RTOL, MITR)
),
rtol=RTOL, atol=XTOL
)
# test brenth
def test_brenth():
npt.assert_allclose(
EXPECTED,
list(
_zeros.loop_example('brenth', A0, ARGS, XLO, XHI, XTOL, RTOL, MITR)
),
rtol=RTOL, atol=XTOL
)
# test brentq
def test_brentq():
npt.assert_allclose(
EXPECTED,
list(
_zeros.loop_example('brentq', A0, ARGS, XLO, XHI, XTOL, RTOL, MITR)
),
rtol=RTOL, atol=XTOL
)
# test brentq with full output
def test_brentq_full_output():
output = _zeros.full_output_example(
(A0[0],) + ARGS, XLO, XHI, XTOL, RTOL, MITR)
npt.assert_allclose(EXPECTED[0], output['root'], rtol=RTOL, atol=XTOL)
npt.assert_equal(6, output['iterations'])
npt.assert_equal(7, output['funcalls'])
npt.assert_equal(0, output['error_num'])

View file

@ -0,0 +1,321 @@
"""
Unit test for DIRECT optimization algorithm.
"""
from numpy.testing import (assert_allclose,
assert_array_less)
import pytest
import numpy as np
from scipy.optimize import direct, Bounds
import threading
class TestDIRECT:
def setup_method(self):
self.fun_calls = threading.local()
self.bounds_sphere = 4*[(-2, 3)]
self.optimum_sphere_pos = np.zeros((4, ))
self.optimum_sphere = 0.0
self.bounds_stylinski_tang = Bounds([-4., -4.], [4., 4.])
self.maxiter = 1000
# test functions
def sphere(self, x):
if not hasattr(self.fun_calls, 'c'):
self.fun_calls.c = 0
self.fun_calls.c += 1
return np.square(x).sum()
def inv(self, x):
if np.sum(x) == 0:
raise ZeroDivisionError()
return 1/np.sum(x)
def nan_fun(self, x):
return np.nan
def inf_fun(self, x):
return np.inf
def styblinski_tang(self, pos):
x, y = pos
return 0.5 * (x**4 - 16 * x**2 + 5 * x + y**4 - 16 * y**2 + 5 * y)
@pytest.mark.parametrize("locally_biased", [True, False])
def test_direct(self, locally_biased):
res = direct(self.sphere, self.bounds_sphere,
locally_biased=locally_biased)
# test accuracy
assert_allclose(res.x, self.optimum_sphere_pos,
rtol=1e-3, atol=1e-3)
assert_allclose(res.fun, self.optimum_sphere, atol=1e-5, rtol=1e-5)
# test that result lies within bounds
_bounds = np.asarray(self.bounds_sphere)
assert_array_less(_bounds[:, 0], res.x)
assert_array_less(res.x, _bounds[:, 1])
# test number of function evaluations. Original DIRECT overshoots by
# up to 500 evaluations in last iteration
assert res.nfev <= 1000 * (len(self.bounds_sphere) + 1)
# test that number of function evaluations is correct
assert res.nfev == self.fun_calls.c
# test that number of iterations is below supplied maximum
assert res.nit <= self.maxiter
@pytest.mark.parametrize("locally_biased", [True, False])
def test_direct_callback(self, locally_biased):
# test that callback does not change the result
res = direct(self.sphere, self.bounds_sphere,
locally_biased=locally_biased)
def callback(x):
x = 2*x
dummy = np.square(x)
print("DIRECT minimization algorithm callback test")
return dummy
res_callback = direct(self.sphere, self.bounds_sphere,
locally_biased=locally_biased,
callback=callback)
assert_allclose(res.x, res_callback.x)
assert res.nit == res_callback.nit
assert res.nfev == res_callback.nfev
assert res.status == res_callback.status
assert res.success == res_callback.success
assert res.fun == res_callback.fun
assert_allclose(res.x, res_callback.x)
assert res.message == res_callback.message
# test accuracy
assert_allclose(res_callback.x, self.optimum_sphere_pos,
rtol=1e-3, atol=1e-3)
assert_allclose(res_callback.fun, self.optimum_sphere,
atol=1e-5, rtol=1e-5)
@pytest.mark.parametrize("locally_biased", [True, False])
def test_exception(self, locally_biased):
bounds = 4*[(-10, 10)]
with pytest.raises(ZeroDivisionError):
direct(self.inv, bounds=bounds,
locally_biased=locally_biased)
@pytest.mark.parametrize("locally_biased", [True, False])
def test_nan(self, locally_biased):
bounds = 4*[(-10, 10)]
direct(self.nan_fun, bounds=bounds,
locally_biased=locally_biased)
@pytest.mark.parametrize("len_tol", [1e-3, 1e-4])
@pytest.mark.parametrize("locally_biased", [True, False])
def test_len_tol(self, len_tol, locally_biased):
bounds = 4*[(-10., 10.)]
res = direct(self.sphere, bounds=bounds, len_tol=len_tol,
vol_tol=1e-30, locally_biased=locally_biased)
assert res.status == 5
assert res.success
assert_allclose(res.x, np.zeros((4, )))
message = ("The side length measure of the hyperrectangle containing "
"the lowest function value found is below "
f"len_tol={len_tol}")
assert res.message == message
@pytest.mark.parametrize("vol_tol", [1e-6, 1e-8])
@pytest.mark.parametrize("locally_biased", [True, False])
def test_vol_tol(self, vol_tol, locally_biased):
bounds = 4*[(-10., 10.)]
res = direct(self.sphere, bounds=bounds, vol_tol=vol_tol,
len_tol=0., locally_biased=locally_biased)
assert res.status == 4
assert res.success
assert_allclose(res.x, np.zeros((4, )))
message = ("The volume of the hyperrectangle containing the lowest "
f"function value found is below vol_tol={vol_tol}")
assert res.message == message
@pytest.mark.parametrize("f_min_rtol", [1e-3, 1e-5, 1e-7])
@pytest.mark.parametrize("locally_biased", [True, False])
def test_f_min(self, f_min_rtol, locally_biased):
# test that desired function value is reached within
# relative tolerance of f_min_rtol
f_min = 1.
bounds = 4*[(-2., 10.)]
res = direct(self.sphere, bounds=bounds, f_min=f_min,
f_min_rtol=f_min_rtol,
locally_biased=locally_biased)
assert res.status == 3
assert res.success
assert res.fun < f_min * (1. + f_min_rtol)
message = ("The best function value found is within a relative "
f"error={f_min_rtol} of the (known) global optimum f_min")
assert res.message == message
def circle_with_args(self, x, a, b):
return np.square(x[0] - a) + np.square(x[1] - b).sum()
@pytest.mark.parametrize("locally_biased", [True, False])
def test_f_circle_with_args(self, locally_biased):
bounds = 2*[(-2.0, 2.0)]
res = direct(self.circle_with_args, bounds, args=(1, 1), maxfun=1250,
locally_biased=locally_biased)
assert_allclose(res.x, np.array([1., 1.]), rtol=1e-5)
@pytest.mark.parametrize("locally_biased", [True, False])
def test_failure_maxfun(self, locally_biased):
# test that if optimization runs for the maximal number of
# evaluations, success = False is returned
maxfun = 100
result = direct(self.styblinski_tang, self.bounds_stylinski_tang,
maxfun=maxfun, locally_biased=locally_biased)
assert result.success is False
assert result.status == 1
assert result.nfev >= maxfun
message = ("Number of function evaluations done is "
f"larger than maxfun={maxfun}")
assert result.message == message
@pytest.mark.parametrize("locally_biased", [True, False])
def test_failure_maxiter(self, locally_biased):
# test that if optimization runs for the maximal number of
# iterations, success = False is returned
maxiter = 10
result = direct(self.styblinski_tang, self.bounds_stylinski_tang,
maxiter=maxiter, locally_biased=locally_biased)
assert result.success is False
assert result.status == 2
assert result.nit >= maxiter
message = f"Number of iterations is larger than maxiter={maxiter}"
assert result.message == message
@pytest.mark.parametrize("locally_biased", [True, False])
def test_bounds_variants(self, locally_biased):
# test that new and old bounds yield same result
lb = [-6., 1., -5.]
ub = [-1., 3., 5.]
x_opt = np.array([-1., 1., 0.])
bounds_old = list(zip(lb, ub))
bounds_new = Bounds(lb, ub)
res_old_bounds = direct(self.sphere, bounds_old,
locally_biased=locally_biased)
res_new_bounds = direct(self.sphere, bounds_new,
locally_biased=locally_biased)
assert res_new_bounds.nfev == res_old_bounds.nfev
assert res_new_bounds.message == res_old_bounds.message
assert res_new_bounds.success == res_old_bounds.success
assert res_new_bounds.nit == res_old_bounds.nit
assert_allclose(res_new_bounds.x, res_old_bounds.x)
assert_allclose(res_new_bounds.x, x_opt, rtol=1e-2)
@pytest.mark.parametrize("locally_biased", [True, False])
@pytest.mark.parametrize("eps", [1e-5, 1e-4, 1e-3])
def test_epsilon(self, eps, locally_biased):
result = direct(self.styblinski_tang, self.bounds_stylinski_tang,
eps=eps, vol_tol=1e-6,
locally_biased=locally_biased)
assert result.status == 4
assert result.success
@pytest.mark.xslow
@pytest.mark.parametrize("locally_biased", [True, False])
def test_no_segmentation_fault(self, locally_biased):
# test that an excessive number of function evaluations
# does not result in segmentation fault
bounds = [(-5., 20.)] * 100
result = direct(self.sphere, bounds, maxfun=10000000,
maxiter=1000000, locally_biased=locally_biased)
assert result is not None
@pytest.mark.parametrize("locally_biased", [True, False])
def test_inf_fun(self, locally_biased):
# test that an objective value of infinity does not crash DIRECT
bounds = [(-5., 5.)] * 2
result = direct(self.inf_fun, bounds,
locally_biased=locally_biased)
assert result is not None
@pytest.mark.parametrize("len_tol", [-1, 2])
def test_len_tol_validation(self, len_tol):
error_msg = "len_tol must be between 0 and 1."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
len_tol=len_tol)
@pytest.mark.parametrize("vol_tol", [-1, 2])
def test_vol_tol_validation(self, vol_tol):
error_msg = "vol_tol must be between 0 and 1."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
vol_tol=vol_tol)
@pytest.mark.parametrize("f_min_rtol", [-1, 2])
def test_fmin_rtol_validation(self, f_min_rtol):
error_msg = "f_min_rtol must be between 0 and 1."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
f_min_rtol=f_min_rtol, f_min=0.)
@pytest.mark.parametrize("maxfun", [1.5, "string", (1, 2)])
def test_maxfun_wrong_type(self, maxfun):
error_msg = "maxfun must be of type int."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
maxfun=maxfun)
@pytest.mark.parametrize("maxiter", [1.5, "string", (1, 2)])
def test_maxiter_wrong_type(self, maxiter):
error_msg = "maxiter must be of type int."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
maxiter=maxiter)
def test_negative_maxiter(self):
error_msg = "maxiter must be > 0."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
maxiter=-1)
def test_negative_maxfun(self):
error_msg = "maxfun must be > 0."
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
maxfun=-1)
@pytest.mark.parametrize("bounds", ["bounds", 2., 0])
def test_invalid_bounds_type(self, bounds):
error_msg = ("bounds must be a sequence or "
"instance of Bounds class")
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, bounds)
@pytest.mark.parametrize("bounds",
[Bounds([-1., -1], [-2, 1]),
Bounds([-np.nan, -1], [-2, np.nan]),
]
)
def test_incorrect_bounds(self, bounds):
error_msg = 'Bounds are not consistent min < max'
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, bounds)
def test_inf_bounds(self):
error_msg = 'Bounds must not be inf.'
bounds = Bounds([-np.inf, -1], [-2, np.inf])
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, bounds)
@pytest.mark.parametrize("locally_biased", ["bias", [0, 0], 2.])
def test_locally_biased_validation(self, locally_biased):
error_msg = 'locally_biased must be True or False.'
with pytest.raises(ValueError, match=error_msg):
direct(self.styblinski_tang, self.bounds_stylinski_tang,
locally_biased=locally_biased)

View file

@ -0,0 +1,28 @@
import os
import platform
import sysconfig
import pytest
from scipy._lib._testutils import IS_EDITABLE, _test_cython_extension, cython
@pytest.mark.fail_slow(40)
# essential per https://github.com/scipy/scipy/pull/20487#discussion_r1567057247
@pytest.mark.skipif(IS_EDITABLE,
reason='Editable install cannot find .pxd headers.')
@pytest.mark.skipif((platform.system() == 'Windows' and
sysconfig.get_config_var('Py_GIL_DISABLED')),
reason='gh-22039')
@pytest.mark.skipif(platform.machine() in ["wasm32", "wasm64"],
reason="Can't start subprocess")
@pytest.mark.skipif(cython is None, reason="requires cython")
def test_cython(tmp_path):
srcdir = os.path.dirname(os.path.dirname(__file__))
extensions, extensions_cpp = _test_cython_extension(tmp_path, srcdir)
# actually test the cython c-extensions
# From docstring for scipy.optimize.cython_optimize module
x = extensions.brentq_example()
assert x == 0.6999942848231314
x = extensions_cpp.brentq_example()
assert x == 0.6999942848231314

View file

@ -0,0 +1,300 @@
import re
from copy import deepcopy
import numpy as np
import pytest
from numpy.linalg import norm
from numpy.testing import (TestCase, assert_array_almost_equal,
assert_array_equal, assert_array_less)
from scipy.optimize import (BFGS, SR1)
class Rosenbrock:
"""Rosenbrock function.
The following optimization problem:
minimize sum(100.0*(x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0)
"""
def __init__(self, n=2, random_state=0):
rng = np.random.RandomState(random_state)
self.x0 = rng.uniform(-1, 1, n)
self.x_opt = np.ones(n)
def fun(self, x):
x = np.asarray(x)
r = np.sum(100.0 * (x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0,
axis=0)
return r
def grad(self, x):
x = np.asarray(x)
xm = x[1:-1]
xm_m1 = x[:-2]
xm_p1 = x[2:]
der = np.zeros_like(x)
der[1:-1] = (200 * (xm - xm_m1**2) -
400 * (xm_p1 - xm**2) * xm - 2 * (1 - xm))
der[0] = -400 * x[0] * (x[1] - x[0]**2) - 2 * (1 - x[0])
der[-1] = 200 * (x[-1] - x[-2]**2)
return der
def hess(self, x):
x = np.atleast_1d(x)
H = np.diag(-400 * x[:-1], 1) - np.diag(400 * x[:-1], -1)
diagonal = np.zeros(len(x), dtype=x.dtype)
diagonal[0] = 1200 * x[0]**2 - 400 * x[1] + 2
diagonal[-1] = 200
diagonal[1:-1] = 202 + 1200 * x[1:-1]**2 - 400 * x[2:]
H = H + np.diag(diagonal)
return H
class TestHessianUpdateStrategy(TestCase):
def test_hessian_initialization(self):
ndims = 5
symmetric_matrix = np.array([[43, 24, 33, 34, 49],
[24, 36, 44, 15, 44],
[33, 44, 37, 1, 30],
[34, 15, 1, 5, 46],
[49, 44, 30, 46, 22]])
init_scales = (
('auto', np.eye(ndims)),
(2, np.eye(ndims) * 2),
(np.arange(1, ndims + 1) * np.eye(ndims),
np.arange(1, ndims + 1) * np.eye(ndims)),
(symmetric_matrix, symmetric_matrix),)
for approx_type in ['hess', 'inv_hess']:
for init_scale, true_matrix in init_scales:
# large min_{denominator,curvatur} makes them skip an update,
# so we can have our initial matrix
quasi_newton = (BFGS(init_scale=init_scale,
min_curvature=1e50,
exception_strategy='skip_update'),
SR1(init_scale=init_scale,
min_denominator=1e50))
for qn in quasi_newton:
qn.initialize(ndims, approx_type)
B = qn.get_matrix()
assert_array_equal(B, np.eye(ndims))
# don't test the auto init scale
if isinstance(init_scale, str) and init_scale == 'auto':
continue
qn.update(np.ones(ndims) * 1e-5, np.arange(ndims) + 0.2)
B = qn.get_matrix()
assert_array_equal(B, true_matrix)
# For this list of points, it is known
# that no exception occur during the
# Hessian update. Hence no update is
# skiped or damped.
def test_initialize_catch_illegal(self):
ndims = 3
# no complex allowed
inits_msg_errtype = ((complex(3.14),
r"float\(\) argument must be a string or a "
r"(real )?number, not 'complex'",
TypeError),
(np.array([3.2, 2.3, 1.2]).astype(np.complex128),
"init_scale contains complex elements, "
"must be real.",
TypeError),
(np.array([[43, 24, 33],
[24, 36, 44, ],
[33, 44, 37, ]]).astype(np.complex128),
"init_scale contains complex elements, "
"must be real.",
TypeError),
# not square
(np.array([[43, 55, 66]]),
re.escape(
"If init_scale is an array, it must have the "
"dimensions of the hess/inv_hess: (3, 3)."
" Got (1, 3)."),
ValueError),
# not symmetric
(np.array([[43, 24, 33],
[24.1, 36, 44, ],
[33, 44, 37, ]]),
re.escape("If init_scale is an array, it must be"
" symmetric (passing scipy.linalg.issymmetric)"
" to be an approximation of a hess/inv_hess."),
ValueError),
)
for approx_type in ['hess', 'inv_hess']:
for init_scale, message, errortype in inits_msg_errtype:
# large min_{denominator,curvatur} makes it skip an update,
# so we can retrieve our initial matrix
quasi_newton = (BFGS(init_scale=init_scale),
SR1(init_scale=init_scale))
for qn in quasi_newton:
qn.initialize(ndims, approx_type)
with pytest.raises(errortype, match=message):
qn.update(np.ones(ndims), np.arange(ndims))
def test_rosenbrock_with_no_exception(self):
# Define auxiliary problem
prob = Rosenbrock(n=5)
# Define iteration points
x_list = [[0.0976270, 0.4303787, 0.2055267, 0.0897663, -0.15269040],
[0.1847239, 0.0505757, 0.2123832, 0.0255081, 0.00083286],
[0.2142498, -0.0188480, 0.0503822, 0.0347033, 0.03323606],
[0.2071680, -0.0185071, 0.0341337, -0.0139298, 0.02881750],
[0.1533055, -0.0322935, 0.0280418, -0.0083592, 0.01503699],
[0.1382378, -0.0276671, 0.0266161, -0.0074060, 0.02801610],
[0.1651957, -0.0049124, 0.0269665, -0.0040025, 0.02138184],
[0.2354930, 0.0443711, 0.0173959, 0.0041872, 0.00794563],
[0.4168118, 0.1433867, 0.0111714, 0.0126265, -0.00658537],
[0.4681972, 0.2153273, 0.0225249, 0.0152704, -0.00463809],
[0.6023068, 0.3346815, 0.0731108, 0.0186618, -0.00371541],
[0.6415743, 0.3985468, 0.1324422, 0.0214160, -0.00062401],
[0.7503690, 0.5447616, 0.2804541, 0.0539851, 0.00242230],
[0.7452626, 0.5644594, 0.3324679, 0.0865153, 0.00454960],
[0.8059782, 0.6586838, 0.4229577, 0.1452990, 0.00976702],
[0.8549542, 0.7226562, 0.4991309, 0.2420093, 0.02772661],
[0.8571332, 0.7285741, 0.5279076, 0.2824549, 0.06030276],
[0.8835633, 0.7727077, 0.5957984, 0.3411303, 0.09652185],
[0.9071558, 0.8299587, 0.6771400, 0.4402896, 0.17469338],
[0.9190793, 0.8486480, 0.7163332, 0.5083780, 0.26107691],
[0.9371223, 0.8762177, 0.7653702, 0.5773109, 0.32181041],
[0.9554613, 0.9119893, 0.8282687, 0.6776178, 0.43162744],
[0.9545744, 0.9099264, 0.8270244, 0.6822220, 0.45237623],
[0.9688112, 0.9351710, 0.8730961, 0.7546601, 0.56622448],
[0.9743227, 0.9491953, 0.9005150, 0.8086497, 0.64505437],
[0.9807345, 0.9638853, 0.9283012, 0.8631675, 0.73812581],
[0.9886746, 0.9777760, 0.9558950, 0.9123417, 0.82726553],
[0.9899096, 0.9803828, 0.9615592, 0.9255600, 0.85822149],
[0.9969510, 0.9935441, 0.9864657, 0.9726775, 0.94358663],
[0.9979533, 0.9960274, 0.9921724, 0.9837415, 0.96626288],
[0.9995981, 0.9989171, 0.9974178, 0.9949954, 0.99023356],
[1.0002640, 1.0005088, 1.0010594, 1.0021161, 1.00386912],
[0.9998903, 0.9998459, 0.9997795, 0.9995484, 0.99916305],
[1.0000008, 0.9999905, 0.9999481, 0.9998903, 0.99978047],
[1.0000004, 0.9999983, 1.0000001, 1.0000031, 1.00000297],
[0.9999995, 1.0000003, 1.0000005, 1.0000001, 1.00000032],
[0.9999999, 0.9999997, 0.9999994, 0.9999989, 0.99999786],
[0.9999999, 0.9999999, 0.9999999, 0.9999999, 0.99999991]]
# Get iteration points
grad_list = [prob.grad(x) for x in x_list]
delta_x = [np.array(x_list[i+1])-np.array(x_list[i])
for i in range(len(x_list)-1)]
delta_grad = [grad_list[i+1]-grad_list[i]
for i in range(len(grad_list)-1)]
# Check curvature condition
for s, y in zip(delta_x, delta_grad):
if np.dot(s, y) <= 0:
raise ArithmeticError()
# Define QuasiNewton update
for quasi_newton in (BFGS(init_scale=1, min_curvature=1e-4),
SR1(init_scale=1)):
hess = deepcopy(quasi_newton)
inv_hess = deepcopy(quasi_newton)
hess.initialize(len(x_list[0]), 'hess')
inv_hess.initialize(len(x_list[0]), 'inv_hess')
# Compare the hessian and its inverse
for s, y in zip(delta_x, delta_grad):
hess.update(s, y)
inv_hess.update(s, y)
B = hess.get_matrix()
H = inv_hess.get_matrix()
assert_array_almost_equal(np.linalg.inv(B), H, decimal=10)
B_true = prob.hess(x_list[len(delta_x)])
assert_array_less(norm(B - B_true)/norm(B_true), 0.1)
def test_SR1_skip_update(self):
# Define auxiliary problem
prob = Rosenbrock(n=5)
# Define iteration points
x_list = [[0.0976270, 0.4303787, 0.2055267, 0.0897663, -0.15269040],
[0.1847239, 0.0505757, 0.2123832, 0.0255081, 0.00083286],
[0.2142498, -0.0188480, 0.0503822, 0.0347033, 0.03323606],
[0.2071680, -0.0185071, 0.0341337, -0.0139298, 0.02881750],
[0.1533055, -0.0322935, 0.0280418, -0.0083592, 0.01503699],
[0.1382378, -0.0276671, 0.0266161, -0.0074060, 0.02801610],
[0.1651957, -0.0049124, 0.0269665, -0.0040025, 0.02138184],
[0.2354930, 0.0443711, 0.0173959, 0.0041872, 0.00794563],
[0.4168118, 0.1433867, 0.0111714, 0.0126265, -0.00658537],
[0.4681972, 0.2153273, 0.0225249, 0.0152704, -0.00463809],
[0.6023068, 0.3346815, 0.0731108, 0.0186618, -0.00371541],
[0.6415743, 0.3985468, 0.1324422, 0.0214160, -0.00062401],
[0.7503690, 0.5447616, 0.2804541, 0.0539851, 0.00242230],
[0.7452626, 0.5644594, 0.3324679, 0.0865153, 0.00454960],
[0.8059782, 0.6586838, 0.4229577, 0.1452990, 0.00976702],
[0.8549542, 0.7226562, 0.4991309, 0.2420093, 0.02772661],
[0.8571332, 0.7285741, 0.5279076, 0.2824549, 0.06030276],
[0.8835633, 0.7727077, 0.5957984, 0.3411303, 0.09652185],
[0.9071558, 0.8299587, 0.6771400, 0.4402896, 0.17469338]]
# Get iteration points
grad_list = [prob.grad(x) for x in x_list]
delta_x = [np.array(x_list[i+1])-np.array(x_list[i])
for i in range(len(x_list)-1)]
delta_grad = [grad_list[i+1]-grad_list[i]
for i in range(len(grad_list)-1)]
hess = SR1(init_scale=1, min_denominator=1e-2)
hess.initialize(len(x_list[0]), 'hess')
# Compare the Hessian and its inverse
for i in range(len(delta_x)-1):
s = delta_x[i]
y = delta_grad[i]
hess.update(s, y)
# Test skip update
B = np.copy(hess.get_matrix())
s = delta_x[17]
y = delta_grad[17]
hess.update(s, y)
B_updated = np.copy(hess.get_matrix())
assert_array_equal(B, B_updated)
def test_BFGS_skip_update(self):
# Define auxiliary problem
prob = Rosenbrock(n=5)
# Define iteration points
x_list = [[0.0976270, 0.4303787, 0.2055267, 0.0897663, -0.15269040],
[0.1847239, 0.0505757, 0.2123832, 0.0255081, 0.00083286],
[0.2142498, -0.0188480, 0.0503822, 0.0347033, 0.03323606],
[0.2071680, -0.0185071, 0.0341337, -0.0139298, 0.02881750],
[0.1533055, -0.0322935, 0.0280418, -0.0083592, 0.01503699],
[0.1382378, -0.0276671, 0.0266161, -0.0074060, 0.02801610],
[0.1651957, -0.0049124, 0.0269665, -0.0040025, 0.02138184]]
# Get iteration points
grad_list = [prob.grad(x) for x in x_list]
delta_x = [np.array(x_list[i+1])-np.array(x_list[i])
for i in range(len(x_list)-1)]
delta_grad = [grad_list[i+1]-grad_list[i]
for i in range(len(grad_list)-1)]
hess = BFGS(init_scale=1, min_curvature=10)
hess.initialize(len(x_list[0]), 'hess')
# Compare the Hessian and its inverse
for i in range(len(delta_x)-1):
s = delta_x[i]
y = delta_grad[i]
hess.update(s, y)
# Test skip update
B = np.copy(hess.get_matrix())
s = delta_x[5]
y = delta_grad[5]
hess.update(s, y)
B_updated = np.copy(hess.get_matrix())
assert_array_equal(B, B_updated)
@pytest.mark.parametrize('strategy', [BFGS, SR1])
@pytest.mark.parametrize('approx_type', ['hess', 'inv_hess'])
def test_matmul_equals_dot(strategy, approx_type):
H = strategy(init_scale=1)
H.initialize(2, approx_type)
v = np.array([1, 2])
assert_array_equal(H @ v, H.dot(v))

View file

@ -0,0 +1,167 @@
import numpy as np
from numpy.testing import assert_allclose, assert_equal
import pytest
from scipy.optimize._pava_pybind import pava
from scipy.optimize import isotonic_regression
class TestIsotonicRegression:
@pytest.mark.parametrize(
("y", "w", "msg"),
[
([[0, 1]], None,
"array has incorrect number of dimensions: 2; expected 1"),
([0, 1], [[1, 2]],
"Input arrays y and w must have one dimension of equal length"),
([0, 1], [1],
"Input arrays y and w must have one dimension of equal length"),
(1, [1, 2],
"Input arrays y and w must have one dimension of equal length"),
([1, 2], 1,
"Input arrays y and w must have one dimension of equal length"),
([0, 1], [0, 1],
"Weights w must be strictly positive"),
]
)
def test_raise_error(self, y, w, msg):
with pytest.raises(ValueError, match=msg):
isotonic_regression(y=y, weights=w)
def test_simple_pava(self):
# Test case of Busing 2020
# https://doi.org/10.18637/jss.v102.c01
y = np.array([8, 4, 8, 2, 2, 0, 8], dtype=np.float64)
w = np.ones_like(y)
r = np.full(shape=y.shape[0] + 1, fill_value=-1, dtype=np.intp)
pava(y, w, r)
assert_allclose(y, [4, 4, 4, 4, 4, 4, 8])
# Only first 2 elements of w are changed.
assert_allclose(w, [6, 1, 1, 1, 1, 1, 1])
# Only first 3 elements of r are changed.
assert_allclose(r, [0, 6, 7, -1, -1, -1, -1, -1])
@pytest.mark.parametrize("y_dtype", [np.float64, np.float32, np.int64, np.int32])
@pytest.mark.parametrize("w_dtype", [np.float64, np.float32, np.int64, np.int32])
@pytest.mark.parametrize("w", [None, "ones"])
def test_simple_isotonic_regression(self, w, w_dtype, y_dtype):
# Test case of Busing 2020
# https://doi.org/10.18637/jss.v102.c01
y = np.array([8, 4, 8, 2, 2, 0, 8], dtype=y_dtype)
if w is not None:
w = np.ones_like(y, dtype=w_dtype)
res = isotonic_regression(y, weights=w)
assert res.x.dtype == np.float64
assert res.weights.dtype == np.float64
assert_allclose(res.x, [4, 4, 4, 4, 4, 4, 8])
assert_allclose(res.weights, [6, 1])
assert_allclose(res.blocks, [0, 6, 7])
# Assert that y was not overwritten
assert_equal(y, np.array([8, 4, 8, 2, 2, 0, 8], dtype=np.float64))
@pytest.mark.parametrize("increasing", [True, False])
def test_linspace(self, increasing):
n = 10
y = np.linspace(0, 1, n) if increasing else np.linspace(1, 0, n)
res = isotonic_regression(y, increasing=increasing)
assert_allclose(res.x, y)
assert_allclose(res.blocks, np.arange(n + 1))
def test_weights(self):
w = np.array([1, 2, 5, 0.5, 0.5, 0.5, 1, 3])
y = np.array([3, 2, 1, 10, 9, 8, 20, 10])
res = isotonic_regression(y, weights=w)
assert_allclose(res.x, [12/8, 12/8, 12/8, 9, 9, 9, 50/4, 50/4])
assert_allclose(res.weights, [8, 1.5, 4])
assert_allclose(res.blocks, [0, 3, 6, 8])
# weights are like repeated observations, we repeat the 3rd element 5
# times.
w2 = np.array([1, 2, 1, 1, 1, 1, 1, 0.5, 0.5, 0.5, 1, 3])
y2 = np.array([3, 2, 1, 1, 1, 1, 1, 10, 9, 8, 20, 10])
res2 = isotonic_regression(y2, weights=w2)
assert_allclose(np.diff(res2.x[0:7]), 0)
assert_allclose(res2.x[4:], res.x)
assert_allclose(res2.weights, res.weights)
assert_allclose(res2.blocks[1:] - 4, res.blocks[1:])
def test_against_R_monotone(self):
y = [0, 6, 8, 3, 5, 2, 1, 7, 9, 4]
res = isotonic_regression(y)
# R code
# library(monotone)
# options(digits=8)
# monotone(c(0, 6, 8, 3, 5, 2, 1, 7, 9, 4))
x_R = [
0, 4.1666667, 4.1666667, 4.1666667, 4.1666667, 4.1666667,
4.1666667, 6.6666667, 6.6666667, 6.6666667,
]
assert_allclose(res.x, x_R)
assert_equal(res.blocks, [0, 1, 7, 10])
n = 100
y = np.linspace(0, 1, num=n, endpoint=False)
y = 5 * y + np.sin(10 * y)
res = isotonic_regression(y)
# R code
# library(monotone)
# n <- 100
# y <- 5 * ((1:n)-1)/n + sin(10 * ((1:n)-1)/n)
# options(digits=8)
# monotone(y)
x_R = [
0.00000000, 0.14983342, 0.29866933, 0.44552021, 0.58941834, 0.72942554,
0.86464247, 0.99421769, 1.11735609, 1.23332691, 1.34147098, 1.44120736,
1.53203909, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
1.57081100, 1.57081100, 1.57081100, 1.62418532, 1.71654534, 1.81773256,
1.92723551, 2.04445967, 2.16873336, 2.29931446, 2.43539782, 2.57612334,
2.72058450, 2.86783750, 3.01691060, 3.16681390, 3.31654920, 3.46511999,
3.61154136, 3.75484992, 3.89411335, 4.02843976, 4.15698660, 4.27896904,
4.39366786, 4.50043662, 4.59870810, 4.68799998, 4.76791967, 4.83816823,
4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
4.86564130, 4.86564130, 4.86564130, 4.86564130,
]
assert_allclose(res.x, x_R)
# Test increasing
assert np.all(np.diff(res.x) >= 0)
# Test balance property: sum(y) == sum(x)
assert_allclose(np.sum(res.x), np.sum(y))
# Reverse order
res_inv = isotonic_regression(-y, increasing=False)
assert_allclose(-res_inv.x, res.x)
assert_equal(res_inv.blocks, res.blocks)
def test_readonly(self):
x = np.arange(3, dtype=float)
w = np.ones(3, dtype=float)
x.flags.writeable = False
w.flags.writeable = False
res = isotonic_regression(x, weights=w)
assert np.all(np.isfinite(res.x))
assert np.all(np.isfinite(res.weights))
assert np.all(np.isfinite(res.blocks))
def test_non_contiguous_arrays(self):
x = np.arange(10, dtype=float)[::3]
w = np.ones(10, dtype=float)[::3]
assert not x.flags.c_contiguous
assert not x.flags.f_contiguous
assert not w.flags.c_contiguous
assert not w.flags.f_contiguous
res = isotonic_regression(x, weights=w)
assert np.all(np.isfinite(res.x))
assert np.all(np.isfinite(res.weights))
assert np.all(np.isfinite(res.blocks))

View file

@ -0,0 +1,65 @@
import numpy as np
from numpy.testing import assert_allclose
import scipy.linalg
from scipy.optimize import minimize
def test_1():
def f(x):
return x**4, 4*x**3
for gtol in [1e-8, 1e-12, 1e-20]:
for maxcor in range(20, 35):
result = minimize(fun=f, jac=True, method='L-BFGS-B', x0=20,
options={'gtol': gtol, 'maxcor': maxcor})
H1 = result.hess_inv(np.array([1])).reshape(1,1)
H2 = result.hess_inv.todense()
assert_allclose(H1, H2)
def test_2():
H0 = [[3, 0], [1, 2]]
def f(x):
return np.dot(x, np.dot(scipy.linalg.inv(H0), x))
result1 = minimize(fun=f, method='L-BFGS-B', x0=[10, 20])
result2 = minimize(fun=f, method='BFGS', x0=[10, 20])
H1 = result1.hess_inv.todense()
H2 = np.vstack((
result1.hess_inv(np.array([1, 0])),
result1.hess_inv(np.array([0, 1]))))
assert_allclose(
result1.hess_inv(np.array([1, 0]).reshape(2,1)).reshape(-1),
result1.hess_inv(np.array([1, 0])))
assert_allclose(H1, H2)
assert_allclose(H1, result2.hess_inv, rtol=1e-2, atol=0.03)
def test_3():
def todense_old_impl(self):
s, y, n_corrs, rho = self.sk, self.yk, self.n_corrs, self.rho
I_arr = np.eye(*self.shape, dtype=self.dtype)
Hk = I_arr
for i in range(n_corrs):
A1 = I_arr - s[i][:, np.newaxis] * y[i][np.newaxis, :] * rho[i]
A2 = I_arr - y[i][:, np.newaxis] * s[i][np.newaxis, :] * rho[i]
Hk = np.dot(A1, np.dot(Hk, A2)) + (rho[i] * s[i][:, np.newaxis] *
s[i][np.newaxis, :])
return Hk
H0 = [[3, 0], [1, 2]]
def f(x):
return np.dot(x, np.dot(scipy.linalg.inv(H0), x))
result1 = minimize(fun=f, method='L-BFGS-B', x0=[10, 20])
assert_allclose(result1.hess_inv.todense(), todense_old_impl(result1.hess_inv))

View file

@ -0,0 +1,122 @@
import numpy as np
from scipy.optimize import _lbfgsb, minimize
def objfun(x):
"""simplified objective func to test lbfgsb bound violation"""
x0 = [0.8750000000000278,
0.7500000000000153,
0.9499999999999722,
0.8214285714285992,
0.6363636363636085]
x1 = [1.0, 0.0, 1.0, 0.0, 0.0]
x2 = [1.0,
0.0,
0.9889733043149325,
0.0,
0.026353554421041155]
x3 = [1.0,
0.0,
0.9889917442915558,
0.0,
0.020341986743231205]
f0 = 5163.647901211178
f1 = 5149.8181642072905
f2 = 5149.379332309634
f3 = 5149.374490771297
g0 = np.array([-0.5934820547965749,
1.6251549718258351,
-71.99168459202559,
5.346636965797545,
37.10732723092604])
g1 = np.array([-0.43295349282641515,
1.008607936794592,
18.223666726602975,
31.927010036981997,
-19.667512518739386])
g2 = np.array([-0.4699874455100256,
0.9466285353668347,
-0.016874360242016825,
48.44999161133457,
5.819631620590712])
g3 = np.array([-0.46970678696829116,
0.9612719312174818,
0.006129809488833699,
48.43557729419473,
6.005481418498221])
if np.allclose(x, x0):
f = f0
g = g0
elif np.allclose(x, x1):
f = f1
g = g1
elif np.allclose(x, x2):
f = f2
g = g2
elif np.allclose(x, x3):
f = f3
g = g3
else:
raise ValueError(
'Simplified objective function not defined '
'at requested point')
return (np.copy(f), np.copy(g))
def test_setulb_floatround():
"""test if setulb() violates bounds
checks for violation due to floating point rounding error
"""
n = 5
m = 10
factr = 1e7
pgtol = 1e-5
maxls = 20
nbd = np.full(shape=(n,), fill_value=2, dtype=np.int32)
low_bnd = np.zeros(n, dtype=np.float64)
upper_bnd = np.ones(n, dtype=np.float64)
x0 = np.array(
[0.8750000000000278,
0.7500000000000153,
0.9499999999999722,
0.8214285714285992,
0.6363636363636085])
x = np.copy(x0)
f = np.array(0.0, dtype=np.float64)
g = np.zeros(n, dtype=np.float64)
wa = np.zeros(2*m*n + 5*n + 11*m*m + 8*m, dtype=np.float64)
iwa = np.zeros(3*n, dtype=np.int32)
task = np.zeros(2, dtype=np.int32)
ln_task = np.zeros(2, dtype=np.int32)
lsave = np.zeros(4, dtype=np.int32)
isave = np.zeros(44, dtype=np.int32)
dsave = np.zeros(29, dtype=np.float64)
for n_iter in range(7): # 7 steps required to reproduce error
f, g = objfun(x)
_lbfgsb.setulb(m, x, low_bnd, upper_bnd, nbd, f, g, factr, pgtol, wa,
iwa, task, lsave, isave, dsave, maxls, ln_task)
assert (x <= upper_bnd).all() and (x >= low_bnd).all(), (
"_lbfgsb.setulb() stepped to a point outside of the bounds")
def test_gh_issue18730():
# issue 18730 reported that l-bfgs-b did not work with objectives
# returning single precision gradient arrays
def fun_single_precision(x):
x = x.astype(np.float32)
return np.sum(x**2), (2*x)
res = minimize(fun_single_precision, x0=np.array([1., 1.]), jac=True,
method="l-bfgs-b")
np.testing.assert_allclose(res.fun, 0., atol=1e-15)

View file

@ -0,0 +1,986 @@
from itertools import product
from multiprocessing import Pool
import numpy as np
from numpy.linalg import norm
from numpy.testing import (assert_, assert_allclose,
assert_equal, suppress_warnings)
import pytest
from pytest import raises as assert_raises
from scipy.sparse import issparse, lil_array
from scipy.sparse.linalg import aslinearoperator
from scipy.optimize import least_squares, Bounds
from scipy.optimize._lsq.least_squares import IMPLEMENTED_LOSSES
from scipy.optimize._lsq.common import EPS, make_strictly_feasible, CL_scaling_vector
from scipy.optimize import OptimizeResult
def fun_trivial(x, a=0):
return (x - a)**2 + 5.0
def jac_trivial(x, a=0.0):
return 2 * (x - a)
def fun_2d_trivial(x):
return np.array([x[0], x[1]])
def jac_2d_trivial(x):
return np.identity(2)
def fun_rosenbrock(x):
return np.array([10 * (x[1] - x[0]**2), (1 - x[0])])
class Fun_Rosenbrock:
def __init__(self):
self.nfev = 0
def __call__(self, x, a=0):
self.nfev += 1
return fun_rosenbrock(x)
def jac_rosenbrock(x):
return np.array([
[-20 * x[0], 10],
[-1, 0]
])
def jac_rosenbrock_bad_dim(x):
return np.array([
[-20 * x[0], 10],
[-1, 0],
[0.0, 0.0]
])
def fun_rosenbrock_cropped(x):
return fun_rosenbrock(x)[0]
def jac_rosenbrock_cropped(x):
return jac_rosenbrock(x)[0]
# When x is 1-D array, return is 2-D array.
def fun_wrong_dimensions(x):
return np.array([x, x**2, x**3])
def jac_wrong_dimensions(x, a=0.0):
return np.atleast_3d(jac_trivial(x, a=a))
def fun_bvp(x):
n = int(np.sqrt(x.shape[0]))
u = np.zeros((n + 2, n + 2))
x = x.reshape((n, n))
u[1:-1, 1:-1] = x
y = u[:-2, 1:-1] + u[2:, 1:-1] + u[1:-1, :-2] + u[1:-1, 2:] - 4 * x + x**3
return y.ravel()
class BroydenTridiagonal:
def __init__(self, n=100, mode='sparse'):
rng = np.random.RandomState(0)
self.n = n
self.x0 = -np.ones(n)
self.lb = np.linspace(-2, -1.5, n)
self.ub = np.linspace(-0.8, 0.0, n)
self.lb += 0.1 * rng.randn(n)
self.ub += 0.1 * rng.randn(n)
self.x0 += 0.1 * rng.randn(n)
self.x0 = make_strictly_feasible(self.x0, self.lb, self.ub)
if mode == 'sparse':
self.sparsity = lil_array((n, n), dtype=int)
i = np.arange(n)
self.sparsity[i, i] = 1
i = np.arange(1, n)
self.sparsity[i, i - 1] = 1
i = np.arange(n - 1)
self.sparsity[i, i + 1] = 1
self.jac = self._jac
elif mode == 'operator':
self.jac = lambda x: aslinearoperator(self._jac(x))
elif mode == 'dense':
self.sparsity = None
self.jac = lambda x: self._jac(x).toarray()
else:
assert_(False)
def fun(self, x):
f = (3 - x) * x + 1
f[1:] -= x[:-1]
f[:-1] -= 2 * x[1:]
return f
def _jac(self, x):
J = lil_array((self.n, self.n))
i = np.arange(self.n)
J[i, i] = 3 - 2 * x
i = np.arange(1, self.n)
J[i, i - 1] = -1
i = np.arange(self.n - 1)
J[i, i + 1] = -2
return J
class ExponentialFittingProblem:
"""Provide data and function for exponential fitting in the form
y = a + exp(b * x) + noise."""
def __init__(self, a, b, noise, n_outliers=1, x_range=(-1, 1),
n_points=11, random_seed=None):
rng = np.random.RandomState(random_seed)
self.m = n_points
self.n = 2
self.p0 = np.zeros(2)
self.x = np.linspace(x_range[0], x_range[1], n_points)
self.y = a + np.exp(b * self.x)
self.y += noise * rng.randn(self.m)
outliers = rng.randint(0, self.m, n_outliers)
self.y[outliers] += 50 * noise * rng.rand(n_outliers)
self.p_opt = np.array([a, b])
def fun(self, p):
return p[0] + np.exp(p[1] * self.x) - self.y
def jac(self, p):
J = np.empty((self.m, self.n))
J[:, 0] = 1
J[:, 1] = self.x * np.exp(p[1] * self.x)
return J
def cubic_soft_l1(z):
rho = np.empty((3, z.size))
t = 1 + z
rho[0] = 3 * (t**(1/3) - 1)
rho[1] = t ** (-2/3)
rho[2] = -2/3 * t**(-5/3)
return rho
LOSSES = list(IMPLEMENTED_LOSSES.keys()) + [cubic_soft_l1]
class BaseMixin:
def test_basic(self):
# Test that the basic calling sequence works.
res = least_squares(fun_trivial, 2., method=self.method)
assert_allclose(res.x, 0, atol=1e-4)
assert_allclose(res.fun, fun_trivial(res.x))
def test_args_kwargs(self):
# Test that args and kwargs are passed correctly to the functions.
a = 3.0
for jac in ['2-point', '3-point', 'cs', jac_trivial]:
with suppress_warnings() as sup:
sup.filter(
UserWarning,
"jac='(3-point|cs)' works equivalently to '2-point' for method='lm'"
)
res = least_squares(fun_trivial, 2.0, jac, args=(a,),
method=self.method)
res1 = least_squares(fun_trivial, 2.0, jac, kwargs={'a': a},
method=self.method)
assert_allclose(res.x, a, rtol=1e-4)
assert_allclose(res1.x, a, rtol=1e-4)
assert_raises(TypeError, least_squares, fun_trivial, 2.0,
args=(3, 4,), method=self.method)
assert_raises(TypeError, least_squares, fun_trivial, 2.0,
kwargs={'kaboom': 3}, method=self.method)
def test_jac_options(self):
for jac in ['2-point', '3-point', 'cs', jac_trivial]:
with suppress_warnings() as sup:
sup.filter(
UserWarning,
"jac='(3-point|cs)' works equivalently to '2-point' for method='lm'"
)
res = least_squares(fun_trivial, 2.0, jac, method=self.method)
assert_allclose(res.x, 0, atol=1e-4)
assert_raises(ValueError, least_squares, fun_trivial, 2.0, jac='oops',
method=self.method)
def test_nfev_options(self):
for max_nfev in [None, 20]:
res = least_squares(fun_trivial, 2.0, max_nfev=max_nfev,
method=self.method)
assert_allclose(res.x, 0, atol=1e-4)
def test_x_scale_options(self):
for x_scale in [1.0, np.array([0.5]), 'jac']:
res = least_squares(fun_trivial, 2.0, x_scale=x_scale)
assert_allclose(res.x, 0)
assert_raises(ValueError, least_squares, fun_trivial,
2.0, x_scale='auto', method=self.method)
assert_raises(ValueError, least_squares, fun_trivial,
2.0, x_scale=-1.0, method=self.method)
assert_raises(ValueError, least_squares, fun_trivial,
2.0, x_scale=1.0+2.0j, method=self.method)
def test_diff_step(self):
res1 = least_squares(fun_trivial, 2.0, diff_step=1e-1,
method=self.method)
res3 = least_squares(fun_trivial, 2.0,
diff_step=None, method=self.method)
assert_allclose(res1.x, 0, atol=1e-4)
assert_allclose(res3.x, 0, atol=1e-4)
def test_incorrect_options_usage(self):
assert_raises(TypeError, least_squares, fun_trivial, 2.0,
method=self.method, options={'no_such_option': 100})
assert_raises(TypeError, least_squares, fun_trivial, 2.0,
method=self.method, options={'max_nfev': 100})
def test_full_result(self):
# MINPACK doesn't work very well with factor=100 on this problem,
# thus using low 'atol'.
res = least_squares(fun_trivial, 2.0, method=self.method)
assert_allclose(res.x, 0, atol=1e-4)
assert_allclose(res.cost, 12.5)
assert_allclose(res.fun, 5)
assert_allclose(res.jac, 0, atol=1e-4)
assert_allclose(res.grad, 0, atol=1e-2)
assert_allclose(res.optimality, 0, atol=1e-2)
assert_equal(res.active_mask, 0)
if self.method == 'lm':
assert_(res.njev is None)
else:
assert_(res.nfev < 10)
assert_(res.njev < 10)
assert_(res.status > 0)
assert_(res.success)
def test_full_result_single_fev(self):
# MINPACK checks the number of nfev after the iteration,
# so it's hard to tell what he is going to compute.
if self.method == 'lm':
return
res = least_squares(fun_trivial, 2.0, method=self.method,
max_nfev=1)
assert_equal(res.x, np.array([2]))
assert_equal(res.cost, 40.5)
assert_equal(res.fun, np.array([9]))
assert_equal(res.jac, np.array([[4]]))
assert_equal(res.grad, np.array([36]))
assert_equal(res.optimality, 36)
assert_equal(res.active_mask, np.array([0]))
assert_equal(res.nfev, 1)
assert_equal(res.njev, 1)
assert_equal(res.status, 0)
assert_equal(res.success, 0)
def test_nfev(self):
# checks that the true number of nfev are being consumed
for i in range(1, 3):
rng = np.random.default_rng(128908)
x0 = rng.uniform(size=2) * 10
ftrivial = Fun_Rosenbrock()
res = least_squares(
ftrivial, x0, jac=jac_rosenbrock, method=self.method, max_nfev=i
)
assert res.nfev == ftrivial.nfev
def test_rosenbrock(self):
x0 = [-2, 1]
x_opt = [1, 1]
for jac, x_scale, tr_solver in product(
['2-point', '3-point', 'cs', jac_rosenbrock],
[1.0, np.array([1.0, 0.2]), 'jac'],
['exact', 'lsmr']):
with suppress_warnings() as sup:
sup.filter(
UserWarning,
"jac='(3-point|cs)' works equivalently to '2-point' for method='lm'"
)
res = least_squares(fun_rosenbrock, x0, jac, x_scale=x_scale,
tr_solver=tr_solver, method=self.method)
assert_allclose(res.x, x_opt)
def test_rosenbrock_cropped(self):
x0 = [-2, 1]
if self.method == 'lm':
assert_raises(ValueError, least_squares, fun_rosenbrock_cropped,
x0, method='lm')
else:
for jac, x_scale, tr_solver in product(
['2-point', '3-point', 'cs', jac_rosenbrock_cropped],
[1.0, np.array([1.0, 0.2]), 'jac'],
['exact', 'lsmr']):
res = least_squares(
fun_rosenbrock_cropped, x0, jac, x_scale=x_scale,
tr_solver=tr_solver, method=self.method)
assert_allclose(res.cost, 0, atol=1e-14)
def test_fun_wrong_dimensions(self):
assert_raises(ValueError, least_squares, fun_wrong_dimensions,
2.0, method=self.method)
def test_jac_wrong_dimensions(self):
assert_raises(ValueError, least_squares, fun_trivial,
2.0, jac_wrong_dimensions, method=self.method)
def test_fun_and_jac_inconsistent_dimensions(self):
x0 = [1, 2]
assert_raises(ValueError, least_squares, fun_rosenbrock, x0,
jac_rosenbrock_bad_dim, method=self.method)
def test_x0_multidimensional(self):
x0 = np.ones(4).reshape(2, 2)
assert_raises(ValueError, least_squares, fun_trivial, x0,
method=self.method)
def test_x0_complex_scalar(self):
x0 = 2.0 + 0.0*1j
assert_raises(ValueError, least_squares, fun_trivial, x0,
method=self.method)
def test_x0_complex_array(self):
x0 = [1.0, 2.0 + 0.0*1j]
assert_raises(ValueError, least_squares, fun_trivial, x0,
method=self.method)
def test_bvp(self):
# This test was introduced with fix #5556. It turned out that
# dogbox solver had a bug with trust-region radius update, which
# could block its progress and create an infinite loop. And this
# discrete boundary value problem is the one which triggers it.
n = 10
x0 = np.ones(n**2)
if self.method == 'lm':
max_nfev = 5000 # To account for Jacobian estimation.
else:
max_nfev = 100
res = least_squares(fun_bvp, x0, ftol=1e-2, method=self.method,
max_nfev=max_nfev)
assert_(res.nfev < max_nfev)
assert_(res.cost < 0.5)
def test_error_raised_when_all_tolerances_below_eps(self):
# Test that all 0 tolerances are not allowed.
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
method=self.method, ftol=None, xtol=None, gtol=None)
def test_convergence_with_only_one_tolerance_enabled(self):
if self.method == 'lm':
return # should not do test
x0 = [-2, 1]
x_opt = [1, 1]
for ftol, xtol, gtol in [(1e-8, None, None),
(None, 1e-8, None),
(None, None, 1e-8)]:
res = least_squares(fun_rosenbrock, x0, jac=jac_rosenbrock,
ftol=ftol, gtol=gtol, xtol=xtol,
method=self.method)
assert_allclose(res.x, x_opt)
@pytest.mark.fail_slow(5.0)
def test_workers(self):
serial = least_squares(fun_trivial, 2.0, method=self.method)
reses = []
for workers in [None, 2]:
res = least_squares(
fun_trivial, 2.0, method=self.method, workers=workers
)
reses.append(res)
with Pool() as workers:
res = least_squares(
fun_trivial, 2.0, method=self.method, workers=workers.map
)
reses.append(res)
for res in reses:
assert res.success
assert_equal(res.x, serial.x)
assert_equal(res.nfev, serial.nfev)
assert_equal(res.njev, serial.njev)
class BoundsMixin:
def test_inconsistent(self):
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
bounds=(10.0, 0.0), method=self.method)
def test_infeasible(self):
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
bounds=(3., 4), method=self.method)
def test_wrong_number(self):
assert_raises(ValueError, least_squares, fun_trivial, 2.,
bounds=(1., 2, 3), method=self.method)
def test_inconsistent_shape(self):
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
bounds=(1.0, [2.0, 3.0]), method=self.method)
# 1-D array won't be broadcast
assert_raises(ValueError, least_squares, fun_rosenbrock, [1.0, 2.0],
bounds=([0.0], [3.0, 4.0]), method=self.method)
def test_in_bounds(self):
for jac in ['2-point', '3-point', 'cs', jac_trivial]:
res = least_squares(fun_trivial, 2.0, jac=jac,
bounds=(-1.0, 3.0), method=self.method)
assert_allclose(res.x, 0.0, atol=1e-4)
assert_equal(res.active_mask, [0])
assert_(-1 <= res.x <= 3)
res = least_squares(fun_trivial, 2.0, jac=jac,
bounds=(0.5, 3.0), method=self.method)
assert_allclose(res.x, 0.5, atol=1e-4)
assert_equal(res.active_mask, [-1])
assert_(0.5 <= res.x <= 3)
def test_bounds_shape(self):
def get_bounds_direct(lb, ub):
return lb, ub
def get_bounds_instances(lb, ub):
return Bounds(lb, ub)
for jac in ['2-point', '3-point', 'cs', jac_2d_trivial]:
for bounds_func in [get_bounds_direct, get_bounds_instances]:
x0 = [1.0, 1.0]
res = least_squares(fun_2d_trivial, x0, jac=jac)
assert_allclose(res.x, [0.0, 0.0])
res = least_squares(fun_2d_trivial, x0, jac=jac,
bounds=bounds_func(0.5, [2.0, 2.0]),
method=self.method)
assert_allclose(res.x, [0.5, 0.5])
res = least_squares(fun_2d_trivial, x0, jac=jac,
bounds=bounds_func([0.3, 0.2], 3.0),
method=self.method)
assert_allclose(res.x, [0.3, 0.2])
res = least_squares(
fun_2d_trivial, x0, jac=jac,
bounds=bounds_func([-1, 0.5], [1.0, 3.0]),
method=self.method)
assert_allclose(res.x, [0.0, 0.5], atol=1e-5)
def test_bounds_instances(self):
res = least_squares(fun_trivial, 0.5, bounds=Bounds())
assert_allclose(res.x, 0.0, atol=1e-4)
res = least_squares(fun_trivial, 3.0, bounds=Bounds(lb=1.0))
assert_allclose(res.x, 1.0, atol=1e-4)
res = least_squares(fun_trivial, 0.5, bounds=Bounds(lb=-1.0, ub=1.0))
assert_allclose(res.x, 0.0, atol=1e-4)
res = least_squares(fun_trivial, -3.0, bounds=Bounds(ub=-1.0))
assert_allclose(res.x, -1.0, atol=1e-4)
res = least_squares(fun_2d_trivial, [0.5, 0.5],
bounds=Bounds(lb=[-1.0, -1.0], ub=1.0))
assert_allclose(res.x, [0.0, 0.0], atol=1e-5)
res = least_squares(fun_2d_trivial, [0.5, 0.5],
bounds=Bounds(lb=[0.1, 0.1]))
assert_allclose(res.x, [0.1, 0.1], atol=1e-5)
@pytest.mark.fail_slow(10)
def test_rosenbrock_bounds(self):
x0_1 = np.array([-2.0, 1.0])
x0_2 = np.array([2.0, 2.0])
x0_3 = np.array([-2.0, 2.0])
x0_4 = np.array([0.0, 2.0])
x0_5 = np.array([-1.2, 1.0])
problems = [
(x0_1, ([-np.inf, -1.5], np.inf)),
(x0_2, ([-np.inf, 1.5], np.inf)),
(x0_3, ([-np.inf, 1.5], np.inf)),
(x0_4, ([-np.inf, 1.5], [1.0, np.inf])),
(x0_2, ([1.0, 1.5], [3.0, 3.0])),
(x0_5, ([-50.0, 0.0], [0.5, 100]))
]
for x0, bounds in problems:
for jac, x_scale, tr_solver in product(
['2-point', '3-point', 'cs', jac_rosenbrock],
[1.0, [1.0, 0.5], 'jac'],
['exact', 'lsmr']):
res = least_squares(fun_rosenbrock, x0, jac, bounds,
x_scale=x_scale, tr_solver=tr_solver,
method=self.method)
assert_allclose(res.optimality, 0.0, atol=1e-5)
class SparseMixin:
def test_exact_tr_solver(self):
p = BroydenTridiagonal()
assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
tr_solver='exact', method=self.method)
assert_raises(ValueError, least_squares, p.fun, p.x0,
tr_solver='exact', jac_sparsity=p.sparsity,
method=self.method)
def test_equivalence(self):
sparse = BroydenTridiagonal(mode='sparse')
dense = BroydenTridiagonal(mode='dense')
res_sparse = least_squares(
sparse.fun, sparse.x0, jac=sparse.jac,
method=self.method)
res_dense = least_squares(
dense.fun, dense.x0, jac=sparse.jac,
method=self.method)
assert_equal(res_sparse.nfev, res_dense.nfev)
assert_allclose(res_sparse.x, res_dense.x, atol=1e-20)
assert_allclose(res_sparse.cost, 0, atol=1e-20)
assert_allclose(res_dense.cost, 0, atol=1e-20)
def test_tr_options(self):
p = BroydenTridiagonal()
res = least_squares(p.fun, p.x0, p.jac, method=self.method,
tr_options={'btol': 1e-10})
assert_allclose(res.cost, 0, atol=1e-20)
def test_wrong_parameters(self):
p = BroydenTridiagonal()
assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
tr_solver='best', method=self.method)
assert_raises(TypeError, least_squares, p.fun, p.x0, p.jac,
tr_solver='lsmr', tr_options={'tol': 1e-10})
def test_solver_selection(self):
sparse = BroydenTridiagonal(mode='sparse')
dense = BroydenTridiagonal(mode='dense')
res_sparse = least_squares(sparse.fun, sparse.x0, jac=sparse.jac,
method=self.method)
res_dense = least_squares(dense.fun, dense.x0, jac=dense.jac,
method=self.method)
assert_allclose(res_sparse.cost, 0, atol=1e-20)
assert_allclose(res_dense.cost, 0, atol=1e-20)
assert_(issparse(res_sparse.jac))
assert_(isinstance(res_dense.jac, np.ndarray))
def test_numerical_jac(self):
p = BroydenTridiagonal()
for jac in ['2-point', '3-point', 'cs']:
res_dense = least_squares(p.fun, p.x0, jac, method=self.method)
res_sparse = least_squares(
p.fun, p.x0, jac,method=self.method,
jac_sparsity=p.sparsity)
assert_equal(res_dense.nfev, res_sparse.nfev)
assert_allclose(res_dense.x, res_sparse.x, atol=1e-20)
assert_allclose(res_dense.cost, 0, atol=1e-20)
assert_allclose(res_sparse.cost, 0, atol=1e-20)
@pytest.mark.fail_slow(10)
def test_with_bounds(self):
p = BroydenTridiagonal()
for jac, jac_sparsity in product(
[p.jac, '2-point', '3-point', 'cs'], [None, p.sparsity]):
res_1 = least_squares(
p.fun, p.x0, jac, bounds=(p.lb, np.inf),
method=self.method,jac_sparsity=jac_sparsity)
res_2 = least_squares(
p.fun, p.x0, jac, bounds=(-np.inf, p.ub),
method=self.method, jac_sparsity=jac_sparsity)
res_3 = least_squares(
p.fun, p.x0, jac, bounds=(p.lb, p.ub),
method=self.method, jac_sparsity=jac_sparsity)
assert_allclose(res_1.optimality, 0, atol=1e-10)
assert_allclose(res_2.optimality, 0, atol=1e-10)
assert_allclose(res_3.optimality, 0, atol=1e-10)
def test_wrong_jac_sparsity(self):
p = BroydenTridiagonal()
sparsity = p.sparsity[:-1]
assert_raises(ValueError, least_squares, p.fun, p.x0,
jac_sparsity=sparsity, method=self.method)
def test_linear_operator(self):
p = BroydenTridiagonal(mode='operator')
res = least_squares(p.fun, p.x0, p.jac, method=self.method)
assert_allclose(res.cost, 0.0, atol=1e-20)
assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
method=self.method, tr_solver='exact')
def test_x_scale_jac_scale(self):
p = BroydenTridiagonal()
res = least_squares(p.fun, p.x0, p.jac, method=self.method,
x_scale='jac')
assert_allclose(res.cost, 0.0, atol=1e-20)
p = BroydenTridiagonal(mode='operator')
assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
method=self.method, x_scale='jac')
class LossFunctionMixin:
def test_options(self):
for loss in LOSSES:
res = least_squares(fun_trivial, 2.0, loss=loss,
method=self.method)
assert_allclose(res.x, 0, atol=1e-15)
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
loss='hinge', method=self.method)
def test_fun(self):
# Test that res.fun is actual residuals, and not modified by loss
# function stuff.
for loss in LOSSES:
res = least_squares(fun_trivial, 2.0, loss=loss,
method=self.method)
assert_equal(res.fun, fun_trivial(res.x))
def test_grad(self):
# Test that res.grad is true gradient of loss function at the
# solution. Use max_nfev = 1, to avoid reaching minimum.
x = np.array([2.0]) # res.x will be this.
res = least_squares(fun_trivial, x, jac_trivial, loss='linear',
max_nfev=1, method=self.method)
assert_equal(res.grad, 2 * x * (x**2 + 5))
res = least_squares(fun_trivial, x, jac_trivial, loss='huber',
max_nfev=1, method=self.method)
assert_equal(res.grad, 2 * x)
res = least_squares(fun_trivial, x, jac_trivial, loss='soft_l1',
max_nfev=1, method=self.method)
assert_allclose(res.grad,
2 * x * (x**2 + 5) / (1 + (x**2 + 5)**2)**0.5)
res = least_squares(fun_trivial, x, jac_trivial, loss='cauchy',
max_nfev=1, method=self.method)
assert_allclose(res.grad, 2 * x * (x**2 + 5) / (1 + (x**2 + 5)**2))
res = least_squares(fun_trivial, x, jac_trivial, loss='arctan',
max_nfev=1, method=self.method)
assert_allclose(res.grad, 2 * x * (x**2 + 5) / (1 + (x**2 + 5)**4))
res = least_squares(fun_trivial, x, jac_trivial, loss=cubic_soft_l1,
max_nfev=1, method=self.method)
assert_allclose(res.grad,
2 * x * (x**2 + 5) / (1 + (x**2 + 5)**2)**(2/3))
def test_jac(self):
# Test that res.jac.T.dot(res.jac) gives Gauss-Newton approximation
# of Hessian. This approximation is computed by doubly differentiating
# the cost function and dropping the part containing second derivative
# of f. For a scalar function it is computed as
# H = (rho' + 2 * rho'' * f**2) * f'**2, if the expression inside the
# brackets is less than EPS it is replaced by EPS. Here, we check
# against the root of H.
x = 2.0 # res.x will be this.
f = x**2 + 5 # res.fun will be this.
res = least_squares(fun_trivial, x, jac_trivial, loss='linear',
max_nfev=1, method=self.method)
assert_equal(res.jac, 2 * x)
# For `huber` loss the Jacobian correction is identically zero
# in outlier region, in such cases it is modified to be equal EPS**0.5.
res = least_squares(fun_trivial, x, jac_trivial, loss='huber',
max_nfev=1, method=self.method)
assert_equal(res.jac, 2 * x * EPS**0.5)
# Now, let's apply `loss_scale` to turn the residual into an inlier.
# The loss function becomes linear.
res = least_squares(fun_trivial, x, jac_trivial, loss='huber',
f_scale=10, max_nfev=1)
assert_equal(res.jac, 2 * x)
# 'soft_l1' always gives a positive scaling.
res = least_squares(fun_trivial, x, jac_trivial, loss='soft_l1',
max_nfev=1, method=self.method)
assert_allclose(res.jac, 2 * x * (1 + f**2)**-0.75)
# For 'cauchy' the correction term turns out to be negative, and it
# replaced by EPS**0.5.
res = least_squares(fun_trivial, x, jac_trivial, loss='cauchy',
max_nfev=1, method=self.method)
assert_allclose(res.jac, 2 * x * EPS**0.5)
# Now use scaling to turn the residual to inlier.
res = least_squares(fun_trivial, x, jac_trivial, loss='cauchy',
f_scale=10, max_nfev=1, method=self.method)
fs = f / 10
assert_allclose(res.jac, 2 * x * (1 - fs**2)**0.5 / (1 + fs**2))
# 'arctan' gives an outlier.
res = least_squares(fun_trivial, x, jac_trivial, loss='arctan',
max_nfev=1, method=self.method)
assert_allclose(res.jac, 2 * x * EPS**0.5)
# Turn to inlier.
res = least_squares(fun_trivial, x, jac_trivial, loss='arctan',
f_scale=20.0, max_nfev=1, method=self.method)
fs = f / 20
assert_allclose(res.jac, 2 * x * (1 - 3 * fs**4)**0.5 / (1 + fs**4))
# cubic_soft_l1 will give an outlier.
res = least_squares(fun_trivial, x, jac_trivial, loss=cubic_soft_l1,
max_nfev=1)
assert_allclose(res.jac, 2 * x * EPS**0.5)
# Turn to inlier.
res = least_squares(fun_trivial, x, jac_trivial,
loss=cubic_soft_l1, f_scale=6, max_nfev=1)
fs = f / 6
assert_allclose(res.jac,
2 * x * (1 - fs**2 / 3)**0.5 * (1 + fs**2)**(-5/6))
def test_robustness(self):
for noise in [0.1, 1.0]:
p = ExponentialFittingProblem(1, 0.1, noise, random_seed=0)
for jac in ['2-point', '3-point', 'cs', p.jac]:
res_lsq = least_squares(p.fun, p.p0, jac=jac,
method=self.method)
assert_allclose(res_lsq.optimality, 0, atol=1e-2)
for loss in LOSSES:
if loss == 'linear':
continue
res_robust = least_squares(
p.fun, p.p0, jac=jac, loss=loss, f_scale=noise,
method=self.method)
assert_allclose(res_robust.optimality, 0, atol=1e-2)
assert_(norm(res_robust.x - p.p_opt) <
norm(res_lsq.x - p.p_opt))
class TestDogbox(BaseMixin, BoundsMixin, SparseMixin, LossFunctionMixin):
method = 'dogbox'
class TestTRF(BaseMixin, BoundsMixin, SparseMixin, LossFunctionMixin):
method = 'trf'
def test_lsmr_regularization(self):
p = BroydenTridiagonal()
for regularize in [True, False]:
res = least_squares(p.fun, p.x0, p.jac, method='trf',
tr_options={'regularize': regularize})
assert_allclose(res.cost, 0, atol=1e-20)
class TestLM(BaseMixin):
method = 'lm'
def test_bounds_not_supported(self):
assert_raises(ValueError, least_squares, fun_trivial,
2.0, bounds=(-3.0, 3.0), method='lm')
def test_m_less_n_not_supported(self):
x0 = [-2, 1]
assert_raises(ValueError, least_squares, fun_rosenbrock_cropped, x0,
method='lm')
def test_sparse_not_supported(self):
p = BroydenTridiagonal()
assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
method='lm')
def test_jac_sparsity_not_supported(self):
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
jac_sparsity=[1], method='lm')
def test_LinearOperator_not_supported(self):
p = BroydenTridiagonal(mode="operator")
assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
method='lm')
def test_loss(self):
res = least_squares(fun_trivial, 2.0, loss='linear', method='lm')
assert_allclose(res.x, 0.0, atol=1e-4)
assert_raises(ValueError, least_squares, fun_trivial, 2.0,
method='lm', loss='huber')
def test_callback_with_lm_method(self):
def callback(x):
assert(False) # Dummy callback function
with suppress_warnings() as sup:
sup.filter(
UserWarning,
"Callback function specified, but not supported with `lm` method."
)
least_squares(fun_trivial, x0=[0], method='lm', callback=callback)
def test_basic():
# test that 'method' arg is really optional
res = least_squares(fun_trivial, 2.0)
assert_allclose(res.x, 0, atol=1e-10)
def test_callback():
# test that callback function works as expected
results = []
def my_callback_optimresult(intermediate_result: OptimizeResult):
results.append(intermediate_result)
def my_callback_x(x):
r = OptimizeResult()
r.nit = 1
r.x = x
results.append(r)
return False
def my_callback_optimresult_stop_exception(
intermediate_result: OptimizeResult):
results.append(intermediate_result)
raise StopIteration
def my_callback_x_stop_exception(x):
r = OptimizeResult()
r.nit = 1
r.x = x
results.append(r)
raise StopIteration
# Try for different function signatures and stop methods
callbacks_nostop = [my_callback_optimresult, my_callback_x]
callbacks_stop = [my_callback_optimresult_stop_exception,
my_callback_x_stop_exception]
# Try for all the implemented methods: trf, trf_bounds and dogbox
calls = [
lambda callback: least_squares(fun_trivial, 5.0, method='trf',
callback=callback),
lambda callback: least_squares(fun_trivial, 5.0, method='trf',
bounds=(-8.0, 8.0), callback=callback),
lambda callback: least_squares(fun_trivial, 5.0, method='dogbox',
callback=callback)
]
for mycallback, call in product(callbacks_nostop, calls):
results.clear()
# Call the different implemented methods
res = call(mycallback)
# Check that callback was called
assert len(results) > 0
# Check that results data makes sense
assert results[-1].nit > 0
# Check that it didn't stop because of the callback
assert res.status != -2
# final callback x should be same as final result
assert_allclose(results[-1].x, res.x)
for mycallback, call in product(callbacks_stop, calls):
results.clear()
# Call the different implemented methods
res = call(mycallback)
# Check that callback was called
assert len(results) > 0
# Check that only one iteration was run
assert results[-1].nit == 1
# Check that it stopped because of the callback
assert res.status == -2
def test_small_tolerances_for_lm():
for ftol, xtol, gtol in [(None, 1e-13, 1e-13),
(1e-13, None, 1e-13),
(1e-13, 1e-13, None)]:
assert_raises(ValueError, least_squares, fun_trivial, 2.0, xtol=xtol,
ftol=ftol, gtol=gtol, method='lm')
def test_fp32_gh12991():
# checks that smaller FP sizes can be used in least_squares
# this is the minimum working example reported for gh12991
rng = np.random.RandomState(1)
x = np.linspace(0, 1, 100).astype("float32")
y = rng.random(100).astype("float32")
def func(p, x):
return p[0] + p[1] * x
def err(p, x, y):
return func(p, x) - y
res = least_squares(err, [-1.0, -1.0], args=(x, y))
# previously the initial jacobian calculated for this would be all 0
# and the minimize would terminate immediately, with nfev=1, would
# report a successful minimization (it shouldn't have done), but be
# unchanged from the initial solution.
# It was terminating early because the underlying approx_derivative
# used a step size for FP64 when the working space was FP32.
assert res.nfev > 2
assert_allclose(res.x, np.array([0.4082241, 0.15530563]), atol=5e-5)
def test_gh_18793_and_19351():
answer = 1e-12
initial_guess = 1.1e-12
def chi2(x):
return (x-answer)**2
gtol = 1e-15
res = least_squares(chi2, x0=initial_guess, gtol=1e-15, bounds=(0, np.inf))
# Original motivation: gh-18793
# if we choose an initial condition that is close to the solution
# we shouldn't return an answer that is further away from the solution
# Update: gh-19351
# However this requirement does not go well with 'trf' algorithm logic.
# Some regressions were reported after the presumed fix.
# The returned solution is good as long as it satisfies the convergence
# conditions.
# Specifically in this case the scaled gradient will be sufficiently low.
scaling, _ = CL_scaling_vector(res.x, res.grad,
np.atleast_1d(0), np.atleast_1d(np.inf))
assert res.status == 1 # Converged by gradient
assert np.linalg.norm(res.grad * scaling, ord=np.inf) < gtol
def test_gh_19103():
# Checks that least_squares trf method selects a strictly feasible point,
# and thus succeeds instead of failing,
# when the initial guess is reported exactly at a boundary point.
# This is a reduced example from gh191303
ydata = np.array([0.] * 66 + [
1., 0., 0., 0., 0., 0., 1., 1., 0., 0., 1.,
1., 1., 1., 0., 0., 0., 1., 0., 0., 2., 1.,
0., 3., 1., 6., 5., 0., 0., 2., 8., 4., 4.,
6., 9., 7., 2., 7., 8., 2., 13., 9., 8., 11.,
10., 13., 14., 19., 11., 15., 18., 26., 19., 32., 29.,
28., 36., 32., 35., 36., 43., 52., 32., 58., 56., 52.,
67., 53., 72., 88., 77., 95., 94., 84., 86., 101., 107.,
108., 118., 96., 115., 138., 137.,
])
xdata = np.arange(0, ydata.size) * 0.1
def exponential_wrapped(params):
A, B, x0 = params
return A * np.exp(B * (xdata - x0)) - ydata
x0 = [0.01, 1., 5.]
bounds = ((0.01, 0, 0), (np.inf, 10, 20.9))
res = least_squares(exponential_wrapped, x0, method='trf', bounds=bounds)
assert res.success

View file

@ -0,0 +1,116 @@
# Author: Brian M. Clapper, G. Varoquaux, Lars Buitinck
# License: BSD
from numpy.testing import assert_array_equal
import pytest
import numpy as np
from scipy.optimize import linear_sum_assignment
from scipy.sparse import random_array
from scipy.sparse._sputils import matrix
from scipy.sparse.csgraph import min_weight_full_bipartite_matching
from scipy.sparse.csgraph.tests.test_matching import (
linear_sum_assignment_assertions, linear_sum_assignment_test_cases
)
def test_linear_sum_assignment_input_shape():
with pytest.raises(ValueError, match="expected a matrix"):
linear_sum_assignment([1, 2, 3])
def test_linear_sum_assignment_input_object():
C = [[1, 2, 3], [4, 5, 6]]
assert_array_equal(linear_sum_assignment(C),
linear_sum_assignment(np.asarray(C)))
assert_array_equal(linear_sum_assignment(C),
linear_sum_assignment(matrix(C)))
def test_linear_sum_assignment_input_bool():
I = np.identity(3)
assert_array_equal(linear_sum_assignment(I.astype(np.bool_)),
linear_sum_assignment(I))
def test_linear_sum_assignment_input_string():
I = np.identity(3)
with pytest.raises(TypeError, match="Cannot cast array data"):
linear_sum_assignment(I.astype(str))
def test_linear_sum_assignment_input_nan():
I = np.diag([np.nan, 1, 1])
with pytest.raises(ValueError, match="contains invalid numeric entries"):
linear_sum_assignment(I)
def test_linear_sum_assignment_input_neginf():
I = np.diag([1, -np.inf, 1])
with pytest.raises(ValueError, match="contains invalid numeric entries"):
linear_sum_assignment(I)
def test_linear_sum_assignment_input_inf():
I = np.identity(3)
I[:, 0] = np.inf
with pytest.raises(ValueError, match="cost matrix is infeasible"):
linear_sum_assignment(I)
def test_constant_cost_matrix():
# Fixes #11602
n = 8
C = np.ones((n, n))
row_ind, col_ind = linear_sum_assignment(C)
assert_array_equal(row_ind, np.arange(n))
assert_array_equal(col_ind, np.arange(n))
@pytest.mark.parametrize('num_rows,num_cols', [(0, 0), (2, 0), (0, 3)])
def test_linear_sum_assignment_trivial_cost(num_rows, num_cols):
C = np.empty(shape=(num_cols, num_rows))
row_ind, col_ind = linear_sum_assignment(C)
assert len(row_ind) == 0
assert len(col_ind) == 0
@pytest.mark.parametrize('sign,test_case', linear_sum_assignment_test_cases)
def test_linear_sum_assignment_small_inputs(sign, test_case):
linear_sum_assignment_assertions(
linear_sum_assignment, np.array, sign, test_case)
# Tests that combine scipy.optimize.linear_sum_assignment and
# scipy.sparse.csgraph.min_weight_full_bipartite_matching
def test_two_methods_give_same_result_on_many_sparse_inputs():
# As opposed to the test above, here we do not spell out the expected
# output; only assert that the two methods give the same result.
# Concretely, the below tests 100 cases of size 100x100, out of which
# 36 are infeasible.
np.random.seed(1234)
for _ in range(100):
lsa_raises = False
mwfbm_raises = False
sparse = random_array((100, 100), density=0.06,
data_sampler=lambda size: np.random.randint(1, 100, size))
# In csgraph, zeros correspond to missing edges, so we explicitly
# replace those with infinities
dense = np.full(sparse.shape, np.inf)
dense[sparse.row, sparse.col] = sparse.data
sparse = sparse.tocsr()
try:
row_ind, col_ind = linear_sum_assignment(dense)
lsa_cost = dense[row_ind, col_ind].sum()
except ValueError:
lsa_raises = True
try:
row_ind, col_ind = min_weight_full_bipartite_matching(sparse)
mwfbm_cost = sparse[row_ind, col_ind].sum()
except ValueError:
mwfbm_raises = True
# Ensure that if one method raises, so does the other one.
assert lsa_raises == mwfbm_raises
if not lsa_raises:
assert lsa_cost == mwfbm_cost

View file

@ -0,0 +1,328 @@
"""
Tests for line search routines
"""
from numpy.testing import (assert_equal, assert_array_almost_equal,
assert_array_almost_equal_nulp, assert_warns,
suppress_warnings)
import scipy.optimize._linesearch as ls
from scipy.optimize._linesearch import LineSearchWarning
import numpy as np
import pytest
import threading
def assert_wolfe(s, phi, derphi, c1=1e-4, c2=0.9, err_msg=""):
"""
Check that strong Wolfe conditions apply
"""
phi1 = phi(s)
phi0 = phi(0)
derphi0 = derphi(0)
derphi1 = derphi(s)
msg = (f"s = {s}; phi(0) = {phi0}; phi(s) = {phi1}; phi'(0) = {derphi0};"
f" phi'(s) = {derphi1}; {err_msg}")
assert phi1 <= phi0 + c1*s*derphi0, "Wolfe 1 failed: " + msg
assert abs(derphi1) <= abs(c2*derphi0), "Wolfe 2 failed: " + msg
def assert_armijo(s, phi, c1=1e-4, err_msg=""):
"""
Check that Armijo condition applies
"""
phi1 = phi(s)
phi0 = phi(0)
msg = f"s = {s}; phi(0) = {phi0}; phi(s) = {phi1}; {err_msg}"
assert phi1 <= (1 - c1*s)*phi0, msg
def assert_line_wolfe(x, p, s, f, fprime, **kw):
assert_wolfe(s, phi=lambda sp: f(x + p*sp),
derphi=lambda sp: np.dot(fprime(x + p*sp), p), **kw)
def assert_line_armijo(x, p, s, f, **kw):
assert_armijo(s, phi=lambda sp: f(x + p*sp), **kw)
def assert_fp_equal(x, y, err_msg="", nulp=50):
"""Assert two arrays are equal, up to some floating-point rounding error"""
try:
assert_array_almost_equal_nulp(x, y, nulp)
except AssertionError as e:
raise AssertionError(f"{e}\n{err_msg}") from e
class TestLineSearch:
# -- scalar functions; must have dphi(0.) < 0
def _scalar_func_1(self, s): # skip name check
if not hasattr(self.fcount, 'c'):
self.fcount.c = 0
self.fcount.c += 1
p = -s - s**3 + s**4
dp = -1 - 3*s**2 + 4*s**3
return p, dp
def _scalar_func_2(self, s): # skip name check
if not hasattr(self.fcount, 'c'):
self.fcount.c = 0
self.fcount.c += 1
p = np.exp(-4*s) + s**2
dp = -4*np.exp(-4*s) + 2*s
return p, dp
def _scalar_func_3(self, s): # skip name check
if not hasattr(self.fcount, 'c'):
self.fcount.c = 0
self.fcount.c += 1
p = -np.sin(10*s)
dp = -10*np.cos(10*s)
return p, dp
# -- n-d functions
def _line_func_1(self, x): # skip name check
if not hasattr(self.fcount, 'c'):
self.fcount.c = 0
self.fcount.c += 1
f = np.dot(x, x)
df = 2*x
return f, df
def _line_func_2(self, x): # skip name check
if not hasattr(self.fcount, 'c'):
self.fcount.c = 0
self.fcount.c += 1
f = np.dot(x, np.dot(self.A, x)) + 1
df = np.dot(self.A + self.A.T, x)
return f, df
# --
def setup_method(self):
self.scalar_funcs = []
self.line_funcs = []
self.N = 20
self.fcount = threading.local()
def bind_index(func, idx):
# Remember Python's closure semantics!
return lambda *a, **kw: func(*a, **kw)[idx]
for name in sorted(dir(self)):
if name.startswith('_scalar_func_'):
value = getattr(self, name)
self.scalar_funcs.append(
(name, bind_index(value, 0), bind_index(value, 1)))
elif name.startswith('_line_func_'):
value = getattr(self, name)
self.line_funcs.append(
(name, bind_index(value, 0), bind_index(value, 1)))
np.random.seed(1234)
self.A = np.random.randn(self.N, self.N)
def scalar_iter(self):
for name, phi, derphi in self.scalar_funcs:
for old_phi0 in np.random.randn(3):
yield name, phi, derphi, old_phi0
def line_iter(self):
rng = np.random.RandomState(1234)
for name, f, fprime in self.line_funcs:
k = 0
while k < 9:
x = rng.randn(self.N)
p = rng.randn(self.N)
if np.dot(p, fprime(x)) >= 0:
# always pick a descent direction
continue
k += 1
old_fv = float(rng.randn())
yield name, f, fprime, x, p, old_fv
# -- Generic scalar searches
def test_scalar_search_wolfe1(self):
c = 0
for name, phi, derphi, old_phi0 in self.scalar_iter():
c += 1
s, phi1, phi0 = ls.scalar_search_wolfe1(phi, derphi, phi(0),
old_phi0, derphi(0))
assert_fp_equal(phi0, phi(0), name)
assert_fp_equal(phi1, phi(s), name)
assert_wolfe(s, phi, derphi, err_msg=name)
assert c > 3 # check that the iterator really works...
def test_scalar_search_wolfe2(self):
for name, phi, derphi, old_phi0 in self.scalar_iter():
s, phi1, phi0, derphi1 = ls.scalar_search_wolfe2(
phi, derphi, phi(0), old_phi0, derphi(0))
assert_fp_equal(phi0, phi(0), name)
assert_fp_equal(phi1, phi(s), name)
if derphi1 is not None:
assert_fp_equal(derphi1, derphi(s), name)
assert_wolfe(s, phi, derphi, err_msg=f"{name} {old_phi0:g}")
def test_scalar_search_wolfe2_with_low_amax(self):
def phi(alpha):
return (alpha - 5) ** 2
def derphi(alpha):
return 2 * (alpha - 5)
alpha_star, _, _, derphi_star = ls.scalar_search_wolfe2(phi, derphi, amax=0.001)
assert alpha_star is None # Not converged
assert derphi_star is None # Not converged
def test_scalar_search_wolfe2_regression(self):
# Regression test for gh-12157
# This phi has its minimum at alpha=4/3 ~ 1.333.
def phi(alpha):
if alpha < 1:
return - 3*np.pi/2 * (alpha - 1)
else:
return np.cos(3*np.pi/2 * alpha - np.pi)
def derphi(alpha):
if alpha < 1:
return - 3*np.pi/2
else:
return - 3*np.pi/2 * np.sin(3*np.pi/2 * alpha - np.pi)
s, _, _, _ = ls.scalar_search_wolfe2(phi, derphi)
# Without the fix in gh-13073, the scalar_search_wolfe2
# returned s=2.0 instead.
assert s < 1.5
def test_scalar_search_armijo(self):
for name, phi, derphi, old_phi0 in self.scalar_iter():
s, phi1 = ls.scalar_search_armijo(phi, phi(0), derphi(0))
assert_fp_equal(phi1, phi(s), name)
assert_armijo(s, phi, err_msg=f"{name} {old_phi0:g}")
# -- Generic line searches
def test_line_search_wolfe1(self):
c = 0
smax = 100
for name, f, fprime, x, p, old_f in self.line_iter():
f0 = f(x)
g0 = fprime(x)
self.fcount.c = 0
s, fc, gc, fv, ofv, gv = ls.line_search_wolfe1(f, fprime, x, p,
g0, f0, old_f,
amax=smax)
assert_equal(self.fcount.c, fc+gc)
assert_fp_equal(ofv, f(x))
if s is None:
continue
assert_fp_equal(fv, f(x + s*p))
assert_array_almost_equal(gv, fprime(x + s*p), decimal=14)
if s < smax:
c += 1
assert_line_wolfe(x, p, s, f, fprime, err_msg=name)
assert c > 3 # check that the iterator really works...
def test_line_search_wolfe2(self):
c = 0
smax = 512
for name, f, fprime, x, p, old_f in self.line_iter():
f0 = f(x)
g0 = fprime(x)
self.fcount.c = 0
with suppress_warnings() as sup:
sup.filter(LineSearchWarning,
"The line search algorithm could not find a solution")
sup.filter(LineSearchWarning,
"The line search algorithm did not converge")
s, fc, gc, fv, ofv, gv = ls.line_search_wolfe2(f, fprime, x, p,
g0, f0, old_f,
amax=smax)
assert_equal(self.fcount.c, fc+gc)
assert_fp_equal(ofv, f(x))
assert_fp_equal(fv, f(x + s*p))
if gv is not None:
assert_array_almost_equal(gv, fprime(x + s*p), decimal=14)
if s < smax:
c += 1
assert_line_wolfe(x, p, s, f, fprime, err_msg=name)
assert c > 3 # check that the iterator really works...
@pytest.mark.thread_unsafe
def test_line_search_wolfe2_bounds(self):
# See gh-7475
# For this f and p, starting at a point on axis 0, the strong Wolfe
# condition 2 is met if and only if the step length s satisfies
# |x + s| <= c2 * |x|
def f(x):
return np.dot(x, x)
def fp(x):
return 2 * x
p = np.array([1, 0])
# Smallest s satisfying strong Wolfe conditions for these arguments is 30
x = -60 * p
c2 = 0.5
s, _, _, _, _, _ = ls.line_search_wolfe2(f, fp, x, p, amax=30, c2=c2)
assert_line_wolfe(x, p, s, f, fp)
s, _, _, _, _, _ = assert_warns(LineSearchWarning,
ls.line_search_wolfe2, f, fp, x, p,
amax=29, c2=c2)
assert s is None
# s=30 will only be tried on the 6th iteration, so this won't converge
assert_warns(LineSearchWarning, ls.line_search_wolfe2, f, fp, x, p,
c2=c2, maxiter=5)
def test_line_search_armijo(self):
c = 0
for name, f, fprime, x, p, old_f in self.line_iter():
f0 = f(x)
g0 = fprime(x)
self.fcount.c = 0
s, fc, fv = ls.line_search_armijo(f, x, p, g0, f0)
c += 1
assert_equal(self.fcount.c, fc)
assert_fp_equal(fv, f(x + s*p))
assert_line_armijo(x, p, s, f, err_msg=name)
assert c >= 9
# -- More specific tests
def test_armijo_terminate_1(self):
# Armijo should evaluate the function only once if the trial step
# is already suitable
count = [0]
def phi(s):
count[0] += 1
return -s + 0.01*s**2
s, phi1 = ls.scalar_search_armijo(phi, phi(0), -1, alpha0=1)
assert_equal(s, 1)
assert_equal(count[0], 2)
assert_armijo(s, phi)
def test_wolfe_terminate(self):
# wolfe1 and wolfe2 should also evaluate the function only a few
# times if the trial step is already suitable
def phi(s):
count[0] += 1
return -s + 0.05*s**2
def derphi(s):
count[0] += 1
return -1 + 0.05*2*s
for func in [ls.scalar_search_wolfe1, ls.scalar_search_wolfe2]:
count = [0]
r = func(phi, derphi, phi(0), None, derphi(0))
assert r[0] is not None, (r, func)
assert count[0] <= 2 + 2, (count, func)
assert_wolfe(r[0], phi, derphi, err_msg=str(func))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,297 @@
from numpy.testing import assert_, assert_allclose, assert_equal
from pytest import raises as assert_raises
import numpy as np
from scipy.optimize._lsq.common import (
step_size_to_bound, find_active_constraints, make_strictly_feasible,
CL_scaling_vector, intersect_trust_region, build_quadratic_1d,
minimize_quadratic_1d, evaluate_quadratic, reflective_transformation,
left_multiplied_operator, right_multiplied_operator)
class TestBounds:
def test_step_size_to_bounds(self):
lb = np.array([-1.0, 2.5, 10.0])
ub = np.array([1.0, 5.0, 100.0])
x = np.array([0.0, 2.5, 12.0])
s = np.array([0.1, 0.0, 0.0])
step, hits = step_size_to_bound(x, s, lb, ub)
assert_equal(step, 10)
assert_equal(hits, [1, 0, 0])
s = np.array([0.01, 0.05, -1.0])
step, hits = step_size_to_bound(x, s, lb, ub)
assert_equal(step, 2)
assert_equal(hits, [0, 0, -1])
s = np.array([10.0, -0.0001, 100.0])
step, hits = step_size_to_bound(x, s, lb, ub)
assert_equal(step, np.array(-0))
assert_equal(hits, [0, -1, 0])
s = np.array([1.0, 0.5, -2.0])
step, hits = step_size_to_bound(x, s, lb, ub)
assert_equal(step, 1.0)
assert_equal(hits, [1, 0, -1])
s = np.zeros(3)
step, hits = step_size_to_bound(x, s, lb, ub)
assert_equal(step, np.inf)
assert_equal(hits, [0, 0, 0])
def test_find_active_constraints(self):
lb = np.array([0.0, -10.0, 1.0])
ub = np.array([1.0, 0.0, 100.0])
x = np.array([0.5, -5.0, 2.0])
active = find_active_constraints(x, lb, ub)
assert_equal(active, [0, 0, 0])
x = np.array([0.0, 0.0, 10.0])
active = find_active_constraints(x, lb, ub)
assert_equal(active, [-1, 1, 0])
active = find_active_constraints(x, lb, ub, rtol=0)
assert_equal(active, [-1, 1, 0])
x = np.array([1e-9, -1e-8, 100 - 1e-9])
active = find_active_constraints(x, lb, ub)
assert_equal(active, [0, 0, 1])
active = find_active_constraints(x, lb, ub, rtol=1.5e-9)
assert_equal(active, [-1, 0, 1])
lb = np.array([1.0, -np.inf, -np.inf])
ub = np.array([np.inf, 10.0, np.inf])
x = np.ones(3)
active = find_active_constraints(x, lb, ub)
assert_equal(active, [-1, 0, 0])
# Handles out-of-bound cases.
x = np.array([0.0, 11.0, 0.0])
active = find_active_constraints(x, lb, ub)
assert_equal(active, [-1, 1, 0])
active = find_active_constraints(x, lb, ub, rtol=0)
assert_equal(active, [-1, 1, 0])
def test_make_strictly_feasible(self):
lb = np.array([-0.5, -0.8, 2.0])
ub = np.array([0.8, 1.0, 3.0])
x = np.array([-0.5, 0.0, 2 + 1e-10])
x_new = make_strictly_feasible(x, lb, ub, rstep=0)
assert_(x_new[0] > -0.5)
assert_equal(x_new[1:], x[1:])
x_new = make_strictly_feasible(x, lb, ub, rstep=1e-4)
assert_equal(x_new, [-0.5 + 1e-4, 0.0, 2 * (1 + 1e-4)])
x = np.array([-0.5, -1, 3.1])
x_new = make_strictly_feasible(x, lb, ub)
assert_(np.all((x_new >= lb) & (x_new <= ub)))
x_new = make_strictly_feasible(x, lb, ub, rstep=0)
assert_(np.all((x_new >= lb) & (x_new <= ub)))
lb = np.array([-1, 100.0])
ub = np.array([1, 100.0 + 1e-10])
x = np.array([0, 100.0])
x_new = make_strictly_feasible(x, lb, ub, rstep=1e-8)
assert_equal(x_new, [0, 100.0 + 0.5e-10])
def test_scaling_vector(self):
lb = np.array([-np.inf, -5.0, 1.0, -np.inf])
ub = np.array([1.0, np.inf, 10.0, np.inf])
x = np.array([0.5, 2.0, 5.0, 0.0])
g = np.array([1.0, 0.1, -10.0, 0.0])
v, dv = CL_scaling_vector(x, g, lb, ub)
assert_equal(v, [1.0, 7.0, 5.0, 1.0])
assert_equal(dv, [0.0, 1.0, -1.0, 0.0])
class TestQuadraticFunction:
def setup_method(self):
self.J = np.array([
[0.1, 0.2],
[-1.0, 1.0],
[0.5, 0.2]])
self.g = np.array([0.8, -2.0])
self.diag = np.array([1.0, 2.0])
def test_build_quadratic_1d(self):
s = np.zeros(2)
a, b = build_quadratic_1d(self.J, self.g, s)
assert_equal(a, 0)
assert_equal(b, 0)
a, b = build_quadratic_1d(self.J, self.g, s, diag=self.diag)
assert_equal(a, 0)
assert_equal(b, 0)
s = np.array([1.0, -1.0])
a, b = build_quadratic_1d(self.J, self.g, s)
assert_equal(a, 2.05)
assert_equal(b, 2.8)
a, b = build_quadratic_1d(self.J, self.g, s, diag=self.diag)
assert_equal(a, 3.55)
assert_equal(b, 2.8)
s0 = np.array([0.5, 0.5])
a, b, c = build_quadratic_1d(self.J, self.g, s, diag=self.diag, s0=s0)
assert_equal(a, 3.55)
assert_allclose(b, 2.39)
assert_allclose(c, -0.1525)
def test_minimize_quadratic_1d(self):
a = 5
b = -1
t, y = minimize_quadratic_1d(a, b, 1, 2)
assert_equal(t, 1)
assert_allclose(y, a * t**2 + b * t, rtol=1e-15)
t, y = minimize_quadratic_1d(a, b, -2, -1)
assert_equal(t, -1)
assert_allclose(y, a * t**2 + b * t, rtol=1e-15)
t, y = minimize_quadratic_1d(a, b, -1, 1)
assert_equal(t, 0.1)
assert_allclose(y, a * t**2 + b * t, rtol=1e-15)
c = 10
t, y = minimize_quadratic_1d(a, b, -1, 1, c=c)
assert_equal(t, 0.1)
assert_allclose(y, a * t**2 + b * t + c, rtol=1e-15)
t, y = minimize_quadratic_1d(a, b, -np.inf, np.inf, c=c)
assert_equal(t, 0.1)
assert_allclose(y, a * t ** 2 + b * t + c, rtol=1e-15)
t, y = minimize_quadratic_1d(a, b, 0, np.inf, c=c)
assert_equal(t, 0.1)
assert_allclose(y, a * t ** 2 + b * t + c, rtol=1e-15)
t, y = minimize_quadratic_1d(a, b, -np.inf, 0, c=c)
assert_equal(t, 0)
assert_allclose(y, a * t ** 2 + b * t + c, rtol=1e-15)
a = -1
b = 0.2
t, y = minimize_quadratic_1d(a, b, -np.inf, np.inf)
assert_equal(y, -np.inf)
t, y = minimize_quadratic_1d(a, b, 0, np.inf)
assert_equal(t, np.inf)
assert_equal(y, -np.inf)
t, y = minimize_quadratic_1d(a, b, -np.inf, 0)
assert_equal(t, -np.inf)
assert_equal(y, -np.inf)
def test_evaluate_quadratic(self):
s = np.array([1.0, -1.0])
value = evaluate_quadratic(self.J, self.g, s)
assert_equal(value, 4.85)
value = evaluate_quadratic(self.J, self.g, s, diag=self.diag)
assert_equal(value, 6.35)
s = np.array([[1.0, -1.0],
[1.0, 1.0],
[0.0, 0.0]])
values = evaluate_quadratic(self.J, self.g, s)
assert_allclose(values, [4.85, -0.91, 0.0])
values = evaluate_quadratic(self.J, self.g, s, diag=self.diag)
assert_allclose(values, [6.35, 0.59, 0.0])
class TestTrustRegion:
def test_intersect(self):
Delta = 1.0
x = np.zeros(3)
s = np.array([1.0, 0.0, 0.0])
t_neg, t_pos = intersect_trust_region(x, s, Delta)
assert_equal(t_neg, -1)
assert_equal(t_pos, 1)
s = np.array([-1.0, 1.0, -1.0])
t_neg, t_pos = intersect_trust_region(x, s, Delta)
assert_allclose(t_neg, -3**-0.5)
assert_allclose(t_pos, 3**-0.5)
x = np.array([0.5, -0.5, 0])
s = np.array([0, 0, 1.0])
t_neg, t_pos = intersect_trust_region(x, s, Delta)
assert_allclose(t_neg, -2**-0.5)
assert_allclose(t_pos, 2**-0.5)
x = np.ones(3)
assert_raises(ValueError, intersect_trust_region, x, s, Delta)
x = np.zeros(3)
s = np.zeros(3)
assert_raises(ValueError, intersect_trust_region, x, s, Delta)
def test_reflective_transformation():
lb = np.array([-1, -2], dtype=float)
ub = np.array([5, 3], dtype=float)
y = np.array([0, 0])
x, g = reflective_transformation(y, lb, ub)
assert_equal(x, y)
assert_equal(g, np.ones(2))
y = np.array([-4, 4], dtype=float)
x, g = reflective_transformation(y, lb, np.array([np.inf, np.inf]))
assert_equal(x, [2, 4])
assert_equal(g, [-1, 1])
x, g = reflective_transformation(y, np.array([-np.inf, -np.inf]), ub)
assert_equal(x, [-4, 2])
assert_equal(g, [1, -1])
x, g = reflective_transformation(y, lb, ub)
assert_equal(x, [2, 2])
assert_equal(g, [-1, -1])
lb = np.array([-np.inf, -2])
ub = np.array([5, np.inf])
y = np.array([10, 10], dtype=float)
x, g = reflective_transformation(y, lb, ub)
assert_equal(x, [0, 10])
assert_equal(g, [-1, 1])
def test_linear_operators():
A = np.arange(6).reshape((3, 2))
d_left = np.array([-1, 2, 5])
DA = np.diag(d_left).dot(A)
J_left = left_multiplied_operator(A, d_left)
d_right = np.array([5, 10])
AD = A.dot(np.diag(d_right))
J_right = right_multiplied_operator(A, d_right)
x = np.array([-2, 3])
X = -2 * np.arange(2, 8).reshape((2, 3))
xt = np.array([0, -2, 15])
assert_allclose(DA.dot(x), J_left.matvec(x))
assert_allclose(DA.dot(X), J_left.matmat(X))
assert_allclose(DA.T.dot(xt), J_left.rmatvec(xt))
assert_allclose(AD.dot(x), J_right.matvec(x))
assert_allclose(AD.dot(X), J_right.matmat(X))
assert_allclose(AD.T.dot(xt), J_right.rmatvec(xt))

View file

@ -0,0 +1,287 @@
import pytest
import numpy as np
from numpy.linalg import lstsq
from numpy.testing import assert_allclose, assert_equal, assert_
from scipy.sparse import random_array, coo_array
from scipy.sparse.linalg import aslinearoperator
from scipy.optimize import lsq_linear
from scipy.optimize._minimize import Bounds
A = np.array([
[0.171, -0.057],
[-0.049, -0.248],
[-0.166, 0.054],
])
b = np.array([0.074, 1.014, -0.383])
class BaseMixin:
def setup_method(self):
self.rnd = np.random.RandomState(0)
def test_dense_no_bounds(self):
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, method=self.method, lsq_solver=lsq_solver)
assert_allclose(res.x, lstsq(A, b, rcond=-1)[0])
assert_allclose(res.x, res.unbounded_sol[0])
def test_dense_bounds(self):
# Solutions for comparison are taken from MATLAB.
lb = np.array([-1, -10])
ub = np.array([1, 0])
unbounded_sol = lstsq(A, b, rcond=-1)[0]
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (lb, ub), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, lstsq(A, b, rcond=-1)[0])
assert_allclose(res.unbounded_sol[0], unbounded_sol)
lb = np.array([0.0, -np.inf])
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (lb, np.inf), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, np.array([0.0, -4.084174437334673]),
atol=1e-6)
assert_allclose(res.unbounded_sol[0], unbounded_sol)
lb = np.array([-1, 0])
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (lb, np.inf), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, np.array([0.448427311733504, 0]),
atol=1e-15)
assert_allclose(res.unbounded_sol[0], unbounded_sol)
ub = np.array([np.inf, -5])
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (-np.inf, ub), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, np.array([-0.105560998682388, -5]))
assert_allclose(res.unbounded_sol[0], unbounded_sol)
ub = np.array([-1, np.inf])
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (-np.inf, ub), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, np.array([-1, -4.181102129483254]))
assert_allclose(res.unbounded_sol[0], unbounded_sol)
lb = np.array([0, -4])
ub = np.array([1, 0])
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (lb, ub), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, np.array([0.005236663400791, -4]))
assert_allclose(res.unbounded_sol[0], unbounded_sol)
def test_bounds_variants(self):
x = np.array([1, 3])
A = self.rnd.uniform(size=(2, 2))
b = A@x
lb = np.array([1, 1])
ub = np.array([2, 2])
bounds_old = (lb, ub)
bounds_new = Bounds(lb, ub)
res_old = lsq_linear(A, b, bounds_old)
res_new = lsq_linear(A, b, bounds_new)
assert not np.allclose(res_new.x, res_new.unbounded_sol[0])
assert_allclose(res_old.x, res_new.x)
def test_np_matrix(self):
# gh-10711
with np.testing.suppress_warnings() as sup:
sup.filter(PendingDeprecationWarning)
A = np.matrix([[20, -4, 0, 2, 3], [10, -2, 1, 0, -1]])
k = np.array([20, 15])
lsq_linear(A, k)
def test_dense_rank_deficient(self):
A = np.array([[-0.307, -0.184]])
b = np.array([0.773])
lb = [-0.1, -0.1]
ub = [0.1, 0.1]
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (lb, ub), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.x, [-0.1, -0.1])
assert_allclose(res.unbounded_sol[0], lstsq(A, b, rcond=-1)[0])
A = np.array([
[0.334, 0.668],
[-0.516, -1.032],
[0.192, 0.384],
])
b = np.array([-1.436, 0.135, 0.909])
lb = [0, -1]
ub = [1, -0.5]
for lsq_solver in self.lsq_solvers:
res = lsq_linear(A, b, (lb, ub), method=self.method,
lsq_solver=lsq_solver)
assert_allclose(res.optimality, 0, atol=1e-11)
assert_allclose(res.unbounded_sol[0], lstsq(A, b, rcond=-1)[0])
def test_full_result(self):
lb = np.array([0, -4])
ub = np.array([1, 0])
res = lsq_linear(A, b, (lb, ub), method=self.method)
assert_allclose(res.x, [0.005236663400791, -4])
assert_allclose(res.unbounded_sol[0], lstsq(A, b, rcond=-1)[0])
r = A.dot(res.x) - b
assert_allclose(res.cost, 0.5 * np.dot(r, r))
assert_allclose(res.fun, r)
assert_allclose(res.optimality, 0.0, atol=1e-12)
assert_equal(res.active_mask, [0, -1])
assert_(res.nit < 15)
assert_(res.status == 1 or res.status == 3)
assert_(isinstance(res.message, str))
assert_(res.success)
# This is a test for issue #9982.
def test_almost_singular(self):
A = np.array(
[[0.8854232310355122, 0.0365312146937765, 0.0365312146836789],
[0.3742460132129041, 0.0130523214078376, 0.0130523214077873],
[0.9680633871281361, 0.0319366128718639, 0.0319366128718388]])
b = np.array(
[0.0055029366538097, 0.0026677442422208, 0.0066612514782381])
result = lsq_linear(A, b, method=self.method)
assert_(result.cost < 1.1e-8)
@pytest.mark.xslow
def test_large_rank_deficient(self):
np.random.seed(0)
n, m = np.sort(np.random.randint(2, 1000, size=2))
m *= 2 # make m >> n
A = 1.0 * np.random.randint(-99, 99, size=[m, n])
b = 1.0 * np.random.randint(-99, 99, size=[m])
bounds = 1.0 * np.sort(np.random.randint(-99, 99, size=(2, n)), axis=0)
bounds[1, :] += 1.0 # ensure up > lb
# Make the A matrix strongly rank deficient by replicating some columns
w = np.random.choice(n, n) # Select random columns with duplicates
A = A[:, w]
x_bvls = lsq_linear(A, b, bounds=bounds, method='bvls').x
x_trf = lsq_linear(A, b, bounds=bounds, method='trf').x
cost_bvls = np.sum((A @ x_bvls - b)**2)
cost_trf = np.sum((A @ x_trf - b)**2)
assert_(abs(cost_bvls - cost_trf) < cost_trf*1e-10)
def test_convergence_small_array(self):
A = np.array([[49.0, 41.0, -32.0],
[-19.0, -32.0, -8.0],
[-13.0, 10.0, 69.0]])
b = np.array([-41.0, -90.0, 47.0])
bounds = np.array([[31.0, -44.0, 26.0],
[54.0, -32.0, 28.0]])
x_bvls = lsq_linear(A, b, bounds=bounds, method='bvls').x
x_trf = lsq_linear(A, b, bounds=bounds, method='trf').x
cost_bvls = np.sum((A @ x_bvls - b)**2)
cost_trf = np.sum((A @ x_trf - b)**2)
assert_(abs(cost_bvls - cost_trf) < cost_trf*1e-10)
class SparseMixin:
def test_sparse_and_LinearOperator(self):
m = 5000
n = 1000
rng = np.random.RandomState(0)
A = random_array((m, n), random_state=rng)
b = rng.randn(m)
res = lsq_linear(A, b)
assert_allclose(res.optimality, 0, atol=1e-6)
A = aslinearoperator(A)
res = lsq_linear(A, b)
assert_allclose(res.optimality, 0, atol=1e-6)
@pytest.mark.fail_slow(10)
def test_sparse_bounds(self):
m = 5000
n = 1000
rng = np.random.RandomState(0)
A = random_array((m, n), random_state=rng)
b = rng.randn(m)
lb = rng.randn(n)
ub = lb + 1
res = lsq_linear(A, b, (lb, ub))
assert_allclose(res.optimality, 0.0, atol=1e-6)
res = lsq_linear(A, b, (lb, ub), lsmr_tol=1e-13,
lsmr_maxiter=1500)
assert_allclose(res.optimality, 0.0, atol=1e-6)
res = lsq_linear(A, b, (lb, ub), lsmr_tol='auto')
assert_allclose(res.optimality, 0.0, atol=1e-6)
def test_sparse_ill_conditioned(self):
# Sparse matrix with condition number of ~4 million
data = np.array([1., 1., 1., 1. + 1e-6, 1.])
row = np.array([0, 0, 1, 2, 2])
col = np.array([0, 2, 1, 0, 2])
A = coo_array((data, (row, col)), shape=(3, 3))
# Get the exact solution
exact_sol = lsq_linear(A.toarray(), b, lsq_solver='exact')
# Default lsmr arguments should not fully converge the solution
default_lsmr_sol = lsq_linear(A, b, lsq_solver='lsmr')
with pytest.raises(AssertionError):
assert_allclose(exact_sol.x, default_lsmr_sol.x)
# By increasing the maximum lsmr iters, it will converge
conv_lsmr = lsq_linear(A, b, lsq_solver='lsmr', lsmr_maxiter=10)
assert_allclose(exact_sol.x, conv_lsmr.x)
class TestTRF(BaseMixin, SparseMixin):
method = 'trf'
lsq_solvers = ['exact', 'lsmr']
class TestBVLS(BaseMixin):
method = 'bvls'
lsq_solvers = ['exact']
class TestErrorChecking:
def test_option_lsmr_tol(self):
# Should work with a positive float, string equal to 'auto', or None
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_tol=1e-2)
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_tol='auto')
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_tol=None)
# Should raise error with negative float, strings
# other than 'auto', and integers
err_message = "`lsmr_tol` must be None, 'auto', or positive float."
with pytest.raises(ValueError, match=err_message):
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_tol=-0.1)
with pytest.raises(ValueError, match=err_message):
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_tol='foo')
with pytest.raises(ValueError, match=err_message):
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_tol=1)
def test_option_lsmr_maxiter(self):
# Should work with positive integers or None
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_maxiter=1)
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_maxiter=None)
# Should raise error with 0 or negative max iter
err_message = "`lsmr_maxiter` must be None or positive integer."
with pytest.raises(ValueError, match=err_message):
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_maxiter=0)
with pytest.raises(ValueError, match=err_message):
_ = lsq_linear(A, b, lsq_solver='lsmr', lsmr_maxiter=-1)

View file

@ -0,0 +1,459 @@
"""
Unit test for Mixed Integer Linear Programming
"""
import re
import sys
import numpy as np
from numpy.testing import assert_allclose, assert_array_equal
import pytest
from .test_linprog import magic_square
from scipy.optimize import milp, Bounds, LinearConstraint
from scipy import sparse
_IS_32BIT = (sys.maxsize < 2**32)
def test_milp_iv():
message = "`c` must be a dense array"
with pytest.raises(ValueError, match=message):
milp(sparse.coo_array([0, 0]))
message = "`c` must be a one-dimensional array of finite numbers with"
with pytest.raises(ValueError, match=message):
milp(np.zeros((3, 4)))
with pytest.raises(ValueError, match=message):
milp([])
with pytest.raises(ValueError, match=message):
milp(None)
message = "`bounds` must be convertible into an instance of..."
with pytest.raises(ValueError, match=message):
milp(1, bounds=10)
message = "`constraints` (or each element within `constraints`) must be"
with pytest.raises(ValueError, match=re.escape(message)):
milp(1, constraints=10)
with pytest.raises(ValueError, match=re.escape(message)):
milp(np.zeros(3), constraints=([[1, 2, 3]], [2, 3], [2, 3]))
with pytest.raises(ValueError, match=re.escape(message)):
milp(np.zeros(2), constraints=([[1, 2]], [2], sparse.coo_array([2])))
message = "The shape of `A` must be (len(b_l), len(c))."
with pytest.raises(ValueError, match=re.escape(message)):
milp(np.zeros(3), constraints=([[1, 2]], [2], [2]))
message = "`integrality` must be a dense array"
with pytest.raises(ValueError, match=message):
milp([1, 2], integrality=sparse.coo_array([1, 2]))
message = ("`integrality` must contain integers 0-3 and be broadcastable "
"to `c.shape`.")
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], integrality=[1, 2])
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], integrality=[1, 5, 3])
message = "Lower and upper bounds must be dense arrays."
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], bounds=([1, 2], sparse.coo_array([3, 4])))
message = "`lb`, `ub`, and `keep_feasible` must be broadcastable."
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], bounds=([1, 2], [3, 4, 5]))
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], bounds=([1, 2, 3], [4, 5]))
message = "`bounds.lb` and `bounds.ub` must contain reals and..."
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], bounds=([1, 2], [3, 4]))
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], bounds=([1, 2, 3], ["3+4", 4, 5]))
with pytest.raises(ValueError, match=message):
milp([1, 2, 3], bounds=([1, 2, 3], [set(), 4, 5]))
@pytest.mark.xfail(run=False,
reason="Needs to be fixed in `_highs_wrapper`")
def test_milp_options(capsys):
# run=False now because of gh-16347
message = "Unrecognized options detected: {'ekki'}..."
options = {'ekki': True}
with pytest.warns(RuntimeWarning, match=message):
milp(1, options=options)
A, b, c, numbers, M = magic_square(3)
options = {"disp": True, "presolve": False, "time_limit": 0.05}
res = milp(c=c, constraints=(A, b, b), bounds=(0, 1), integrality=1,
options=options)
captured = capsys.readouterr()
assert "Presolve is switched off" in captured.out
assert "Time Limit Reached" in captured.out
assert not res.success
def test_result():
A, b, c, numbers, M = magic_square(3)
res = milp(c=c, constraints=(A, b, b), bounds=(0, 1), integrality=1)
assert res.status == 0
assert res.success
msg = "Optimization terminated successfully. (HiGHS Status 7:"
assert res.message.startswith(msg)
assert isinstance(res.x, np.ndarray)
assert isinstance(res.fun, float)
assert isinstance(res.mip_node_count, int)
assert isinstance(res.mip_dual_bound, float)
assert isinstance(res.mip_gap, float)
A, b, c, numbers, M = magic_square(6)
res = milp(c=c*0, constraints=(A, b, b), bounds=(0, 1), integrality=1,
options={'time_limit': 0.05})
assert res.status == 1
assert not res.success
msg = "Time limit reached. (HiGHS Status 13:"
assert res.message.startswith(msg)
assert (res.fun is res.mip_dual_bound is res.mip_gap
is res.mip_node_count is res.x is None)
res = milp(1, bounds=(1, -1))
assert res.status == 2
assert not res.success
msg = "The problem is infeasible. (HiGHS Status 8:"
assert res.message.startswith(msg)
assert (res.fun is res.mip_dual_bound is res.mip_gap
is res.mip_node_count is res.x is None)
res = milp(-1)
assert res.status == 3
assert not res.success
msg = "The problem is unbounded. (HiGHS Status 10:"
assert res.message.startswith(msg)
assert (res.fun is res.mip_dual_bound is res.mip_gap
is res.mip_node_count is res.x is None)
def test_milp_optional_args():
# check that arguments other than `c` are indeed optional
res = milp(1)
assert res.fun == 0
assert_array_equal(res.x, [0])
def test_milp_1():
# solve magic square problem
n = 3
A, b, c, numbers, M = magic_square(n)
A = sparse.csc_array(A) # confirm that sparse arrays are accepted
res = milp(c=c*0, constraints=(A, b, b), bounds=(0, 1), integrality=1)
# check that solution is a magic square
x = np.round(res.x)
s = (numbers.flatten() * x).reshape(n**2, n, n)
square = np.sum(s, axis=0)
np.testing.assert_allclose(square.sum(axis=0), M)
np.testing.assert_allclose(square.sum(axis=1), M)
np.testing.assert_allclose(np.diag(square).sum(), M)
np.testing.assert_allclose(np.diag(square[:, ::-1]).sum(), M)
def test_milp_2():
# solve MIP with inequality constraints and all integer constraints
# source: slide 5,
# https://www.cs.upc.edu/~erodri/webpage/cps/theory/lp/milp/slides.pdf
# also check that `milp` accepts all valid ways of specifying constraints
c = -np.ones(2)
A = [[-2, 2], [-8, 10]]
b_l = [1, -np.inf]
b_u = [np.inf, 13]
linear_constraint = LinearConstraint(A, b_l, b_u)
# solve original problem
res1 = milp(c=c, constraints=(A, b_l, b_u), integrality=True)
res2 = milp(c=c, constraints=linear_constraint, integrality=True)
res3 = milp(c=c, constraints=[(A, b_l, b_u)], integrality=True)
res4 = milp(c=c, constraints=[linear_constraint], integrality=True)
res5 = milp(c=c, integrality=True,
constraints=[(A[:1], b_l[:1], b_u[:1]),
(A[1:], b_l[1:], b_u[1:])])
res6 = milp(c=c, integrality=True,
constraints=[LinearConstraint(A[:1], b_l[:1], b_u[:1]),
LinearConstraint(A[1:], b_l[1:], b_u[1:])])
res7 = milp(c=c, integrality=True,
constraints=[(A[:1], b_l[:1], b_u[:1]),
LinearConstraint(A[1:], b_l[1:], b_u[1:])])
xs = np.array([res1.x, res2.x, res3.x, res4.x, res5.x, res6.x, res7.x])
funs = np.array([res1.fun, res2.fun, res3.fun,
res4.fun, res5.fun, res6.fun, res7.fun])
np.testing.assert_allclose(xs, np.broadcast_to([1, 2], xs.shape))
np.testing.assert_allclose(funs, -3)
# solve relaxed problem
res = milp(c=c, constraints=(A, b_l, b_u))
np.testing.assert_allclose(res.x, [4, 4.5])
np.testing.assert_allclose(res.fun, -8.5)
def test_milp_3():
# solve MIP with inequality constraints and all integer constraints
# source: https://en.wikipedia.org/wiki/Integer_programming#Example
c = [0, -1]
A = [[-1, 1], [3, 2], [2, 3]]
b_u = [1, 12, 12]
b_l = np.full_like(b_u, -np.inf, dtype=np.float64)
constraints = LinearConstraint(A, b_l, b_u)
integrality = np.ones_like(c)
# solve original problem
res = milp(c=c, constraints=constraints, integrality=integrality)
assert_allclose(res.fun, -2)
# two optimal solutions possible, just need one of them
assert np.allclose(res.x, [1, 2]) or np.allclose(res.x, [2, 2])
# solve relaxed problem
res = milp(c=c, constraints=constraints)
assert_allclose(res.fun, -2.8)
assert_allclose(res.x, [1.8, 2.8])
def test_milp_4():
# solve MIP with inequality constraints and only one integer constraint
# source: https://www.mathworks.com/help/optim/ug/intlinprog.html
c = [8, 1]
integrality = [0, 1]
A = [[1, 2], [-4, -1], [2, 1]]
b_l = [-14, -np.inf, -np.inf]
b_u = [np.inf, -33, 20]
constraints = LinearConstraint(A, b_l, b_u)
bounds = Bounds(-np.inf, np.inf)
res = milp(c, integrality=integrality, bounds=bounds,
constraints=constraints)
assert_allclose(res.fun, 59)
assert_allclose(res.x, [6.5, 7])
def test_milp_5():
# solve MIP with inequality and equality constraints
# source: https://www.mathworks.com/help/optim/ug/intlinprog.html
c = [-3, -2, -1]
integrality = [0, 0, 1]
lb = [0, 0, 0]
ub = [np.inf, np.inf, 1]
bounds = Bounds(lb, ub)
A = [[1, 1, 1], [4, 2, 1]]
b_l = [-np.inf, 12]
b_u = [7, 12]
constraints = LinearConstraint(A, b_l, b_u)
res = milp(c, integrality=integrality, bounds=bounds,
constraints=constraints)
# there are multiple solutions
assert_allclose(res.fun, -12)
@pytest.mark.xslow
def test_milp_6():
# solve a larger MIP with only equality constraints
# source: https://www.mathworks.com/help/optim/ug/intlinprog.html
integrality = 1
A_eq = np.array([[22, 13, 26, 33, 21, 3, 14, 26],
[39, 16, 22, 28, 26, 30, 23, 24],
[18, 14, 29, 27, 30, 38, 26, 26],
[41, 26, 28, 36, 18, 38, 16, 26]])
b_eq = np.array([7872, 10466, 11322, 12058])
c = np.array([2, 10, 13, 17, 7, 5, 7, 3])
res = milp(c=c, constraints=(A_eq, b_eq, b_eq), integrality=integrality)
np.testing.assert_allclose(res.fun, 1854)
def test_infeasible_prob_16609():
# Ensure presolve does not mark trivially infeasible problems
# as Optimal -- see gh-16609
c = [1.0, 0.0]
integrality = [0, 1]
lb = [0, -np.inf]
ub = [np.inf, np.inf]
bounds = Bounds(lb, ub)
A_eq = [[0.0, 1.0]]
b_eq = [0.5]
constraints = LinearConstraint(A_eq, b_eq, b_eq)
res = milp(c, integrality=integrality, bounds=bounds,
constraints=constraints)
np.testing.assert_equal(res.status, 2)
_msg_time = "Time limit reached. (HiGHS Status 13:"
_msg_iter = "Iteration limit reached. (HiGHS Status 14:"
@pytest.mark.thread_unsafe
# See https://github.com/scipy/scipy/pull/19255#issuecomment-1778438888
@pytest.mark.xfail(reason="Often buggy, revisit with callbacks, gh-19255")
@pytest.mark.skipif(np.intp(0).itemsize < 8,
reason="Unhandled 32-bit GCC FP bug")
@pytest.mark.slow
@pytest.mark.parametrize(["options", "msg"], [({"time_limit": 0.1}, _msg_time),
({"node_limit": 1}, _msg_iter)])
def test_milp_timeout_16545(options, msg):
# Ensure solution is not thrown away if MILP solver times out
# -- see gh-16545
rng = np.random.default_rng(5123833489170494244)
A = rng.integers(0, 5, size=(100, 100))
b_lb = np.full(100, fill_value=-np.inf)
b_ub = np.full(100, fill_value=25)
constraints = LinearConstraint(A, b_lb, b_ub)
variable_lb = np.zeros(100)
variable_ub = np.ones(100)
variable_bounds = Bounds(variable_lb, variable_ub)
integrality = np.ones(100)
c_vector = -np.ones(100)
res = milp(
c_vector,
integrality=integrality,
bounds=variable_bounds,
constraints=constraints,
options=options,
)
assert res.message.startswith(msg)
assert res["x"] is not None
# ensure solution is feasible
x = res["x"]
tol = 1e-8 # sometimes needed due to finite numerical precision
assert np.all(b_lb - tol <= A @ x) and np.all(A @ x <= b_ub + tol)
assert np.all(variable_lb - tol <= x) and np.all(x <= variable_ub + tol)
assert np.allclose(x, np.round(x))
def test_three_constraints_16878():
# `milp` failed when exactly three constraints were passed
# Ensure that this is no longer the case.
rng = np.random.default_rng(5123833489170494244)
A = rng.integers(0, 5, size=(6, 6))
bl = np.full(6, fill_value=-np.inf)
bu = np.full(6, fill_value=10)
constraints = [LinearConstraint(A[:2], bl[:2], bu[:2]),
LinearConstraint(A[2:4], bl[2:4], bu[2:4]),
LinearConstraint(A[4:], bl[4:], bu[4:])]
constraints2 = [(A[:2], bl[:2], bu[:2]),
(A[2:4], bl[2:4], bu[2:4]),
(A[4:], bl[4:], bu[4:])]
lb = np.zeros(6)
ub = np.ones(6)
variable_bounds = Bounds(lb, ub)
c = -np.ones(6)
res1 = milp(c, bounds=variable_bounds, constraints=constraints)
res2 = milp(c, bounds=variable_bounds, constraints=constraints2)
ref = milp(c, bounds=variable_bounds, constraints=(A, bl, bu))
assert res1.success and res2.success
assert_allclose(res1.x, ref.x)
assert_allclose(res2.x, ref.x)
@pytest.mark.xslow
def test_mip_rel_gap_passdown():
# Solve problem with decreasing mip_gap to make sure mip_rel_gap decreases
# Adapted from test_linprog::TestLinprogHiGHSMIP::test_mip_rel_gap_passdown
# MIP taken from test_mip_6 above
A_eq = np.array([[22, 13, 26, 33, 21, 3, 14, 26],
[39, 16, 22, 28, 26, 30, 23, 24],
[18, 14, 29, 27, 30, 38, 26, 26],
[41, 26, 28, 36, 18, 38, 16, 26]])
b_eq = np.array([7872, 10466, 11322, 12058])
c = np.array([2, 10, 13, 17, 7, 5, 7, 3])
mip_rel_gaps = [0.25, 0.01, 0.001]
sol_mip_gaps = []
for mip_rel_gap in mip_rel_gaps:
res = milp(c=c, bounds=(0, np.inf), constraints=(A_eq, b_eq, b_eq),
integrality=True, options={"mip_rel_gap": mip_rel_gap})
# assert that the solution actually has mip_gap lower than the
# required mip_rel_gap supplied
assert res.mip_gap <= mip_rel_gap
# check that `res.mip_gap` is as defined in the documentation
assert res.mip_gap == (res.fun - res.mip_dual_bound)/res.fun
sol_mip_gaps.append(res.mip_gap)
# make sure that the mip_rel_gap parameter is actually doing something
# check that differences between solution gaps are declining
# monotonically with the mip_rel_gap parameter.
assert np.all(np.diff(sol_mip_gaps) < 0)
@pytest.mark.xfail(reason='Upstream / Wrapper issue, see gh-20116')
def test_large_numbers_gh20116():
h = 10 ** 12
A = np.array([[100.4534, h], [100.4534, -h]])
b = np.array([h, 0])
constraints = LinearConstraint(A=A, ub=b)
bounds = Bounds([0, 0], [1, 1])
c = np.array([0, 0])
res = milp(c=c, constraints=constraints, bounds=bounds, integrality=1)
assert res.status == 0
assert np.all(A @ res.x < b)
def test_presolve_gh18907():
from scipy.optimize import milp
import numpy as np
inf = np.inf
# set up problem
c = np.array([-0.85850509, -0.82892676, -0.80026454, -0.63015535, -0.5099006,
-0.50077193, -0.4894404, -0.47285865, -0.39867774, -0.38069646,
-0.36733012, -0.36733012, -0.35820411, -0.31576141, -0.20626091,
-0.12466144, -0.10679516, -0.1061887, -0.1061887, -0.1061887,
-0., -0., -0., -0., 0., 0., 0., 0.])
A = np.array([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
1., 0., 0., 0., 0., 0., 1., 0., 0., 0., -25., -0., -0., -0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
-1., 0., 0., 0., 0., 0., -1., 0., 0., 0., 2., 0., 0., 0.],
[0., 0., 0., 0., 1., 1., 1., 1., 0., 1., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., -0., -25., -0., -0.],
[0., 0., 0., 0., -1., -1., -1., -1., 0., -1., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 2., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 1., 1., 1., 0., 0., 0., 0., -0., -0., -25., -0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., -1., -1., -1., 0., 0., 0., 0., 0., 0., 2., 0.],
[1., 1., 1., 1., 0., 0., 0., 0., 1., 0., 1., 1., 1., 1., 0.,
1., 1., 0., 0., 0., 0., 1., 1., 1., -0., -0., -0., -25.],
[-1., -1., -1., -1., 0., 0., 0., 0., -1., 0., -1., -1., -1., -1.,
0., -1., -1., 0., 0., 0., 0., -1., -1., -1., 0., 0., 0., 2.]])
bl = np.array([-inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf, -inf])
bu = np.array([100., 0., 0., 0., 0., 0., 0., 0., 0.])
constraints = LinearConstraint(A, bl, bu)
integrality = 1
bounds = (0, 1)
r1 = milp(c=c, constraints=constraints, integrality=integrality, bounds=bounds,
options={'presolve': True})
r2 = milp(c=c, constraints=constraints, integrality=integrality, bounds=bounds,
options={'presolve': False})
assert r1.status == r2.status
assert_allclose(r1.x, r2.x)
# another example from the same issue
bounds = Bounds(lb=0, ub=1)
integrality = [1, 1, 0, 0]
c = [10, 9.52380952, -1000, -952.38095238]
A = [[1, 1, 0, 0], [0, 0, 1, 1], [200, 0, 0, 0], [0, 200, 0, 0],
[0, 0, 2000, 0], [0, 0, 0, 2000], [-1, 0, 1, 0], [-1, -1, 0, 1]]
ub = [1, 1, 200, 200, 1000, 1000, 0, 0]
constraints = LinearConstraint(A, ub=ub)
r1 = milp(c=c, constraints=constraints, bounds=bounds,
integrality=integrality, options={"presolve": False})
r2 = milp(c=c, constraints=constraints, bounds=bounds,
integrality=integrality, options={"presolve": False})
assert r1.status == r2.status
assert_allclose(r1.x, r2.x)

View file

@ -0,0 +1,845 @@
import numpy as np
import pytest
from scipy.linalg import block_diag
from scipy.sparse import csc_array
from numpy.testing import (assert_array_almost_equal,
assert_array_less, assert_,
suppress_warnings)
from scipy.optimize import (NonlinearConstraint,
LinearConstraint,
Bounds,
minimize,
BFGS,
SR1,
rosen)
class Maratos:
"""Problem 15.4 from Nocedal and Wright
The following optimization problem:
minimize 2*(x[0]**2 + x[1]**2 - 1) - x[0]
Subject to: x[0]**2 + x[1]**2 - 1 = 0
"""
def __init__(self, degrees=60, constr_jac=None, constr_hess=None):
rads = degrees/180*np.pi
self.x0 = [np.cos(rads), np.sin(rads)]
self.x_opt = np.array([1.0, 0.0])
self.constr_jac = constr_jac
self.constr_hess = constr_hess
self.bounds = None
def fun(self, x):
return 2*(x[0]**2 + x[1]**2 - 1) - x[0]
def grad(self, x):
return np.array([4*x[0]-1, 4*x[1]])
def hess(self, x):
return 4*np.eye(2)
@property
def constr(self):
def fun(x):
return x[0]**2 + x[1]**2
if self.constr_jac is None:
def jac(x):
return [[2*x[0], 2*x[1]]]
else:
jac = self.constr_jac
if self.constr_hess is None:
def hess(x, v):
return 2*v[0]*np.eye(2)
else:
hess = self.constr_hess
return NonlinearConstraint(fun, 1, 1, jac, hess)
class MaratosTestArgs:
"""Problem 15.4 from Nocedal and Wright
The following optimization problem:
minimize 2*(x[0]**2 + x[1]**2 - 1) - x[0]
Subject to: x[0]**2 + x[1]**2 - 1 = 0
"""
def __init__(self, a, b, degrees=60, constr_jac=None, constr_hess=None):
rads = degrees/180*np.pi
self.x0 = [np.cos(rads), np.sin(rads)]
self.x_opt = np.array([1.0, 0.0])
self.constr_jac = constr_jac
self.constr_hess = constr_hess
self.a = a
self.b = b
self.bounds = None
def _test_args(self, a, b):
if self.a != a or self.b != b:
raise ValueError()
def fun(self, x, a, b):
self._test_args(a, b)
return 2*(x[0]**2 + x[1]**2 - 1) - x[0]
def grad(self, x, a, b):
self._test_args(a, b)
return np.array([4*x[0]-1, 4*x[1]])
def hess(self, x, a, b):
self._test_args(a, b)
return 4*np.eye(2)
@property
def constr(self):
def fun(x):
return x[0]**2 + x[1]**2
if self.constr_jac is None:
def jac(x):
return [[4*x[0], 4*x[1]]]
else:
jac = self.constr_jac
if self.constr_hess is None:
def hess(x, v):
return 2*v[0]*np.eye(2)
else:
hess = self.constr_hess
return NonlinearConstraint(fun, 1, 1, jac, hess)
class MaratosGradInFunc:
"""Problem 15.4 from Nocedal and Wright
The following optimization problem:
minimize 2*(x[0]**2 + x[1]**2 - 1) - x[0]
Subject to: x[0]**2 + x[1]**2 - 1 = 0
"""
def __init__(self, degrees=60, constr_jac=None, constr_hess=None):
rads = degrees/180*np.pi
self.x0 = [np.cos(rads), np.sin(rads)]
self.x_opt = np.array([1.0, 0.0])
self.constr_jac = constr_jac
self.constr_hess = constr_hess
self.bounds = None
def fun(self, x):
return (2*(x[0]**2 + x[1]**2 - 1) - x[0],
np.array([4*x[0]-1, 4*x[1]]))
@property
def grad(self):
return True
def hess(self, x):
return 4*np.eye(2)
@property
def constr(self):
def fun(x):
return x[0]**2 + x[1]**2
if self.constr_jac is None:
def jac(x):
return [[4*x[0], 4*x[1]]]
else:
jac = self.constr_jac
if self.constr_hess is None:
def hess(x, v):
return 2*v[0]*np.eye(2)
else:
hess = self.constr_hess
return NonlinearConstraint(fun, 1, 1, jac, hess)
class HyperbolicIneq:
"""Problem 15.1 from Nocedal and Wright
The following optimization problem:
minimize 1/2*(x[0] - 2)**2 + 1/2*(x[1] - 1/2)**2
Subject to: 1/(x[0] + 1) - x[1] >= 1/4
x[0] >= 0
x[1] >= 0
"""
def __init__(self, constr_jac=None, constr_hess=None):
self.x0 = [0, 0]
self.x_opt = [1.952823, 0.088659]
self.constr_jac = constr_jac
self.constr_hess = constr_hess
self.bounds = Bounds(0, np.inf)
def fun(self, x):
return 1/2*(x[0] - 2)**2 + 1/2*(x[1] - 1/2)**2
def grad(self, x):
return [x[0] - 2, x[1] - 1/2]
def hess(self, x):
return np.eye(2)
@property
def constr(self):
def fun(x):
return 1/(x[0] + 1) - x[1]
if self.constr_jac is None:
def jac(x):
return [[-1/(x[0] + 1)**2, -1]]
else:
jac = self.constr_jac
if self.constr_hess is None:
def hess(x, v):
return 2*v[0]*np.array([[1/(x[0] + 1)**3, 0],
[0, 0]])
else:
hess = self.constr_hess
return NonlinearConstraint(fun, 0.25, np.inf, jac, hess)
class Rosenbrock:
"""Rosenbrock function.
The following optimization problem:
minimize sum(100.0*(x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0)
"""
def __init__(self, n=2, random_state=0):
rng = np.random.RandomState(random_state)
self.x0 = rng.uniform(-1, 1, n)
self.x_opt = np.ones(n)
self.bounds = None
def fun(self, x):
x = np.asarray(x)
r = np.sum(100.0 * (x[1:] - x[:-1]**2.0)**2.0 + (1 - x[:-1])**2.0,
axis=0)
return r
def grad(self, x):
x = np.asarray(x)
xm = x[1:-1]
xm_m1 = x[:-2]
xm_p1 = x[2:]
der = np.zeros_like(x)
der[1:-1] = (200 * (xm - xm_m1**2) -
400 * (xm_p1 - xm**2) * xm - 2 * (1 - xm))
der[0] = -400 * x[0] * (x[1] - x[0]**2) - 2 * (1 - x[0])
der[-1] = 200 * (x[-1] - x[-2]**2)
return der
def hess(self, x):
x = np.atleast_1d(x)
H = np.diag(-400 * x[:-1], 1) - np.diag(400 * x[:-1], -1)
diagonal = np.zeros(len(x), dtype=x.dtype)
diagonal[0] = 1200 * x[0]**2 - 400 * x[1] + 2
diagonal[-1] = 200
diagonal[1:-1] = 202 + 1200 * x[1:-1]**2 - 400 * x[2:]
H = H + np.diag(diagonal)
return H
@property
def constr(self):
return ()
class IneqRosenbrock(Rosenbrock):
"""Rosenbrock subject to inequality constraints.
The following optimization problem:
minimize sum(100.0*(x[1] - x[0]**2)**2.0 + (1 - x[0])**2)
subject to: x[0] + 2 x[1] <= 1
Taken from matlab ``fmincon`` documentation.
"""
def __init__(self, random_state=0):
Rosenbrock.__init__(self, 2, random_state)
self.x0 = [-1, -0.5]
self.x_opt = [0.5022, 0.2489]
self.bounds = None
@property
def constr(self):
A = [[1, 2]]
b = 1
return LinearConstraint(A, -np.inf, b)
class BoundedRosenbrock(Rosenbrock):
"""Rosenbrock subject to inequality constraints.
The following optimization problem:
minimize sum(100.0*(x[1] - x[0]**2)**2.0 + (1 - x[0])**2)
subject to: -2 <= x[0] <= 0
0 <= x[1] <= 2
Taken from matlab ``fmincon`` documentation.
"""
def __init__(self, random_state=0):
Rosenbrock.__init__(self, 2, random_state)
self.x0 = [-0.2, 0.2]
self.x_opt = None
self.bounds = Bounds([-2, 0], [0, 2])
class EqIneqRosenbrock(Rosenbrock):
"""Rosenbrock subject to equality and inequality constraints.
The following optimization problem:
minimize sum(100.0*(x[1] - x[0]**2)**2.0 + (1 - x[0])**2)
subject to: x[0] + 2 x[1] <= 1
2 x[0] + x[1] = 1
Taken from matlab ``fimincon`` documentation.
"""
def __init__(self, random_state=0):
Rosenbrock.__init__(self, 2, random_state)
self.x0 = [-1, -0.5]
self.x_opt = [0.41494, 0.17011]
self.bounds = None
@property
def constr(self):
A_ineq = [[1, 2]]
b_ineq = 1
A_eq = [[2, 1]]
b_eq = 1
return (LinearConstraint(A_ineq, -np.inf, b_ineq),
LinearConstraint(A_eq, b_eq, b_eq))
class Elec:
"""Distribution of electrons on a sphere.
Problem no 2 from COPS collection [2]_. Find
the equilibrium state distribution (of minimal
potential) of the electrons positioned on a
conducting sphere.
References
----------
.. [1] E. D. Dolan, J. J. Mor\'{e}, and T. S. Munson,
"Benchmarking optimization software with COPS 3.0.",
Argonne National Lab., Argonne, IL (US), 2004.
"""
def __init__(self, n_electrons=200, random_state=0,
constr_jac=None, constr_hess=None):
self.n_electrons = n_electrons
self.rng = np.random.RandomState(random_state)
# Initial Guess
phi = self.rng.uniform(0, 2 * np.pi, self.n_electrons)
theta = self.rng.uniform(-np.pi, np.pi, self.n_electrons)
x = np.cos(theta) * np.cos(phi)
y = np.cos(theta) * np.sin(phi)
z = np.sin(theta)
self.x0 = np.hstack((x, y, z))
self.x_opt = None
self.constr_jac = constr_jac
self.constr_hess = constr_hess
self.bounds = None
def _get_cordinates(self, x):
x_coord = x[:self.n_electrons]
y_coord = x[self.n_electrons:2 * self.n_electrons]
z_coord = x[2 * self.n_electrons:]
return x_coord, y_coord, z_coord
def _compute_coordinate_deltas(self, x):
x_coord, y_coord, z_coord = self._get_cordinates(x)
dx = x_coord[:, None] - x_coord
dy = y_coord[:, None] - y_coord
dz = z_coord[:, None] - z_coord
return dx, dy, dz
def fun(self, x):
dx, dy, dz = self._compute_coordinate_deltas(x)
with np.errstate(divide='ignore'):
dm1 = (dx**2 + dy**2 + dz**2) ** -0.5
dm1[np.diag_indices_from(dm1)] = 0
return 0.5 * np.sum(dm1)
def grad(self, x):
dx, dy, dz = self._compute_coordinate_deltas(x)
with np.errstate(divide='ignore'):
dm3 = (dx**2 + dy**2 + dz**2) ** -1.5
dm3[np.diag_indices_from(dm3)] = 0
grad_x = -np.sum(dx * dm3, axis=1)
grad_y = -np.sum(dy * dm3, axis=1)
grad_z = -np.sum(dz * dm3, axis=1)
return np.hstack((grad_x, grad_y, grad_z))
def hess(self, x):
dx, dy, dz = self._compute_coordinate_deltas(x)
d = (dx**2 + dy**2 + dz**2) ** 0.5
with np.errstate(divide='ignore'):
dm3 = d ** -3
dm5 = d ** -5
i = np.arange(self.n_electrons)
dm3[i, i] = 0
dm5[i, i] = 0
Hxx = dm3 - 3 * dx**2 * dm5
Hxx[i, i] = -np.sum(Hxx, axis=1)
Hxy = -3 * dx * dy * dm5
Hxy[i, i] = -np.sum(Hxy, axis=1)
Hxz = -3 * dx * dz * dm5
Hxz[i, i] = -np.sum(Hxz, axis=1)
Hyy = dm3 - 3 * dy**2 * dm5
Hyy[i, i] = -np.sum(Hyy, axis=1)
Hyz = -3 * dy * dz * dm5
Hyz[i, i] = -np.sum(Hyz, axis=1)
Hzz = dm3 - 3 * dz**2 * dm5
Hzz[i, i] = -np.sum(Hzz, axis=1)
H = np.vstack((
np.hstack((Hxx, Hxy, Hxz)),
np.hstack((Hxy, Hyy, Hyz)),
np.hstack((Hxz, Hyz, Hzz))
))
return H
@property
def constr(self):
def fun(x):
x_coord, y_coord, z_coord = self._get_cordinates(x)
return x_coord**2 + y_coord**2 + z_coord**2 - 1
if self.constr_jac is None:
def jac(x):
x_coord, y_coord, z_coord = self._get_cordinates(x)
Jx = 2 * np.diag(x_coord)
Jy = 2 * np.diag(y_coord)
Jz = 2 * np.diag(z_coord)
return csc_array(np.hstack((Jx, Jy, Jz)))
else:
jac = self.constr_jac
if self.constr_hess is None:
def hess(x, v):
D = 2 * np.diag(v)
return block_diag(D, D, D)
else:
hess = self.constr_hess
return NonlinearConstraint(fun, -np.inf, 0, jac, hess)
class TestTrustRegionConstr:
list_of_problems = [Maratos(),
Maratos(constr_hess='2-point'),
Maratos(constr_hess=SR1()),
Maratos(constr_jac='2-point', constr_hess=SR1()),
MaratosGradInFunc(),
HyperbolicIneq(),
HyperbolicIneq(constr_hess='3-point'),
HyperbolicIneq(constr_hess=BFGS()),
HyperbolicIneq(constr_jac='3-point',
constr_hess=BFGS()),
Rosenbrock(),
IneqRosenbrock(),
EqIneqRosenbrock(),
BoundedRosenbrock(),
Elec(n_electrons=2),
Elec(n_electrons=2, constr_hess='2-point'),
Elec(n_electrons=2, constr_hess=SR1()),
Elec(n_electrons=2, constr_jac='3-point',
constr_hess=SR1())]
@pytest.mark.thread_unsafe
@pytest.mark.parametrize('prob', list_of_problems)
@pytest.mark.parametrize('grad', ('prob.grad', '3-point', False))
@pytest.mark.parametrize('hess', ("prob.hess", '3-point', SR1(),
BFGS(exception_strategy='damp_update'),
BFGS(exception_strategy='skip_update')))
def test_list_of_problems(self, prob, grad, hess):
grad = prob.grad if grad == "prob.grad" else grad
hess = prob.hess if hess == "prob.hess" else hess
# Remove exceptions
if (grad in {'2-point', '3-point', 'cs', False} and
hess in {'2-point', '3-point', 'cs'}):
pytest.skip("Numerical Hessian needs analytical gradient")
if prob.grad is True and grad in {'3-point', False}:
pytest.skip("prob.grad incompatible with grad in {'3-point', False}")
sensitive = (isinstance(prob, BoundedRosenbrock) and grad == '3-point'
and isinstance(hess, BFGS))
if sensitive:
pytest.xfail("Seems sensitive to initial conditions w/ Accelerate")
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
result = minimize(prob.fun, prob.x0,
method='trust-constr',
jac=grad, hess=hess,
bounds=prob.bounds,
constraints=prob.constr)
if prob.x_opt is not None:
assert_array_almost_equal(result.x, prob.x_opt,
decimal=5)
# gtol
if result.status == 1:
assert_array_less(result.optimality, 1e-8)
# xtol
if result.status == 2:
assert_array_less(result.tr_radius, 1e-8)
if result.method == "tr_interior_point":
assert_array_less(result.barrier_parameter, 1e-8)
# check for max iter
message = f"Invalid termination condition: {result.status}."
assert result.status not in {0, 3}, message
def test_default_jac_and_hess(self):
def fun(x):
return (x - 1) ** 2
bounds = [(-2, 2)]
res = minimize(fun, x0=[-1.5], bounds=bounds, method='trust-constr')
assert_array_almost_equal(res.x, 1, decimal=5)
def test_default_hess(self):
def fun(x):
return (x - 1) ** 2
bounds = [(-2, 2)]
res = minimize(fun, x0=[-1.5], bounds=bounds, method='trust-constr',
jac='2-point')
assert_array_almost_equal(res.x, 1, decimal=5)
def test_no_constraints(self):
prob = Rosenbrock()
result = minimize(prob.fun, prob.x0,
method='trust-constr',
jac=prob.grad, hess=prob.hess)
result1 = minimize(prob.fun, prob.x0,
method='L-BFGS-B',
jac='2-point')
result2 = minimize(prob.fun, prob.x0,
method='L-BFGS-B',
jac='3-point')
assert_array_almost_equal(result.x, prob.x_opt, decimal=5)
assert_array_almost_equal(result1.x, prob.x_opt, decimal=5)
assert_array_almost_equal(result2.x, prob.x_opt, decimal=5)
def test_hessp(self):
prob = Maratos()
def hessp(x, p):
H = prob.hess(x)
return H.dot(p)
result = minimize(prob.fun, prob.x0,
method='trust-constr',
jac=prob.grad, hessp=hessp,
bounds=prob.bounds,
constraints=prob.constr)
if prob.x_opt is not None:
assert_array_almost_equal(result.x, prob.x_opt, decimal=2)
# gtol
if result.status == 1:
assert_array_less(result.optimality, 1e-8)
# xtol
if result.status == 2:
assert_array_less(result.tr_radius, 1e-8)
if result.method == "tr_interior_point":
assert_array_less(result.barrier_parameter, 1e-8)
# max iter
if result.status in (0, 3):
raise RuntimeError("Invalid termination condition.")
def test_args(self):
prob = MaratosTestArgs("a", 234)
result = minimize(prob.fun, prob.x0, ("a", 234),
method='trust-constr',
jac=prob.grad, hess=prob.hess,
bounds=prob.bounds,
constraints=prob.constr)
if prob.x_opt is not None:
assert_array_almost_equal(result.x, prob.x_opt, decimal=2)
# gtol
if result.status == 1:
assert_array_less(result.optimality, 1e-8)
# xtol
if result.status == 2:
assert_array_less(result.tr_radius, 1e-8)
if result.method == "tr_interior_point":
assert_array_less(result.barrier_parameter, 1e-8)
# max iter
if result.status in (0, 3):
raise RuntimeError("Invalid termination condition.")
def test_raise_exception(self):
prob = Maratos()
message = "Whenever the gradient is estimated via finite-differences"
with pytest.raises(ValueError, match=message):
minimize(prob.fun, prob.x0, method='trust-constr', jac='2-point',
hess='2-point', constraints=prob.constr)
def test_issue_9044(self):
# https://github.com/scipy/scipy/issues/9044
# Test the returned `OptimizeResult` contains keys consistent with
# other solvers.
def callback(x, info):
assert_('nit' in info)
assert_('niter' in info)
result = minimize(lambda x: x**2, [0], jac=lambda x: 2*x,
hess=lambda x: 2, callback=callback,
method='trust-constr')
assert_(result.get('success'))
assert_(result.get('nit', -1) == 1)
# Also check existence of the 'niter' attribute, for backward
# compatibility
assert_(result.get('niter', -1) == 1)
def test_issue_15093(self):
# scipy docs define bounds as inclusive, so it shouldn't be
# an issue to set x0 on the bounds even if keep_feasible is
# True. Previously, trust-constr would treat bounds as
# exclusive.
x0 = np.array([0., 0.5])
def obj(x):
x1 = x[0]
x2 = x[1]
return x1 ** 2 + x2 ** 2
bounds = Bounds(np.array([0., 0.]), np.array([1., 1.]),
keep_feasible=True)
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
result = minimize(
method='trust-constr',
fun=obj,
x0=x0,
bounds=bounds)
assert result['success']
class TestEmptyConstraint:
"""
Here we minimize x^2+y^2 subject to x^2-y^2>1.
The actual minimum is at (0, 0) which fails the constraint.
Therefore we will find a minimum on the boundary at (+/-1, 0).
When minimizing on the boundary, optimize uses a set of
constraints that removes the constraint that sets that
boundary. In our case, there's only one constraint, so
the result is an empty constraint.
This tests that the empty constraint works.
"""
def test_empty_constraint(self):
def function(x):
return x[0]**2 + x[1]**2
def functionjacobian(x):
return np.array([2.*x[0], 2.*x[1]])
def functionhvp(x, v):
return 2.*v
def constraint(x):
return np.array([x[0]**2 - x[1]**2])
def constraintjacobian(x):
return np.array([[2*x[0], -2*x[1]]])
def constraintlcoh(x, v):
return np.array([[2., 0.], [0., -2.]]) * v[0]
constraint = NonlinearConstraint(constraint, 1., np.inf,
constraintjacobian, constraintlcoh)
startpoint = [1., 2.]
bounds = Bounds([-np.inf, -np.inf], [np.inf, np.inf])
result = minimize(
function,
startpoint,
method='trust-constr',
jac=functionjacobian,
hessp=functionhvp,
constraints=[constraint],
bounds=bounds,
)
assert_array_almost_equal(abs(result.x), np.array([1, 0]), decimal=4)
def test_bug_11886():
def opt(x):
return x[0]**2+x[1]**2
with np.testing.suppress_warnings() as sup:
sup.filter(PendingDeprecationWarning)
A = np.matrix(np.diag([1, 1]))
lin_cons = LinearConstraint(A, -1, np.inf)
# just checking that there are no errors
minimize(opt, 2*[1], constraints = lin_cons)
def test_gh11649():
# trust - constr error when attempting to keep bound constrained solutions
# feasible. Algorithm attempts to go outside bounds when evaluating finite
# differences. (don't give objective an analytic gradient)
bnds = Bounds(lb=[-1, -1], ub=[1, 1], keep_feasible=True)
def assert_inbounds(x):
assert np.all(x >= bnds.lb)
assert np.all(x <= bnds.ub)
def obj(x):
assert_inbounds(x)
return np.exp(x[0])*(4*x[0]**2 + 2*x[1]**2 + 4*x[0]*x[1] + 2*x[1] + 1)
def nce(x):
assert_inbounds(x)
return x[0]**2 + x[1]
def nce_jac(x):
return np.array([2*x[0], 1])
def nci(x):
assert_inbounds(x)
return x[0]*x[1]
x0 = np.array((0.99, -0.99))
nlcs = [NonlinearConstraint(nci, -10, np.inf),
NonlinearConstraint(nce, 1, 1, jac=nce_jac)]
res = minimize(fun=obj, x0=x0, method='trust-constr',
bounds=bnds, constraints=nlcs)
assert_inbounds(res.x)
assert nlcs[0].lb < nlcs[0].fun(res.x) < nlcs[0].ub
def test_gh20665_too_many_constraints():
# gh-20665 reports a confusing error message when there are more equality
# constraints than variables. Check that the error message is improved.
message = "...more equality constraints than independent variables..."
with pytest.raises(ValueError, match=message):
x0 = np.ones((2,))
A_eq, b_eq = np.arange(6).reshape((3, 2)), np.ones((3,))
g = NonlinearConstraint(lambda x: A_eq @ x, lb=b_eq, ub=b_eq)
minimize(rosen, x0, method='trust-constr', constraints=[g])
# no error with `SVDFactorization`
with np.testing.suppress_warnings() as sup:
sup.filter(UserWarning)
minimize(rosen, x0, method='trust-constr', constraints=[g],
options={'factorization_method': 'SVDFactorization'})
def test_issue_18882():
def lsf(u):
u1, u2 = u
a, b = [3.0, 4.0]
return 1.0 + u1**2 / a**2 - u2**2 / b**2
def of(u):
return np.sum(u**2)
with suppress_warnings() as sup:
sup.filter(UserWarning, "delta_grad == 0.0")
sup.filter(UserWarning, "Singular Jacobian matrix.")
res = minimize(
of,
[0.0, 0.0],
method="trust-constr",
constraints=NonlinearConstraint(lsf, 0, 0),
)
assert (not res.success) and (res.constr_violation > 1e-8)
class TestBoundedNelderMead:
@pytest.mark.parametrize('bounds, x_opt',
[(Bounds(-np.inf, np.inf), Rosenbrock().x_opt),
(Bounds(-np.inf, -0.8), [-0.8, -0.8]),
(Bounds(3.0, np.inf), [3.0, 9.0]),
(Bounds([3.0, 1.0], [4.0, 5.0]), [3., 5.]),
])
def test_rosen_brock_with_bounds(self, bounds, x_opt):
prob = Rosenbrock()
with suppress_warnings() as sup:
sup.filter(UserWarning, "Initial guess is not within "
"the specified bounds")
result = minimize(prob.fun, [-10, -10],
method='Nelder-Mead',
bounds=bounds)
assert np.less_equal(bounds.lb, result.x).all()
assert np.less_equal(result.x, bounds.ub).all()
assert np.allclose(prob.fun(result.x), result.fun)
assert np.allclose(result.x, x_opt, atol=1.e-3)
def test_equal_all_bounds(self):
prob = Rosenbrock()
bounds = Bounds([4.0, 5.0], [4.0, 5.0])
with suppress_warnings() as sup:
sup.filter(UserWarning, "Initial guess is not within "
"the specified bounds")
result = minimize(prob.fun, [-10, 8],
method='Nelder-Mead',
bounds=bounds)
assert np.allclose(result.x, [4.0, 5.0])
def test_equal_one_bounds(self):
prob = Rosenbrock()
bounds = Bounds([4.0, 5.0], [4.0, 20.0])
with suppress_warnings() as sup:
sup.filter(UserWarning, "Initial guess is not within "
"the specified bounds")
result = minimize(prob.fun, [-10, 8],
method='Nelder-Mead',
bounds=bounds)
assert np.allclose(result.x, [4.0, 16.0])
def test_invalid_bounds(self):
prob = Rosenbrock()
message = 'An upper bound is less than the corresponding lower bound.'
with pytest.raises(ValueError, match=message):
bounds = Bounds([-np.inf, 1.0], [4.0, -5.0])
minimize(prob.fun, [-10, 3],
method='Nelder-Mead',
bounds=bounds)
@pytest.mark.xfail(reason="Failing on Azure Linux and macOS builds, "
"see gh-13846")
def test_outside_bounds_warning(self):
prob = Rosenbrock()
message = "Initial guess is not within the specified bounds"
with pytest.warns(UserWarning, match=message):
bounds = Bounds([-np.inf, 1.0], [4.0, 5.0])
minimize(prob.fun, [-10, 8],
method='Nelder-Mead',
bounds=bounds)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,469 @@
import numpy as np
from numpy.testing import assert_allclose
from pytest import raises as assert_raises
from scipy.optimize import nnls
import pytest
class TestNNLS:
def setup_method(self):
self.rng = np.random.default_rng(1685225766635251)
def test_nnls(self):
a = np.arange(25.0).reshape(-1, 5)
x = np.arange(5.0)
y = a @ x
x, res = nnls(a, y)
assert res < 1e-7
assert np.linalg.norm((a @ x) - y) < 1e-7
def test_nnls_tall(self):
a = self.rng.uniform(low=-10, high=10, size=[50, 10])
x = np.abs(self.rng.uniform(low=-2, high=2, size=[10]))
x[::2] = 0
b = a @ x
xact, rnorm = nnls(a, b)
assert_allclose(xact, x, rtol=0., atol=1e-10)
assert rnorm < 1e-12
def test_nnls_wide(self):
# If too wide then problem becomes too ill-conditioned ans starts
# emitting warnings, hence small m, n difference.
a = self.rng.uniform(low=-10, high=10, size=[100, 120])
x = np.abs(self.rng.uniform(low=-2, high=2, size=[120]))
x[::2] = 0
b = a @ x
xact, rnorm = nnls(a, b)
assert_allclose(xact, x, rtol=0., atol=1e-10)
assert rnorm < 1e-12
def test_maxiter(self):
# test that maxiter argument does stop iterations
a = self.rng.uniform(size=(5, 10))
b = self.rng.uniform(size=5)
with assert_raises(RuntimeError):
nnls(a, b, maxiter=1)
def test_nnls_inner_loop_case1(self):
# See gh-20168
n = np.array(
[3, 2, 0, 1, 1, 1, 3, 8, 14, 16, 29, 23, 41, 47, 53, 57, 67, 76,
103, 89, 97, 94, 85, 95, 78, 78, 78, 77, 73, 50, 50, 56, 68, 98,
95, 112, 134, 145, 158, 172, 213, 234, 222, 215, 216, 216, 206,
183, 135, 156, 110, 92, 63, 60, 52, 29, 20, 16, 12, 5, 5, 5, 1, 2,
3, 0, 2])
k = np.array(
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0.7205812007860187, 0., 1.4411624015720375,
0.7205812007860187, 2.882324803144075, 5.76464960628815,
5.76464960628815, 12.249880413362318, 15.132205216506394,
20.176273622008523, 27.382085629868712, 48.27894045266326,
47.558359251877235, 68.45521407467177, 97.99904330689854,
108.0871801179028, 135.46926574777152, 140.51333415327366,
184.4687874012208, 171.49832578707245, 205.36564222401535,
244.27702706646033, 214.01261663344755, 228.42424064916793,
232.02714665309804, 205.36564222401535, 172.9394881886445,
191.67459940908097, 162.1307701768542, 153.48379576742198,
110.96950492104689, 103.04311171240067, 86.46974409432225,
60.528820866025576, 43.234872047161126, 23.779179625938617,
24.499760826724636, 17.29394881886445, 11.5292992125763,
5.76464960628815, 5.044068405502131, 3.6029060039300935, 0.,
2.882324803144075, 0., 0., 0.])
d = np.array(
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0.003889242101538, 0., 0.007606268390096, 0.,
0.025457371599973, 0.036952882091577, 0., 0.08518359183449,
0.048201126400243, 0.196234990022205, 0.144116240157247,
0.171145134062442, 0., 0., 0.269555036538714, 0., 0., 0.,
0.010893241091872, 0., 0., 0., 0., 0., 0., 0., 0.,
0.048167058272886, 0.011238724891049, 0., 0., 0.055162603456078,
0., 0., 0., 0., 0.027753339088588, 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0.])
# The following code sets up a system of equations such that
# $k_i-p_i*n_i$ is minimized for $p_i$ with weights $n_i$ and
# monotonicity constraints on $p_i$. This translates to a system of
# equations of the form $k_i - (d_1 + ... + d_i) * n_i$ and
# non-negativity constraints on the $d_i$. If $n_i$ is zero the
# system is modified such that $d_i - d_{i+1}$ is then minimized.
N = len(n)
A = np.diag(n) @ np.tril(np.ones((N, N)))
w = n ** 0.5
nz = (n == 0).nonzero()[0]
A[nz, nz] = 1
A[nz, np.minimum(nz + 1, N - 1)] = -1
w[nz] = 1
k[nz] = 0
W = np.diag(w)
# Small perturbations can already make the infinite loop go away (just
# uncomment the next line)
# k = k + 1e-10 * np.random.normal(size=N)
dact, _ = nnls(W @ A, W @ k)
assert_allclose(dact, d, rtol=0., atol=1e-10)
def test_nnls_inner_loop_case2(self):
# See gh-20168
n = np.array(
[1, 0, 1, 2, 2, 2, 3, 3, 5, 4, 14, 14, 19, 26, 36, 42, 36, 64, 64,
64, 81, 85, 85, 95, 95, 95, 75, 76, 69, 81, 62, 59, 68, 64, 71, 67,
74, 78, 118, 135, 153, 159, 210, 195, 218, 243, 236, 215, 196, 175,
185, 149, 144, 103, 104, 75, 56, 40, 32, 26, 17, 9, 12, 8, 2, 1, 1,
1])
k = np.array(
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0.7064355064917867, 0., 0., 2.11930651947536,
0.7064355064917867, 0., 3.5321775324589333, 7.064355064917867,
11.302968103868587, 16.95445215580288, 20.486629688261814,
20.486629688261814, 37.44108184406469, 55.808405012851146,
78.41434122058831, 103.13958394780086, 105.965325973768,
125.74552015553803, 149.057891869767, 176.60887662294667,
197.09550631120848, 211.930651947536, 204.86629688261814,
233.8301526487814, 221.1143135319292, 195.6826352982249,
197.80194181770025, 191.4440222592742, 187.91184472681525,
144.11284332432447, 131.39700420747232, 116.5618585711448,
93.24948685691584, 89.01087381796512, 53.68909849337579,
45.211872415474346, 31.083162285638615, 24.72524272721253,
16.95445215580288, 9.890097090885014, 9.890097090885014,
2.8257420259671466, 2.8257420259671466, 1.4128710129835733,
0.7064355064917867, 1.4128710129835733])
d = np.array(
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0.0021916146355674473, 0., 0.,
0.011252740799789484, 0., 0., 0.037746623295934395,
0.03602328132946222, 0.09509167709829734, 0.10505765870204821,
0.01391037014274718, 0.0188296228752321, 0.20723559202324254,
0.3056220879462608, 0.13304643490426477, 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0.043185876949706214, 0.0037266261379722554,
0., 0., 0., 0., 0., 0.094797899357143, 0., 0., 0., 0., 0., 0., 0.,
0., 0.23450935613672663, 0., 0., 0.07064355064917871])
# The following code sets up a system of equations such that
# $k_i-p_i*n_i$ is minimized for $p_i$ with weights $n_i$ and
# monotonicity constraints on $p_i$. This translates to a system of
# equations of the form $k_i - (d_1 + ... + d_i) * n_i$ and
# non-negativity constraints on the $d_i$. If $n_i$ is zero the
# system is modified such that $d_i - d_{i+1}$ is then minimized.
N = len(n)
A = np.diag(n) @ np.tril(np.ones((N, N)))
w = n ** 0.5
nz = (n == 0).nonzero()[0]
A[nz, nz] = 1
A[nz, np.minimum(nz + 1, N - 1)] = -1
w[nz] = 1
k[nz] = 0
W = np.diag(w)
dact, _ = nnls(W @ A, W @ k)
p = np.cumsum(dact)
assert np.all(dact >= 0)
assert np.linalg.norm(k - n * p, ord=np.inf) < 28
assert_allclose(dact, d, rtol=0., atol=1e-10)
def test_nnls_gh20302(self):
# See gh-20302
A = np.array(
[0.33408569134321575, 0.11136189711440525, 0.049140798007949286,
0.03712063237146841, 0.055680948557202625, 0.16642814595936478,
0.11095209730624318, 0.09791993030943345, 0.14793612974165757,
0.44380838922497273, 0.11099502671044059, 0.11099502671044059,
0.14693672599330593, 0.3329850801313218, 1.498432860590948,
0.0832374225132955, 0.11098323001772734, 0.19589481249472837,
0.5919105600945457, 3.5514633605672747, 0.06658716751427037,
0.11097861252378394, 0.24485832778293645, 0.9248217710315328,
6.936163282736496, 0.05547609388181014, 0.11095218776362029,
0.29376003042571264, 1.3314262531634435, 11.982836278470993,
0.047506113282944136, 0.11084759766020298, 0.3423969672933396,
1.8105107617833156, 19.010362998724812, 0.041507335004505576,
0.11068622667868154, 0.39074115283013344, 2.361306169145206,
28.335674029742474, 0.03682846280947718, 0.11048538842843154,
0.4387861797121048, 2.9831054875676517, 40.2719240821633,
0.03311278164362387, 0.11037593881207958, 0.4870572300443105,
3.6791979604026523, 55.187969406039784, 0.030079304092299915,
0.11029078167176636, 0.5353496017200152, 4.448394860761242,
73.3985152025605, 0.02545939709595835, 0.11032405408248619,
0.6328767609778363, 6.214921713313388, 121.19097340961108,
0.022080881724881523, 0.11040440862440762, 0.7307742886903428,
8.28033064683057, 186.30743955368786, 0.020715838214945492,
0.1104844704797093, 0.7800578384588346, 9.42800814760186,
226.27219554244465, 0.01843179728340054, 0.11059078370040323,
0.8784095015912599, 11.94380463964355, 322.48272527037585,
0.015812787653789077, 0.11068951357652354, 1.0257259848595766,
16.27135849574896, 512.5477926160922, 0.014438550529330062,
0.11069555405819713, 1.1234754801775881, 19.519316032262093,
673.4164031130423, 0.012760770585072577, 0.110593345070629,
1.2688431112524712, 24.920367089248398, 971.8943164806875,
0.011427556646114315, 0.11046638091243838, 1.413623342459821,
30.967408782453557, 1347.0822820367298, 0.010033330264470307,
0.11036663290917338, 1.6071533470570285, 40.063087746029936,
1983.122843428482, 0.008950061496507258, 0.11038409179025618,
1.802244865119193, 50.37194055362024, 2795.642700725923,
0.008071078821135658, 0.11030474388885401, 1.9956465761433504,
61.80742482572119, 3801.1566267818534, 0.007191031207777556,
0.11026247851925586, 2.238160187262168, 77.7718015155818,
5366.2543045751445, 0.00636834224248, 0.11038459886965334,
2.5328963107984297, 99.49331844784753, 7760.4788389321075,
0.005624259098118485, 0.11061042892966355, 2.879742607664547,
128.34496770138628, 11358.529641572684, 0.0050354270614989555,
0.11077939535297703, 3.2263279459292575, 160.85168205252265,
15924.316523199741, 0.0044997853165982555, 0.1109947044760903,
3.6244287189055613, 202.60233390369015, 22488.859063309606,
0.004023601950058174, 0.1113196539516095, 4.07713905729421,
255.6270320242126, 31825.565487014468, 0.0036024117873727094,
0.111674765408554, 4.582933773135057, 321.9583486728612,
44913.18963986413, 0.003201503089582304, 0.11205260813538065,
5.191786833370116, 411.79333489752383, 64857.45024636,
0.0028633044552448853, 0.11262330857296549, 5.864295861648949,
522.7223161899905, 92521.84996562831, 0.0025691897303891965,
0.11304434813712465, 6.584584405106342, 656.5615739804199,
129999.19164812315, 0.0022992911894424675, 0.11343169867916175,
7.4080129906658305, 828.2026426227864, 183860.98666225857,
0.0020449922071108764, 0.11383789952917212, 8.388975556433872,
1058.2750599896935, 265097.9025274183, 0.001831274615120854,
0.11414945100919989, 9.419351803810935, 1330.564050780237,
373223.2162438565, 0.0016363333454631633, 0.11454333418242145,
10.6143816579462, 1683.787012481595, 530392.9089317025,
0.0014598610433380044, 0.11484240207592301, 11.959688127956882,
2132.0874753402027, 754758.9662704318, 0.0012985240015312626,
0.11513579480243862, 13.514425358573531, 2715.5160990137824,
1083490.9235064993, 0.0011614735761289934, 0.11537304189548002,
15.171418602667567, 3415.195870828736, 1526592.554260445,
0.0010347472698811352, 0.11554677847006009, 17.080800985009617,
4322.412404600832, 2172012.2333119176, 0.0009232988811258664,
0.1157201264344419, 19.20004861829407, 5453.349531598553,
3075689.135821584, 0.0008228871862975205, 0.11602709326795038,
21.65735242414206, 6920.203923780365, 4390869.389638642,
0.00073528900066722, 0.11642075843897651, 24.40223571298994,
8755.811207598026, 6238515.485413593, 0.0006602764384729194,
0.11752920604817965, 27.694443541914293, 11171.386093291572,
8948280.260726549, 0.0005935538977939806, 0.11851292825953147,
31.325508920763063, 14174.185724149384, 12735505.873148222,
0.0005310755355633124, 0.11913794514470308, 35.381052949627765,
17987.010118815077, 18157886.71494382, 0.00047239949671590953,
0.1190446731724092, 39.71342528048061, 22679.438775422022,
25718483.571328573, 0.00041829129789387623, 0.11851586773659825,
44.45299332965028, 28542.57147989741, 36391778.63686921,
0.00037321512015419886, 0.11880681324908665, 50.0668539579632,
36118.26128449941, 51739409.29004541, 0.0003315539616702064,
0.1184752823034871, 56.04387059062639, 45383.29960621684,
72976345.76679668, 0.00029456064937920213, 0.11831519416731286,
62.91195073220101, 57265.53993693082, 103507463.43600245,
0.00026301867496859703, 0.11862142241083726, 70.8217262087034,
72383.14781936012, 146901598.49939138, 0.00023618734450420032,
0.11966825454879482, 80.26535457124461, 92160.51176984518,
210125966.835247, 0.00021165918071578316, 0.12043407382728061,
90.7169587544247, 116975.56852918258, 299515943.218972,
0.00018757727511329545, 0.11992440455576689, 101.49899864101785,
147056.26174166967, 423080865.0307836, 0.00016654469159895833,
0.11957908856805206, 113.65970431102812, 184937.67016486943,
597533612.3026931, 0.00014717439179415048, 0.11872067604728138,
126.77899683346702, 231758.58906776624, 841283678.3159915,
0.00012868496382376066, 0.1166314722122684, 139.93635237349534,
287417.30847929465, 1172231492.6328032, 0.00011225559452625302,
0.11427619522772557, 154.0034283704458, 355281.4912295324,
1627544511.322488, 9.879511142981067e-05, 0.11295574406808354,
170.96532050841535, 442971.0111288653, 2279085852.2580123,
8.71257780313587e-05, 0.11192758284428547, 190.35067416684697,
554165.2523674504, 3203629323.93623, 7.665069027765277e-05,
0.11060694607065294, 211.28835951100046, 690933.608546013,
4486577387.093535, 6.734021094824451e-05, 0.10915848194710433,
234.24338803525194, 860487.9079859136, 6276829044.8032465,
5.9191625040287665e-05, 0.10776821865668373, 259.7454711820425,
1071699.0387579766, 8780430224.544102, 5.1856803674907676e-05,
0.10606444911641115, 287.1843540288165, 1331126.3723998806,
12251687131.5685, 4.503421404759231e-05, 0.10347361247668461,
314.7338642485931, 1638796.0697522392, 16944331963.203278,
3.90470387455642e-05, 0.1007804070023012, 344.3427560918527,
2014064.4865519698, 23392351979.057854, 3.46557661636393e-05,
0.10046706610839032, 385.56603915081587, 2533036.2523656,
33044724430.235435, 3.148745865254635e-05, 0.1025441570117926,
442.09038234164746, 3262712.3882769793, 47815050050.199135,
2.9790762078715404e-05, 0.1089845379379672, 527.8068231298969,
4375751.903321453, 72035815708.42941, 2.8772639817606534e-05,
0.11823636789048445, 643.2048194503195, 5989838.001888927,
110764084330.93005, 2.7951691815106586e-05, 0.12903432664913705,
788.5500418523591, 8249371.000613411, 171368308481.2427,
2.6844392423114212e-05, 0.1392060709754626, 955.6296403631383,
11230229.319931043, 262063016295.25085, 2.499458273851386e-05,
0.14559344445184325, 1122.7022399726002, 14820229.698461473,
388475270970.9214, 2.337386729019776e-05, 0.15294300496886065,
1324.8158105672455, 19644861.137128454, 578442936182.7473,
2.0081014872174113e-05, 0.14760215298210377, 1436.2385042492353,
23923681.729276657, 791311658718.4193, 1.773374462991839e-05,
0.14642752940923615, 1600.5596278736678, 29949429.82503553,
1112815989293.9326, 1.5303115839590797e-05, 0.14194150045081785,
1742.873058605698, 36634451.931305364, 1529085389160.7544,
1.3148448731163076e-05, 0.13699368732998807, 1889.5284359054356,
44614279.74469635, 2091762812969.9607, 1.1739194407590062e-05,
0.13739553134643406, 2128.794599579694, 56462810.11822766,
2973783283306.8145, 1.0293367506254706e-05, 0.13533033372723272,
2355.372854690074, 70176508.28667311, 4151852759764.441,
9.678312586863569e-06, 0.14293577249119244, 2794.531827932675,
93528671.31952812, 6215821967224.52, -1.174086323572049e-05,
0.1429501325944908, 3139.4804810720925, 118031680.16618933,
-6466892421886.174, -2.1188265307407812e-05, 0.1477108290912869,
3644.1133424610953, 153900132.62392554, -4828013117542.036,
-8.614483025123122e-05, 0.16037100755883044, 4444.386620899393,
210846007.89660168, -1766340937974.433, 4.981445776141726e-05,
0.16053420251962536, 4997.558254401547, 266327328.4755411,
3862250287024.725, 1.8500019169456637e-05, 0.15448417164977674,
5402.289867444643, 323399508.1475582, 12152445411933.408,
-5.647882376069748e-05, 0.1406372975946189, 5524.633133597753,
371512945.9909363, -4162951345292.1514, 2.8048523486337994e-05,
0.13183417571186926, 5817.462495763679, 439447252.3728975,
9294740538175.03]).reshape(89, 5)
b = np.ones(89, dtype=np.float64)
sol, rnorm = nnls(A, b)
assert_allclose(sol, np.array([0.61124315, 8.22262829, 0., 0., 0.]))
assert_allclose(rnorm, 1.0556460808977297)
def test_nnls_gh21021_ex1(self):
# Review examples used in gh-21021
A = [[0.004734199143798789, -0.09661916455815653, -0.04308779048103441,
0.4039475561867938, -0.27742598780954364, -0.20816924034369574,
-0.17264070902176, 0.05251808558963846],
[-0.030263548855047975, -0.30356483926431466, 0.18080406600591398,
-0.06892233941254086, -0.41837298885432317, 0.30245352819647003,
-0.19008975278116397, -0.00990809825429995],
[-0.2561747595787612, -0.04376282125249583, 0.4422181991706678,
-0.13720906318924858, -0.0069523811763796475, -0.059238287107464795,
0.028663214369642594, 0.5415531284893763],
[0.2949336072968401, 0.33997647534935094, 0.38441519339815755,
-0.306001783010386, 0.18120773805949028, -0.36669767490747895,
-0.021539960590992304, -0.2784251712424615],
[0.5009075736232653, -0.20161970347571165, 0.08404512586550646,
0.2520496489348788, 0.14812015101612894, -0.25823455803981266,
-0.1596872058396596, 0.5960141613922691]
]
b = [18.036779281222124, -18.126530733870887, 13.535652034584029,
-2.6654275476795966, 9.166315328199575]
# Obtained from matlab's lstnonneg
des_sol = np.array([0., 118.017802006619, 45.1996532316584, 102.62156313537,
0., 55.8590204314398, 0., 29.7328833253434])
sol, res = nnls(A, b)
assert_allclose(sol, des_sol)
assert np.abs(np.linalg.norm(A@sol - b) - res) < 5e-14
def test_nnls_gh21021_ex2(self):
A = np.array([
[0.2508259992635229, -0.24031300195203256],
[0.510647748500133, 0.2872936081767836],
[0.8196387904102849, -0.03520620107046682],
[0.030739759120097084, -0.07768656359879388]])
b = np.array([24.456141951303913,
28.047143273432333,
41.10526799545987,
-1.2078282698324068])
sol, res = nnls(A, b)
assert_allclose(sol, np.array([54.3047953202271, 0.0]))
assert np.abs(np.linalg.norm(A@sol - b) - res) < 5e-14
def test_nnls_gh21021_ex3(self):
A = np.array([
[0.08247592017366788, 0.058398241636675674, -0.1031496693415968,
0.03156983127072098, -0.029503680182026665],
[0.21463607509982277, -0.2164518969308173, -0.10816833396662294,
0.12133867146012027, -0.15025010408668332],
[0.07251900316494089, -0.003044559315020767, 0.042682817961676424,
-0.018157525489298176, 0.11561953260568134],
[0.2328797918159187, -0.09112909645892767, 0.21348169727099078,
0.00449447624089599, -0.16615256386885716],
[-0.02440856024843897, -0.20131427208575386, 0.030275781997161483,
-0.04560777213546784, 0.11007266012013553],
[-0.2928391429686263, -0.20437574856615687, -0.020892110811574407,
-0.10455040720819309, 0.05337267000160461],
[0.22041503019400316, 0.014262782992311842, 0.08274606359871121,
-0.17933172096518907, -0.11809690350702161],
[0.10440436007469953, 0.09171452270577712, 0.03942347724809893,
0.11457669688231396, 0.07529747295631585],
[-0.052087576116032056, -0.15787717158077047, -0.08232202515883282,
-0.03194837933710708, -0.0546812506025729],
[-0.010388407673304468, 0.015174707581808923, 0.04764509565386281,
-0.1781221936030805, 0.10218894080536609],
[0.03272263140115928, -0.27576456949442574, 0.024897570959901753,
-0.1417129166632282, -0.03320796462136591],
[-0.12490006751823997, -0.03012003515442302, -0.051495264012509506,
0.012070729698374614, 0.04811700123118234],
[0.15254854117990788, -0.051863547789218374, 0.058012914127346174,
-0.06717991061422621, -0.14514671564242257],
[0.12251250415395559, -0.17462495626695362, -0.025334728552179834,
0.11425350676877533, 0.06183915953812639],
[0.19334259720491218, 0.2164301986218955, -0.018882278726614483,
0.07950236716817938, -0.2220529357431092],
[-0.01822205701890852, 0.12630444976752267, -0.03118092027244001,
0.02773743885242581, 0.06444433740044248],
[0.13344116850581977, -0.05142877469996826, 0.3385702016705455,
-0.25814970787123004, 0.2679034842977378],
[0.1309747058619377, 0.12090608957940627, -0.13957978654106512,
0.17048819760322642, -0.241775259969348],
[0.28613102173467275, -0.47153463906732174, 0.20359970518269746,
-0.0962095202871843, -0.07703076550836387],
[0.2212788380372723, 0.02569245145758152, -0.021596152392209966,
0.04610005150029433, -0.2024454395619734],
[-0.043225338359410316, 0.17816095186290315, -0.014709092962616079,
0.06993970293287989, -0.09033722782555903],
[0.17747622942563512, -0.20991014784011458, 0.06265720409894943,
0.0689704059061795, 0.024474319398401525],
[-0.1163880385601698, 0.29989570587630027, 0.033443765320984545,
0.008470296514656, -0.0014457113271462002],
[0.024375314902718406, 0.05279830705548363, 0.02691082431023144,
0.05265079368002343, 0.15542988147487913],
[-0.01855218360922308, -0.050265869142888164, 0.2567912677240452,
-0.2606428528561333, 0.25334396245022245]])
b = np.array([-7.876625373734849, -8.259856278691373, 3.2593082374900963,
16.30170376973345, 2.311892943629045, -1.595345202555738,
6.318582970536518, 3.0104212955340093, -6.286202915842167,
3.6382333725029294, 1.9012066681249356, -3.932236581436514,
4.4299317131740406, -1.9345885161292682, -1.4418721521970805,
-2.3810103256943926, 25.853603392922526, -10.658470311610483,
15.547103681119214, -1.6491066136547277, -1.1232029689817422,
4.7845749463206975, 2.553803732013229, 2.0549409701753705,
19.60887153608244])
sol, res = nnls(A, b)
assert_allclose(sol, np.array([0.0, 0.0, 76.3611306173957, 0.0, 0.0]),
atol=5e-14)
assert np.abs(np.linalg.norm(A@sol - b) - res) < 5e-14
def test_atol_deprecation_warning(self):
"""Test that using atol parameter triggers deprecation warning"""
a = np.array([[1, 0], [1, 0], [0, 1]])
b = np.array([2, 1, 1])
with pytest.warns(DeprecationWarning, match="{'atol'}"):
nnls(a, b, atol=1e-8)
def test_2D_singleton_RHS_input(self):
# Test that a 2D singleton RHS input is accepted
A = np.array([[1.0, 0.5, -1.],
[1.0, 0.5, 0.0],
[-1., 0.0, 1.0]])
b = np.array([[-1.0, 2.0, 2.0]]).T
x, r = nnls(A, b)
assert_allclose(x, np.array([1.0, 2.0, 3.0]))
assert_allclose(r, 0.0)
def test_2D_not_singleton_RHS_input_2(self):
# Test that a 2D but not a column vector RHS input is rejected
A = np.array([[1.0, 0.5, -1.],
[1.0, 0.5, 0.0],
[1.0, 0.5, 0.0],
[0.0, 0.0, 1.0]])
b = np.ones(shape=[4, 2], dtype=np.float64)
with pytest.raises(ValueError, match="Expected a 1D array"):
nnls(A, b)
def test_gh_22791_32bit(self):
# Scikit-learn got hit by this problem on 32-bit arch.
desired = [0, 0, 1.05617285, 0, 0, 0, 0, 0.23123048, 0, 0, 0, 0.26128651]
rng = np.random.RandomState(42)
n_samples, n_features = 5, 12
X = rng.randn(n_samples, n_features)
X[:2, :] = 0
y = rng.randn(n_samples)
coef, _ = nnls(X, y)
assert_allclose(coef, desired)

View file

@ -0,0 +1,572 @@
""" Unit tests for nonlinear solvers
Author: Ondrej Certik
May 2007
"""
from numpy.testing import assert_
import pytest
from functools import partial
from scipy.optimize import _nonlin as nonlin, root
from scipy.sparse import csr_array
from numpy import diag, dot
from numpy.linalg import inv
import numpy as np
import scipy
from scipy.sparse.linalg import minres
from .test_minpack import pressure_network
SOLVERS = {'anderson': nonlin.anderson,
'diagbroyden': nonlin.diagbroyden,
'linearmixing': nonlin.linearmixing,
'excitingmixing': nonlin.excitingmixing,
'broyden1': nonlin.broyden1,
'broyden2': nonlin.broyden2,
'krylov': nonlin.newton_krylov}
MUST_WORK = {'anderson': nonlin.anderson, 'broyden1': nonlin.broyden1,
'broyden2': nonlin.broyden2, 'krylov': nonlin.newton_krylov}
# ----------------------------------------------------------------------------
# Test problems
# ----------------------------------------------------------------------------
def F(x):
x = np.asarray(x).T
d = diag([3, 2, 1.5, 1, 0.5])
c = 0.01
f = -d @ x - c * float(x.T @ x) * x
return f
F.xin = [1, 1, 1, 1, 1]
F.KNOWN_BAD = {}
F.JAC_KSP_BAD = {}
F.ROOT_JAC_KSP_BAD = {}
def F2(x):
return x
F2.xin = [1, 2, 3, 4, 5, 6]
F2.KNOWN_BAD = {'linearmixing': nonlin.linearmixing,
'excitingmixing': nonlin.excitingmixing}
F2.JAC_KSP_BAD = {}
F2.ROOT_JAC_KSP_BAD = {}
def F2_lucky(x):
return x
F2_lucky.xin = [0, 0, 0, 0, 0, 0]
F2_lucky.KNOWN_BAD = {}
F2_lucky.JAC_KSP_BAD = {}
F2_lucky.ROOT_JAC_KSP_BAD = {}
def F3(x):
A = np.array([[-2, 1, 0.], [1, -2, 1], [0, 1, -2]])
b = np.array([1, 2, 3.])
return A @ x - b
F3.xin = [1, 2, 3]
F3.KNOWN_BAD = {}
F3.JAC_KSP_BAD = {}
F3.ROOT_JAC_KSP_BAD = {}
def F4_powell(x):
A = 1e4
return [A*x[0]*x[1] - 1, np.exp(-x[0]) + np.exp(-x[1]) - (1 + 1/A)]
F4_powell.xin = [-1, -2]
F4_powell.KNOWN_BAD = {'linearmixing': nonlin.linearmixing,
'excitingmixing': nonlin.excitingmixing,
'diagbroyden': nonlin.diagbroyden}
# In the extreme case, it does not converge for nolinear problem solved by
# MINRES and root problem solved by GMRES/BiCGStab/CGS/MINRES/TFQMR when using
# Krylov method to approximate Jacobian
F4_powell.JAC_KSP_BAD = {'minres'}
F4_powell.ROOT_JAC_KSP_BAD = {'gmres', 'bicgstab', 'cgs', 'minres', 'tfqmr'}
def F5(x):
return pressure_network(x, 4, np.array([.5, .5, .5, .5]))
F5.xin = [2., 0, 2, 0]
F5.KNOWN_BAD = {'excitingmixing': nonlin.excitingmixing,
'linearmixing': nonlin.linearmixing,
'diagbroyden': nonlin.diagbroyden}
# In the extreme case, the Jacobian inversion yielded zero vector for nonlinear
# problem solved by CGS/MINRES and it does not converge for root problem solved
# by MINRES and when using Krylov method to approximate Jacobian
F5.JAC_KSP_BAD = {'cgs', 'minres'}
F5.ROOT_JAC_KSP_BAD = {'minres'}
def F6(x):
x1, x2 = x
J0 = np.array([[-4.256, 14.7],
[0.8394989, 0.59964207]])
v = np.array([(x1 + 3) * (x2**5 - 7) + 3*6,
np.sin(x2 * np.exp(x1) - 1)])
return -np.linalg.solve(J0, v)
F6.xin = [-0.5, 1.4]
F6.KNOWN_BAD = {'excitingmixing': nonlin.excitingmixing,
'linearmixing': nonlin.linearmixing,
'diagbroyden': nonlin.diagbroyden}
F6.JAC_KSP_BAD = {}
F6.ROOT_JAC_KSP_BAD = {}
# ----------------------------------------------------------------------------
# Tests
# ----------------------------------------------------------------------------
class TestNonlin:
"""
Check the Broyden methods for a few test problems.
broyden1, broyden2, and newton_krylov must succeed for
all functions. Some of the others don't -- tests in KNOWN_BAD are skipped.
"""
def _check_nonlin_func(self, f, func, f_tol=1e-2):
# Test all methods mentioned in the class `KrylovJacobian`
if func == SOLVERS['krylov']:
for method in ['gmres', 'bicgstab', 'cgs', 'minres', 'tfqmr']:
if method in f.JAC_KSP_BAD:
continue
x = func(f, f.xin, method=method, line_search=None,
f_tol=f_tol, maxiter=200, verbose=0)
assert_(np.absolute(f(x)).max() < f_tol)
x = func(f, f.xin, f_tol=f_tol, maxiter=200, verbose=0)
assert_(np.absolute(f(x)).max() < f_tol)
def _check_root(self, f, method, f_tol=1e-2):
# Test Krylov methods
if method == 'krylov':
for jac_method in ['gmres', 'bicgstab', 'cgs', 'minres', 'tfqmr']:
if jac_method in f.ROOT_JAC_KSP_BAD:
continue
res = root(f, f.xin, method=method,
options={'ftol': f_tol, 'maxiter': 200,
'disp': 0,
'jac_options': {'method': jac_method}})
assert_(np.absolute(res.fun).max() < f_tol)
res = root(f, f.xin, method=method,
options={'ftol': f_tol, 'maxiter': 200, 'disp': 0})
assert_(np.absolute(res.fun).max() < f_tol)
@pytest.mark.xfail
def _check_func_fail(self, *a, **kw):
pass
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
def test_problem_nonlin(self):
for f in [F, F2, F2_lucky, F3, F4_powell, F5, F6]:
for func in SOLVERS.values():
if func in f.KNOWN_BAD.values():
if func in MUST_WORK.values():
self._check_func_fail(f, func)
continue
self._check_nonlin_func(f, func)
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
@pytest.mark.parametrize("method", ['lgmres', 'gmres', 'bicgstab', 'cgs',
'minres', 'tfqmr'])
def test_tol_norm_called(self, method):
# Check that supplying tol_norm keyword to nonlin_solve works
self._tol_norm_used = False
def local_norm_func(x):
self._tol_norm_used = True
return np.absolute(x).max()
nonlin.newton_krylov(F, F.xin, method=method, f_tol=1e-2,
maxiter=200, verbose=0,
tol_norm=local_norm_func)
assert_(self._tol_norm_used)
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
def test_problem_root(self):
for f in [F, F2, F2_lucky, F3, F4_powell, F5, F6]:
for meth in SOLVERS:
if meth in f.KNOWN_BAD:
if meth in MUST_WORK:
self._check_func_fail(f, meth)
continue
self._check_root(f, meth)
def test_no_convergence(self):
def wont_converge(x):
return 1e3 + x
with pytest.raises(scipy.optimize.NoConvergence):
nonlin.newton_krylov(wont_converge, xin=[0], maxiter=1)
def test_warnings_invalid_inner_param(self):
"""
Test for ENH #21986, for behavior of `nonlin.newton_krylov`
Test the following scenarios:
1. Raise warning for invalid inner param
2. No warning for valid inner param
3. No warning for user-provided callable method
"""
# This should raise exactly one warning
# (`inner_atol` is not valid for `minres`)
with pytest.warns(UserWarning,
match="Please check inner method documentation"):
nonlin.newton_krylov(F, F.xin, method="minres", inner_atol=1e-5)
# This should not raise a warning (`minres` without `inner_atol`,
# but with `inner_maxiter` which is valid)
nonlin.newton_krylov(F, F.xin, method="minres", inner_maxiter=100,
inner_callback= lambda _ : ...)
# Test newton_krylov with a user-provided callable method
def user_provided_callable_method_enh_21986(op, rhs, **kwargs):
"""A dummy user-provided callable method for testing."""
# Return a dummy result (mimicking minres)
return minres(op, rhs, **kwargs)
# This should not raise any warnings
nonlin.newton_krylov(F, F.xin,
method=user_provided_callable_method_enh_21986)
def test_non_inner_prefix(self):
with pytest.raises(ValueError,
match="Unknown parameter"
):
# Pass a parameter without 'inner_' prefix
nonlin.newton_krylov(F, F.xin, method="minres", invalid_param=1e-5)
class TestSecant:
"""Check that some Jacobian approximations satisfy the secant condition"""
xs = [np.array([1., 2., 3., 4., 5.]),
np.array([2., 3., 4., 5., 1.]),
np.array([3., 4., 5., 1., 2.]),
np.array([4., 5., 1., 2., 3.]),
np.array([9., 1., 9., 1., 3.]),
np.array([0., 1., 9., 1., 3.]),
np.array([5., 5., 7., 1., 1.]),
np.array([1., 2., 7., 5., 1.]),]
fs = [x**2 - 1 for x in xs]
def _check_secant(self, jac_cls, npoints=1, **kw):
"""
Check that the given Jacobian approximation satisfies secant
conditions for last `npoints` points.
"""
jac = jac_cls(**kw)
jac.setup(self.xs[0], self.fs[0], None)
for j, (x, f) in enumerate(zip(self.xs[1:], self.fs[1:])):
jac.update(x, f)
for k in range(min(npoints, j+1)):
dx = self.xs[j-k+1] - self.xs[j-k]
df = self.fs[j-k+1] - self.fs[j-k]
assert_(np.allclose(dx, jac.solve(df)))
# Check that the `npoints` secant bound is strict
if j >= npoints:
dx = self.xs[j-npoints+1] - self.xs[j-npoints]
df = self.fs[j-npoints+1] - self.fs[j-npoints]
assert_(not np.allclose(dx, jac.solve(df)))
def test_broyden1(self):
self._check_secant(nonlin.BroydenFirst)
def test_broyden2(self):
self._check_secant(nonlin.BroydenSecond)
def test_broyden1_update(self):
# Check that BroydenFirst update works as for a dense matrix
jac = nonlin.BroydenFirst(alpha=0.1)
jac.setup(self.xs[0], self.fs[0], None)
B = np.identity(5) * (-1/0.1)
for last_j, (x, f) in enumerate(zip(self.xs[1:], self.fs[1:])):
df = f - self.fs[last_j]
dx = x - self.xs[last_j]
B += (df - dot(B, dx))[:, None] * dx[None, :] / dot(dx, dx)
jac.update(x, f)
assert_(np.allclose(jac.todense(), B, rtol=1e-10, atol=1e-13))
def test_broyden2_update(self):
# Check that BroydenSecond update works as for a dense matrix
jac = nonlin.BroydenSecond(alpha=0.1)
jac.setup(self.xs[0], self.fs[0], None)
H = np.identity(5) * (-0.1)
for last_j, (x, f) in enumerate(zip(self.xs[1:], self.fs[1:])):
df = f - self.fs[last_j]
dx = x - self.xs[last_j]
H += (dx - dot(H, df))[:, None] * df[None, :] / dot(df, df)
jac.update(x, f)
assert_(np.allclose(jac.todense(), inv(H), rtol=1e-10, atol=1e-13))
def test_anderson(self):
# Anderson mixing (with w0=0) satisfies secant conditions
# for the last M iterates, see [Ey]_
#
# .. [Ey] V. Eyert, J. Comp. Phys., 124, 271 (1996).
self._check_secant(nonlin.Anderson, M=3, w0=0, npoints=3)
class TestLinear:
"""Solve a linear equation;
some methods find the exact solution in a finite number of steps"""
def _check(self, jac, N, maxiter, complex=False, **kw):
np.random.seed(123)
A = np.random.randn(N, N)
if complex:
A = A + 1j*np.random.randn(N, N)
b = np.random.randn(N)
if complex:
b = b + 1j*np.random.randn(N)
def func(x):
return dot(A, x) - b
sol = nonlin.nonlin_solve(func, np.zeros(N), jac, maxiter=maxiter,
f_tol=1e-6, line_search=None, verbose=0)
assert_(np.allclose(dot(A, sol), b, atol=1e-6))
def test_broyden1(self):
# Broyden methods solve linear systems exactly in 2*N steps
self._check(nonlin.BroydenFirst(alpha=1.0), 20, 41, False)
self._check(nonlin.BroydenFirst(alpha=1.0), 20, 41, True)
def test_broyden2(self):
# Broyden methods solve linear systems exactly in 2*N steps
self._check(nonlin.BroydenSecond(alpha=1.0), 20, 41, False)
self._check(nonlin.BroydenSecond(alpha=1.0), 20, 41, True)
def test_anderson(self):
# Anderson is rather similar to Broyden, if given enough storage space
self._check(nonlin.Anderson(M=50, alpha=1.0), 20, 29, False)
self._check(nonlin.Anderson(M=50, alpha=1.0), 20, 29, True)
def test_krylov(self):
# Krylov methods solve linear systems exactly in N inner steps
self._check(nonlin.KrylovJacobian, 20, 2, False, inner_m=10)
self._check(nonlin.KrylovJacobian, 20, 2, True, inner_m=10)
def _check_autojac(self, A, b):
def func(x):
return A.dot(x) - b
def jac(v):
return A
sol = nonlin.nonlin_solve(func, np.zeros(b.shape[0]), jac, maxiter=2,
f_tol=1e-6, line_search=None, verbose=0)
np.testing.assert_allclose(A @ sol, b, atol=1e-6)
# test jac input as array -- not a function
sol = nonlin.nonlin_solve(func, np.zeros(b.shape[0]), A, maxiter=2,
f_tol=1e-6, line_search=None, verbose=0)
np.testing.assert_allclose(A @ sol, b, atol=1e-6)
def test_jac_sparse(self):
A = csr_array([[1, 2], [2, 1]])
b = np.array([1, -1])
self._check_autojac(A, b)
self._check_autojac((1 + 2j) * A, (2 + 2j) * b)
def test_jac_ndarray(self):
A = np.array([[1, 2], [2, 1]])
b = np.array([1, -1])
self._check_autojac(A, b)
self._check_autojac((1 + 2j) * A, (2 + 2j) * b)
class TestJacobianDotSolve:
"""
Check that solve/dot methods in Jacobian approximations are consistent
"""
def _func(self, x, A=None):
return x**2 - 1 + np.dot(A, x)
def _check_dot(self, jac_cls, complex=False, tol=1e-6, **kw):
rng = np.random.RandomState(123)
N = 7
def rand(*a):
q = rng.rand(*a)
if complex:
q = q + 1j*rng.rand(*a)
return q
def assert_close(a, b, msg):
d = abs(a - b).max()
f = tol + abs(b).max()*tol
if d > f:
raise AssertionError(f'{msg}: err {d:g}')
A = rand(N, N)
# initialize
x0 = rng.rand(N)
jac = jac_cls(**kw)
jac.setup(x0, self._func(x0, A), partial(self._func, A=A))
# check consistency
for k in range(2*N):
v = rand(N)
if hasattr(jac, '__array__'):
Jd = np.array(jac)
if hasattr(jac, 'solve'):
Gv = jac.solve(v)
Gv2 = np.linalg.solve(Jd, v)
assert_close(Gv, Gv2, 'solve vs array')
if hasattr(jac, 'rsolve'):
Gv = jac.rsolve(v)
Gv2 = np.linalg.solve(Jd.T.conj(), v)
assert_close(Gv, Gv2, 'rsolve vs array')
if hasattr(jac, 'matvec'):
Jv = jac.matvec(v)
Jv2 = np.dot(Jd, v)
assert_close(Jv, Jv2, 'dot vs array')
if hasattr(jac, 'rmatvec'):
Jv = jac.rmatvec(v)
Jv2 = np.dot(Jd.T.conj(), v)
assert_close(Jv, Jv2, 'rmatvec vs array')
if hasattr(jac, 'matvec') and hasattr(jac, 'solve'):
Jv = jac.matvec(v)
Jv2 = jac.solve(jac.matvec(Jv))
assert_close(Jv, Jv2, 'dot vs solve')
if hasattr(jac, 'rmatvec') and hasattr(jac, 'rsolve'):
Jv = jac.rmatvec(v)
Jv2 = jac.rmatvec(jac.rsolve(Jv))
assert_close(Jv, Jv2, 'rmatvec vs rsolve')
x = rand(N)
jac.update(x, self._func(x, A))
def test_broyden1(self):
self._check_dot(nonlin.BroydenFirst, complex=False)
self._check_dot(nonlin.BroydenFirst, complex=True)
def test_broyden2(self):
self._check_dot(nonlin.BroydenSecond, complex=False)
self._check_dot(nonlin.BroydenSecond, complex=True)
def test_anderson(self):
self._check_dot(nonlin.Anderson, complex=False)
self._check_dot(nonlin.Anderson, complex=True)
def test_diagbroyden(self):
self._check_dot(nonlin.DiagBroyden, complex=False)
self._check_dot(nonlin.DiagBroyden, complex=True)
def test_linearmixing(self):
self._check_dot(nonlin.LinearMixing, complex=False)
self._check_dot(nonlin.LinearMixing, complex=True)
def test_excitingmixing(self):
self._check_dot(nonlin.ExcitingMixing, complex=False)
self._check_dot(nonlin.ExcitingMixing, complex=True)
@pytest.mark.thread_unsafe
def test_krylov(self):
self._check_dot(nonlin.KrylovJacobian, complex=False, tol=1e-3)
self._check_dot(nonlin.KrylovJacobian, complex=True, tol=1e-3)
class TestNonlinOldTests:
""" Test case for a simple constrained entropy maximization problem
(the machine translation example of Berger et al in
Computational Linguistics, vol 22, num 1, pp 39--72, 1996.)
"""
def test_broyden1(self):
x = nonlin.broyden1(F, F.xin, iter=12, alpha=1)
assert_(nonlin.norm(x) < 1e-9)
assert_(nonlin.norm(F(x)) < 1e-9)
def test_broyden2(self):
x = nonlin.broyden2(F, F.xin, iter=12, alpha=1)
assert_(nonlin.norm(x) < 1e-9)
assert_(nonlin.norm(F(x)) < 1e-9)
def test_anderson(self):
x = nonlin.anderson(F, F.xin, iter=12, alpha=0.03, M=5)
assert_(nonlin.norm(x) < 0.33)
def test_linearmixing(self):
x = nonlin.linearmixing(F, F.xin, iter=60, alpha=0.5)
assert_(nonlin.norm(x) < 1e-7)
assert_(nonlin.norm(F(x)) < 1e-7)
def test_exciting(self):
x = nonlin.excitingmixing(F, F.xin, iter=20, alpha=0.5)
assert_(nonlin.norm(x) < 1e-5)
assert_(nonlin.norm(F(x)) < 1e-5)
def test_diagbroyden(self):
x = nonlin.diagbroyden(F, F.xin, iter=11, alpha=1)
assert_(nonlin.norm(x) < 1e-8)
assert_(nonlin.norm(F(x)) < 1e-8)
def test_root_broyden1(self):
res = root(F, F.xin, method='broyden1',
options={'nit': 12, 'jac_options': {'alpha': 1}})
assert_(nonlin.norm(res.x) < 1e-9)
assert_(nonlin.norm(res.fun) < 1e-9)
def test_root_broyden2(self):
res = root(F, F.xin, method='broyden2',
options={'nit': 12, 'jac_options': {'alpha': 1}})
assert_(nonlin.norm(res.x) < 1e-9)
assert_(nonlin.norm(res.fun) < 1e-9)
def test_root_anderson(self):
res = root(F, F.xin, method='anderson',
options={'nit': 12,
'jac_options': {'alpha': 0.03, 'M': 5}})
assert_(nonlin.norm(res.x) < 0.33)
def test_root_linearmixing(self):
res = root(F, F.xin, method='linearmixing',
options={'nit': 60,
'jac_options': {'alpha': 0.5}})
assert_(nonlin.norm(res.x) < 1e-7)
assert_(nonlin.norm(res.fun) < 1e-7)
def test_root_excitingmixing(self):
res = root(F, F.xin, method='excitingmixing',
options={'nit': 20,
'jac_options': {'alpha': 0.5}})
assert_(nonlin.norm(res.x) < 1e-5)
assert_(nonlin.norm(res.fun) < 1e-5)
def test_root_diagbroyden(self):
res = root(F, F.xin, method='diagbroyden',
options={'nit': 11,
'jac_options': {'alpha': 1}})
assert_(nonlin.norm(res.x) < 1e-8)
assert_(nonlin.norm(res.fun) < 1e-8)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,455 @@
import pytest
import numpy as np
from numpy.random import default_rng
from scipy.optimize import quadratic_assignment, OptimizeWarning
from scipy.optimize._qap import _calc_score as _score
from numpy.testing import assert_equal, assert_, assert_warns
################
# Common Tests #
################
def chr12c():
A = [
[0, 90, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[90, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0],
[10, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0],
[0, 23, 0, 0, 0, 88, 0, 0, 0, 0, 0, 0],
[0, 0, 43, 0, 0, 0, 26, 0, 0, 0, 0, 0],
[0, 0, 0, 88, 0, 0, 0, 16, 0, 0, 0, 0],
[0, 0, 0, 0, 26, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 16, 0, 0, 0, 96, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 0],
[0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 37],
[0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0],
]
B = [
[0, 36, 54, 26, 59, 72, 9, 34, 79, 17, 46, 95],
[36, 0, 73, 35, 90, 58, 30, 78, 35, 44, 79, 36],
[54, 73, 0, 21, 10, 97, 58, 66, 69, 61, 54, 63],
[26, 35, 21, 0, 93, 12, 46, 40, 37, 48, 68, 85],
[59, 90, 10, 93, 0, 64, 5, 29, 76, 16, 5, 76],
[72, 58, 97, 12, 64, 0, 96, 55, 38, 54, 0, 34],
[9, 30, 58, 46, 5, 96, 0, 83, 35, 11, 56, 37],
[34, 78, 66, 40, 29, 55, 83, 0, 44, 12, 15, 80],
[79, 35, 69, 37, 76, 38, 35, 44, 0, 64, 39, 33],
[17, 44, 61, 48, 16, 54, 11, 12, 64, 0, 70, 86],
[46, 79, 54, 68, 5, 0, 56, 15, 39, 70, 0, 18],
[95, 36, 63, 85, 76, 34, 37, 80, 33, 86, 18, 0],
]
A, B = np.array(A), np.array(B)
n = A.shape[0]
opt_perm = np.array([7, 5, 1, 3, 10, 4, 8, 6, 9, 11, 2, 12]) - [1] * n
return A, B, opt_perm
@pytest.mark.filterwarnings("ignore:The NumPy global RNG was seeded by calling")
class QAPCommonTests:
"""
Base class for `quadratic_assignment` tests.
"""
# Test global optima of problem from Umeyama IVB
# https://pcl.sitehost.iu.edu/rgoldsto/papers/weighted%20graph%20match2.pdf
# Graph matching maximum is in the paper
# QAP minimum determined by brute force
def test_accuracy_1(self):
# besides testing accuracy, check that A and B can be lists
rng = np.random.default_rng(4358764578823597324)
A = [[0, 3, 4, 2],
[0, 0, 1, 2],
[1, 0, 0, 1],
[0, 0, 1, 0]]
B = [[0, 4, 2, 4],
[0, 0, 1, 0],
[0, 2, 0, 2],
[0, 1, 2, 0]]
res = quadratic_assignment(A, B, method=self.method,
options={"rng": rng, "maximize": False})
assert_equal(res.fun, 10)
assert_equal(res.col_ind, np.array([1, 2, 3, 0]))
res = quadratic_assignment(A, B, method=self.method,
options={"rng": rng, "maximize": True})
if self.method == 'faq':
# Global optimum is 40, but FAQ gets 37
assert_equal(res.fun, 37)
assert_equal(res.col_ind, np.array([0, 2, 3, 1]))
else:
assert_equal(res.fun, 40)
assert_equal(res.col_ind, np.array([0, 3, 1, 2]))
quadratic_assignment(A, B, method=self.method,
options={"rng": rng, "maximize": True})
# Test global optima of problem from Umeyama IIIB
# https://pcl.sitehost.iu.edu/rgoldsto/papers/weighted%20graph%20match2.pdf
# Graph matching maximum is in the paper
# QAP minimum determined by brute force
def test_accuracy_2(self):
rng = np.random.default_rng(4358764578823597324)
A = np.array([[0, 5, 8, 6],
[5, 0, 5, 1],
[8, 5, 0, 2],
[6, 1, 2, 0]])
B = np.array([[0, 1, 8, 4],
[1, 0, 5, 2],
[8, 5, 0, 5],
[4, 2, 5, 0]])
res = quadratic_assignment(A, B, method=self.method,
options={"rng": rng, "maximize": False})
if self.method == 'faq':
# Global optimum is 176, but FAQ gets 178
assert_equal(res.fun, 178)
assert_equal(res.col_ind, np.array([1, 0, 3, 2]))
else:
assert_equal(res.fun, 176)
assert_equal(res.col_ind, np.array([1, 2, 3, 0]))
res = quadratic_assignment(A, B, method=self.method,
options={"rng": rng, "maximize": True})
assert_equal(res.fun, 286)
assert_equal(res.col_ind, np.array([2, 3, 0, 1]))
def test_accuracy_3(self):
rng = np.random.default_rng(4358764578823597324)
A, B, opt_perm = chr12c()
# basic minimization
res = quadratic_assignment(A, B, method=self.method,
options={"rng": rng})
assert_(11156 <= res.fun < 21000)
assert_equal(res.fun, _score(A, B, res.col_ind))
# basic maximization
res = quadratic_assignment(A, B, method=self.method,
options={"rng": rng, 'maximize': True})
assert_(74000 <= res.fun < 85000)
assert_equal(res.fun, _score(A, B, res.col_ind))
# check ofv with strictly partial match
seed_cost = np.array([4, 8, 10])
seed = np.asarray([seed_cost, opt_perm[seed_cost]]).T
res = quadratic_assignment(A, B, method=self.method,
options={'partial_match': seed, "rng": rng})
assert_(11156 <= res.fun < 21000)
assert_equal(res.col_ind[seed_cost], opt_perm[seed_cost])
# check performance when partial match is the global optimum
seed = np.asarray([np.arange(len(A)), opt_perm]).T
res = quadratic_assignment(A, B, method=self.method,
options={'partial_match': seed, "rng": rng})
assert_equal(res.col_ind, seed[:, 1].T)
assert_equal(res.fun, 11156)
assert_equal(res.nit, 0)
# check performance with zero sized matrix inputs
empty = np.empty((0, 0))
res = quadratic_assignment(empty, empty, method=self.method,
options={"rng": rng})
assert_equal(res.nit, 0)
assert_equal(res.fun, 0)
@pytest.mark.thread_unsafe
def test_unknown_options(self):
A, B, opt_perm = chr12c()
def f():
quadratic_assignment(A, B, method=self.method,
options={"ekki-ekki": True})
assert_warns(OptimizeWarning, f)
@pytest.mark.thread_unsafe
def test_deprecation_future_warnings(self):
# May be removed after SPEC-7 transition is complete in SciPy 1.17
A = np.arange(16).reshape((4, 4))
B = np.arange(16).reshape((4, 4))
with pytest.warns(DeprecationWarning, match="Use of `RandomState`*"):
rng = np.random.RandomState(0)
quadratic_assignment(A, B, method=self.method,
options={"rng": rng, "maximize": False})
with pytest.warns(FutureWarning, match="The NumPy global RNG was seeded*"):
np.random.seed(0)
quadratic_assignment(A, B, method=self.method,
options={"maximize": False})
with pytest.warns(FutureWarning, match="The behavior when the rng option*"):
quadratic_assignment(A, B, method=self.method,
options={"rng": 0, "maximize": False})
class TestFAQ(QAPCommonTests):
method = "faq"
def test_options(self):
# cost and distance matrices of QAPLIB instance chr12c
rng = np.random.default_rng(4358764578823597324)
A, B, opt_perm = chr12c()
n = len(A)
# check that max_iter is obeying with low input value
res = quadratic_assignment(A, B, options={'maxiter': 5})
assert_equal(res.nit, 5)
# test with shuffle
res = quadratic_assignment(A, B, options={'shuffle_input': True})
assert_(11156 <= res.fun < 21000)
# test with randomized init
res = quadratic_assignment(A, B, options={'rng': rng, 'P0': "randomized"})
assert_(11156 <= res.fun < 21000)
# check with specified P0
K = np.ones((n, n)) / float(n)
K = _doubly_stochastic(K)
res = quadratic_assignment(A, B, options={'P0': K})
assert_(11156 <= res.fun < 21000)
def test_specific_input_validation(self):
A = np.identity(2)
B = A
# method is implicitly faq
# ValueError Checks: making sure single value parameters are of
# correct value
with pytest.raises(ValueError, match="Invalid 'P0' parameter"):
quadratic_assignment(A, B, options={'P0': "random"})
with pytest.raises(
ValueError, match="'maxiter' must be a positive integer"):
quadratic_assignment(A, B, options={'maxiter': -1})
with pytest.raises(ValueError, match="'tol' must be a positive float"):
quadratic_assignment(A, B, options={'tol': -1})
# TypeError Checks: making sure single value parameters are of
# correct type
with pytest.raises(TypeError):
quadratic_assignment(A, B, options={'maxiter': 1.5})
# test P0 matrix input
with pytest.raises(
ValueError,
match="`P0` matrix must have shape m' x m', where m'=n-m"):
quadratic_assignment(
np.identity(4), np.identity(4),
options={'P0': np.ones((3, 3))}
)
K = [[0.4, 0.2, 0.3],
[0.3, 0.6, 0.2],
[0.2, 0.2, 0.7]]
# matrix that isn't quite doubly stochastic
with pytest.raises(
ValueError, match="`P0` matrix must be doubly stochastic"):
quadratic_assignment(
np.identity(3), np.identity(3), options={'P0': K}
)
class Test2opt(QAPCommonTests):
method = "2opt"
def test_deterministic(self):
n = 20
rng = default_rng(51982908)
A = rng.random(size=(n, n))
B = rng.random(size=(n, n))
res1 = quadratic_assignment(A, B, method=self.method, options={'rng': rng})
rng = default_rng(51982908)
A = rng.random(size=(n, n))
B = rng.random(size=(n, n))
res2 = quadratic_assignment(A, B, method=self.method, options={'rng': rng})
assert_equal(res1.nit, res2.nit)
def test_partial_guess(self):
n = 5
rng = np.random.default_rng(4358764578823597324)
A = rng.random(size=(n, n))
B = rng.random(size=(n, n))
res1 = quadratic_assignment(A, B, method=self.method,
options={'rng': rng})
guess = np.array([np.arange(5), res1.col_ind]).T
res2 = quadratic_assignment(A, B, method=self.method,
options={'rng': rng, 'partial_guess': guess})
fix = [2, 4]
match = np.array([np.arange(5)[fix], res1.col_ind[fix]]).T
res3 = quadratic_assignment(A, B, method=self.method,
options={'rng': rng, 'partial_guess': guess,
'partial_match': match})
assert_(res1.nit != n*(n+1)/2)
assert_equal(res2.nit, n*(n+1)/2) # tests each swap exactly once
assert_equal(res3.nit, (n-2)*(n-1)/2) # tests free swaps exactly once
def test_specific_input_validation(self):
# can't have more seed nodes than cost/dist nodes
_rm = _range_matrix
with pytest.raises(
ValueError,
match="`partial_guess` can have only as many entries as"):
quadratic_assignment(np.identity(3), np.identity(3),
method=self.method,
options={'partial_guess': _rm(5, 2)})
# test for only two seed columns
with pytest.raises(
ValueError, match="`partial_guess` must have two columns"):
quadratic_assignment(
np.identity(3), np.identity(3), method=self.method,
options={'partial_guess': _range_matrix(2, 3)}
)
# test that seed has no more than two dimensions
with pytest.raises(
ValueError, match="`partial_guess` must have exactly two"):
quadratic_assignment(
np.identity(3), np.identity(3), method=self.method,
options={'partial_guess': np.random.rand(3, 2, 2)}
)
# seeds cannot be negative valued
with pytest.raises(
ValueError, match="`partial_guess` must contain only pos"):
quadratic_assignment(
np.identity(3), np.identity(3), method=self.method,
options={'partial_guess': -1 * _range_matrix(2, 2)}
)
# seeds can't have values greater than number of nodes
with pytest.raises(
ValueError,
match="`partial_guess` entries must be less than number"):
quadratic_assignment(
np.identity(5), np.identity(5), method=self.method,
options={'partial_guess': 2 * _range_matrix(4, 2)}
)
# columns of seed matrix must be unique
with pytest.raises(
ValueError,
match="`partial_guess` column entries must be unique"):
quadratic_assignment(
np.identity(3), np.identity(3), method=self.method,
options={'partial_guess': np.ones((2, 2))}
)
@pytest.mark.filterwarnings("ignore:The NumPy global RNG was seeded by calling")
class TestQAPOnce:
# these don't need to be repeated for each method
def test_common_input_validation(self):
rng = default_rng(12349038)
# test that non square matrices return error
with pytest.raises(ValueError, match="`A` must be square"):
quadratic_assignment(
rng.random((3, 4)),
rng.random((3, 3)),
)
with pytest.raises(ValueError, match="`B` must be square"):
quadratic_assignment(
rng.random((3, 3)),
rng.random((3, 4)),
)
# test that cost and dist matrices have no more than two dimensions
with pytest.raises(
ValueError, match="`A` and `B` must have exactly two"):
quadratic_assignment(
rng.random((3, 3, 3)),
rng.random((3, 3, 3)),
)
# test that cost and dist matrices of different sizes return error
with pytest.raises(
ValueError,
match="`A` and `B` matrices must be of equal size"):
quadratic_assignment(
rng.random((3, 3)),
rng.random((4, 4)),
)
# can't have more seed nodes than cost/dist nodes
_rm = _range_matrix
with pytest.raises(
ValueError,
match="`partial_match` can have only as many seeds as"):
quadratic_assignment(np.identity(3), np.identity(3),
options={'partial_match': _rm(5, 2)})
# test for only two seed columns
with pytest.raises(
ValueError, match="`partial_match` must have two columns"):
quadratic_assignment(
np.identity(3), np.identity(3),
options={'partial_match': _range_matrix(2, 3)}
)
# test that seed has no more than two dimensions
with pytest.raises(
ValueError, match="`partial_match` must have exactly two"):
quadratic_assignment(
np.identity(3), np.identity(3),
options={'partial_match': np.random.rand(3, 2, 2)}
)
# seeds cannot be negative valued
with pytest.raises(
ValueError, match="`partial_match` must contain only pos"):
quadratic_assignment(
np.identity(3), np.identity(3),
options={'partial_match': -1 * _range_matrix(2, 2)}
)
# seeds can't have values greater than number of nodes
with pytest.raises(
ValueError,
match="`partial_match` entries must be less than number"):
quadratic_assignment(
np.identity(5), np.identity(5),
options={'partial_match': 2 * _range_matrix(4, 2)}
)
# columns of seed matrix must be unique
with pytest.raises(
ValueError,
match="`partial_match` column entries must be unique"):
quadratic_assignment(
np.identity(3), np.identity(3),
options={'partial_match': np.ones((2, 2))}
)
def _range_matrix(a, b):
mat = np.zeros((a, b))
for i in range(b):
mat[:, i] = np.arange(a)
return mat
def _doubly_stochastic(P, tol=1e-3):
# cleaner implementation of btaba/sinkhorn_knopp
max_iter = 1000
c = 1 / P.sum(axis=0)
r = 1 / (P @ c)
P_eps = P
for it in range(max_iter):
if ((np.abs(P_eps.sum(axis=1) - 1) < tol).all() and
(np.abs(P_eps.sum(axis=0) - 1) < tol).all()):
# All column/row sums ~= 1 within threshold
break
c = 1 / (r @ P)
r = 1 / (P @ c)
P_eps = r[:, None] * P * c
return P_eps

View file

@ -0,0 +1,40 @@
"""Regression tests for optimize.
"""
import numpy as np
from numpy.testing import assert_almost_equal
from pytest import raises as assert_raises
import scipy.optimize
class TestRegression:
def test_newton_x0_is_0(self):
# Regression test for gh-1601
tgt = 1
res = scipy.optimize.newton(lambda x: x - 1, 0)
assert_almost_equal(res, tgt)
def test_newton_integers(self):
# Regression test for gh-1741
root = scipy.optimize.newton(lambda x: x**2 - 1, x0=2,
fprime=lambda x: 2*x)
assert_almost_equal(root, 1.0)
def test_lmdif_errmsg(self):
# This shouldn't cause a crash on Python 3
class SomeError(Exception):
pass
counter = [0]
def func(x):
counter[0] += 1
if counter[0] < 3:
return x**2 - np.array([9, 10, 11])
else:
raise SomeError()
assert_raises(SomeError,
scipy.optimize.leastsq,
func, [1, 2, 3])

View file

@ -0,0 +1,645 @@
"""
Unit test for SLSQP optimization.
"""
from numpy.testing import (assert_, assert_array_almost_equal,
assert_allclose, assert_equal)
from pytest import raises as assert_raises
import pytest
import numpy as np
import scipy
from scipy.optimize import fmin_slsqp, minimize, Bounds, NonlinearConstraint
class MyCallBack:
"""pass a custom callback function
This makes sure it's being used.
"""
def __init__(self):
self.been_called = False
self.ncalls = 0
def __call__(self, x):
self.been_called = True
self.ncalls += 1
class TestSLSQP:
"""
Test SLSQP algorithm using Example 14.4 from Numerical Methods for
Engineers by Steven Chapra and Raymond Canale.
This example maximizes the function f(x) = 2*x*y + 2*x - x**2 - 2*y**2,
which has a maximum at x=2, y=1.
"""
def setup_method(self):
self.opts = {'disp': False}
def fun(self, d, sign=1.0):
"""
Arguments:
d - A list of two elements, where d[0] represents x and d[1] represents y
in the following equation.
sign - A multiplier for f. Since we want to optimize it, and the SciPy
optimizers can only minimize functions, we need to multiply it by
-1 to achieve the desired solution
Returns:
2*x*y + 2*x - x**2 - 2*y**2
"""
x = d[0]
y = d[1]
return sign*(2*x*y + 2*x - x**2 - 2*y**2)
def jac(self, d, sign=1.0):
"""
This is the derivative of fun, returning a NumPy array
representing df/dx and df/dy.
"""
x = d[0]
y = d[1]
dfdx = sign*(-2*x + 2*y + 2)
dfdy = sign*(2*x - 4*y)
return np.array([dfdx, dfdy], float)
def fun_and_jac(self, d, sign=1.0):
return self.fun(d, sign), self.jac(d, sign)
def f_eqcon(self, x, sign=1.0):
""" Equality constraint """
return np.array([x[0] - x[1]])
def fprime_eqcon(self, x, sign=1.0):
""" Equality constraint, derivative """
return np.array([[1, -1]])
def f_eqcon_scalar(self, x, sign=1.0):
""" Scalar equality constraint """
return self.f_eqcon(x, sign)[0]
def fprime_eqcon_scalar(self, x, sign=1.0):
""" Scalar equality constraint, derivative """
return self.fprime_eqcon(x, sign)[0].tolist()
def f_ieqcon(self, x, sign=1.0):
""" Inequality constraint """
return np.array([x[0] - x[1] - 1.0])
def fprime_ieqcon(self, x, sign=1.0):
""" Inequality constraint, derivative """
return np.array([[1, -1]])
def f_ieqcon2(self, x):
""" Vector inequality constraint """
return np.asarray(x)
def fprime_ieqcon2(self, x):
""" Vector inequality constraint, derivative """
return np.identity(x.shape[0])
# minimize
def test_minimize_unbounded_approximated(self):
# Minimize, method='SLSQP': unbounded, approximated jacobian.
jacs = [None, False, '2-point', '3-point']
for jac in jacs:
res = minimize(self.fun, [-1.0, 1.0], args=(-1.0, ),
jac=jac, method='SLSQP',
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [2, 1])
def test_minimize_unbounded_given(self):
# Minimize, method='SLSQP': unbounded, given Jacobian.
res = minimize(self.fun, [-1.0, 1.0], args=(-1.0, ),
jac=self.jac, method='SLSQP', options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [2, 1])
def test_minimize_bounded_approximated(self):
# Minimize, method='SLSQP': bounded, approximated jacobian.
jacs = [None, False, '2-point', '3-point']
for jac in jacs:
with np.errstate(invalid='ignore'):
res = minimize(self.fun, [-1.0, 1.0], args=(-1.0, ),
jac=jac,
bounds=((2.5, None), (None, 0.5)),
method='SLSQP', options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [2.5, 0.5])
assert_(2.5 <= res.x[0])
assert_(res.x[1] <= 0.5)
def test_minimize_unbounded_combined(self):
# Minimize, method='SLSQP': unbounded, combined function and Jacobian.
res = minimize(self.fun_and_jac, [-1.0, 1.0], args=(-1.0, ),
jac=True, method='SLSQP', options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [2, 1])
def test_minimize_equality_approximated(self):
# Minimize with method='SLSQP': equality constraint, approx. jacobian.
jacs = [None, False, '2-point', '3-point']
for jac in jacs:
res = minimize(self.fun, [-1.0, 1.0], args=(-1.0, ),
jac=jac,
constraints={'type': 'eq',
'fun': self.f_eqcon,
'args': (-1.0, )},
method='SLSQP', options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [1, 1])
def test_minimize_equality_given(self):
# Minimize with method='SLSQP': equality constraint, given Jacobian.
res = minimize(self.fun, [-1.0, 1.0], jac=self.jac,
method='SLSQP', args=(-1.0,),
constraints={'type': 'eq', 'fun':self.f_eqcon,
'args': (-1.0, )},
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [1, 1])
def test_minimize_equality_given2(self):
# Minimize with method='SLSQP': equality constraint, given Jacobian
# for fun and const.
res = minimize(self.fun, [-1.0, 1.0], method='SLSQP',
jac=self.jac, args=(-1.0,),
constraints={'type': 'eq',
'fun': self.f_eqcon,
'args': (-1.0, ),
'jac': self.fprime_eqcon},
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [1, 1])
def test_minimize_equality_given_cons_scalar(self):
# Minimize with method='SLSQP': scalar equality constraint, given
# Jacobian for fun and const.
res = minimize(self.fun, [-1.0, 1.0], method='SLSQP',
jac=self.jac, args=(-1.0,),
constraints={'type': 'eq',
'fun': self.f_eqcon_scalar,
'args': (-1.0, ),
'jac': self.fprime_eqcon_scalar},
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [1, 1])
def test_minimize_inequality_given(self):
# Minimize with method='SLSQP': inequality constraint, given Jacobian.
res = minimize(self.fun, [-1.0, 1.0], method='SLSQP',
jac=self.jac, args=(-1.0, ),
constraints={'type': 'ineq',
'fun': self.f_ieqcon,
'args': (-1.0, )},
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [2, 1], atol=1e-3)
def test_minimize_inequality_given_vector_constraints(self):
# Minimize with method='SLSQP': vector inequality constraint, given
# Jacobian.
res = minimize(self.fun, [-1.0, 1.0], jac=self.jac,
method='SLSQP', args=(-1.0,),
constraints={'type': 'ineq',
'fun': self.f_ieqcon2,
'jac': self.fprime_ieqcon2},
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [2, 1])
def test_minimize_bounded_constraint(self):
# when the constraint makes the solver go up against a parameter
# bound make sure that the numerical differentiation of the
# jacobian doesn't try to exceed that bound using a finite difference.
# gh11403
def c(x):
assert 0 <= x[0] <= 1 and 0 <= x[1] <= 1, x
return x[0] ** 0.5 + x[1]
def f(x):
assert 0 <= x[0] <= 1 and 0 <= x[1] <= 1, x
return -x[0] ** 2 + x[1] ** 2
cns = [NonlinearConstraint(c, 0, 1.5)]
x0 = np.asarray([0.9, 0.5])
bnd = Bounds([0., 0.], [1.0, 1.0])
minimize(f, x0, method='SLSQP', bounds=bnd, constraints=cns)
def test_minimize_bound_equality_given2(self):
# Minimize with method='SLSQP': bounds, eq. const., given jac. for
# fun. and const.
res = minimize(self.fun, [-1.0, 1.0], method='SLSQP',
jac=self.jac, args=(-1.0, ),
bounds=[(-0.8, 1.), (-1, 0.8)],
constraints={'type': 'eq',
'fun': self.f_eqcon,
'args': (-1.0, ),
'jac': self.fprime_eqcon},
options=self.opts)
assert_(res['success'], res['message'])
assert_allclose(res.x, [0.8, 0.8], atol=1e-3)
assert_(-0.8 <= res.x[0] <= 1)
assert_(-1 <= res.x[1] <= 0.8)
# fmin_slsqp
def test_unbounded_approximated(self):
# SLSQP: unbounded, approximated Jacobian.
res = fmin_slsqp(self.fun, [-1.0, 1.0], args=(-1.0, ),
iprint = 0, full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [2, 1])
def test_unbounded_given(self):
# SLSQP: unbounded, given Jacobian.
res = fmin_slsqp(self.fun, [-1.0, 1.0], args=(-1.0, ),
fprime = self.jac, iprint = 0,
full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [2, 1])
def test_equality_approximated(self):
# SLSQP: equality constraint, approximated Jacobian.
res = fmin_slsqp(self.fun,[-1.0,1.0], args=(-1.0,),
eqcons = [self.f_eqcon],
iprint = 0, full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [1, 1])
def test_equality_given(self):
# SLSQP: equality constraint, given Jacobian.
res = fmin_slsqp(self.fun, [-1.0, 1.0],
fprime=self.jac, args=(-1.0,),
eqcons = [self.f_eqcon], iprint = 0,
full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [1, 1])
def test_equality_given2(self):
# SLSQP: equality constraint, given Jacobian for fun and const.
res = fmin_slsqp(self.fun, [-1.0, 1.0],
fprime=self.jac, args=(-1.0,),
f_eqcons = self.f_eqcon,
fprime_eqcons = self.fprime_eqcon,
iprint = 0,
full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [1, 1])
def test_inequality_given(self):
# SLSQP: inequality constraint, given Jacobian.
res = fmin_slsqp(self.fun, [-1.0, 1.0],
fprime=self.jac, args=(-1.0, ),
ieqcons = [self.f_ieqcon],
iprint = 0, full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [2, 1], decimal=3)
def test_bound_equality_given2(self):
# SLSQP: bounds, eq. const., given jac. for fun. and const.
res = fmin_slsqp(self.fun, [-1.0, 1.0],
fprime=self.jac, args=(-1.0, ),
bounds = [(-0.8, 1.), (-1, 0.8)],
f_eqcons = self.f_eqcon,
fprime_eqcons = self.fprime_eqcon,
iprint = 0, full_output = 1)
x, fx, its, imode, smode = res
assert_(imode == 0, imode)
assert_array_almost_equal(x, [0.8, 0.8], decimal=3)
assert_(-0.8 <= x[0] <= 1)
assert_(-1 <= x[1] <= 0.8)
def test_scalar_constraints(self):
# Regression test for gh-2182
x = fmin_slsqp(lambda z: z**2, [3.],
ieqcons=[lambda z: z[0] - 1],
iprint=0)
assert_array_almost_equal(x, [1.])
x = fmin_slsqp(lambda z: z**2, [3.],
f_ieqcons=lambda z: [z[0] - 1],
iprint=0)
assert_array_almost_equal(x, [1.])
def test_integer_bounds(self):
# This should not raise an exception
fmin_slsqp(lambda z: z**2 - 1, [0], bounds=[[0, 1]], iprint=0)
def test_array_bounds(self):
# NumPy used to treat n-dimensional 1-element arrays as scalars
# in some cases. The handling of `bounds` by `fmin_slsqp` still
# supports this behavior.
bounds = [(-np.inf, np.inf), (np.array([2]), np.array([3]))]
x = fmin_slsqp(lambda z: np.sum(z**2 - 1), [2.5, 2.5], bounds=bounds,
iprint=0)
assert_array_almost_equal(x, [0, 2])
def test_obj_must_return_scalar(self):
# Regression test for Github Issue #5433
# If objective function does not return a scalar, raises ValueError
with assert_raises(ValueError):
fmin_slsqp(lambda x: [0, 1], [1, 2, 3])
def test_obj_returns_scalar_in_list(self):
# Test for Github Issue #5433 and PR #6691
# Objective function should be able to return length-1 Python list
# containing the scalar
fmin_slsqp(lambda x: [0], [1, 2, 3], iprint=0)
def test_callback(self):
# Minimize, method='SLSQP': unbounded, approximated jacobian. Check for callback
callback = MyCallBack()
res = minimize(self.fun, [-1.0, 1.0], args=(-1.0, ),
method='SLSQP', callback=callback, options=self.opts)
assert_(res['success'], res['message'])
assert_(callback.been_called)
assert_equal(callback.ncalls, res['nit'])
def test_inconsistent_linearization(self):
# SLSQP must be able to solve this problem, even if the
# linearized problem at the starting point is infeasible.
# Linearized constraints are
#
# 2*x0[0]*x[0] >= 1
#
# At x0 = [0, 1], the second constraint is clearly infeasible.
# This triggers a call with n2==1 in the LSQ subroutine.
x = [0, 1]
def f1(x):
return x[0] + x[1] - 2
def f2(x):
return x[0] ** 2 - 1
sol = minimize(
lambda x: x[0]**2 + x[1]**2,
x,
constraints=({'type':'eq','fun': f1},
{'type':'ineq','fun': f2}),
bounds=((0,None), (0,None)),
method='SLSQP')
x = sol.x
assert_allclose(f1(x), 0, atol=1e-8)
assert_(f2(x) >= -1e-8)
assert_(sol.success, sol)
def test_regression_5743(self):
# SLSQP must not indicate success for this problem,
# which is infeasible.
x = [1, 2]
sol = minimize(
lambda x: x[0]**2 + x[1]**2,
x,
constraints=({'type':'eq','fun': lambda x: x[0]+x[1]-1},
{'type':'ineq','fun': lambda x: x[0]-2}),
bounds=((0,None), (0,None)),
method='SLSQP')
assert_(not sol.success, sol)
def test_gh_6676(self):
def func(x):
return (x[0] - 1)**2 + 2*(x[1] - 1)**2 + 0.5*(x[2] - 1)**2
sol = minimize(func, [0, 0, 0], method='SLSQP')
assert_(sol.jac.shape == (3,))
def test_invalid_bounds(self):
# Raise correct error when lower bound is greater than upper bound.
# See Github issue 6875.
bounds_list = [
((1, 2), (2, 1)),
((2, 1), (1, 2)),
((2, 1), (2, 1)),
((np.inf, 0), (np.inf, 0)),
((1, -np.inf), (0, 1)),
]
for bounds in bounds_list:
with assert_raises(ValueError):
minimize(self.fun, [-1.0, 1.0], bounds=bounds, method='SLSQP')
def test_bounds_clipping(self):
#
# SLSQP returns bogus results for initial guess out of bounds, gh-6859
#
def f(x):
return (x[0] - 1)**2
sol = minimize(f, [10], method='slsqp', bounds=[(None, 0)])
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
sol = minimize(f, [-10], method='slsqp', bounds=[(2, None)])
assert_(sol.success)
assert_allclose(sol.x, 2, atol=1e-10)
sol = minimize(f, [-10], method='slsqp', bounds=[(None, 0)])
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
sol = minimize(f, [10], method='slsqp', bounds=[(2, None)])
assert_(sol.success)
assert_allclose(sol.x, 2, atol=1e-10)
sol = minimize(f, [-0.5], method='slsqp', bounds=[(-1, 0)])
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
sol = minimize(f, [10], method='slsqp', bounds=[(-1, 0)])
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
def test_infeasible_initial(self):
# Check SLSQP behavior with infeasible initial point
def f(x):
x, = x
return x*x - 2*x + 1
cons_u = [{'type': 'ineq', 'fun': lambda x: 0 - x}]
cons_l = [{'type': 'ineq', 'fun': lambda x: x - 2}]
cons_ul = [{'type': 'ineq', 'fun': lambda x: 0 - x},
{'type': 'ineq', 'fun': lambda x: x + 1}]
sol = minimize(f, [10], method='slsqp', constraints=cons_u)
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
sol = minimize(f, [-10], method='slsqp', constraints=cons_l)
assert_(sol.success)
assert_allclose(sol.x, 2, atol=1e-10)
sol = minimize(f, [-10], method='slsqp', constraints=cons_u)
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
sol = minimize(f, [10], method='slsqp', constraints=cons_l)
assert_(sol.success)
assert_allclose(sol.x, 2, atol=1e-10)
sol = minimize(f, [-0.5], method='slsqp', constraints=cons_ul)
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
sol = minimize(f, [10], method='slsqp', constraints=cons_ul)
assert_(sol.success)
assert_allclose(sol.x, 0, atol=1e-10)
@pytest.mark.xfail(scipy.show_config(mode='dicts')['Compilers']['fortran']['name']
== "intel-llvm",
reason="Runtime warning due to floating point issues, not logic")
def test_inconsistent_inequalities(self):
# gh-7618
def cost(x):
return -1 * x[0] + 4 * x[1]
def ineqcons1(x):
return x[1] - x[0] - 1
def ineqcons2(x):
return x[0] - x[1]
# The inequalities are inconsistent, so no solution can exist:
#
# x1 >= x0 + 1
# x0 >= x1
x0 = (1,5)
bounds = ((-5, 5), (-5, 5))
cons = (dict(type='ineq', fun=ineqcons1), dict(type='ineq', fun=ineqcons2))
res = minimize(cost, x0, method='SLSQP', bounds=bounds, constraints=cons)
assert_(not res.success)
def test_new_bounds_type(self):
def f(x):
return x[0] ** 2 + x[1] ** 2
bounds = Bounds([1, 0], [np.inf, np.inf])
sol = minimize(f, [0, 0], method='slsqp', bounds=bounds)
assert_(sol.success)
assert_allclose(sol.x, [1, 0])
def test_nested_minimization(self):
class NestedProblem:
def __init__(self):
self.F_outer_count = 0
def F_outer(self, x):
self.F_outer_count += 1
if self.F_outer_count > 1000:
raise Exception("Nested minimization failed to terminate.")
inner_res = minimize(self.F_inner, (3, 4), method="SLSQP")
assert_(inner_res.success)
assert_allclose(inner_res.x, [1, 1])
return x[0]**2 + x[1]**2 + x[2]**2
def F_inner(self, x):
return (x[0] - 1)**2 + (x[1] - 1)**2
def solve(self):
outer_res = minimize(self.F_outer, (5, 5, 5), method="SLSQP")
assert_(outer_res.success)
assert_allclose(outer_res.x, [0, 0, 0])
problem = NestedProblem()
problem.solve()
def test_gh1758(self):
# the test suggested in gh1758
# https://nlopt.readthedocs.io/en/latest/NLopt_Tutorial/
# implement two equality constraints, in R^2.
def fun(x):
return np.sqrt(x[1])
def f_eqcon(x):
""" Equality constraint """
return x[1] - (2 * x[0]) ** 3
def f_eqcon2(x):
""" Equality constraint """
return x[1] - (-x[0] + 1) ** 3
c1 = {'type': 'eq', 'fun': f_eqcon}
c2 = {'type': 'eq', 'fun': f_eqcon2}
res = minimize(fun, [8, 0.25], method='SLSQP',
constraints=[c1, c2], bounds=[(-0.5, 1), (0, 8)])
np.testing.assert_allclose(res.fun, 0.5443310539518)
np.testing.assert_allclose(res.x, [0.33333333, 0.2962963])
assert res.success
def test_gh9640(self):
np.random.seed(10)
cons = ({'type': 'ineq', 'fun': lambda x: -x[0] - x[1] - 3},
{'type': 'ineq', 'fun': lambda x: x[1] + x[2] - 2})
bnds = ((-2, 2), (-2, 2), (-2, 2))
def target(x):
return 1
x0 = [-1.8869783504471584, -0.640096352696244, -0.8174212253407696]
res = minimize(target, x0, method='SLSQP', bounds=bnds, constraints=cons,
options={'disp':False, 'maxiter':10000})
# The problem is infeasible, so it cannot succeed
assert not res.success
def test_parameters_stay_within_bounds(self):
# gh11403. For some problems the SLSQP Fortran code suggests a step
# outside one of the lower/upper bounds. When this happens
# approx_derivative complains because it's being asked to evaluate
# a gradient outside its domain.
np.random.seed(1)
bounds = Bounds(np.array([0.1]), np.array([1.0]))
n_inputs = len(bounds.lb)
x0 = np.array(bounds.lb + (bounds.ub - bounds.lb) *
np.random.random(n_inputs))
def f(x):
assert (x >= bounds.lb).all()
return np.linalg.norm(x)
# The following should not raise any warnings which was the case, with the
# old Fortran code.
res = minimize(f, x0, method='SLSQP', bounds=bounds)
assert res.success
def test_slsqp_segfault_wrong_workspace_computation():
# See gh-14915
# This problem is not well-defined, however should not cause a segfault.
# The previous F77 workspace computation did not handle only equality-
# constrained problems correctly.
rng = np.random.default_rng(1742651087222879)
x = rng.uniform(size=[22,365])
target = np.linspace(0.9, 4.0, 50)
def metric(v, weights):
return [[0, 0],[1, 1]]
def efficient_metric(v, target):
def metric_a(weights):
return metric(v, weights)[1][0]
def metric_b(weights, v):
return metric(v, weights)[0][0]
constraints = ({'type': 'eq', 'fun': lambda x: metric_a(x) - target},
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
weights = np.array([len(v)*[1./len(v)]])[0]
result = minimize(metric_b,
weights,
args=(v,),
method='SLSQP',
constraints=constraints)
return result
efficient_metric(x, target)

View file

@ -0,0 +1,345 @@
"""
Unit tests for TNC optimization routine from tnc.py
"""
import pytest
from numpy.testing import assert_allclose, assert_equal
import numpy as np
from math import pow
from scipy import optimize
class TestTnc:
"""TNC non-linear optimization.
These tests are taken from Prof. K. Schittkowski's test examples
for constrained non-linear programming.
http://www.uni-bayreuth.de/departments/math/~kschittkowski/home.htm
"""
def setup_method(self):
# options for minimize
self.opts = {'disp': False, 'maxfun': 200}
# objective functions and Jacobian for each test
def f1(self, x, a=100.0):
return a * pow((x[1] - pow(x[0], 2)), 2) + pow(1.0 - x[0], 2)
def g1(self, x, a=100.0):
dif = [0, 0]
dif[1] = 2 * a * (x[1] - pow(x[0], 2))
dif[0] = -2.0 * (x[0] * (dif[1] - 1.0) + 1.0)
return dif
def fg1(self, x, a=100.0):
return self.f1(x, a), self.g1(x, a)
def f3(self, x):
return x[1] + pow(x[1] - x[0], 2) * 1.0e-5
def g3(self, x):
dif = [0, 0]
dif[0] = -2.0 * (x[1] - x[0]) * 1.0e-5
dif[1] = 1.0 - dif[0]
return dif
def fg3(self, x):
return self.f3(x), self.g3(x)
def f4(self, x):
return pow(x[0] + 1.0, 3) / 3.0 + x[1]
def g4(self, x):
dif = [0, 0]
dif[0] = pow(x[0] + 1.0, 2)
dif[1] = 1.0
return dif
def fg4(self, x):
return self.f4(x), self.g4(x)
def f5(self, x):
return np.sin(x[0] + x[1]) + pow(x[0] - x[1], 2) - \
1.5 * x[0] + 2.5 * x[1] + 1.0
def g5(self, x):
dif = [0, 0]
v1 = np.cos(x[0] + x[1])
v2 = 2.0*(x[0] - x[1])
dif[0] = v1 + v2 - 1.5
dif[1] = v1 - v2 + 2.5
return dif
def fg5(self, x):
return self.f5(x), self.g5(x)
def f38(self, x):
return (100.0 * pow(x[1] - pow(x[0], 2), 2) +
pow(1.0 - x[0], 2) + 90.0 * pow(x[3] - pow(x[2], 2), 2) +
pow(1.0 - x[2], 2) + 10.1 * (pow(x[1] - 1.0, 2) +
pow(x[3] - 1.0, 2)) +
19.8 * (x[1] - 1.0) * (x[3] - 1.0)) * 1.0e-5
def g38(self, x):
dif = [0, 0, 0, 0]
dif[0] = (-400.0 * x[0] * (x[1] - pow(x[0], 2)) -
2.0 * (1.0 - x[0])) * 1.0e-5
dif[1] = (200.0 * (x[1] - pow(x[0], 2)) + 20.2 * (x[1] - 1.0) +
19.8 * (x[3] - 1.0)) * 1.0e-5
dif[2] = (- 360.0 * x[2] * (x[3] - pow(x[2], 2)) -
2.0 * (1.0 - x[2])) * 1.0e-5
dif[3] = (180.0 * (x[3] - pow(x[2], 2)) + 20.2 * (x[3] - 1.0) +
19.8 * (x[1] - 1.0)) * 1.0e-5
return dif
def fg38(self, x):
return self.f38(x), self.g38(x)
def f45(self, x):
return 2.0 - x[0] * x[1] * x[2] * x[3] * x[4] / 120.0
def g45(self, x):
dif = [0] * 5
dif[0] = - x[1] * x[2] * x[3] * x[4] / 120.0
dif[1] = - x[0] * x[2] * x[3] * x[4] / 120.0
dif[2] = - x[0] * x[1] * x[3] * x[4] / 120.0
dif[3] = - x[0] * x[1] * x[2] * x[4] / 120.0
dif[4] = - x[0] * x[1] * x[2] * x[3] / 120.0
return dif
def fg45(self, x):
return self.f45(x), self.g45(x)
# tests
# minimize with method=TNC
def test_minimize_tnc1(self):
x0, bnds = [-2, 1], ([-np.inf, None], [-1.5, None])
xopt = [1, 1]
iterx = [] # to test callback
res = optimize.minimize(self.f1, x0, method='TNC', jac=self.g1,
bounds=bnds, options=self.opts,
callback=iterx.append)
assert_allclose(res.fun, self.f1(xopt), atol=1e-8)
assert_equal(len(iterx), res.nit)
def test_minimize_tnc1b(self):
x0, bnds = np.array([-2, 1]), ([-np.inf, None], [-1.5, None])
xopt = [1, 1]
x = optimize.minimize(self.f1, x0, method='TNC',
bounds=bnds, options=self.opts).x
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-4)
def test_minimize_tnc1c(self):
x0, bnds = [-2, 1], ([-np.inf, None],[-1.5, None])
xopt = [1, 1]
x = optimize.minimize(self.fg1, x0, method='TNC',
jac=True, bounds=bnds,
options=self.opts).x
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-8)
def test_minimize_tnc2(self):
x0, bnds = [-2, 1], ([-np.inf, None], [1.5, None])
xopt = [-1.2210262419616387, 1.5]
x = optimize.minimize(self.f1, x0, method='TNC',
jac=self.g1, bounds=bnds,
options=self.opts).x
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-8)
def test_minimize_tnc3(self):
x0, bnds = [10, 1], ([-np.inf, None], [0.0, None])
xopt = [0, 0]
x = optimize.minimize(self.f3, x0, method='TNC',
jac=self.g3, bounds=bnds,
options=self.opts).x
assert_allclose(self.f3(x), self.f3(xopt), atol=1e-8)
def test_minimize_tnc4(self):
x0,bnds = [1.125, 0.125], [(1, None), (0, None)]
xopt = [1, 0]
x = optimize.minimize(self.f4, x0, method='TNC',
jac=self.g4, bounds=bnds,
options=self.opts).x
assert_allclose(self.f4(x), self.f4(xopt), atol=1e-8)
def test_minimize_tnc5(self):
x0, bnds = [0, 0], [(-1.5, 4),(-3, 3)]
xopt = [-0.54719755119659763, -1.5471975511965976]
x = optimize.minimize(self.f5, x0, method='TNC',
jac=self.g5, bounds=bnds,
options=self.opts).x
assert_allclose(self.f5(x), self.f5(xopt), atol=1e-8)
def test_minimize_tnc38(self):
x0, bnds = np.array([-3, -1, -3, -1]), [(-10, 10)]*4
xopt = [1]*4
x = optimize.minimize(self.f38, x0, method='TNC',
jac=self.g38, bounds=bnds,
options=self.opts).x
assert_allclose(self.f38(x), self.f38(xopt), atol=1e-8)
def test_minimize_tnc45(self):
x0, bnds = [2] * 5, [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]
xopt = [1, 2, 3, 4, 5]
x = optimize.minimize(self.f45, x0, method='TNC',
jac=self.g45, bounds=bnds,
options=self.opts).x
assert_allclose(self.f45(x), self.f45(xopt), atol=1e-8)
# fmin_tnc
def test_tnc1(self):
fg, x, bounds = self.fg1, [-2, 1], ([-np.inf, None], [-1.5, None])
xopt = [1, 1]
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds, args=(100.0, ),
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc1b(self):
x, bounds = [-2, 1], ([-np.inf, None], [-1.5, None])
xopt = [1, 1]
x, nf, rc = optimize.fmin_tnc(self.f1, x, approx_grad=True,
bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-4,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc1c(self):
x, bounds = [-2, 1], ([-np.inf, None], [-1.5, None])
xopt = [1, 1]
x, nf, rc = optimize.fmin_tnc(self.f1, x, fprime=self.g1,
bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc2(self):
fg, x, bounds = self.fg1, [-2, 1], ([-np.inf, None], [1.5, None])
xopt = [-1.2210262419616387, 1.5]
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f1(x), self.f1(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc3(self):
fg, x, bounds = self.fg3, [10, 1], ([-np.inf, None], [0.0, None])
xopt = [0, 0]
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f3(x), self.f3(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc4(self):
fg, x, bounds = self.fg4, [1.125, 0.125], [(1, None), (0, None)]
xopt = [1, 0]
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f4(x), self.f4(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc5(self):
fg, x, bounds = self.fg5, [0, 0], [(-1.5, 4),(-3, 3)]
xopt = [-0.54719755119659763, -1.5471975511965976]
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f5(x), self.f5(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc38(self):
fg, x, bounds = self.fg38, np.array([-3, -1, -3, -1]), [(-10, 10)]*4
xopt = [1]*4
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f38(x), self.f38(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_tnc45(self):
fg, x, bounds = self.fg45, [2] * 5, [(0, 1), (0, 2), (0, 3),
(0, 4), (0, 5)]
xopt = [1, 2, 3, 4, 5]
x, nf, rc = optimize.fmin_tnc(fg, x, bounds=bounds,
messages=optimize._tnc.MSG_NONE,
maxfun=200)
assert_allclose(self.f45(x), self.f45(xopt), atol=1e-8,
err_msg="TNC failed with status: " +
optimize._tnc.RCSTRINGS[rc])
def test_raising_exceptions(self):
# tnc was ported to cython from hand-crafted cpython code
# check that Exception handling works.
def myfunc(x):
raise RuntimeError("myfunc")
def myfunc1(x):
return optimize.rosen(x)
def callback(x):
raise ValueError("callback")
with pytest.raises(RuntimeError):
optimize.minimize(myfunc, [0, 1], method="TNC")
with pytest.raises(ValueError):
optimize.minimize(
myfunc1, [0, 1], method="TNC", callback=callback
)
def test_callback_shouldnt_affect_minimization(self):
# gh14879. The output of a TNC minimization was different depending
# on whether a callback was used or not. The two should be equivalent.
# The issue was that TNC was unscaling/scaling x, and this process was
# altering x in the process. Now the callback uses an unscaled
# temporary copy of x.
def callback(x):
pass
fun = optimize.rosen
bounds = [(0, 10)] * 4
x0 = [1, 2, 3, 4.]
res = optimize.minimize(
fun, x0, bounds=bounds, method="TNC", options={"maxfun": 1000}
)
res2 = optimize.minimize(
fun, x0, bounds=bounds, method="TNC", options={"maxfun": 1000},
callback=callback
)
assert_allclose(res2.x, res.x)
assert_allclose(res2.fun, res.fun)
assert_equal(res2.nfev, res.nfev)

View file

@ -0,0 +1,110 @@
"""
Unit tests for trust-region optimization routines.
"""
import pytest
import numpy as np
from numpy.testing import assert_, assert_equal, assert_allclose
from scipy.optimize import (minimize, rosen, rosen_der, rosen_hess,
rosen_hess_prod)
class Accumulator:
""" This is for testing callbacks."""
def __init__(self):
self.count = 0
self.accum = None
def __call__(self, x):
self.count += 1
if self.accum is None:
self.accum = np.array(x)
else:
self.accum += x
class TestTrustRegionSolvers:
def setup_method(self):
self.x_opt = [1.0, 1.0]
self.easy_guess = [2.0, 2.0]
self.hard_guess = [-1.2, 1.0]
def test_dogleg_accuracy(self):
# test the accuracy and the return_all option
x0 = self.hard_guess
r = minimize(rosen, x0, jac=rosen_der, hess=rosen_hess, tol=1e-8,
method='dogleg', options={'return_all': True},)
assert_allclose(x0, r['allvecs'][0])
assert_allclose(r['x'], r['allvecs'][-1])
assert_allclose(r['x'], self.x_opt)
def test_dogleg_callback(self):
# test the callback mechanism and the maxiter and return_all options
accumulator = Accumulator()
maxiter = 5
r = minimize(rosen, self.hard_guess, jac=rosen_der, hess=rosen_hess,
callback=accumulator, method='dogleg',
options={'return_all': True, 'maxiter': maxiter},)
assert_equal(accumulator.count, maxiter)
assert_equal(len(r['allvecs']), maxiter+1)
assert_allclose(r['x'], r['allvecs'][-1])
assert_allclose(sum(r['allvecs'][1:]), accumulator.accum)
@pytest.mark.thread_unsafe
def test_dogleg_user_warning(self):
with pytest.warns(RuntimeWarning,
match=r'Maximum number of iterations'):
minimize(rosen, self.hard_guess, jac=rosen_der,
hess=rosen_hess, method='dogleg',
options={'disp': True, 'maxiter': 1}, )
def test_solver_concordance(self):
# Assert that dogleg uses fewer iterations than ncg on the Rosenbrock
# test function, although this does not necessarily mean
# that dogleg is faster or better than ncg even for this function
# and especially not for other test functions.
f = rosen
g = rosen_der
h = rosen_hess
for x0 in (self.easy_guess, self.hard_guess):
r_dogleg = minimize(f, x0, jac=g, hess=h, tol=1e-8,
method='dogleg', options={'return_all': True})
r_trust_ncg = minimize(f, x0, jac=g, hess=h, tol=1e-8,
method='trust-ncg',
options={'return_all': True})
r_trust_krylov = minimize(f, x0, jac=g, hess=h, tol=1e-8,
method='trust-krylov',
options={'return_all': True})
r_ncg = minimize(f, x0, jac=g, hess=h, tol=1e-8,
method='newton-cg', options={'return_all': True})
r_iterative = minimize(f, x0, jac=g, hess=h, tol=1e-8,
method='trust-exact',
options={'return_all': True})
assert_allclose(self.x_opt, r_dogleg['x'])
assert_allclose(self.x_opt, r_trust_ncg['x'])
assert_allclose(self.x_opt, r_trust_krylov['x'])
assert_allclose(self.x_opt, r_ncg['x'])
assert_allclose(self.x_opt, r_iterative['x'])
assert_(len(r_dogleg['allvecs']) < len(r_ncg['allvecs']))
def test_trust_ncg_hessp(self):
for x0 in (self.easy_guess, self.hard_guess, self.x_opt):
r = minimize(rosen, x0, jac=rosen_der, hessp=rosen_hess_prod,
tol=1e-8, method='trust-ncg')
assert_allclose(self.x_opt, r['x'])
def test_trust_ncg_start_in_optimum(self):
r = minimize(rosen, x0=self.x_opt, jac=rosen_der, hess=rosen_hess,
tol=1e-8, method='trust-ncg')
assert_allclose(self.x_opt, r['x'])
def test_trust_krylov_start_in_optimum(self):
r = minimize(rosen, x0=self.x_opt, jac=rosen_der, hess=rosen_hess,
tol=1e-8, method='trust-krylov')
assert_allclose(self.x_opt, r['x'])
def test_trust_exact_start_in_optimum(self):
r = minimize(rosen, x0=self.x_opt, jac=rosen_der, hess=rosen_hess,
tol=1e-8, method='trust-exact')
assert_allclose(self.x_opt, r['x'])

View file

@ -0,0 +1,351 @@
"""
Unit tests for trust-region iterative subproblem.
"""
import pytest
import numpy as np
from scipy.optimize._trustregion_exact import (
estimate_smallest_singular_value,
singular_leading_submatrix,
IterativeSubproblem)
from scipy.linalg import (svd, get_lapack_funcs, det, qr, norm)
from numpy.testing import (assert_array_equal,
assert_equal, assert_array_almost_equal)
def random_entry(n, min_eig, max_eig, case):
# Generate random matrix
rand = np.random.uniform(-1, 1, (n, n))
# QR decomposition
Q, _, _ = qr(rand, pivoting='True')
# Generate random eigenvalues
eigvalues = np.random.uniform(min_eig, max_eig, n)
eigvalues = np.sort(eigvalues)[::-1]
# Generate matrix
Qaux = np.multiply(eigvalues, Q)
A = np.dot(Qaux, Q.T)
# Generate gradient vector accordingly
# to the case is being tested.
if case == 'hard':
g = np.zeros(n)
g[:-1] = np.random.uniform(-1, 1, n-1)
g = np.dot(Q, g)
elif case == 'jac_equal_zero':
g = np.zeros(n)
else:
g = np.random.uniform(-1, 1, n)
return A, g
class TestEstimateSmallestSingularValue:
def test_for_ill_condiotioned_matrix(self):
# Ill-conditioned triangular matrix
C = np.array([[1, 2, 3, 4],
[0, 0.05, 60, 7],
[0, 0, 0.8, 9],
[0, 0, 0, 10]])
# Get svd decomposition
U, s, Vt = svd(C)
# Get smallest singular value and correspondent right singular vector.
smin_svd = s[-1]
zmin_svd = Vt[-1, :]
# Estimate smallest singular value
smin, zmin = estimate_smallest_singular_value(C)
# Check the estimation
assert_array_almost_equal(smin, smin_svd, decimal=8)
assert_array_almost_equal(abs(zmin), abs(zmin_svd), decimal=8)
class TestSingularLeadingSubmatrix:
def test_for_already_singular_leading_submatrix(self):
# Define test matrix A.
# Note that the leading 2x2 submatrix is singular.
A = np.array([[1, 2, 3],
[2, 4, 5],
[3, 5, 6]])
# Get Cholesky from lapack functions
cholesky, = get_lapack_funcs(('potrf',), (A,))
# Compute Cholesky Decomposition
c, k = cholesky(A, lower=False, overwrite_a=False, clean=True)
delta, v = singular_leading_submatrix(A, c, k)
A[k-1, k-1] += delta
# Check if the leading submatrix is singular.
assert_array_almost_equal(det(A[:k, :k]), 0)
# Check if `v` fulfil the specified properties
quadratic_term = np.dot(v, np.dot(A, v))
assert_array_almost_equal(quadratic_term, 0)
def test_for_simetric_indefinite_matrix(self):
# Define test matrix A.
# Note that the leading 5x5 submatrix is indefinite.
A = np.asarray([[1, 2, 3, 7, 8],
[2, 5, 5, 9, 0],
[3, 5, 11, 1, 2],
[7, 9, 1, 7, 5],
[8, 0, 2, 5, 8]])
# Get Cholesky from lapack functions
cholesky, = get_lapack_funcs(('potrf',), (A,))
# Compute Cholesky Decomposition
c, k = cholesky(A, lower=False, overwrite_a=False, clean=True)
delta, v = singular_leading_submatrix(A, c, k)
A[k-1, k-1] += delta
# Check if the leading submatrix is singular.
assert_array_almost_equal(det(A[:k, :k]), 0)
# Check if `v` fulfil the specified properties
quadratic_term = np.dot(v, np.dot(A, v))
assert_array_almost_equal(quadratic_term, 0)
def test_for_first_element_equal_to_zero(self):
# Define test matrix A.
# Note that the leading 2x2 submatrix is singular.
A = np.array([[0, 3, 11],
[3, 12, 5],
[11, 5, 6]])
# Get Cholesky from lapack functions
cholesky, = get_lapack_funcs(('potrf',), (A,))
# Compute Cholesky Decomposition
c, k = cholesky(A, lower=False, overwrite_a=False, clean=True)
delta, v = singular_leading_submatrix(A, c, k)
A[k-1, k-1] += delta
# Check if the leading submatrix is singular
assert_array_almost_equal(det(A[:k, :k]), 0)
# Check if `v` fulfil the specified properties
quadratic_term = np.dot(v, np.dot(A, v))
assert_array_almost_equal(quadratic_term, 0)
class TestIterativeSubproblem:
def test_for_the_easy_case(self):
# `H` is chosen such that `g` is not orthogonal to the
# eigenvector associated with the smallest eigenvalue `s`.
H = [[10, 2, 3, 4],
[2, 1, 7, 1],
[3, 7, 1, 7],
[4, 1, 7, 2]]
g = [1, 1, 1, 1]
# Trust Radius
trust_radius = 1
# Solve Subproblem
subprob = IterativeSubproblem(x=0,
fun=lambda x: 0,
jac=lambda x: np.array(g),
hess=lambda x: np.array(H),
k_easy=1e-10,
k_hard=1e-10)
p, hits_boundary = subprob.solve(trust_radius)
assert_array_almost_equal(p, [0.00393332, -0.55260862,
0.67065477, -0.49480341])
assert_array_almost_equal(hits_boundary, True)
def test_for_the_hard_case(self):
# `H` is chosen such that `g` is orthogonal to the
# eigenvector associated with the smallest eigenvalue `s`.
H = [[10, 2, 3, 4],
[2, 1, 7, 1],
[3, 7, 1, 7],
[4, 1, 7, 2]]
g = [6.4852641521327437, 1, 1, 1]
s = -8.2151519874416614
# Trust Radius
trust_radius = 1
# Solve Subproblem
subprob = IterativeSubproblem(x=0,
fun=lambda x: 0,
jac=lambda x: np.array(g),
hess=lambda x: np.array(H),
k_easy=1e-10,
k_hard=1e-10)
p, hits_boundary = subprob.solve(trust_radius)
assert_array_almost_equal(-s, subprob.lambda_current)
def test_for_interior_convergence(self):
H = [[1.812159, 0.82687265, 0.21838879, -0.52487006, 0.25436988],
[0.82687265, 2.66380283, 0.31508988, -0.40144163, 0.08811588],
[0.21838879, 0.31508988, 2.38020726, -0.3166346, 0.27363867],
[-0.52487006, -0.40144163, -0.3166346, 1.61927182, -0.42140166],
[0.25436988, 0.08811588, 0.27363867, -0.42140166, 1.33243101]]
g = [0.75798952, 0.01421945, 0.33847612, 0.83725004, -0.47909534]
# Solve Subproblem
subprob = IterativeSubproblem(x=0,
fun=lambda x: 0,
jac=lambda x: np.array(g),
hess=lambda x: np.array(H))
p, hits_boundary = subprob.solve(1.1)
assert_array_almost_equal(p, [-0.68585435, 0.1222621, -0.22090999,
-0.67005053, 0.31586769])
assert_array_almost_equal(hits_boundary, False)
assert_array_almost_equal(subprob.lambda_current, 0)
assert_array_almost_equal(subprob.niter, 1)
def test_for_jac_equal_zero(self):
H = [[0.88547534, 2.90692271, 0.98440885, -0.78911503, -0.28035809],
[2.90692271, -0.04618819, 0.32867263, -0.83737945, 0.17116396],
[0.98440885, 0.32867263, -0.87355957, -0.06521957, -1.43030957],
[-0.78911503, -0.83737945, -0.06521957, -1.645709, -0.33887298],
[-0.28035809, 0.17116396, -1.43030957, -0.33887298, -1.68586978]]
g = [0, 0, 0, 0, 0]
# Solve Subproblem
subprob = IterativeSubproblem(x=0,
fun=lambda x: 0,
jac=lambda x: np.array(g),
hess=lambda x: np.array(H),
k_easy=1e-10,
k_hard=1e-10)
p, hits_boundary = subprob.solve(1.1)
assert_array_almost_equal(p, [0.06910534, -0.01432721,
-0.65311947, -0.23815972,
-0.84954934])
assert_array_almost_equal(hits_boundary, True)
def test_for_jac_very_close_to_zero(self):
H = [[0.88547534, 2.90692271, 0.98440885, -0.78911503, -0.28035809],
[2.90692271, -0.04618819, 0.32867263, -0.83737945, 0.17116396],
[0.98440885, 0.32867263, -0.87355957, -0.06521957, -1.43030957],
[-0.78911503, -0.83737945, -0.06521957, -1.645709, -0.33887298],
[-0.28035809, 0.17116396, -1.43030957, -0.33887298, -1.68586978]]
g = [0, 0, 0, 0, 1e-15]
# Solve Subproblem
subprob = IterativeSubproblem(x=0,
fun=lambda x: 0,
jac=lambda x: np.array(g),
hess=lambda x: np.array(H),
k_easy=1e-10,
k_hard=1e-10)
p, hits_boundary = subprob.solve(1.1)
assert_array_almost_equal(p, [0.06910534, -0.01432721,
-0.65311947, -0.23815972,
-0.84954934])
assert_array_almost_equal(hits_boundary, True)
@pytest.mark.fail_slow(10)
def test_for_random_entries(self):
# Seed
np.random.seed(1)
# Dimension
n = 5
for case in ('easy', 'hard', 'jac_equal_zero'):
eig_limits = [(-20, -15),
(-10, -5),
(-10, 0),
(-5, 5),
(-10, 10),
(0, 10),
(5, 10),
(15, 20)]
for min_eig, max_eig in eig_limits:
# Generate random symmetric matrix H with
# eigenvalues between min_eig and max_eig.
H, g = random_entry(n, min_eig, max_eig, case)
# Trust radius
trust_radius_list = [0.1, 0.3, 0.6, 0.8, 1, 1.2, 3.3, 5.5, 10]
for trust_radius in trust_radius_list:
# Solve subproblem with very high accuracy
subprob_ac = IterativeSubproblem(0,
lambda x: 0,
lambda x: g,
lambda x: H,
k_easy=1e-10,
k_hard=1e-10)
p_ac, hits_boundary_ac = subprob_ac.solve(trust_radius)
# Compute objective function value
J_ac = 1/2*np.dot(p_ac, np.dot(H, p_ac))+np.dot(g, p_ac)
stop_criteria = [(0.1, 2),
(0.5, 1.1),
(0.9, 1.01)]
for k_opt, k_trf in stop_criteria:
# k_easy and k_hard computed in function
# of k_opt and k_trf accordingly to
# Conn, A. R., Gould, N. I., & Toint, P. L. (2000).
# "Trust region methods". Siam. p. 197.
k_easy = min(k_trf-1,
1-np.sqrt(k_opt))
k_hard = 1-k_opt
# Solve subproblem
subprob = IterativeSubproblem(0,
lambda x: 0,
lambda x: g,
lambda x: H,
k_easy=k_easy,
k_hard=k_hard)
p, hits_boundary = subprob.solve(trust_radius)
# Compute objective function value
J = 1/2*np.dot(p, np.dot(H, p))+np.dot(g, p)
# Check if it respect k_trf
if hits_boundary:
assert_array_equal(np.abs(norm(p)-trust_radius) <=
(k_trf-1)*trust_radius, True)
else:
assert_equal(norm(p) <= trust_radius, True)
# Check if it respect k_opt
assert_equal(J <= k_opt*J_ac, True)

View file

@ -0,0 +1,170 @@
"""
Unit tests for Krylov space trust-region subproblem solver.
"""
import pytest
import numpy as np
from scipy.optimize._trlib import (get_trlib_quadratic_subproblem)
from numpy.testing import (assert_,
assert_almost_equal,
assert_equal, assert_array_almost_equal)
KrylovQP = get_trlib_quadratic_subproblem(tol_rel_i=1e-8, tol_rel_b=1e-6)
KrylovQP_disp = get_trlib_quadratic_subproblem(tol_rel_i=1e-8, tol_rel_b=1e-6,
disp=True)
class TestKrylovQuadraticSubproblem:
def test_for_the_easy_case(self):
# `H` is chosen such that `g` is not orthogonal to the
# eigenvector associated with the smallest eigenvalue.
H = np.array([[1.0, 0.0, 4.0],
[0.0, 2.0, 0.0],
[4.0, 0.0, 3.0]])
g = np.array([5.0, 0.0, 4.0])
# Trust Radius
trust_radius = 1.0
# Solve Subproblem
subprob = KrylovQP(x=0,
fun=lambda x: 0,
jac=lambda x: g,
hess=lambda x: None,
hessp=lambda x, y: H.dot(y))
p, hits_boundary = subprob.solve(trust_radius)
assert_array_almost_equal(p, np.array([-1.0, 0.0, 0.0]))
assert_equal(hits_boundary, True)
# check kkt satisfaction
assert_almost_equal(
np.linalg.norm(H.dot(p) + subprob.lam * p + g),
0.0)
# check trust region constraint
assert_almost_equal(np.linalg.norm(p), trust_radius)
trust_radius = 0.5
p, hits_boundary = subprob.solve(trust_radius)
assert_array_almost_equal(p,
np.array([-0.46125446, 0., -0.19298788]))
assert_equal(hits_boundary, True)
# check kkt satisfaction
assert_almost_equal(
np.linalg.norm(H.dot(p) + subprob.lam * p + g),
0.0)
# check trust region constraint
assert_almost_equal(np.linalg.norm(p), trust_radius)
def test_for_the_hard_case(self):
# `H` is chosen such that `g` is orthogonal to the
# eigenvector associated with the smallest eigenvalue.
H = np.array([[1.0, 0.0, 4.0],
[0.0, 2.0, 0.0],
[4.0, 0.0, 3.0]])
g = np.array([0.0, 2.0, 0.0])
# Trust Radius
trust_radius = 1.0
# Solve Subproblem
subprob = KrylovQP(x=0,
fun=lambda x: 0,
jac=lambda x: g,
hess=lambda x: None,
hessp=lambda x, y: H.dot(y))
p, hits_boundary = subprob.solve(trust_radius)
assert_array_almost_equal(p, np.array([0.0, -1.0, 0.0]))
# check kkt satisfaction
assert_almost_equal(
np.linalg.norm(H.dot(p) + subprob.lam * p + g),
0.0)
# check trust region constraint
assert_almost_equal(np.linalg.norm(p), trust_radius)
trust_radius = 0.5
p, hits_boundary = subprob.solve(trust_radius)
assert_array_almost_equal(p, np.array([0.0, -0.5, 0.0]))
# check kkt satisfaction
assert_almost_equal(
np.linalg.norm(H.dot(p) + subprob.lam * p + g),
0.0)
# check trust region constraint
assert_almost_equal(np.linalg.norm(p), trust_radius)
def test_for_interior_convergence(self):
H = np.array([[1.812159, 0.82687265, 0.21838879, -0.52487006, 0.25436988],
[0.82687265, 2.66380283, 0.31508988, -0.40144163, 0.08811588],
[0.21838879, 0.31508988, 2.38020726, -0.3166346, 0.27363867],
[-0.52487006, -0.40144163, -0.3166346, 1.61927182, -0.42140166],
[0.25436988, 0.08811588, 0.27363867, -0.42140166, 1.33243101]])
g = np.array([0.75798952, 0.01421945, 0.33847612, 0.83725004, -0.47909534])
trust_radius = 1.1
# Solve Subproblem
subprob = KrylovQP(x=0,
fun=lambda x: 0,
jac=lambda x: g,
hess=lambda x: None,
hessp=lambda x, y: H.dot(y))
p, hits_boundary = subprob.solve(trust_radius)
# check kkt satisfaction
assert_almost_equal(
np.linalg.norm(H.dot(p) + subprob.lam * p + g),
0.0)
assert_array_almost_equal(p, [-0.68585435, 0.1222621, -0.22090999,
-0.67005053, 0.31586769])
assert_array_almost_equal(hits_boundary, False)
def test_for_very_close_to_zero(self):
H = np.array([[0.88547534, 2.90692271, 0.98440885, -0.78911503, -0.28035809],
[2.90692271, -0.04618819, 0.32867263, -0.83737945, 0.17116396],
[0.98440885, 0.32867263, -0.87355957, -0.06521957, -1.43030957],
[-0.78911503, -0.83737945, -0.06521957, -1.645709, -0.33887298],
[-0.28035809, 0.17116396, -1.43030957, -0.33887298, -1.68586978]])
g = np.array([0, 0, 0, 0, 1e-6])
trust_radius = 1.1
# Solve Subproblem
subprob = KrylovQP(x=0,
fun=lambda x: 0,
jac=lambda x: g,
hess=lambda x: None,
hessp=lambda x, y: H.dot(y))
p, hits_boundary = subprob.solve(trust_radius)
# check kkt satisfaction
assert_almost_equal(
np.linalg.norm(H.dot(p) + subprob.lam * p + g),
0.0)
# check trust region constraint
assert_almost_equal(np.linalg.norm(p), trust_radius)
assert_array_almost_equal(p, [0.06910534, -0.01432721,
-0.65311947, -0.23815972,
-0.84954934])
assert_array_almost_equal(hits_boundary, True)
@pytest.mark.thread_unsafe
def test_disp(self, capsys):
H = -np.eye(5)
g = np.array([0, 0, 0, 0, 1e-6])
trust_radius = 1.1
subprob = KrylovQP_disp(x=0,
fun=lambda x: 0,
jac=lambda x: g,
hess=lambda x: None,
hessp=lambda x, y: H.dot(y))
p, hits_boundary = subprob.solve(trust_radius)
out, err = capsys.readouterr()
assert_(out.startswith(' TR Solving trust region problem'), repr(out))

View file

@ -0,0 +1,998 @@
import pytest
from functools import lru_cache
from numpy.testing import (assert_warns, assert_,
assert_allclose,
assert_equal,
assert_array_equal,
suppress_warnings)
import numpy as np
from numpy import finfo, power, nan, isclose, sqrt, exp, sin, cos
from scipy import optimize
from scipy.optimize import (_zeros_py as zeros, newton, root_scalar,
OptimizeResult)
from scipy._lib._util import getfullargspec_no_self as _getfullargspec
# Import testing parameters
from scipy.optimize._tstutils import get_tests, functions as tstutils_functions
TOL = 4*np.finfo(float).eps # tolerance
_FLOAT_EPS = finfo(float).eps
bracket_methods = [zeros.bisect, zeros.ridder, zeros.brentq, zeros.brenth,
zeros.toms748]
gradient_methods = [zeros.newton]
all_methods = bracket_methods + gradient_methods
# A few test functions used frequently:
# # A simple quadratic, (x-1)^2 - 1
def f1(x):
return x ** 2 - 2 * x - 1
def f1_1(x):
return 2 * x - 2
def f1_2(x):
return 2.0 + 0 * x
def f1_and_p_and_pp(x):
return f1(x), f1_1(x), f1_2(x)
# Simple transcendental function
def f2(x):
return exp(x) - cos(x)
def f2_1(x):
return exp(x) + sin(x)
def f2_2(x):
return exp(x) + cos(x)
# lru cached function
@lru_cache
def f_lrucached(x):
return x
class TestScalarRootFinders:
# Basic tests for all scalar root finders
xtol = 4 * np.finfo(float).eps
rtol = 4 * np.finfo(float).eps
def _run_one_test(self, tc, method, sig_args_keys=None,
sig_kwargs_keys=None, **kwargs):
method_args = []
for k in sig_args_keys or []:
if k not in tc:
# If a,b not present use x0, x1. Similarly for f and func
k = {'a': 'x0', 'b': 'x1', 'func': 'f'}.get(k, k)
method_args.append(tc[k])
method_kwargs = dict(**kwargs)
method_kwargs.update({'full_output': True, 'disp': False})
for k in sig_kwargs_keys or []:
method_kwargs[k] = tc[k]
root = tc.get('root')
func_args = tc.get('args', ())
try:
r, rr = method(*method_args, args=func_args, **method_kwargs)
return root, rr, tc
except Exception:
return root, zeros.RootResults(nan, -1, -1, zeros._EVALUEERR, method), tc
def run_tests(self, tests, method, name, known_fail=None, **kwargs):
r"""Run test-cases using the specified method and the supplied signature.
Extract the arguments for the method call from the test case
dictionary using the supplied keys for the method's signature."""
# The methods have one of two base signatures:
# (f, a, b, **kwargs) # newton
# (func, x0, **kwargs) # bisect/brentq/...
# FullArgSpec with args, varargs, varkw, defaults, ...
sig = _getfullargspec(method)
assert_(not sig.kwonlyargs)
nDefaults = len(sig.defaults)
nRequired = len(sig.args) - nDefaults
sig_args_keys = sig.args[:nRequired]
sig_kwargs_keys = []
if name in ['secant', 'newton', 'halley']:
if name in ['newton', 'halley']:
sig_kwargs_keys.append('fprime')
if name in ['halley']:
sig_kwargs_keys.append('fprime2')
kwargs['tol'] = self.xtol
else:
kwargs['xtol'] = self.xtol
kwargs['rtol'] = self.rtol
results = [list(self._run_one_test(
tc, method, sig_args_keys=sig_args_keys,
sig_kwargs_keys=sig_kwargs_keys, **kwargs)) for tc in tests]
# results= [[true root, full output, tc], ...]
known_fail = known_fail or []
notcvgd = [elt for elt in results if not elt[1].converged]
notcvgd = [elt for elt in notcvgd if elt[-1]['ID'] not in known_fail]
notcvged_IDS = [elt[-1]['ID'] for elt in notcvgd]
assert_equal([len(notcvged_IDS), notcvged_IDS], [0, []])
# The usable xtol and rtol depend on the test
tols = {'xtol': self.xtol, 'rtol': self.rtol}
tols.update(**kwargs)
rtol = tols['rtol']
atol = tols.get('tol', tols['xtol'])
cvgd = [elt for elt in results if elt[1].converged]
approx = [elt[1].root for elt in cvgd]
correct = [elt[0] for elt in cvgd]
# See if the root matches the reference value
notclose = [[a] + elt for a, c, elt in zip(approx, correct, cvgd) if
not isclose(a, c, rtol=rtol, atol=atol)
and elt[-1]['ID'] not in known_fail]
# If not, evaluate the function and see if is 0 at the purported root
fvs = [tc['f'](aroot, *tc.get('args', tuple()))
for aroot, c, fullout, tc in notclose]
notclose = [[fv] + elt for fv, elt in zip(fvs, notclose) if fv != 0]
assert_equal([notclose, len(notclose)], [[], 0])
method_from_result = [result[1].method for result in results]
expected_method = [name for _ in results]
assert_equal(method_from_result, expected_method)
def run_collection(self, collection, method, name, smoothness=None,
known_fail=None, **kwargs):
r"""Run a collection of tests using the specified method.
The name is used to determine some optional arguments."""
tests = get_tests(collection, smoothness=smoothness)
self.run_tests(tests, method, name, known_fail=known_fail, **kwargs)
class TestBracketMethods(TestScalarRootFinders):
@pytest.mark.parametrize('method', bracket_methods)
@pytest.mark.parametrize('function', tstutils_functions)
def test_basic_root_scalar(self, method, function):
# Tests bracketing root finders called via `root_scalar` on a small
# set of simple problems, each of which has a root at `x=1`. Checks for
# converged status and that the root was found.
a, b = .5, sqrt(3)
r = root_scalar(function, method=method.__name__, bracket=[a, b], x0=a,
xtol=self.xtol, rtol=self.rtol)
assert r.converged
assert_allclose(r.root, 1.0, atol=self.xtol, rtol=self.rtol)
assert r.method == method.__name__
@pytest.mark.parametrize('method', bracket_methods)
@pytest.mark.parametrize('function', tstutils_functions)
def test_basic_individual(self, method, function):
# Tests individual bracketing root finders on a small set of simple
# problems, each of which has a root at `x=1`. Checks for converged
# status and that the root was found.
a, b = .5, sqrt(3)
root, r = method(function, a, b, xtol=self.xtol, rtol=self.rtol,
full_output=True)
assert r.converged
assert_allclose(root, 1.0, atol=self.xtol, rtol=self.rtol)
@pytest.mark.parametrize('method', bracket_methods)
@pytest.mark.parametrize('function', tstutils_functions)
def test_bracket_is_array(self, method, function):
# Test bracketing root finders called via `root_scalar` on a small set
# of simple problems, each of which has a root at `x=1`. Check that
# passing `bracket` as a `ndarray` is accepted and leads to finding the
# correct root.
a, b = .5, sqrt(3)
r = root_scalar(function, method=method.__name__,
bracket=np.array([a, b]), x0=a, xtol=self.xtol,
rtol=self.rtol)
assert r.converged
assert_allclose(r.root, 1.0, atol=self.xtol, rtol=self.rtol)
assert r.method == method.__name__
@pytest.mark.parametrize('method', bracket_methods)
def test_aps_collection(self, method):
self.run_collection('aps', method, method.__name__, smoothness=1)
@pytest.mark.parametrize('method', [zeros.bisect, zeros.ridder,
zeros.toms748])
def test_chandrupatla_collection(self, method):
known_fail = {'fun7.4'} if method == zeros.ridder else {}
self.run_collection('chandrupatla', method, method.__name__,
known_fail=known_fail)
@pytest.mark.parametrize('method', bracket_methods)
def test_lru_cached_individual(self, method):
# check that https://github.com/scipy/scipy/issues/10846 is fixed
# (`root_scalar` failed when passed a function that was `@lru_cache`d)
a, b = -1, 1
root, r = method(f_lrucached, a, b, full_output=True)
assert r.converged
assert_allclose(root, 0)
def test_gh_22934(self):
with pytest.raises(ValueError, match="maxiter must be >= 0"):
zeros.brentq(lambda x: x**2 - 1, -2, 0, maxiter=-1)
class TestNewton(TestScalarRootFinders):
def test_newton_collections(self):
known_fail = ['aps.13.00']
known_fail += ['aps.12.05', 'aps.12.17'] # fails under Windows Py27
for collection in ['aps', 'complex']:
self.run_collection(collection, zeros.newton, 'newton',
smoothness=2, known_fail=known_fail)
def test_halley_collections(self):
known_fail = ['aps.12.06', 'aps.12.07', 'aps.12.08', 'aps.12.09',
'aps.12.10', 'aps.12.11', 'aps.12.12', 'aps.12.13',
'aps.12.14', 'aps.12.15', 'aps.12.16', 'aps.12.17',
'aps.12.18', 'aps.13.00']
for collection in ['aps', 'complex']:
self.run_collection(collection, zeros.newton, 'halley',
smoothness=2, known_fail=known_fail)
def test_newton(self):
for f, f_1, f_2 in [(f1, f1_1, f1_2), (f2, f2_1, f2_2)]:
x = zeros.newton(f, 3, tol=1e-6)
assert_allclose(f(x), 0, atol=1e-6)
x = zeros.newton(f, 3, x1=5, tol=1e-6) # secant, x0 and x1
assert_allclose(f(x), 0, atol=1e-6)
x = zeros.newton(f, 3, fprime=f_1, tol=1e-6) # newton
assert_allclose(f(x), 0, atol=1e-6)
x = zeros.newton(f, 3, fprime=f_1, fprime2=f_2, tol=1e-6) # halley
assert_allclose(f(x), 0, atol=1e-6)
def test_newton_by_name(self):
r"""Invoke newton through root_scalar()"""
for f, f_1, f_2 in [(f1, f1_1, f1_2), (f2, f2_1, f2_2)]:
r = root_scalar(f, method='newton', x0=3, fprime=f_1, xtol=1e-6)
assert_allclose(f(r.root), 0, atol=1e-6)
for f, f_1, f_2 in [(f1, f1_1, f1_2), (f2, f2_1, f2_2)]:
r = root_scalar(f, method='newton', x0=3, xtol=1e-6) # without f'
assert_allclose(f(r.root), 0, atol=1e-6)
def test_secant_by_name(self):
r"""Invoke secant through root_scalar()"""
for f, f_1, f_2 in [(f1, f1_1, f1_2), (f2, f2_1, f2_2)]:
r = root_scalar(f, method='secant', x0=3, x1=2, xtol=1e-6)
assert_allclose(f(r.root), 0, atol=1e-6)
r = root_scalar(f, method='secant', x0=3, x1=5, xtol=1e-6)
assert_allclose(f(r.root), 0, atol=1e-6)
for f, f_1, f_2 in [(f1, f1_1, f1_2), (f2, f2_1, f2_2)]:
r = root_scalar(f, method='secant', x0=3, xtol=1e-6) # without x1
assert_allclose(f(r.root), 0, atol=1e-6)
def test_halley_by_name(self):
r"""Invoke halley through root_scalar()"""
for f, f_1, f_2 in [(f1, f1_1, f1_2), (f2, f2_1, f2_2)]:
r = root_scalar(f, method='halley', x0=3,
fprime=f_1, fprime2=f_2, xtol=1e-6)
assert_allclose(f(r.root), 0, atol=1e-6)
def test_root_scalar_fail(self):
message = 'fprime2 must be specified for halley'
with pytest.raises(ValueError, match=message):
root_scalar(f1, method='halley', fprime=f1_1, x0=3, xtol=1e-6) # no fprime2
message = 'fprime must be specified for halley'
with pytest.raises(ValueError, match=message):
root_scalar(f1, method='halley', fprime2=f1_2, x0=3, xtol=1e-6) # no fprime
def test_array_newton(self):
"""test newton with array"""
def f1(x, *a):
b = a[0] + x * a[3]
return a[1] - a[2] * (np.exp(b / a[5]) - 1.0) - b / a[4] - x
def f1_1(x, *a):
b = a[3] / a[5]
return -a[2] * np.exp(a[0] / a[5] + x * b) * b - a[3] / a[4] - 1
def f1_2(x, *a):
b = a[3] / a[5]
return -a[2] * np.exp(a[0] / a[5] + x * b) * b**2
a0 = np.array([
5.32725221, 5.48673747, 5.49539973,
5.36387202, 4.80237316, 1.43764452,
5.23063958, 5.46094772, 5.50512718,
5.42046290
])
a1 = (np.sin(range(10)) + 1.0) * 7.0
args = (a0, a1, 1e-09, 0.004, 10, 0.27456)
x0 = [7.0] * 10
x = zeros.newton(f1, x0, f1_1, args)
x_expected = (
6.17264965, 11.7702805, 12.2219954,
7.11017681, 1.18151293, 0.143707955,
4.31928228, 10.5419107, 12.7552490,
8.91225749
)
assert_allclose(x, x_expected)
# test halley's
x = zeros.newton(f1, x0, f1_1, args, fprime2=f1_2)
assert_allclose(x, x_expected)
# test secant
x = zeros.newton(f1, x0, args=args)
assert_allclose(x, x_expected)
def test_array_newton_complex(self):
def f(x):
return x + 1+1j
def fprime(x):
return 1.0
t = np.full(4, 1j)
x = zeros.newton(f, t, fprime=fprime)
assert_allclose(f(x), 0.)
# should work even if x0 is not complex
t = np.ones(4)
x = zeros.newton(f, t, fprime=fprime)
assert_allclose(f(x), 0.)
x = zeros.newton(f, t)
assert_allclose(f(x), 0.)
def test_array_secant_active_zero_der(self):
"""test secant doesn't continue to iterate zero derivatives"""
x = zeros.newton(lambda x, *a: x*x - a[0], x0=[4.123, 5],
args=[np.array([17, 25])])
assert_allclose(x, (4.123105625617661, 5.0))
def test_array_newton_integers(self):
# test secant with float
x = zeros.newton(lambda y, z: z - y ** 2, [4.0] * 2,
args=([15.0, 17.0],))
assert_allclose(x, (3.872983346207417, 4.123105625617661))
# test integer becomes float
x = zeros.newton(lambda y, z: z - y ** 2, [4] * 2, args=([15, 17],))
assert_allclose(x, (3.872983346207417, 4.123105625617661))
@pytest.mark.thread_unsafe
def test_array_newton_zero_der_failures(self):
# test derivative zero warning
assert_warns(RuntimeWarning, zeros.newton,
lambda y: y**2 - 2, [0., 0.], lambda y: 2 * y)
# test failures and zero_der
with pytest.warns(RuntimeWarning):
results = zeros.newton(lambda y: y**2 - 2, [0., 0.],
lambda y: 2*y, full_output=True)
assert_allclose(results.root, 0)
assert results.zero_der.all()
assert not results.converged.any()
def test_newton_combined(self):
def f1(x):
return x ** 2 - 2 * x - 1
def f1_1(x):
return 2 * x - 2
def f1_2(x):
return 2.0 + 0 * x
def f1_and_p_and_pp(x):
return x**2 - 2*x-1, 2*x-2, 2.0
sol0 = root_scalar(f1, method='newton', x0=3, fprime=f1_1)
sol = root_scalar(f1_and_p_and_pp, method='newton', x0=3, fprime=True)
assert_allclose(sol0.root, sol.root, atol=1e-8)
assert_equal(2*sol.function_calls, sol0.function_calls)
sol0 = root_scalar(f1, method='halley', x0=3, fprime=f1_1, fprime2=f1_2)
sol = root_scalar(f1_and_p_and_pp, method='halley', x0=3, fprime2=True)
assert_allclose(sol0.root, sol.root, atol=1e-8)
assert_equal(3*sol.function_calls, sol0.function_calls)
def test_newton_full_output(self, capsys):
# Test the full_output capability, both when converging and not.
# Use simple polynomials, to avoid hitting platform dependencies
# (e.g., exp & trig) in number of iterations
x0 = 3
expected_counts = [(6, 7), (5, 10), (3, 9)]
for derivs in range(3):
kwargs = {'tol': 1e-6, 'full_output': True, }
for k, v in [['fprime', f1_1], ['fprime2', f1_2]][:derivs]:
kwargs[k] = v
x, r = zeros.newton(f1, x0, disp=False, **kwargs)
assert_(r.converged)
assert_equal(x, r.root)
assert_equal((r.iterations, r.function_calls), expected_counts[derivs])
if derivs == 0:
assert r.function_calls <= r.iterations + 1
else:
assert_equal(r.function_calls, (derivs + 1) * r.iterations)
# Now repeat, allowing one fewer iteration to force convergence failure
iters = r.iterations - 1
x, r = zeros.newton(f1, x0, maxiter=iters, disp=False, **kwargs)
assert_(not r.converged)
assert_equal(x, r.root)
assert_equal(r.iterations, iters)
if derivs == 1:
# Check that the correct Exception is raised and
# validate the start of the message.
msg = f'Failed to converge after {iters} iterations, value is .*'
with pytest.raises(RuntimeError, match=msg):
x, r = zeros.newton(f1, x0, maxiter=iters, disp=True, **kwargs)
@pytest.mark.thread_unsafe
def test_deriv_zero_warning(self):
def func(x):
return x ** 2 - 2.0
def dfunc(x):
return 2 * x
assert_warns(RuntimeWarning, zeros.newton, func, 0.0, dfunc, disp=False)
with pytest.raises(RuntimeError, match='Derivative was zero'):
zeros.newton(func, 0.0, dfunc)
def test_newton_does_not_modify_x0(self):
# https://github.com/scipy/scipy/issues/9964
x0 = np.array([0.1, 3])
x0_copy = x0.copy() # Copy to test for equality.
newton(np.sin, x0, np.cos)
assert_array_equal(x0, x0_copy)
def test_gh17570_defaults(self):
# Previously, when fprime was not specified, root_scalar would default
# to secant. When x1 was not specified, secant failed.
# Check that without fprime, the default is secant if x1 is specified
# and newton otherwise.
# Also confirm that `x` is always a scalar (gh-21148)
def f(x):
assert np.isscalar(x)
return f1(x)
res_newton_default = root_scalar(f, method='newton', x0=3, xtol=1e-6)
res_secant_default = root_scalar(f, method='secant', x0=3, x1=2,
xtol=1e-6)
# `newton` uses the secant method when `x1` and `x2` are specified
res_secant = newton(f, x0=3, x1=2, tol=1e-6, full_output=True)[1]
# all three found a root
assert_allclose(f(res_newton_default.root), 0, atol=1e-6)
assert res_newton_default.root.shape == tuple()
assert_allclose(f(res_secant_default.root), 0, atol=1e-6)
assert res_secant_default.root.shape == tuple()
assert_allclose(f(res_secant.root), 0, atol=1e-6)
assert res_secant.root.shape == tuple()
# Defaults are correct
assert (res_secant_default.root
== res_secant.root
!= res_newton_default.iterations)
assert (res_secant_default.iterations
== res_secant_default.function_calls - 1 # true for secant
== res_secant.iterations
!= res_newton_default.iterations
== res_newton_default.function_calls/2) # newton 2-point diff
@pytest.mark.parametrize('kwargs', [dict(), {'method': 'newton'}])
def test_args_gh19090(self, kwargs):
def f(x, a, b):
assert a == 3
assert b == 1
return (x ** a - b)
res = optimize.root_scalar(f, x0=3, args=(3, 1), **kwargs)
assert res.converged
assert_allclose(res.root, 1)
@pytest.mark.parametrize('method', ['secant', 'newton'])
def test_int_x0_gh19280(self, method):
# Originally, `newton` ensured that only floats were passed to the
# callable. This was inadvertently changed by gh-17669. Check that
# it has been changed back.
def f(x):
# an integer raised to a negative integer power would fail
return x**-2 - 2
res = optimize.root_scalar(f, x0=1, method=method)
assert res.converged
assert_allclose(abs(res.root), 2**-0.5)
assert res.root.dtype == np.dtype(np.float64)
def test_newton_special_parameters(self):
# give zeros.newton() some strange parameters
# and check whether an exception appears
with pytest.raises(ValueError, match="tol too small"):
zeros.newton(f1, 3, tol=-1e-6)
with pytest.raises(ValueError, match="maxiter must be greater than 0"):
zeros.newton(f1, 3, tol=1e-6, maxiter=-50)
with pytest.raises(ValueError, match="x1 and x0 must be different" ):
zeros.newton(f1, 3, x1=3)
def test_gh_5555():
root = 0.1
def f(x):
return x - root
methods = [zeros.bisect, zeros.ridder]
xtol = rtol = TOL
for method in methods:
res = method(f, -1e8, 1e7, xtol=xtol, rtol=rtol)
assert_allclose(root, res, atol=xtol, rtol=rtol,
err_msg=f'method {method.__name__}')
def test_gh_5557():
# Show that without the changes in 5557 brentq and brenth might
# only achieve a tolerance of 2*(xtol + rtol*|res|).
# f linearly interpolates (0, -0.1), (0.5, -0.1), and (1,
# 0.4). The important parts are that |f(0)| < |f(1)| (so that
# brent takes 0 as the initial guess), |f(0)| < atol (so that
# brent accepts 0 as the root), and that the exact root of f lies
# more than atol away from 0 (so that brent doesn't achieve the
# desired tolerance).
def f(x):
if x < 0.5:
return -0.1
else:
return x - 0.6
atol = 0.51
rtol = 4 * _FLOAT_EPS
methods = [zeros.brentq, zeros.brenth]
for method in methods:
res = method(f, 0, 1, xtol=atol, rtol=rtol)
assert_allclose(0.6, res, atol=atol, rtol=rtol)
def test_brent_underflow_in_root_bracketing():
# Testing if an interval [a,b] brackets a zero of a function
# by checking f(a)*f(b) < 0 is not reliable when the product
# underflows/overflows. (reported in issue# 13737)
underflow_scenario = (-450.0, -350.0, -400.0)
overflow_scenario = (350.0, 450.0, 400.0)
for a, b, root in [underflow_scenario, overflow_scenario]:
c = np.exp(root)
for method in [zeros.brenth, zeros.brentq]:
res = method(lambda x: np.exp(x)-c, a, b)
assert_allclose(root, res)
class TestRootResults:
r = zeros.RootResults(root=1.0, iterations=44, function_calls=46, flag=0,
method="newton")
def test_repr(self):
expected_repr = (" converged: True\n flag: converged"
"\n function_calls: 46\n iterations: 44\n"
" root: 1.0\n method: newton")
assert_equal(repr(self.r), expected_repr)
def test_type(self):
assert isinstance(self.r, OptimizeResult)
def test_complex_halley():
"""Test Halley's works with complex roots"""
def f(x, *a):
return a[0] * x**2 + a[1] * x + a[2]
def f_1(x, *a):
return 2 * a[0] * x + a[1]
def f_2(x, *a):
retval = 2 * a[0]
try:
size = len(x)
except TypeError:
return retval
else:
return [retval] * size
z = complex(1.0, 2.0)
coeffs = (2.0, 3.0, 4.0)
y = zeros.newton(f, z, args=coeffs, fprime=f_1, fprime2=f_2, tol=1e-6)
# (-0.75000000000000078+1.1989578808281789j)
assert_allclose(f(y, *coeffs), 0, atol=1e-6)
z = [z] * 10
coeffs = (2.0, 3.0, 4.0)
y = zeros.newton(f, z, args=coeffs, fprime=f_1, fprime2=f_2, tol=1e-6)
assert_allclose(f(y, *coeffs), 0, atol=1e-6)
@pytest.mark.thread_unsafe
def test_zero_der_nz_dp(capsys):
"""Test secant method with a non-zero dp, but an infinite newton step"""
# pick a symmetrical functions and choose a point on the side that with dx
# makes a secant that is a flat line with zero slope, EG: f = (x - 100)**2,
# which has a root at x = 100 and is symmetrical around the line x = 100
# we have to pick a really big number so that it is consistently true
# now find a point on each side so that the secant has a zero slope
dx = np.finfo(float).eps ** 0.33
# 100 - p0 = p1 - 100 = p0 * (1 + dx) + dx - 100
# -> 200 = p0 * (2 + dx) + dx
p0 = (200.0 - dx) / (2.0 + dx)
with suppress_warnings() as sup:
sup.filter(RuntimeWarning, "RMS of")
x = zeros.newton(lambda y: (y - 100.0)**2, x0=[p0] * 10)
assert_allclose(x, [100] * 10)
# test scalar cases too
p0 = (2.0 - 1e-4) / (2.0 + 1e-4)
with suppress_warnings() as sup:
sup.filter(RuntimeWarning, "Tolerance of")
x = zeros.newton(lambda y: (y - 1.0) ** 2, x0=p0, disp=False)
assert_allclose(x, 1)
with pytest.raises(RuntimeError, match='Tolerance of'):
x = zeros.newton(lambda y: (y - 1.0) ** 2, x0=p0, disp=True)
p0 = (-2.0 + 1e-4) / (2.0 + 1e-4)
with suppress_warnings() as sup:
sup.filter(RuntimeWarning, "Tolerance of")
x = zeros.newton(lambda y: (y + 1.0) ** 2, x0=p0, disp=False)
assert_allclose(x, -1)
with pytest.raises(RuntimeError, match='Tolerance of'):
x = zeros.newton(lambda y: (y + 1.0) ** 2, x0=p0, disp=True)
@pytest.mark.thread_unsafe
def test_array_newton_failures():
"""Test that array newton fails as expected"""
# p = 0.68 # [MPa]
# dp = -0.068 * 1e6 # [Pa]
# T = 323 # [K]
diameter = 0.10 # [m]
# L = 100 # [m]
roughness = 0.00015 # [m]
rho = 988.1 # [kg/m**3]
mu = 5.4790e-04 # [Pa*s]
u = 2.488 # [m/s]
reynolds_number = rho * u * diameter / mu # Reynolds number
def colebrook_eqn(darcy_friction, re, dia):
return (1 / np.sqrt(darcy_friction) +
2 * np.log10(roughness / 3.7 / dia +
2.51 / re / np.sqrt(darcy_friction)))
# only some failures
with pytest.warns(RuntimeWarning):
result = zeros.newton(
colebrook_eqn, x0=[0.01, 0.2, 0.02223, 0.3], maxiter=2,
args=[reynolds_number, diameter], full_output=True
)
assert not result.converged.all()
# they all fail
with pytest.raises(RuntimeError):
result = zeros.newton(
colebrook_eqn, x0=[0.01] * 2, maxiter=2,
args=[reynolds_number, diameter], full_output=True
)
# this test should **not** raise a RuntimeWarning
def test_gh8904_zeroder_at_root_fails():
"""Test that Newton or Halley don't warn if zero derivative at root"""
# a function that has a zero derivative at it's root
def f_zeroder_root(x):
return x**3 - x**2
# should work with secant
r = zeros.newton(f_zeroder_root, x0=0)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
# test again with array
r = zeros.newton(f_zeroder_root, x0=[0]*10)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
# 1st derivative
def fder(x):
return 3 * x**2 - 2 * x
# 2nd derivative
def fder2(x):
return 6*x - 2
# should work with newton and halley
r = zeros.newton(f_zeroder_root, x0=0, fprime=fder)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
r = zeros.newton(f_zeroder_root, x0=0, fprime=fder,
fprime2=fder2)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
# test again with array
r = zeros.newton(f_zeroder_root, x0=[0]*10, fprime=fder)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
r = zeros.newton(f_zeroder_root, x0=[0]*10, fprime=fder,
fprime2=fder2)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
# also test that if a root is found we do not raise RuntimeWarning even if
# the derivative is zero, EG: at x = 0.5, then fval = -0.125 and
# fder = -0.25 so the next guess is 0.5 - (-0.125/-0.5) = 0 which is the
# root, but if the solver continued with that guess, then it will calculate
# a zero derivative, so it should return the root w/o RuntimeWarning
r = zeros.newton(f_zeroder_root, x0=0.5, fprime=fder)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
# test again with array
r = zeros.newton(f_zeroder_root, x0=[0.5]*10, fprime=fder)
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol)
# doesn't apply to halley
def test_gh_8881():
r"""Test that Halley's method realizes that the 2nd order adjustment
is too big and drops off to the 1st order adjustment."""
n = 9
def f(x):
return power(x, 1.0/n) - power(n, 1.0/n)
def fp(x):
return power(x, (1.0-n)/n)/n
def fpp(x):
return power(x, (1.0-2*n)/n) * (1.0/n) * (1.0-n)/n
x0 = 0.1
# The root is at x=9.
# The function has positive slope, x0 < root.
# Newton succeeds in 8 iterations
rt, r = newton(f, x0, fprime=fp, full_output=True)
assert r.converged
# Before the Issue 8881/PR 8882, halley would send x in the wrong direction.
# Check that it now succeeds.
rt, r = newton(f, x0, fprime=fp, fprime2=fpp, full_output=True)
assert r.converged
def test_gh_9608_preserve_array_shape():
"""
Test that shape is preserved for array inputs even if fprime or fprime2 is
scalar
"""
def f(x):
return x**2
def fp(x):
return 2 * x
def fpp(x):
return 2
x0 = np.array([-2], dtype=np.float32)
rt, r = newton(f, x0, fprime=fp, fprime2=fpp, full_output=True)
assert r.converged
x0_array = np.array([-2, -3], dtype=np.float32)
# This next invocation should fail
with pytest.raises(IndexError):
result = zeros.newton(
f, x0_array, fprime=fp, fprime2=fpp, full_output=True
)
def fpp_array(x):
return np.full(np.shape(x), 2, dtype=np.float32)
result = zeros.newton(
f, x0_array, fprime=fp, fprime2=fpp_array, full_output=True
)
assert result.converged.all()
@pytest.mark.parametrize(
"maximum_iterations,flag_expected",
[(10, zeros.CONVERR), (100, zeros.CONVERGED)])
def test_gh9254_flag_if_maxiter_exceeded(maximum_iterations, flag_expected):
"""
Test that if the maximum iterations is exceeded that the flag is not
converged.
"""
result = zeros.brentq(
lambda x: ((1.2*x - 2.3)*x + 3.4)*x - 4.5,
-30, 30, (), 1e-6, 1e-6, maximum_iterations,
full_output=True, disp=False)
assert result[1].flag == flag_expected
if flag_expected == zeros.CONVERR:
# didn't converge because exceeded maximum iterations
assert result[1].iterations == maximum_iterations
elif flag_expected == zeros.CONVERGED:
# converged before maximum iterations
assert result[1].iterations < maximum_iterations
@pytest.mark.thread_unsafe
def test_gh9551_raise_error_if_disp_true():
"""Test that if disp is true then zero derivative raises RuntimeError"""
def f(x):
return x*x + 1
def f_p(x):
return 2*x
assert_warns(RuntimeWarning, zeros.newton, f, 1.0, f_p, disp=False)
with pytest.raises(
RuntimeError,
match=r'^Derivative was zero\. Failed to converge after \d+ iterations, '
r'value is [+-]?\d*\.\d+\.$'):
zeros.newton(f, 1.0, f_p)
root = zeros.newton(f, complex(10.0, 10.0), f_p)
assert_allclose(root, complex(0.0, 1.0))
@pytest.mark.parametrize('solver_name',
['brentq', 'brenth', 'bisect', 'ridder', 'toms748'])
def test_gh3089_8394(solver_name):
# gh-3089 and gh-8394 reported that bracketing solvers returned incorrect
# results when they encountered NaNs. Check that this is resolved.
def f(x):
return np.nan
solver = getattr(zeros, solver_name)
with pytest.raises(ValueError, match="The function value at x..."):
solver(f, 0, 1)
@pytest.mark.parametrize('method',
['brentq', 'brenth', 'bisect', 'ridder', 'toms748'])
def test_gh18171(method):
# gh-3089 and gh-8394 reported that bracketing solvers returned incorrect
# results when they encountered NaNs. Check that `root_scalar` returns
# normally but indicates that convergence was unsuccessful. See gh-18171.
def f(x):
f._count += 1
return np.nan
f._count = 0
res = root_scalar(f, bracket=(0, 1), method=method)
assert res.converged is False
assert res.flag.startswith("The function value at x")
assert res.function_calls == f._count
assert str(res.root) in res.flag
@pytest.mark.parametrize('solver_name',
['brentq', 'brenth', 'bisect', 'ridder', 'toms748'])
@pytest.mark.parametrize('rs_interface', [True, False])
def test_function_calls(solver_name, rs_interface):
# There do not appear to be checks that the bracketing solvers report the
# correct number of function evaluations. Check that this is the case.
solver = ((lambda f, a, b, **kwargs: root_scalar(f, bracket=(a, b)))
if rs_interface else getattr(zeros, solver_name))
def f(x):
f.calls += 1
return x**2 - 1
f.calls = 0
res = solver(f, 0, 10, full_output=True)
if rs_interface:
assert res.function_calls == f.calls
else:
assert res[1].function_calls == f.calls
@pytest.mark.thread_unsafe
def test_gh_14486_converged_false():
"""Test that zero slope with secant method results in a converged=False"""
def lhs(x):
return x * np.exp(-x*x) - 0.07
with pytest.warns(RuntimeWarning, match='Tolerance of'):
res = root_scalar(lhs, method='secant', x0=-0.15, x1=1.0)
assert not res.converged
assert res.flag == 'convergence error'
with pytest.warns(RuntimeWarning, match='Tolerance of'):
res = newton(lhs, x0=-0.15, x1=1.0, disp=False, full_output=True)[1]
assert not res.converged
assert res.flag == 'convergence error'
@pytest.mark.parametrize('solver_name',
['brentq', 'brenth', 'bisect', 'ridder', 'toms748'])
@pytest.mark.parametrize('rs_interface', [True, False])
def test_gh5584(solver_name, rs_interface):
# gh-5584 reported that an underflow can cause sign checks in the algorithm
# to fail. Check that this is resolved.
solver = ((lambda f, a, b, **kwargs: root_scalar(f, bracket=(a, b)))
if rs_interface else getattr(zeros, solver_name))
def f(x):
return 1e-200*x
# Report failure when signs are the same
with pytest.raises(ValueError, match='...must have different signs'):
solver(f, -0.5, -0.4, full_output=True)
# Solve successfully when signs are different
res = solver(f, -0.5, 0.4, full_output=True)
res = res if rs_interface else res[1]
assert res.converged
assert_allclose(res.root, 0, atol=1e-8)
# Solve successfully when one side is negative zero
res = solver(f, -0.5, float('-0.0'), full_output=True)
res = res if rs_interface else res[1]
assert res.converged
assert_allclose(res.root, 0, atol=1e-8)
def test_gh13407():
# gh-13407 reported that the message produced by `scipy.optimize.toms748`
# when `rtol < eps` is incorrect, and also that toms748 is unusual in
# accepting `rtol` as low as eps while other solvers raise at 4*eps. Check
# that the error message has been corrected and that `rtol=eps` can produce
# a lower function value than `rtol=4*eps`.
def f(x):
return x**3 - 2*x - 5
xtol = 1e-300
eps = np.finfo(float).eps
x1 = zeros.toms748(f, 1e-10, 1e10, xtol=xtol, rtol=1*eps)
f1 = f(x1)
x4 = zeros.toms748(f, 1e-10, 1e10, xtol=xtol, rtol=4*eps)
f4 = f(x4)
assert f1 < f4
# using old-style syntax to get exactly the same message
message = fr"rtol too small \({eps/2:g} < {eps:g}\)"
with pytest.raises(ValueError, match=message):
zeros.toms748(f, 1e-10, 1e10, xtol=xtol, rtol=eps/2)
def test_newton_complex_gh10103():
# gh-10103 reported a problem when `newton` is pass a Python complex x0,
# no `fprime` (secant method), and no `x1` (`x1` must be constructed).
# Check that this is resolved.
def f(z):
return z - 1
res = newton(f, 1+1j)
assert_allclose(res, 1, atol=1e-12)
res = root_scalar(f, x0=1+1j, x1=2+1.5j, method='secant')
assert_allclose(res.root, 1, atol=1e-12)
@pytest.mark.parametrize('method', all_methods)
def test_maxiter_int_check_gh10236(method):
# gh-10236 reported that the error message when `maxiter` is not an integer
# was difficult to interpret. Check that this was resolved (by gh-10907).
message = "'float' object cannot be interpreted as an integer"
with pytest.raises(TypeError, match=message):
method(f1, 0.0, 1.0, maxiter=72.45)
@pytest.mark.parametrize("method", [zeros.bisect, zeros.ridder,
zeros.brentq, zeros.brenth])
def test_bisect_special_parameter(method):
# give some zeros method strange parameters
# and check whether an exception appears
root = 0.1
args = (1e-09, 0.004, 10, 0.27456)
rtolbad = 4 * np.finfo(float).eps / 2
def f(x):
return x - root
with pytest.raises(ValueError, match="xtol too small"):
method(f, -1e8, 1e7, args=args, xtol=-1e-6, rtol=TOL)
with pytest.raises(ValueError, match="rtol too small"):
method(f, -1e8, 1e7, args=args, xtol=1e-6, rtol=rtolbad)