Skip to content

Commit 1e410e7

Browse files
committed
improvements to fixed-point solvers
1 parent ea2386a commit 1e410e7

9 files changed

Lines changed: 293 additions & 57 deletions

flexsolve/__init__.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,33 @@
66
"""
77
from . import open_solvers
88
from . import bounded_solvers
9-
from . import iterative_solvers
9+
from . import fixed_point_solvers
10+
from . import line_search
11+
from . import numerical_analysis
1012
from . import problem_list
1113
from . import profiler
1214
from . import problem
1315
from . import utils
1416

15-
__all__ = (*open_solvers.__all__,
16-
*bounded_solvers.__all__,
17-
*iterative_solvers.__all__,
18-
*problem_list.__all__,
19-
*problem.__all__,
20-
*profiler.__all__,
21-
'utils',
17+
__all__ = (
18+
*open_solvers.__all__,
19+
*bounded_solvers.__all__,
20+
*fixed_point_solvers.__all__,
21+
*line_search.__all__,
22+
*numerical_analysis.__all__,
23+
*problem_list.__all__,
24+
*problem.__all__,
25+
*profiler.__all__,
26+
'utils',
2227
)
2328

2429
from .open_solvers import *
2530
from .bounded_solvers import *
26-
from .iterative_solvers import *
31+
from .fixed_point_solvers import *
32+
from .line_search import *
33+
from .numerical_analysis import *
2734
from .problem_list import *
2835
from .problem import *
2936
from .profiler import *
3037

31-
__version__ = '0.5.7'
38+
__version__ = '0.5.8'
Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,60 @@
1717
'conditional_aitken',
1818
)
1919

20+
iteration_history = []
21+
22+
class CachedFunction:
23+
__slots__ = ('f', 'inputs', 'outputs')
24+
25+
def __init__(self, f, inputs=None, outputs=None):
26+
self.f = f
27+
self.inputs = [] if inputs is None else inputs
28+
self.outputs = [] if inputs is None else outputs
29+
30+
def __call__(self, x, *args, **kwargs):
31+
self.inputs.append(x)
32+
y = self.f(x, *args, **kwargs)
33+
self.outputs.append(y)
34+
return y
35+
36+
def GPiteration():
37+
pass
38+
39+
def GPacceleration(
40+
f, x, xtol=5e-8, args=(), maxiter=50, memory=10, checkiter=True,
41+
checkconvergence=True, bounds=None, convergenceiter=0, subset=0, *, rtol=0
42+
):
43+
"""Iterative fixed-point solver with GP acceleration."""
44+
x0 = x1 = x
45+
errors = np.zeros(convergenceiter)
46+
f = CachedFunction(f)
47+
fixedpoint_converged = utils.fixedpoint_converged
48+
for iter in range(maxiter):
49+
x1 = GPiteration(f, x0, args, memory, bounds)
50+
e = np.abs(x1 - x0)
51+
if fixedpoint_converged(x0, e, xtol, rtol, subset): return x1
52+
if convergenceiter:
53+
mean = utils.mean(e)
54+
if iter > convergenceiter and mean > errors.mean():
55+
if checkconvergence: utils.raise_convergence_error()
56+
else: return x1
57+
errors = np.roll(errors, shift=1)
58+
errors[-1] = mean
59+
x0 = x1
60+
if checkiter: utils.raise_iter_error()
61+
return x1
62+
2063
@register_jitable(cache=True)
2164
def fixed_point(f, x, xtol=5e-8, args=(), maxiter=50, checkiter=True,
22-
checkconvergence=True, convergenceiter=0, subset=0):
65+
checkconvergence=True, convergenceiter=0, subset=0, *, rtol=0):
2366
"""Iterative fixed-point solver."""
2467
x0 = x1 = x
2568
errors = np.zeros(convergenceiter)
2669
fixedpoint_converged = utils.fixedpoint_converged
2770
for iter in range(maxiter):
2871
x1 = f(x0, *args)
2972
e = np.abs(x1 - x0)
30-
if fixedpoint_converged(e, xtol, subset): return x1
73+
if fixedpoint_converged(x0, e, xtol, rtol, subset): return x1
3174
if convergenceiter:
3275
mean = utils.mean(e)
3376
if iter > convergenceiter and mean > errors.mean():
@@ -51,7 +94,8 @@ def conditional_fixed_point(f, x):
5194

5295
@register_jitable(cache=True)
5396
def wegstein(f, x, xtol=5e-8, args=(), maxiter=50, checkiter=True,
54-
checkconvergence=True, convergenceiter=0, subset=0):
97+
checkconvergence=True, convergenceiter=0, subset=0, *,
98+
lb=-float('inf'), ub=float('inf'), rtol=0, exp=1.0):
5599
"""Iterative Wegstein solver."""
56100
errors = np.zeros(convergenceiter)
57101
x0 = x
@@ -65,9 +109,9 @@ def wegstein(f, x, xtol=5e-8, args=(), maxiter=50, checkiter=True,
65109
x1 = g0
66110
g1 = f(x1, *args)
67111
e = np.abs(g1 - x1)
68-
if fixedpoint_converged(e, xtol, subset): return g1
112+
if fixedpoint_converged(x1, e, xtol, rtol, subset): return g1
69113
x0 = x1
70-
x1 = wegstein_iter(x1, dx, g1, g0)
114+
x1 = wegstein_iter(x1, dx, g1, g0, lb, ub, exp)
71115
g0 = g1
72116
if convergenceiter:
73117
mean = utils.mean(e)
@@ -80,7 +124,7 @@ def wegstein(f, x, xtol=5e-8, args=(), maxiter=50, checkiter=True,
80124
return x1
81125

82126
@register_jitable(cache=True)
83-
def conditional_wegstein(f, x):
127+
def conditional_wegstein(f, x, lb=-float('inf'), ub=float('inf'), exp=0.5):
84128
"""Conditional iterative Wegstein solver."""
85129
x0 = x
86130
g0, condition = f(x0)
@@ -94,13 +138,13 @@ def conditional_wegstein(f, x):
94138
g1 = g1
95139
dx = x1-x0
96140
x0 = x1
97-
x1 = wegstein_iter(x1, dx, g1, g0)
141+
x1 = wegstein_iter(x1, dx, g1, g0, lb, ub, exp)
98142
g0 = g1
99143
return x1
100144

101145
@register_jitable(cache=True)
102146
def aitken(f, x, xtol=5e-8, args=(), maxiter=50, checkiter=True,
103-
checkconvergence=True, convergenceiter=0, subset=0):
147+
checkconvergence=True, convergenceiter=0, subset=0, *, rtol=0):
104148
"""Iterative Aitken solver."""
105149
gg = x
106150
errors = np.zeros(convergenceiter)
@@ -113,10 +157,10 @@ def aitken(f, x, xtol=5e-8, args=(), maxiter=50, checkiter=True,
113157
g = f(x, *args)
114158
dxg = x - g
115159
e = np.abs(dxg)
116-
if fixedpoint_converged(e, xtol, subset): return g
160+
if fixedpoint_converged(x, e, xtol, rtol, subset): return g
117161
gg = f(g, *args)
118162
dgg_g = gg - g
119-
if fixedpoint_converged(np.abs(dgg_g), xtol, subset): return gg
163+
if fixedpoint_converged(g, np.abs(dgg_g), xtol, rtol, subset): return gg
120164
x = aitken_iter(x, gg, dxg, dgg_g)
121165
if convergenceiter:
122166
mean = utils.mean(e)

flexsolve/line_search.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Created on Wed Jul 16 13:18:51 2025
4+
5+
@author: yoelr
6+
"""
7+
from scipy.optimize import fminbound
8+
9+
__all__ = ('line_search',)
10+
11+
def line_search(f, x, descent, t0, t1, ttol=1e-3, maxiter=20):
12+
return fminbound(
13+
lambda t: f(x + t * descent),
14+
x1=t0,
15+
x2=t1,
16+
xtol=ttol,
17+
maxfun=maxiter,
18+
)
19+
20+
21+
22+

flexsolve/numerical_analysis.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Created on Sun Jul 13 16:48:47 2025
4+
5+
@author: yoelr
6+
"""
7+
import numpy as np
8+
from scipy.differentiate import jacobian
9+
from numpy.linalg import cond as matrix_condition_number
10+
from scipy.linalg import block_diag
11+
12+
__all__ = (
13+
'function_condition_number',
14+
'independent_functions_condition_number',
15+
'matrix_condition_number',
16+
)
17+
18+
def norm(x):
19+
return np.linalg.norm(x, ord=2) if np.ndim(x) else x
20+
21+
def independent_functions_condition_number(*fxs, **jac_kwargs):
22+
"""
23+
Estimate the condition number of a set of nonlinear functions at a
24+
given point using finite-difference Jacobian from SciPy.
25+
26+
Parameters
27+
----------
28+
fxs : callable
29+
Pairs of functios and points at which to evaluate the condition number.
30+
The function is from ℝⁿ to ℝᵐ and must accept a 1D NumPy array.
31+
**jac_kwargs : dict, optional
32+
Additional keyword arguments passed to `scipy.differentiate.jacobian`,
33+
such as `initial_step`, `order`, `step_direction`, etc.
34+
35+
Returns
36+
-------
37+
kappa : float
38+
The estimated condition number at point `x`. If `f(x)` has zero norm,
39+
returns `np.inf`.
40+
41+
Notes
42+
-----
43+
The condition number is computed as:
44+
45+
κ(x) = ||J(x)|| * ||x|| / ||f(x)||
46+
47+
where J(x) is the Jacobian of `f` at `x`, and all norms are 2-norms
48+
(by default).
49+
"""
50+
xs = []
51+
fs = []
52+
Js = []
53+
def add(lst, value):
54+
if np.ndim(value):
55+
lst.extend(value)
56+
else:
57+
lst.append(value)
58+
59+
for f, x in fxs:
60+
x = np.asarray(x, dtype=float)
61+
res = jacobian(f, x, **jac_kwargs)
62+
fx = f(x)
63+
J = res.df
64+
add(xs, x)
65+
add(fs, fx)
66+
add(Js, J)
67+
68+
J = block_diag(*Js)
69+
x = np.array(xs)
70+
fx = np.array(fs)
71+
norm_x = norm(x)
72+
norm_fx = norm(fx)
73+
norm_J = norm(J)
74+
if norm_fx == 0:
75+
kappa = np.inf
76+
else:
77+
kappa = norm_J * norm_x / norm_fx
78+
return kappa
79+
80+
def function_condition_number(f, x, **jac_kwargs):
81+
"""
82+
Estimate the condition number of a nonlinear function at a given point
83+
using finite-difference Jacobian from SciPy.
84+
85+
Parameters
86+
----------
87+
f : callable
88+
A function from ℝⁿ to ℝᵐ. Must accept a 1D NumPy array of shape (n,)
89+
and return a 1D array of shape (m,).
90+
x : array_like, shape (n,)
91+
Point at which to evaluate the condition number.
92+
**jac_kwargs : dict, optional
93+
Additional keyword arguments passed to `scipy.differentiate.jacobian`,
94+
such as `initial_step`, `order`, `step_direction`, etc.
95+
96+
Returns
97+
-------
98+
kappa : float
99+
The estimated condition number at point `x`. If `f(x)` has zero norm,
100+
returns `np.inf`.
101+
102+
Notes
103+
-----
104+
The condition number is computed as:
105+
106+
κ(x) = ||J(x)|| * ||x|| / ||f(x)||
107+
108+
where J(x) is the Jacobian of `f` at `x`, and all norms are 2-norms
109+
(by default).
110+
"""
111+
x = np.asarray(x, dtype=float)
112+
# Compute numerical Jacobian using SciPy's differentiate API
113+
res = jacobian(f, x, **jac_kwargs)
114+
J = res.df # The Jacobian matrix (m, n)
115+
fx = f(x)
116+
norm_x = norm(x)
117+
norm_fx = norm(fx)
118+
norm_J = norm(J)
119+
if norm_fx == 0:
120+
kappa = np.inf
121+
else:
122+
kappa = norm_J * norm_x / norm_fx
123+
return kappa
124+
125+
126+
if __name__ == '__main__':
127+
128+
def f0(x0):
129+
return x0[0] ** 2 - x0[1] ** 0.5 + 5
130+
131+
def f1(x1):
132+
return x1[0] ** 0.8 - x1[1] ** 3
133+
134+
def f(x):
135+
return np.array([f0(x[:2]), f1(x[2:])])
136+
137+
x = np.array([2, 3, 0.5, -1])
138+
kappa = function_condition_number(f, x)
139+
kappa_mix = independent_functions_condition_number((f0, x[:2]), (f1, x[2:]))

flexsolve/problem.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
@author: yrc2
66
"""
7-
from .iterative_solvers import wegstein, aitken
7+
from .fixed_point_solvers import wegstein, aitken
88
from .profiler import Profiler
99

1010
__all__ = ('Problem',)

0 commit comments

Comments
 (0)