How to create an equation generator of n parameters to use with scipy?

Viewed 86

I am porting my code from MatLab to Python, and there is this neat trick that I did but cannot reproduce:

function [Equation, EquationComponents] = BezierEquation(n)
syms t x01 x02 x03 x04 x05 x06 x07 x08 x09 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20 x21 x22 x23 x24 x25 x26 x27 x28 x29 x30 x31 x32 x33 x34 x35 x36 x37 x38 x39 x40 x41;
xVar=[x01,x02,x03,x04,x05,x06,x07,x08,x09,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39,x40,x41];
for i = 0:n
    B(:,i+1)= nchoosek(n,i);
    Pol(:,i+1)= (1-t)^(n-i)*t^i;
    xVar2(:,i+1)=xVar(:,i+1);
end
EquationComponents=[xVar2;B;Pol];
Equation=sum(B.*xVar2.*Pol);
end

What it does is it generates a Bezier equation of n degree with n parameters. Manually writing this equation with n=30 or n=40 would be painful.

I am currently trying to do the same with scipy and use it for curve_fit, but I don't understand how to create an equation of a variable number of parameters. I currently have this code with a working, hand-written example for n=5. How to generate for any n? curve_fit doesn't seem to understand that co is not a scalar.

import numpy as np
from scipy.special import comb
from scipy.optimize import curve_fit

class Bezier(object):
    def __init__(self, n):
        self.n = n
        self.i = np.arange(0, n + 1)
        self.n_minus_i = np.flip(self.i)
        self.b = comb(n, self.i)

    def generate_equation(self, x, co):
        b = self.b
        i = self.i
        eq = []
        for j, B in enumerate(b):
            eq.append(B * (1 - x)**(self.n - i[j]) * x**i[j] * co[j])
        return np.sum(eq)

    def equation_5(self, x, a, b, c, d, e, f):
        i = np.arange(0, 6)
        B = comb(5, i)
        return a*B[0]*(1-x)**(5-i[0])*x**i[0] + b*B[1]*(1-x)**(5-i[1])*x**i[1] + c*B[2]*(1-x)**(5-i[2])*x**i[2] + d*B[3]*(1-x)**(5-i[3])*x**i[3] + e*B[4]*(1-x)**(5-i[4])*x**i[4] + f*B[5]*(1-x)**(5-i[5])*x**i[5]

Update:

By looking at the sympy library, I made a simple solution with it. I am sharing it, but I would like to keep that question open for a solution without sympy. Maybe using numpy arrays instead of variables, or if there is a way to create a lambda function by unpacking a n number of arguments. Something equivalent to the unpacking in lambdify([x, *list_of_params], equation, 'numpy') but without sympy.

    import numpy as np
    from scipy.special import comb
    from scipy.optimize import curve_fit
    from sympy import symbols
    from sympy import lambdify
    
def bezier_generator(n):
    if n > 15:
        return
    i = np.arange(0, n + 1)
    b = comb(n, i)
    x, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15 = symbols(
        "x, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15")
    co = [c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15]

    eq = np.sum(b * (1 - x) ** (n - i) * x ** i * co[:n + 1])
    func = lambdify([x, *co[:n + 1]], eq, 'numpy')
    return func
1 Answers

If I understand this correctly:

I would like to keep that question open for a solution without sympy. Maybe using numpy arrays instead of variables

then it sounds like you want to start with the plain Python implementation (outsourcing the binomial function to a library that implements it already, like scipy), and uplift the parts that are still insufficient for what you're ultimately doing with it:

from scipy.stats import binom

def evaluate_bezier_at(t, coords):
    mt = 1 - t
    sum = 0
    n = len(coords)
    for (i, w_i) in enumerate(coords):
        sum += w_i * binom(n,i) * (mt ** (n-i)) * (t ** i)
    return sum

(And of course there are speed optimizations to be had, but if you're working with 30+ degree Bezier curves, I kind of assume you're not writing code that needs to be peak performance code: no one needs Beziers of that high a degree =)

Related