How to find all zeros of a function using numpy (and scipy)?

前端 未结 4 646
忘掉有多难
忘掉有多难 2020-12-16 04:35

Suppose I have a function f(x) defined between a and b. This function can have many zeros, but also many asymptotes. I need to retriev

相关标签:
4条回答
  • 2020-12-16 04:45

    The main problem I see with this is if you can actually find all roots --- as have already been mentioned in comments, this is not always possible. If you are sure that your function is not completely pathological (sin(1/x) was already mentioned), the next one is what's your tolerance to missing a root or several of them. Put differently, it's about to what length you are prepared to go to make sure you did not miss any --- to the best of my knowledge, there is no general method to isolate all the roots for you, so you'll have to do it yourself. What you show is a reasonable first step already. A couple of comments:

    • Brent's method is indeed a good choice here.
    • First of all, deal with the divergencies. Since in your function you have Bessels in the denominators, you can first solve for their roots -- better look them up in e.g., Abramovitch and Stegun (Mathworld link). This will be a better than using an ad hoc grid you're using.
    • What you can do, once you've found two roots or divergencies, x_1 and x_2, run the search again in the interval [x_1+epsilon, x_2-epsilon]. Continue until no more roots are found (Brent's method is guaranteed to converge to a root, provided there is one).
    • If you cannot enumerate all the divergencies, you might want to be a little more careful in verifying a candidate is indeed a divergency: given x don't just check that f(x) is large, check that, e.g. |f(x-epsilon/2)| > |f(x-epsilon)| for several values of epsilon (1e-8, 1e-9, 1e-10, something like that).
    • If you want to make sure you don't have roots which simply touch zero, look for the extrema of the function, and for each extremum, x_e, check the value of f(x_e).
    0 讨论(0)
  • 2020-12-16 04:48

    I've also encountered this problem to solve equations like f(z)=0 where f was an holomorphic function. I wanted to be sure not to miss any zero and finally developed an algorithm which is based on the argument principle.

    It helps to find the exact number of zeros lying in a complex domain. Once you know the number of zeros, it is easier to find them. There are however two concerns which must be taken into account :

    • Take care about multiplicity : when solving (z-1)^2 = 0, you'll get two zeros as z=1 is counting twice
    • If the function is meromorphic (thus contains poles), each pole reduce the number of zero and break the attempt to count them.
    0 讨论(0)
  • 2020-12-16 04:50

    Why are you limited to numpy? Scipy has a package that does exactly what you want:

    http://docs.scipy.org/doc/scipy/reference/optimize.nonlin.html

    One lesson I've learned: numerical programming is hard, so don't do it :)


    Anyway, if you're dead set on building the algorithm yourself, the doc page on scipy I linked (takes forever to load, btw) gives you a list of algorithms to start with. One method that I've used before is to discretize the function to the degree that is necessary for your problem. (That is, tune \delta x so that it is much smaller than the characteristic size in your problem.) This lets you look for features of the function (like changes in sign). AND, you can compute the derivative of a line segment (probably since kindergarten) pretty easily, so your discretized function has a well-defined first derivative. Because you've tuned the dx to be smaller than the characteristic size, you're guaranteed not to miss any features of the function that are important for your problem.

    If you want to know what "characteristic size" means, look for some parameter of your function with units of length or 1/length. That is, for some function f(x), assume x has units of length and f has no units. Then look for the things that multiply x. For example, if you want to discretize cos(\pi x), the parameter that multiplies x (if x has units of length) must have units of 1/length. So the characteristic size of cos(\pi x) is 1/\pi. If you make your discretization much smaller than this, you won't have any issues. To be sure, this trick won't always work, so you may need to do some tinkering.

    0 讨论(0)
  • 2020-12-16 04:58

    I found out it's relatively easy to implement your own root finder using the scipy.optimize.fsolve.

    • Idea: Find any zeroes from interval (start, stop) and stepsize step by calling the fsolve repeatedly with changing x0. Use relatively small stepsize to find all the roots.

    • Can only search for zeroes in one dimension (other dimensions must be fixed). If you have other needs, I would recommend using sympy for calculating the analytical solution.

    • Note: It may not always find all the zeroes, but I saw it giving relatively good results. I put the code also to a gist, which I will update if needed.

    import numpy as np
    import scipy
    from scipy.optimize import fsolve
    from matplotlib import pyplot as plt
    
    # Defined below
    r = RootFinder(1, 20, 0.01)
    args = (90, 5)
    roots = r.find(f, *args)
    print("Roots: ", roots)
    
    # plot results
    u = np.linspace(1, 20, num=600)
    fig, ax = plt.subplots()
    ax.plot(u, f(u, *args))
    ax.scatter(roots, f(np.array(roots), *args), color="r", s=10)
    ax.grid(color="grey", ls="--", lw=0.5)
    plt.show()
    

    Example output:

    Roots:  [ 2.84599497  8.82720551 12.38857782 15.74736542 19.02545276]
    

    zoom-in:

    RootFinder definition

    import numpy as np
    import scipy
    from scipy.optimize import fsolve
    from matplotlib import pyplot as plt
    
    
    class RootFinder:
        def __init__(self, start, stop, step=0.01, root_dtype="float64", xtol=1e-9):
    
            self.start = start
            self.stop = stop
            self.step = step
            self.xtol = xtol
            self.roots = np.array([], dtype=root_dtype)
    
        def add_to_roots(self, x):
    
            if (x < self.start) or (x > self.stop):
                return  # outside range
            if any(abs(self.roots - x) < self.xtol):
                return  # root already found.
    
            self.roots = np.append(self.roots, x)
    
        def find(self, f, *args):
            current = self.start
    
            for x0 in np.arange(self.start, self.stop + self.step, self.step):
                if x0 < current:
                    continue
                x = self.find_root(f, x0, *args)
                if x is None:  # no root found.
                    continue
                current = x
                self.add_to_roots(x)
    
            return self.roots
    
        def find_root(self, f, x0, *args):
    
            x, _, ier, _ = fsolve(f, x0=x0, args=args, full_output=True, xtol=self.xtol)
            if ier == 1:
                return x[0]
            return None
    
    

    Test function

    The scipy.special.jnjn does not exist anymore, but I created similar test function for the case.

    def f(u, V=90, ell=5):
        w = np.sqrt(V ** 2 - u ** 2)
    
        jl = scipy.special.jn(ell, u)
        jl1 = scipy.special.yn(ell - 1, u)
        kl = scipy.special.kn(ell, w)
        kl1 = scipy.special.kn(ell - 1, w)
    
        return jl / (u * jl1) + kl / (w * kl1)
    
    0 讨论(0)
提交回复
热议问题