1

Goal:

Find a fast algorithm in Python that solves the function f(x) below for its positive solution.

def f(x):
    return (l / ((np.tile(r, (n, 1)).transpose() / D / (np.tile(x, (n, 1)) / D).sum(axis = 0)).sum(axis = 1))) - x

l, r, and x are vectors of dimension n and D is a matrix of dimension n x n.

The function is known to have a positive solution that is unique up to a scaling factor. I would like to solve the function for different data and different vector length n. The largest n is approximately 4000.

What I have tried so far:

I tried various scipy.optimize functions. First, I tried fsolve which does not seem appropriate because it sometimes gives a solution vector x with negative entries. Following the answers to a related question, I tried minimize and constraining the solution to positive numbers to avoid negative entries in the solution. Minimize finds the global minimum that solves the function only when provided with the correct solution as a starting value. When the starting value differs (slighlty), the resulting vector does not solve the equation (and I need the exact solution). I assume that the algorithm finds local minima but not the global one. To find the global minimum I tried differential evolution. Here, the problem is that it is very slow for any useful n. I did all testing with n = 5 for which it finds the correct solution.

Question:

Which algorithms are good candidates to solve this equation? (How) Can I use what I know about the equation to speed up the calculation? (i.e. a positive solution exists, it is unique up to scaling)

Minimal working example:

import numpy as np
from scipy.optimize import minimize, fsolve, differential_evolution

np.random.seed(1)

# Vector dimension
n = 250 # differential evolution is slow, better use n = 5 to test

# Data r and D
r = np.random.rand(n)
D = 1 + np.random.rand(n, n)

# True solution x
x_true = np.random.rand(n)

# Normalize x to have geometric mean 1
x_true = x_true / np.prod(x_true) ** (1/n)

# Solve for l implied by true x
l = ((np.tile(r, (n, 1)).transpose() / D) / (np.tile(x_true, (n, 1)) / D).sum(axis = 0)).sum(axis = 1) * x_true


### Fsolve

initial_guess_deviation_factor = 2
x_0 = x_true * np.random.uniform(low = 0.9, high = 1.1, size = n) ** initial_guess_deviation_factor

def f(x):
    return (l / ((np.tile(r, (n, 1)).transpose() / D / (np.tile(x, (n, 1)) / D).sum(axis = 0)).sum(axis = 1))) - x

# The solution is negative
x = fsolve(f, x_0)


### Minimize

def opt(x):
    return (((l / ((np.tile(r, (n, 1)).transpose() / D / (np.tile(x, (n, 1)) / D).sum(axis = 0)).sum(axis = 1))) - x) ** 2).sum()
 
def pos_constraint(x):
    return x

result = minimize(opt, x0=x_0, constraints={'type': 'ineq', 'fun':pos_constraint}, tol = 1e-18)

# The solution is different from the true solution
print(abs(result.x - x_true).mean())
print(result.fun)


### Differential evolution

def opt(x):
    return (((l / ((np.tile(r, (n, 1)).transpose() / D / (np.tile(x, (n, 1)) / D).sum(axis = 0)).sum(axis = 1))) - x) ** 2).sum()
 
# Since the solution is unique up to renormalization, I use bounds between 0 and 1 and renormalize after finding the solution
bounds = [(0, 1)] * n
result = differential_evolution(opt, bounds, seed=1)
result.x, result.fun

# Normalize solution
x_de = result.x / np.prod(result.x) ** (1/n)

print(abs(x_de - x_true).mean())
print(result.fun)
eigenvector
  • 313
  • 1
  • 3
  • 12
  • 1
    By "unique up to a scaling", do you mean that there exists an unbounded coefficient _μ_ and one unique _x_ for which all _μx_ are valid solutions? – Reinderien Jul 17 '23 at 23:44
  • 1
    If so, you might try removing one degree of freedom (one element from x) and fixing it at 1.0. – Reinderien Jul 17 '23 at 23:46
  • @Reinderien: Yes, that's exactly what it means. I tried fixing the last element to 1 and the resulting vector is the same as solving for x unconditionally and then renormalizing so that the last element is 1. So that's a good check! However, my main concern currently is the speed and solving with and without the normalization seems equally fast. My goal is to find an algorithm that finds the global minima (like differential evolution) but that is faster. (n = 10 takes 30 seconds and the time grows exponentially as I increase n.) – eigenvector Jul 18 '23 at 13:57
  • _I need the exact solution_ - note that, since you're using numerical methods and not an analytic method, "exact" isn't really in the cards. – Reinderien Jul 18 '23 at 15:00
  • Agreed. "Exact" as in deviations below, say, 10e-8 are fine. – eigenvector Jul 18 '23 at 15:01

1 Answers1

1

First, do some linear-algebraic analysis to derive the following simplified, equivalent form of your problem (and include regression tests):

import numpy as np
from numpy.random._generator import default_rng
from scipy.optimize import fsolve


rand = default_rng(seed=0)

n = 250  # differential evolution is slow, better use n = 5 to test
r = rand.random(n)
D = rand.uniform(low=1, high=2, size=(n, n))
rDD = r[:, np.newaxis] / (1/D).sum(axis=0) / D

# True solution x with geometric mean 1
x_true = rand.random(n)
x_true = x_true / x_true.prod() ** (1 / n)

# Solve for l implied by true x
l = rDD @ (1 / x_true) * x_true


def f(x: np.ndarray) -> np.ndarray:
    return l / (rDD @ (1 / x)) - x


def regression_test() -> None:
    assert l.shape == (250,)
    assert np.isclose(l[0], 1.8437187927094683)
    assert np.isclose(l.min(), 0.008011379562766192)
    assert np.isclose(l.max(), 4.870546152196413)
    assert np.isclose(l.sum(), 328.4768373368165)

    f_0 = f(x_0)
    assert f_0.shape == (250,)
    assert np.isclose(f_0[0], -0.11599601776615498)
    assert np.isclose(f_0.min(), -0.5897953289580671)
    assert np.isclose(f_0.max(), 0.3885530509026145)
    assert np.isclose(f_0.sum(), -9.253079363636402)


initial_guess_deviation_factor = 2
x_0 = x_true * rand.uniform(low=0.9, high=1.1, size=n) ** initial_guess_deviation_factor
regression_test()

Maybe I'm missing something, but your first attempt is very close at being both fast and accurate; you just need to divide out one term and the rest will be non-negative:

x = fsolve(f, x_0)
x /= x[0]
print(x)
print(f(x))

prints the following for x:

[1.         0.21849311 0.62362266 1.37331647 1.0126681  1.20579018 ...

and the following for f(x):

[ 1.18594023e-12 -2.77555756e-15 -1.30429001e-12 -7.40074668e-13 ...
Reinderien
  • 11,755
  • 5
  • 49
  • 77
  • Hi! I realized that I made an error in the functional form of f(x). For the f(x) provided in my question your answer is correct (and irrespective of my mistake it's very helpful!). For the correct functional form, I run into trouble because fsolve does not converge. Could I edit the question and you give it another try? Or would you prefer that I accept your answer, because technically for the question I asked, it is correct? Let me know. – eigenvector Jul 25 '23 at 09:38
  • I'm not picky - edit away – Reinderien Jul 25 '23 at 12:02
  • I found another (small) error and with the correction fsolve worked just as fine as described in your answer. Thank you. – eigenvector Jul 27 '23 at 11:25