Related
I'm currently implementing a simulation, where a 3D point needs to be transformed to a fisheye image with the equivalent of Blenders 'PANO' camera and the equidistant camera model. Does anyone know where I can find the sources (scientific papers, even technical reports) of Blenders implementation of the 3D->2D transformation?
In detail, I'm looking for:
Transformation from world to camera coordinate system
Transformation from camera coordinate system (carthesian) to camera coordinate system (spherical). This is necessary for the projection of the point to the unit sphere, before projecting the point onto the image plane
Transformation from the sperical camera coordinate system to the sensor plane
"normalization" in terms of shifting the point from the principal point to the left top corner of the sensor
Why this question? While the implementation from a perspective camera is natively available in the bpy_extras module, the transformation for the equidistant camera model is not.
The code, which I've generated is below. Currently the result of the image is different from the result in blender, any suggestions why?
main.py:
#!/usr/bin/env python3
import numpy as np
np.set_printoptions(precision=4, suppress=True)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
import plotting
def compute_R(alpha_deg, beta_deg, gamma_deg):
## Rotation Matrix R (code from CV2 exercise)
# convert into radians...
alpha = alpha_deg * np.pi / 180
beta = beta_deg * np.pi / 180
gamma = gamma_deg * np.pi / 180
# prepare sine and cosine values
ca = np.cos(alpha)
sa = np.sin(alpha)
cb = np.cos(beta)
sb = np.sin(beta)
cg = np.cos(gamma)
sg = np.sin(gamma)
# rotation about x axis
R_x = np.array([
[1, 0, 0],
[0, ca, -sa],
[0, sa, ca],
], dtype=float)
# rotation about y axis
R_y = np.array([
[ cb, 0, sb],
[ 0, 1, 0],
[-sb, 0, cb]
], dtype=float)
# rotation about z axis
R_z = np.array([
[cg, -sg, 0],
[sg, cg, 0],
[ 0, 0, 1]
], dtype=float)
# compose R_xyz
R = R_z.dot(R_y.dot(R_x))
return R
def cart2spherical_wiki(X_cam):
## from https://en.wikipedia.org/wiki/Spherical_coordinate_system
r = np.sqrt(X_cam[0]**2 + X_cam[1]**2 + X_cam[2]**2)
print(f'manual_{r=}')
#r = np.linalg.norm(X_cam, axis=0)
#print(f'np.linalg_{r=}')
theta = np.arccos(X_cam[2] / r)
phi = -np.arctan2(X_cam[1], X_cam[0])
return theta, phi
def cart2spherical_blender(X_cam, fov):
# can be ignored for the moment
r = np.arctan2(np.sqrt(X_cam[1] * X_cam[1] + X_cam[2] * X_cam[2]), X_cam[0]) / fov
phi = np.arctan2(X_cam[2], X_cam[1])
return r, phi
def spherical2normimg(f_x, f_y, theta, phi):
# from phd-thesis michel
x_img_norm = f_x * theta * np.cos(phi)
y_img_norm = f_y * theta * np.sin(phi)
#print(x_img_norm)
#print(y_img_norm)
return x_img_norm, y_img_norm
def spherical2normimg_blender(r, phi):
# can be ignored for the moment
x_img_norm = r * np.cos(phi) + 0.5
y_img_norm = r * np.sin(phi) + 0.5
return x_img_norm, y_img_norm
def points_to_image(x, y, resolution):
# Sensorpunkt (0,0) liegt im Bild an Position (w/2, h/2)
# deshalb Addition der halben Bildauflösung
# (x,y) - Sensorpunkte
# (x_new, y_new) - Bildpunkte
x_new = x + resolution[0]//2
y_new = y + resolution[1]//2
return x_new, y_new
def main():
# plot input/output?
plot = 'True'
## World points
X_wrld = np.array([
[1.5, 5, 1], # right foot
[2.5, 5, 1], # left foot
[2, 5, 3], # hip
[1, 5, 5], # right hand
[3, 5, 5] # left hand
])
## Camera
C = np.array([2, 1, 4]).transpose() # camera projection center in world coordinates
## Plot World with 3D Points and Camera
fig = plt.figure()
if plot == 'True':
title = 'World coordinate system: 3D Points and Camera'
plt1 = plotting.plot_world(X_wrld, C, elev=0, azim=-90, roll=0, title=title, fig=fig)
# transpose X_wrld
X_wrld = X_wrld.T # take the transpose
# add ones for homogenious representation
row_ones = np.ones([1, X_wrld.shape[1]])
X_wrld_hom = np.concatenate([X_wrld, row_ones], axis=0)
#print(f'{X_wrld_hom=}')
## Use Rotation-Matrix and C for creating H-Matrix (Homography, 4x4)
# orientation / rotation
alpha_deg = 0 # first: rotate about alpha degrees around the camera's x-axis
beta_deg = 270 # second: rotate about beta degrees around the camera's y-axis
gamma_deg = 0 # third: rotate about gamma degrees around the camera's z-axis
# Compute 3x3 Rotation Matrix
R = compute_R(alpha_deg, beta_deg, gamma_deg)
#print(f'{R=}')
# Build H-Matrix (4x4)
# RC ist 3x1
RC = np.zeros([3,1])
RC = R.dot(C)
RC = RC.T
H = np.column_stack( (R, -RC) )
H = np.row_stack( (H, [0, 0, 0, 1]) )
#print(f'{H=}')
# Transformation from world to camera coordinates
X_cam = H.dot(X_wrld_hom)
print(f'{X_cam=}')
if plot == 'True':
title = f'camera coordinate system: camera rotation of {alpha_deg=}, {beta_deg=}, {gamma_deg=}'
#plt2 = plotting.plot_camera(X_cam=X_cam.T, C=[0, 0, 0], elev=180, azim=90, roll=90, title=title, fig=fig)
plt2 = plotting.plot_camera(X_cam=X_cam.T, C=[0, 0, 0], elev=alpha_deg, azim=beta_deg, roll=gamma_deg, title=title, fig=fig)
### Intrinsics ###
# focal length from Canon FD 1:5,6 / 7,5mm (see, e.g. https://de.wikipedia.org/wiki/Fischaugenobjektiv)
f_mm = 0.0075 #7.5mm
# field of view
fov = 180 * np.pi / 180
# Vollformat Sensor, e.g. https://de.wikipedia.org/wiki/Sony_Alpha_1
chip_w = 35.7 * 1e-3 # 36mm
chip_h = 23.8 * 1e-3 # 24mm
# Resolution of camera
res_w = 8640 # px
res_h = 5760 # px
# pixel densities
m_x = res_w / chip_w # px/mm
m_y = res_h / chip_h # px/mm
# focal length normalized with pixel densities
f_x = f_mm * m_x # unitless
f_y = f_mm * m_y # unitless, f_x != f_y if pixel on the sensor is not square
### Intrinsics End ###
## From Camera coordinates (carthesian) to spherical camera coordinates (a) Wiki
theta, phi = cart2spherical_wiki(X_cam)
## From spherical camera coordinates to normalized image coordinates (a) Wiki
x_img_norm, y_img_norm = spherical2normimg(f_x, f_y, theta, phi)
# camera to spherical from blender implementation
#r, phi = cart2spherical_blender(X_cam, fov)
# spherical to normalized image coordinates from blender code
#x_img_norm, y_img_norm = spherical2normimg_blender(r, phi)
if plot == 'True':
title = 'Sensor coordinate system: Camera points on sensor plane'
plt3 = plotting.plot_sensor(x_img_norm=x_img_norm, y_img_norm=y_img_norm, title=title, fig=fig)
# From normalized image coordinates to image coordinates by shifting to left top of sensor
x_img, y_img = points_to_image(x_img_norm, y_img_norm, (res_w, res_h))
if plot == 'True':
title = 'Image coordinate system: points on image plane shifted to left top corner'
plotting.plot_image(x_img, y_img, (0, res_w), (0, res_h), title, fig)
plt.show()
if __name__ == "__main__":
main()
plotting.py:
#!/usr/bin/env python3
import numpy as np
np.set_printoptions(precision=4, suppress=True)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
def plot_world(X_wrld, C, elev, azim, roll, title, fig):
plt1 = fig.add_subplot(2, 2, 1, projection='3d')
colors = ['r', 'b', 'g', 'm', 'y']
labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']
for X,Y,Z,c,l in zip(X_wrld[:,0], X_wrld[:,1], X_wrld[:,2], colors, labels):
plt1.plot3D(X, Y, Z, c=c, marker=".", linestyle='None', label=l)
plt1.plot3D(C[0], C[1], C[2], c="red", marker="v", linestyle='None', label="camera")
plt1.set(xlabel='X', ylabel='Y', zlabel='Z')
plt1.set_xlim(0, 4)
plt1.set_ylim(0, 6)
plt1.set_zlim(0, 6)
plt1.legend()
plt1.set_title(title)
plt1.view_init(elev=elev, azim=azim, roll=roll, vertical_axis='z')
return plt1
def plot_camera(X_cam, C, elev, azim, roll, title, fig):
plt2 = fig.add_subplot(2, 2, 2, projection='3d')
colors = ['r', 'b', 'g', 'm', 'y']
labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']
for X,Y,Z,c,l in zip(X_cam[:,0], X_cam[:,1], X_cam[:,2], colors, labels):
plt2.plot3D(X, Y, Z, c=c, marker=".", linestyle='None', label=l)
plt2.plot3D(C[0], C[1], C[2], c="red", marker="v", linestyle='None', label="camera")
plt2.set(xlabel='X', ylabel='Y', zlabel='Z')
plt2.legend()
plt2.set_title(title)
plt2.view_init(elev=elev, azim=azim, roll=roll, vertical_axis='z')
return plt2
def plot_sensor(x_img_norm, y_img_norm, title, fig):
plt3 = fig.add_subplot(2, 2, 3)
colors = ['r', 'b', 'g', 'm', 'y']
labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']
for x, y, c, l in zip(x_img_norm, y_img_norm, colors, labels):
plt3.plot(x, y, c=c, marker=".", linestyle='None', label=l)
plt3.set_title(title)
return plt3
def plot_image(x_img_norm, y_img_norm, limit_x, limit_y, title, fig):
plt4 = fig.add_subplot(2, 2, 4)
colors = ['r', 'b', 'g', 'm', 'y']
labels = ['right foot', 'left foot', 'hip', 'right hand', 'left hand']
for x, y, c, l in zip(x_img_norm, y_img_norm, colors, labels):
plt4.plot(x, y, marker='.', label= l, linestyle='None', c=c)
plt4.set(xlim=limit_x, ylim=limit_y)
plt4.set_title(title)
plt4.legend()
return plt4
The blend-file, where I have tried to simulate the same five points and render it with a equidistant camera, you can download in the following post:
[1] https://blender.stackexchange.com/q/280271/158481
I've tried different combinations of rotations (R_x, R_y, R_z), see script below. I've compared the result of a rendered output from blender with the output of my simulation, where differences lie in a reflection on the vertical axis in the image coordinate system.
I'm trying to add more mass to the system when the time advanced. I'm trying to obtain the solution for a decay equation:
\frac{dy}{dt} = -0.32 * y
The initial condition y(t=0) = 100. I want also at t=6 to add 100 to the solution. I tried doing this in events but without any luck. Any suggestions?
def one(t, y, ke):
ydot = -ke * y
return ydot
def dose(t, y, ke):
return y[0] + 100*(t==6)
tspan = [0.0, 12.0]
teval = np.array([0, 2, 4, 6,8, 10, 12])
y0 = [100.0]
ke = 0.32
D = 100
sol = solve_ivp(one, tspan, y0, t_eval=teval, args=(ke,), events=(dose), dense_output=True)
sol.y
plt.plot(sol.t, sol.y[0])
Thanks!
A way to change the output at a given point is to add an impulse to its derivative, here an example with a gaussian pulse (sigma adjusts the width of the pulse). I also used BDF method instead of the default Runge-Kutta, in order to be able to handle narrower pulses, but the limitation of this approach is that a very narrow pulse will not be noticed by the ODE integration
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
def one(t, y, ke):
sigma = 0.1
ydot = -ke * y + 100/(sigma*np.sqrt(2*np.pi))*np.exp(-(t - 6)**2/(2*sigma**2))
return ydot
tspan = [0.0, 12.0]
teval = np.array([0, 2, 4, 6,8, 10, 12])
y0 = [100.0]
ke = 0.32
D = 100
sol = solve_ivp(one, tspan, y0, t_eval=np.linspace(0, 12, 1000),
args=(ke,), dense_output=True, method='BDF')
sol.y
plt.plot(sol.t, sol.y[0])
I was trying to an example of the book -"Dynamical Systems with Applications using Python" and I was asked to plot the phase portrait of Verhulst equation, then I came across this post: How to plot a phase portrait of Verhulst equation with SciPy (or SymPy) and Matplotlib?
I'm getting the same plot as the user on the previous post. Whenever, I try to use the accepted solution I get a "division by zero" error. Why doesn't the accepted solution in How to plot a phase portrait of Verhulst equation with SciPy (or SymPy) and Matplotlib? works?
Thank you very much for you help!
Edit:
Using the code from the previous post and the correction given by #Lutz Lehmann
beta, delta, gamma = 1, 2, 1
b,d,c = 1,2,1
C1 = gamma*c-delta*d
C2 = gamma*b-beta*d
C3 = beta*c-delta*b
def verhulst(X, t=0):
return np.array([beta*X[0] - delta*X[0]**2 -gamma*X[0]*X[1],
b*X[1] - d*X[1]**2 -c*X[0]*X[1]])
X_O = np.array([0., 0.])
X_R = np.array([C2/C1, C3/C1])
X_P = np.array([0, b/d])
X_Q = np.array([beta/delta, 0])
def jacobian(X, t=0):
return np.array([[beta-delta*2*X[0]-gamma*X[1], -gamma*x[0]],
[b-d*2*X[1]-c*X[0], -c*X[1]]])
values = np.linspace(0.3, 0.9, 5)
vcolors = plt.cm.autumn_r(np.linspace(0.3, 1., len(values)))
f2 = plt.figure(figsize=(4,4))
for v, col in zip(values, vcolors):
X0 = v * X_R
X = odeint(verhulst, X0, t)
plt.plot(X[:,0], X[:,1], color=col, label='X0=(%.f, %.f)' % ( X0[0], X0[1]) )
ymax = plt.ylim(ymin=0)[1]
xmax = plt.xlim(xmin=0)[1]
nb_points = 20
x = np.linspace(0, xmax, nb_points)
y = np.linspace(0, ymax, nb_points)
X1, Y1 = np.meshgrid(x, y)
DX1, DY1 = verhulst([X1, Y1]) # compute growth rate on the gridt
M = (np.hypot(DX1, DY1)) # Norm of the growth rate
M[M == 0] = 1. # Avoid zero division errors
DX1 /= M # Normalize each arrows
DY1 /= M
plt.quiver(X1, Y1, DX1, DY1, M, cmap=plt.cm.jet)
plt.xlabel('Number of Species 1')
plt.ylabel('Number of Species 2')
plt.legend()
plt.grid()
We have:
That is still different from:
What am I missing?
With the help of #Lutz Lehmann I could rewrite the code to get want I needed.
The solutions is something like this:
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 4), dpi= 80, facecolor='whitesmoke', edgecolor='k')
beta, delta, gamma = 1, 2, 1
b,d,c = 1,2,1
t = np.linspace(0, 10, 100)
C1 = gamma*c-delta*d
C2 = gamma*b-beta*d
C3 = beta*c-delta*b
def verhulst(X, t=0):
return np.array([beta*X[0] - delta*X[0]**2 -gamma*X[0]*X[1],
b*X[1] - d*X[1]**2 -c*X[0]*X[1]])
X_O = np.array([0., 0.])
X_R = np.array([C2/C1, C3/C1])
X_P = np.array([0, b/d])
X_Q = np.array([beta/delta, 0])
def jacobian(X, t=0):
return np.array([[beta-delta*2*X[0]-gamma*X[1], -gamma*x[0]],
[b-d*2*X[1]-c*X[0], -c*X[1]]])
values = np.linspace(0.05, 0.15, 5)
vcolors = plt.cm.autumn_r(np.linspace(0.3, 1., len(values)))
for v, col in zip(values, vcolors):
X0 = [v,0.2-v]
X = odeint(verhulst, X0, t)
plt.plot(X[:,0], X[:,1], color=col, label='X0=(%.f, %.f)' % ( X0[0], X0[1]) )
for v, col in zip(values, vcolors):
X0 = [6 * v, 6 *(0.2-v)]
X = odeint(verhulst, X0, t)
plt.plot(X[:,0], X[:,1], color=col, label='X0=(%.f, %.f)' % ( X0[0], X0[1]) )
ymax = plt.ylim(ymin=0)[1]
xmax = plt.xlim(xmin=0)[1]
nb_points = 20
x = np.linspace(0, xmax, nb_points)
y = np.linspace(0, ymax, nb_points)
X1, Y1 = np.meshgrid(x, y)
DX1, DY1 = verhulst([X1, Y1]) # compute growth rate on the gridt
M = (np.hypot(DX1, DY1)) # Norm of the growth rate
M[M == 0] = 1. # Avoid zero division errors
DX1 /= M # Normalize each arrows
DY1 /= M
plt.quiver(X1, Y1, DX1, DY1, M, cmap=plt.cm.jet)
plt.xlabel('Number of Species 1')
plt.ylabel('Number of Species 2')
plt.grid()
We get what we were looking for:
Finally, I would like to thank again #Lutz Lehnmann for the help. I wouldn't have solved without it him.
Edit 1:
I forgot $t = np.linspace(0, 10, 100)$ and if you change figsize = (8,8), we get a nicer shape in the plot. (Thank you #Trenton McKinney for the remarks)
I have to find the distance between two points inside the cluster. I have generated the clusters through the following code:
X = np.arange(0,10.2,0.16)
Y = np.arange(0,10.2,0.16)
A = np.arange(0,2, 0.005)
x, y = np.meshgrid(X, Y)
z = np.zeros(x.shape)
x0 = X.mean()
y0 = Y.mean()
b_center1 = [2,8]
b_center2 = [6,2]
r1=0.1*X.max()
r2=0.1*X.max()
for i in range(1, len(X)):
for j in range(1, len(Y)):
dist = pdist(np.array([[X[i],Y[j]],b_center1]))
if (dist[0] < r1):
z[i][j]=1
dist = pdist(np.array([[X[i],Y[j]],b_center2]))
if (dist[0] < r2):
z[i][j]=1
fig = plt.figure(figsize=(12,10))
plt.scatter(x, y, z, cmap='viridis', edgecolor='k')
plt.gca().set_aspect('equal', adjustable='box')
plt.draw()
plt.xticks([0,1,2,3,4,5,6,7,8,9,10])
plt.yticks([0,1,2,3,4,5,6,7,8,9,10])
plt.show()
This is the clusters I have obtained
d is the distance I have to find
Taking the z array, which is 64*64 matric with 0 and 1.
Get the x_index, y_index where z==1
np.where(z == 1)
(array([ 7, 7, 7, 7, 7, ...),
array([47, 48, 49, 50, 51, ...]))
So it can be seen, for x=7, z=1 for y=47, y=48, y=50 etc.
So we have to calculate the distance between (7,47) and (7,48)
So, the distance is Y[48] - Y[47] or Y[49] - Y[48]
print(Y[48] - Y[47])
>>> 0.15999999999999925 # i.e. around 0.16
And that is simply because you are line spacing by 0.16, your points are separated by 0.16, so you don't have to calculate at all. :p
X = np.arange(0,10.2,0.16)
Y = np.arange(0,10.2,0.16)
A = np.arange(0,2, 0.005)
Using NumPy's polyfit (or something similar) is there an easy way to get a solution where one or more of the coefficients are constrained to a specific value?
For example, we could find the ordinary polynomial fitting using:
x = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([0.0, 0.8, 0.9, 0.1, -0.8, -1.0])
z = np.polyfit(x, y, 3)
yielding
array([ 0.08703704, -0.81349206, 1.69312169, -0.03968254])
But what if I wanted the best fit polynomial where the third coefficient (in the above case z[2]) was required to be 1? Or will I need to write the fitting from scratch?
In this case, I would use curve_fit or lmfit; I quickly show it for the first one.
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
def func(x, a, b, c, d):
return a + b * x + c * x ** 2 + d * x ** 3
x = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([0.0, 0.8, 0.9, 0.1, -0.8, -1.0])
print(np.polyfit(x, y, 3))
popt, _ = curve_fit(func, x, y)
print(popt)
popt_cons, _ = curve_fit(func, x, y, bounds=([-np.inf, 2, -np.inf, -np.inf], [np.inf, 2.001, np.inf, np.inf]))
print(popt_cons)
xnew = np.linspace(x[0], x[-1], 1000)
plt.plot(x, y, 'bo')
plt.plot(xnew, func(xnew, *popt), 'k-')
plt.plot(xnew, func(xnew, *popt_cons), 'r-')
plt.show()
This will print:
[ 0.08703704 -0.81349206 1.69312169 -0.03968254]
[-0.03968254 1.69312169 -0.81349206 0.08703704]
[-0.14331349 2. -0.95913556 0.10494372]
So in the unconstrained case, polyfit and curve_fit give identical results (just the order is different), in the constrained case, the fixed parameter is 2, as desired.
The plot looks then as follows:
In lmfit you can also choose whether a parameter should be fitted or not, so you can then also just set it to a desired value (check this answer).
For completeness, with lmfit the solution would look like this:
import numpy as np
import matplotlib.pyplot as plt
from lmfit import Model
def func(x, a, b, c, d):
return a + b * x + c * x ** 2 + d * x ** 3
x = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([0.0, 0.8, 0.9, 0.1, -0.8, -1.0])
pmodel = Model(func)
params = pmodel.make_params(a=1, b=2, c=1, d=1)
params['b'].vary = False
result = pmodel.fit(y, params, x=x)
print(result.fit_report())
xnew = np.linspace(x[0], x[-1], 1000)
ynew = result.eval(x=xnew)
plt.plot(x, y, 'bo')
plt.plot(x, result.best_fit, 'k-')
plt.plot(xnew, ynew, 'r-')
plt.show()
which would print a comprehensive report, including uncertainties, correlations and fit statistics as:
[[Model]]
Model(func)
[[Fit Statistics]]
# fitting method = leastsq
# function evals = 10
# data points = 6
# variables = 3
chi-square = 0.066
reduced chi-square = 0.022
Akaike info crit = -21.089
Bayesian info crit = -21.714
[[Variables]]
a: -0.14331348 +/- 0.109441 (76.37%) (init= 1)
b: 2 (fixed)
c: -0.95913555 +/- 0.041516 (4.33%) (init= 1)
d: 0.10494371 +/- 0.008231 (7.84%) (init= 1)
[[Correlations]] (unreported correlations are < 0.100)
C(c, d) = -0.987
C(a, c) = -0.695
C(a, d) = 0.610
and produce a plot of
Note that lmfit.Model has many improvements over curve_fit, including automatically naming parameters based on function arguments, allowing any parameter to have bounds or simply be fixed without requiring nonsense like having upper and lower bounds that are almost equal. The key is that lmfit uses Parameter objects that have attributes instead of plain arrays of fitting variables. lmfit also supports mathematical constraints, composite models (eg, adding or multiplying models), and has superior reports.
Sorry for the resurrection
..but I felt that this answer was missing.
To fit a polynomial we solve the following system of equations:
a0*x0^n + a1*x0^(n-1) .. + an*x0^0 = y0
a0*x1^n + a1*x1^(n-1) .. + an*x1^0 = y1
...
a0*xm^n + a1*xm^(n-1) .. + an*xm^0 = ym
Which is a problem of the form V # a = y
where "V" is a Vandermonde matrix:
[[x0^n x0^(n-1) 1],
[x1^n x1^(n-1) 1],
...
[xm^n xm^(n-1) 1]]
"y" is a column vector holding the y-values:
[[y0],
[y1],
...
[ym]]
..and "a" is the column vector of coefficients that we are solving for:
[[a0],
[a1],
...
[an]]
This problem can be solved using linear least squares as follows:
import numpy as np
x = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([0.0, 0.8, 0.9, 0.1, -0.8, -1.0])
deg = 3
V = np.vander(x, deg + 1)
z, *_ = np.linalg.lstsq(V, y, rcond=None)
print(z)
# [ 0.08703704 -0.81349206 1.69312169 -0.03968254]
..which produces the same solution as the polyfit method:
z = np.polyfit(x, y, deg)
print(z)
# [ 0.08703704 -0.81349206 1.69312169 -0.03968254]
Instead we want a solution where a2 = 1
substituting a2 = 1 into the system of equations from the beginning of the answer, and then moving the corresponding term from the lhs to the rhs we get:
a0*x0^n + a1*x0^(n-1) + 1*x0^(n-2) .. + an*x0^0 = y0
a0*x1^n + a1*x1^(n-1) + 1*x0^(n-2) .. + an*x1^0 = y1
...
a0*xm^n + a1*xm^(n-1) + 1*x0^(n-2) .. + an*xm^0 = ym
=>
a0*x0^n + a1*x0^(n-1) .. + an*x0^0 = y0 - 1*x0^(n-2)
a0*x1^n + a1*x1^(n-1) .. + an*x1^0 = y1 - 1*x0^(n-2)
...
a0*xm^n + a1*xm^(n-1) .. + an*xm^0 = ym - 1*x0^(n-2)
This corresponds to removing column 2 from the Vandermonde matrix and subtracting it from the y-vector as follows:
y_ = y - V[:, 2]
V_ = np.delete(V, 2, axis=1)
z_, *_ = np.linalg.lstsq(V_, y_, rcond=None)
z_ = np.insert(z_, 2, 1)
print(z_)
# [ 0.04659264 -0.48453866 1. 0.19438046]
Notice that I inserted the 1 in the coefficient vector after solving the linear least-squares problem, we are no longer solving for a2 since we set it to 1 and removed it from the problem.
For completeness this is what the solution looks like when plotted:
and the complete code that I used:
import numpy as np
x = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([0.0, 0.8, 0.9, 0.1, -0.8, -1.0])
deg = 3
V = np.vander(x, deg + 1)
z, *_ = np.linalg.lstsq(V, y, rcond=None)
print(z)
# [ 0.08703704 -0.81349206 1.69312169 -0.03968254]
z = np.polyfit(x, y, deg)
print(z)
# [ 0.08703704 -0.81349206 1.69312169 -0.03968254]
y_ = y - V[:, 2]
V_ = np.delete(V, 2, axis=1)
z_, *_ = np.linalg.lstsq(V_, y_, rcond=None)
z_ = np.insert(z_, 2, 1)
print(z_)
# [ 0.04659264 -0.48453866 1. 0.19438046]
from matplotlib import pyplot as plt
plt.plot(x, y, 'o', label='data')
plt.plot(x, V # z, label='polyfit')
plt.plot(x, V # z_, label='constrained (a2 = 0)')
plt.legend()
plt.show()
Here is a way to do this using scipy.optimize.curve_fit:
First, let's recreate your example (as a sanity check):
import numpy as np
from scipy.optimize import curve_fit
def f(x, x3, x2, x1, x0):
"""this is the polynomial function"""
return x0 + x1*x + x2*(x*x) + x3*(x*x*x)
popt, pcov = curve_fit(f, x, y)
print(popt)
#array([ 0.08703704, -0.81349206, 1.69312169, -0.03968254])
Which matches the values you get from np.polyfit().
Now adding the constraints for x1:
popt, pcov = curve_fit(
f,
x,
y,
bounds = ([-np.inf, -np.inf, .999999999, -np.inf], [np.inf, np.inf, 1.0, np.inf])
)
print(popt)
#array([ 0.04659264, -0.48453866, 1. , 0.19438046])
I had to use .999999999 because the lower bound must be strictly less than the upper bound.
Alternatively, you could define your function with the constrained coefficient as a constant, and get the values for the other 3:
def f_new(x, x3, x2, x0):
x1 = 1
return x0 + x1*x + x2*(x*x) + x3*(x*x*x)
popt, pcov = curve_fit(f_new, x, y)
print(popt)
#array([ 0.04659264, -0.48453866, 0.19438046])
Here is also a way by using scipy.optimize.curve_fit but aiming to fix whatever the polynomial coefficients are desired. (The code is not so long after removing the comments.)
The guy that does the job:
import numpy as np
from scipy.optimize import curve_fit
def polyfit(x, y, deg, which=-1, to=0):
"""
An extension of ``np.polyfit`` to fix values of the vector
of polynomial coefficients. By default, the last coefficient
(i.e., the constant term) is kept at zero.
Parameters
----------
x : array_like
x-coordinates of the sample points.
y : array_like
y-coordinates of the sample points.
deg : int
Degree of the fitting polynomial.
which : int or array_like, optional
Indexes of the coefficients to remain fixed. By default, -1.
to : float or array_like, optional
Values of the fixed coefficients. By default, 0.
Returns
-------
np.ndarray
(deg + 1) polynomial coefficients.
"""
p0 = np.polyfit(x, y, deg)
# if which == None it is reduced to np.polyfit
if which is None:
return p0
# indexes of the coeffs being fitted
which_not = np.delete(np.arange(deg + 1), which)
# create the array of coeffs
def _fill_p(p):
p_ = np.empty(deg + 1) # empty array
p_[which] = to # fill with custom coeffs
p_[which_not] = p # fill with `p`
return p_
# callback function for fitting
def _polyfit(x, *p):
p_ = _fill_p(p)
return np.polyval(p_, x)
# get the array of coeffs
p0 = np.delete(p0, which) # use `p0` as initial condition
p, _ = curve_fit(_polyfit, x, y, p0=p0) # fitting
p = _fill_p(p) # merge fixed and no-fixed coeffs
return p
Two simple examples on how to use the function above:
import matplotlib.pyplot as plt
# just create some fake data (a parabola)
np.random.seed(0) # seed to reproduce the example
deg = 2 # second order polynomial
p = np.random.randint(1, 5, size=deg+1) # random vector of coefficients
x = np.linspace(0, 10, num=20) # fake data: x-array
y = np.polyval(p, x) + 1.5*np.random.randn(20) # fake data: y-array
print(p) # output:[1, 4, 2]
# fitting
p1 = polyfit(x, y, deg, which=2, to=p[2]) # case 1: last coeff is fixed
p2 = polyfit(x, y, deg, which=[1,2], to=p[1:3]) # case 2: last two coeffs are fixed
y1 = np.polyval(p1, x) # y-array for case 1
y2 = np.polyval(p2, x) # y-array for case 2
print(p1) # output: [1.05, 3.67, 2.]
print(p2) # output: [1.08, 4., 2.]
# plotting
plt.plot(x, y, '.', label='fake data: y = p[0]*x**2 + p[1]*x + p[2]')
plt.plot(x, y1, label='p[2] fixed at 2')
plt.plot(x, y2, label='p[2] and p[1] fixed at [4, 2]')
plt.legend()
plt.show()