How to plot geodesic curves on a surface embedded in 3D? - python
I have in mind this video, or this simulation, and I would like to reproduce the geodesic lines on some sort of surface in 3D, given by a function f(x,y), from some starting point.
The midpoint method seems computationally and code intense, and so I'd like to ask if there is a way to generate an approximate geodesic curve based on the normal vector to the surface at different points. Each point has a tangent vector space associated with it, and therefore, it seems like knowing the normal vector does not determine a specific direction to move forward the curve.
I have tried working with Geogebra, but I realize that it may be necessary to shift to other software platforms, such as Python (or Poser?), Matlab, or others.
Is this idea possible, and can I get some ideas as to how to implement it?
In case it provides some ideas as to how to answer the question, there previously was an answer (now unfortunatley erased) suggesting the midpoint method for a terrain with the functional form z = F(x,y), starting with the straight line between the endpoints, splitting in short segments [I presume the straight line on the XY plane (?)], and lifting [I presume the nodes between segments on the XY plane (?)] on the surface. Next it suggested finding "a midpoint" [I guess a midpoint of the segments joining each consecutive pairs of projected points on the surface (?)], and projecting "it" [I guess each one of these midpoints close, but not quite on the surface(?)] orthogonally on the surface (in the direction of the normal), using the equation Z + t = F(X + t Fx, Y + t Fy) [I guess this is a dot product meant to be zero...
(?)], where (X,Y,Z) are the coordinates of the midpoint, Fx, Fy the partial derivatives of F, and t the unknown [that is my main issue understanding this... What am I supposed to do with this t once I find it? Add it to each coordinate of (X,Y,Z) as in (X+t, Y+t, Z+t)? And then?]. This is a non-linear equation in t, solved via Newton's iterations.
As an update / bookmark, Alvise Vianello has kindly posted a Python computer simulation of geodesic lines inspired on this page on GitHub. Thank you very much!
I have an approach that should be applicable to an arbitrary 3D surface, even when that surface has holes in it or is noisy. It's pretty slow right now, but it seems to work and may give you some ideas for how to do this.
The basic premise is a differential geometric one and is to:
1.) Generate a pointset representing your surface
2.) Generate a k nearest neighbors proximity graph from this pointset (I also normalized distances across dimensions here as I felt it captured the notion of "neighbors" more accurately)
3.) Calculate the tangent spaces associated with each node in this proximity graph by using the point and its neighbors as columns of a matrix that I then perform SVD on. After SVD, the left singular vectors give me a new basis for my tangent space (the first two column vectors are my plane vectors, and the third is normal to the plane)
4.) Use dijkstra's algorithm to move from a starting node to an ending node on this proximity graph, but instead of using euclidean distance as edge weights, use the distance between vectors being parallel transported via tangent spaces.
It's inspired by this paper (minus all the unfolding): https://arxiv.org/pdf/1806.09039.pdf
Note that I left a few helper functions I was using in that probably aren't relevant to you directly (the plane plotting stuff mostly).
The functions you'll want to look at are get_knn, build_proxy_graph, generate_tangent_spaces, and geodesic_single_path_dijkstra.
The implementation could also probably be improved.
Here's the code:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mayavi import mlab
from sklearn.neighbors import NearestNeighbors
from scipy.linalg import svd
import networkx as nx
import heapq
from collections import defaultdict
def surface_squares(x_min, x_max, y_min, y_max, steps):
x = np.linspace(x_min, x_max, steps)
y = np.linspace(y_min, y_max, steps)
xx, yy = np.meshgrid(x, y)
zz = xx**2 + yy**2
return xx, yy, zz
def get_meshgrid_ax(x, y, z):
# fig = plt.figure()
# ax = fig.gca(projection='3d')
# ax.plot_surface(X=x, Y=y, Z=z)
# return ax
fig = mlab.figure()
su = mlab.surf(x.T, y.T, z.T, warp_scale=0.1)
def get_knn(flattened_points, num_neighbors):
# need the +1 because each point is its own nearest neighbor
knn = NearestNeighbors(num_neighbors+1)
# normalize flattened points when finding neighbors
neighbor_flattened = (flattened_points - np.min(flattened_points, axis=0)) / (np.max(flattened_points, axis=0) - np.min(flattened_points, axis=0))
knn.fit(neighbor_flattened)
dist, indices = knn.kneighbors(neighbor_flattened)
return dist, indices
def rotmatrix(axis, costheta):
""" Calculate rotation matrix
Arguments:
- `axis` : Rotation axis
- `costheta` : Rotation angle
"""
x, y, z = axis
c = costheta
s = np.sqrt(1-c*c)
C = 1-c
return np.matrix([[x*x*C+c, x*y*C-z*s, x*z*C+y*s],
[y*x*C+z*s, y*y*C+c, y*z*C-x*s],
[z*x*C-y*s, z*y*C+x*s, z*z*C+c]])
def plane(Lx, Ly, Nx, Ny, n, d):
""" Calculate points of a generic plane
Arguments:
- `Lx` : Plane Length first direction
- `Ly` : Plane Length second direction
- `Nx` : Number of points, first direction
- `Ny` : Number of points, second direction
- `n` : Plane orientation, normal vector
- `d` : distance from the origin
"""
x = np.linspace(-Lx/2, Lx/2, Nx)
y = np.linspace(-Ly/2, Ly/2, Ny)
# Create the mesh grid, of a XY plane sitting on the orgin
X, Y = np.meshgrid(x, y)
Z = np.zeros([Nx, Ny])
n0 = np.array([0, 0, 1])
# Rotate plane to the given normal vector
if any(n0 != n):
costheta = np.dot(n0, n)/(np.linalg.norm(n0)*np.linalg.norm(n))
axis = np.cross(n0, n)/np.linalg.norm(np.cross(n0, n))
rotMatrix = rotmatrix(axis, costheta)
XYZ = np.vstack([X.flatten(), Y.flatten(), Z.flatten()])
X, Y, Z = np.array(rotMatrix*XYZ).reshape(3, Nx, Ny)
eps = 0.000000001
dVec = d #abs((n/np.linalg.norm(n)))*d#np.array([abs(n[i])/np.linalg.norm(n)*val if abs(n[i]) > eps else val for i, val in enumerate(d)]) #
X, Y, Z = X+dVec[0], Y+dVec[1], Z+dVec[2]
return X, Y, Z
def build_proxy_graph(proxy_n_dist, proxy_n_indices):
G = nx.Graph()
for distance_list, neighbor_list in zip(proxy_n_dist, proxy_n_indices):
# first element is always point
current_node = neighbor_list[0]
neighbor_list = neighbor_list[1:]
distance_list = distance_list[1:]
for neighbor, dist in zip(neighbor_list, distance_list):
G.add_edge(current_node, neighbor, weight=dist)
return G
def get_plane_points(normal_vec, initial_point, min_range=-10, max_range=10, steps=1000):
steps_for_plane = np.linspace(min_range, max_range, steps)
xx, yy = np.meshgrid(steps_for_plane, steps_for_plane)
d = -initial_point.dot(normal_vec)
eps = 0.000000001
if abs(normal_vec[2]) < eps and abs(normal_vec[1]) > eps:
zz = (-xx*normal_vec[2] - yy*normal_vec[0] - d)/normal_vec[1]
else:
zz = (-xx*normal_vec[0] - yy*normal_vec[1] - d)/normal_vec[2]
return xx, yy, zz
# def plot_tangent_plane_at_point(pointset, flattened_points, node, normal_vec):
# ax = get_meshgrid_ax(x=pointset[:, :, 0], y=pointset[:, :, 1], z=pointset[:, :, 2])
# node_loc = flattened_points[node]
# print("Node loc: {}".format(node_loc))
# xx, yy, zz = plane(10, 10, 500, 500, normal_vec, node_loc)
# # xx, yy, zz = get_plane_points(normal_vec, node_loc)
# print("Normal Vec: {}".format(normal_vec))
# ax.plot_surface(X=xx, Y=yy, Z=zz)
# ax.plot([node_loc[0]], [node_loc[1]], [node_loc[2]], markerfacecolor='k', markeredgecolor='k', marker='o', markersize=10)
# plt.show()
def generate_tangent_spaces(proxy_graph, flattened_points):
# This depth should gaurantee at least 16 neighbors
tangent_spaces = {}
for node in proxy_graph.nodes():
neighbors = list(nx.neighbors(proxy_graph, node))
node_point = flattened_points[node]
zero_mean_mat = np.zeros((len(neighbors)+1, len(node_point)))
for i, neighbor in enumerate(neighbors):
zero_mean_mat[i] = flattened_points[neighbor]
zero_mean_mat[-1] = node_point
zero_mean_mat = zero_mean_mat - np.mean(zero_mean_mat, axis=0)
u, s, v = svd(zero_mean_mat.T)
# smat = np.zeros(u.shape[0], v.shape[0])
# smat[:s.shape[0], :s.shape[0]] = np.diag(s)
tangent_spaces[node] = u
return tangent_spaces
def geodesic_single_path_dijkstra(flattened_points, proximity_graph, tangent_frames, start, end):
# short circuit
if start == end:
return []
# Create min priority queue
minheap = []
pred = {}
dist = defaultdict(lambda: 1.0e+100)
# for i, point in enumerate(flattened_points):
R = {}
t_dist = {}
geo_dist = {}
R[start] = np.eye(3)
t_dist[start] = np.ones((3,))
dist[start] = 0
start_vector = flattened_points[start]
for neighbor in nx.neighbors(proxy_graph, start):
pred[neighbor] = start
dist[neighbor] = np.linalg.norm(start_vector - flattened_points[neighbor])
heapq.heappush(minheap, (dist[neighbor], neighbor))
while minheap:
r_dist, r_ind = heapq.heappop(minheap)
if r_ind == end:
break
q_ind = pred[r_ind]
u, s, v = svd(tangent_frames[q_ind].T*tangent_frames[r_ind])
R[r_ind] = np.dot(R[q_ind], u * v.T)
t_dist[r_ind] = t_dist[q_ind]+np.dot(R[q_ind], tangent_frames[q_ind].T * (r_dist - dist[q_ind]))
geo_dist[r_ind] = np.linalg.norm(t_dist[r_ind])
for neighbor in nx.neighbors(proxy_graph, r_ind):
temp_dist = dist[r_ind] + np.linalg.norm(flattened_points[neighbor] - flattened_points[r_ind])
if temp_dist < dist[neighbor]:
dist[neighbor] = temp_dist
pred[neighbor] = r_ind
heapq.heappush(minheap, (dist[neighbor], neighbor))
# found ending index, now loop through preds for path
current_ind = end
node_path = [end]
while current_ind != start:
node_path.append(pred[current_ind])
current_ind = pred[current_ind]
return node_path
def plot_path_on_surface(pointset, flattened_points, path):
# ax = get_meshgrid_ax(x=pointset[:, :, 0], y=pointset[:, :, 1], z=pointset[:, :, 2])
# ax.plot(points_in_path[:, 0], points_in_path[:, 1], points_in_path[:, 2], linewidth=10.0)
# plt.show()
get_meshgrid_ax(x=pointset[:, :, 0], y=pointset[:, :, 1], z=pointset[:, :, 2])
points_in_path = flattened_points[path]
mlab.plot3d(points_in_path[:, 0], points_in_path[:, 1], points_in_path[:, 2] *.1)
mlab.show()
"""
True geodesic of graph.
Build proximity graph
Find tangent space using geodisic neighborhood at each point in graph
Parallel transport vectors between tangent space points
Use this as your distance metric
Dijkstra's Algorithm
"""
if __name__ == "__main__":
x, y, z = surface_squares(-5, 5, -5, 5, 500)
# plot_meshgrid(x, y, z)
pointset = np.stack([x, y, z], axis=2)
proxy_graph_num_neighbors = 16
flattened_points = pointset.reshape(pointset.shape[0]*pointset.shape[1], pointset.shape[2])
flattened_points = flattened_points
proxy_n_dist, proxy_n_indices = get_knn(flattened_points, proxy_graph_num_neighbors)
# Generate a proximity graph using proxy_graph_num_neighbors
# Nodes = number of points, max # of edges = number of points * num_neighbors
proxy_graph = build_proxy_graph(proxy_n_dist, proxy_n_indices)
# Now, using the geodesic_num_neighbors, get geodesic neighborshood for tangent space construction
tangent_spaces = generate_tangent_spaces(proxy_graph, flattened_points)
node_to_use = 2968
# 3rd vector of tangent space is normal to plane
# plot_tangent_plane_at_point(pointset, flattened_points, node_to_use, tangent_spaces[node_to_use][:, 2])
path = geodesic_single_path_dijkstra(flattened_points, proxy_graph, tangent_spaces, 250, 249750)
plot_path_on_surface(pointset, flattened_points, path)
Note that I installed and set up mayavi to get a decent output image (matplotlib doesn't have real 3d rendering and consequently, its plots suck). I did however leave the matplotlib code in if you want to use it. If you do, just remove the scaling by .1 in the path plotter and uncomment the plotting code. Anyways, here's an example image for z=x^2+y^2. The white line is the geodesic path:
You could also fairly easily adjust this to return all the pairwise geodesic distances between nodes from dijkstra's algorithm (look in the appendix of the paper to see the minor modifications you'll need to do this). Then you could draw whatever lines you want on your surface.
Using the midpoint search method:
applied to the function f(x,y) = x^3 + y^2, I am projecting the points of the line segment on the XY plane y = x from x = -1 to x = 1.
To get an idea, with one iteration and only 4 points on the line on the XY plane, the black spheres are these 4 original points of the line projected on the surface, while the red dots are the midpoints in a single iteration, and the yellow dots the result of the projection of the red dots along the normal to the surface:
Using Matlab fmincon() and after 5 iterations we can get a geodesic from point A to point B:
Here is the code:
% Creating the surface
x = linspace(-1,1);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = x.^3 + y.^2;
S = [x;y;z];
h = surf(x,y,z)
set(h,'edgecolor','none')
colormap summer
% Number of points
n = 1000;
% Line to project on the surface with n values to get a feel for it...
t = linspace(-1,1,n);
height = t.^3 + t.^2;
P = [t;t;height];
% Plotting the projection of the line on the surface:
hold on
%plot3(P(1,:),P(2,:),P(3,:),'o')
for j=1:5
% First midpoint iteration updates P...
P = [P(:,1), (P(:,1:end-1) + P(:,2:end))/2, P(:,end)];
%plot3(P(1,:), P(2,:), P(3,:), '.', 'MarkerSize', 20)
A = zeros(3,size(P,2));
for i = 1:size(P,2)
% Starting point will be the vertical projection of the mid-points:
A(:,i) = [P(1,i), P(2,i), P(1,i)^3 + P(2,i)^2];
end
% Linear constraints:
nonlincon = #nlcon;
% Placing fmincon in a loop for all the points
for i = 1:(size(A,2))
% Objective function:
objective = #(x)(P(1,i) - x(1))^2 + (P(2,i) - x(2))^2 + (P(3,i)-x(3))^2;
A(:,i) = fmincon(objective, A(:,i), [], [], [], [], [], [], nonlincon);
end
P = A;
end
plot3(P(1,:), P(2,:), P(3,:), '.', 'MarkerSize', 5,'Color','y')
In a separate file with name nlcon.m:
function[c,ceq] = nlcon(x)
c = [];
ceq = x(3) - x(1)^3 - x(2)^2;
Same for a geodesic on a really cool surface with a straight, non-diagonal line on XY:
% Creating the surface
x = linspace(-1,1);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = sin(3*(x.^2+y.^2))/10;
S = [x;y;z];
h = surf(x,y,z)
set(h,'edgecolor','none')
colormap summer
% Number of points
n = 1000;
% Line to project on the surface with n values to get a feel for it...
t = linspace(-1,1,n);
height = sin(3*((.5*ones(1,n)).^2+ t.^2))/10;
P = [(.5*ones(1,n));t;height];
% Plotting the line on the surface:
hold on
%plot3(P(1,:),P(2,:),P(3,:),'o')
for j=1:2
% First midpoint iteration updates P...
P = [P(:,1), (P(:,1:end-1) + P(:,2:end))/2, P(:,end)];
%plot3(P(1,:), P(2,:), P(3,:), '.', 'MarkerSize', 20)
A = zeros(3,size(P,2));
for i = 1:size(P,2)
% Starting point will be the vertical projection of the first mid-point:
A(:,i) = [P(1,i), P(2,i), sin(3*(P(1,i)^2+ P(2,i)^2))/10];
end
% Linear constraints:
nonlincon = #nonlincon;
% Placing fmincon in a loop for all the points
for i = 1:(size(A,2))
% Objective function:
objective = #(x)(P(1,i) - x(1))^2 + (P(2,i) - x(2))^2 + (P(3,i)-x(3))^2;
A(:,i) = fmincon(objective, A(:,i), [], [], [], [], [], [], nonlincon);
end
P = A;
end
plot3(P(1,:), P(2,:), P(3,:), '.', 'MarkerSize',5,'Color','r')
with the nonlinear constraint in nonlincon.m:
function[c,ceq] = nlcon(x)
c = [];
ceq = x(3) - sin(3*(x(1)^2+ x(2)^2))/10;
One nagging concern is the possibility of overfitting to the curve with this method, and this latter plot is an example of it. So I adjusted the code to just select one beginning and one ending point, and allowing the iterative process to find the rest of the curve, which for 100 iterations seemed to be heading in the right direction:
The above examples seem to follow a linear projection on the XY plane, but fortunately this is not a fixed pattern, which would cast further doubt on the method. See for instance the hyperbolic paraboloid x^2 - y^2:
Notice that there are algorithms to advance or push geodesic lines along a surface f(x,y) with small increments determined by the starting points and the normal vector to the surface, as in here. Thanks to the work of Alvise Vianello looking into the JS in that simulation and his sharing in GitHub, I was able to turn that algorithm into Matlab code, generating this plot for the first example, f(x,y) = x^3 + y^2:
Here is the Matlab code:
x = linspace(-1,1);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = x.^3 + y.^2;
S = [x;y;z];
h = surf(x,y,z)
set(h,'edgecolor','none')
colormap('gray');
hold on
f = #(x,y) x.^3 + y.^2; % The actual surface
dfdx = #(x,y) (f(x + eps, y) - f(x - eps, y))/(2 * eps); % ~ partial f wrt x
dfdy = #(x,y) (f(x, y + eps) - f(x, y - eps))/(2 * eps); % ~ partial f wrt y
N = #(x,y) [- dfdx(x,y), - dfdy(x,y), 1]; % Normal vec to surface # any pt.
C = {'k','b','r','g','y','m','c',[.8 .2 .6],[.2,.8,.1],[0.3010 0.7450 0.9330],[0.9290 0.6940 0.1250],[0.8500 0.3250 0.0980]}; % Color scheme
for s = 1:11 % No. of lines to be plotted.
start = -5:5; % Distributing the starting points of the lines.
y0 = start(s)/5; % Fitting the starting pts between -1 and 1 along y axis.
x0 = 1; % Along x axis always starts at 1.
dx0 = 0; % Initial differential increment along x
dy0 = 0.05; % Initial differential increment along y
step_size = 0.000008; % Will determine the progression rate from pt to pt.
eta = step_size / sqrt(dx0^2 + dy0^2); % Normalization.
eps = 0.0001; % Epsilon
max_num_iter = 100000; % Number of dots in each line.
x = [[x0, x0 + eta * dx0], zeros(1,max_num_iter - 2)]; % Vec of x values
y = [[y0, y0 + eta * dy0], zeros(1,max_num_iter - 2)]; % Vec of y values
for i = 2:(max_num_iter - 1) % Creating the geodesic:
xt = x(i); % Values at point t of x, y and the function:
yt = y(i);
ft = f(xt,yt);
xtm1 = x(i - 1); % Values at t minus 1 (prior point) for x,y,f
ytm1 = y(i - 1);
ftm1 = f(xtm1,ytm1);
xsymp = xt + (xt - xtm1); % Adding the prior difference forward:
ysymp = yt + (yt - ytm1);
fsymp = ft + (ft - ftm1);
df = fsymp - f(xsymp,ysymp); % Is the surface changing? How much?
n = N(xt,yt); % Normal vector at point t
gamma = df * n(3); % Scalar x change f x z value of N
xtp1 = xsymp - gamma * n(1); % Gamma to modulate incre. x & y.
ytp1 = ysymp - gamma * n(2);
x(i + 1) = xtp1;
y(i + 1) = ytp1;
end
P = [x; y; f(x,y)]; % Compiling results into a matrix.
indices = find(abs(P(1,:)) < 1); % Avoiding lines overshooting surface.
P = P(:,indices);
indices = find(abs(P(2,:)) < 1);
P = P(:,indices);
units = 15; % Deternines speed (smaller, faster)
packet = floor(size(P,2)/units);
P = P(:,1: packet * units);
for k = 1:packet:(packet * units)
hold on
plot3(P(1, k:(k+packet-1)), P(2,(k:(k+packet-1))), P(3,(k:(k+packet-1))),...
'.', 'MarkerSize', 3.5,'color',C{s})
drawnow
end
end
And here is an earlier example from above, but now calculated differently, and with lines starting side by side, following geodesics (no point-to-point trajectory):
x = linspace(-1,1);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = sin(3*(x.^2+y.^2))/10;
S = [x;y;z];
h = surf(x,y,z)
set(h,'edgecolor','none')
colormap('gray');
hold on
f = #(x,y) sin(3*(x.^2+y.^2))/10; % The actual surface
dfdx = #(x,y) (f(x + eps, y) - f(x - eps, y))/(2 * eps); % ~ partial f wrt x
dfdy = #(x,y) (f(x, y + eps) - f(x, y - eps))/(2 * eps); % ~ partial f wrt y
N = #(x,y) [- dfdx(x,y), - dfdy(x,y), 1]; % Normal vec to surface # any pt.
C = {'k','r','g','y','m','c',[.8 .2 .6],[.2,.8,.1],[0.3010 0.7450 0.9330],[0.7890 0.5040 0.1250],[0.9290 0.6940 0.1250],[0.8500 0.3250 0.0980]}; % Color scheme
for s = 1:11 % No. of lines to be plotted.
start = -5:5; % Distributing the starting points of the lines.
x0 = -start(s)/5; % Fitting the starting pts between -1 and 1 along y axis.
y0 = -1; % Along x axis always starts at 1.
dx0 = 0; % Initial differential increment along x
dy0 = 0.05; % Initial differential increment along y
step_size = 0.00005; % Will determine the progression rate from pt to pt.
eta = step_size / sqrt(dx0^2 + dy0^2); % Normalization.
eps = 0.0001; % Epsilon
max_num_iter = 100000; % Number of dots in each line.
x = [[x0, x0 + eta * dx0], zeros(1,max_num_iter - 2)]; % Vec of x values
y = [[y0, y0 + eta * dy0], zeros(1,max_num_iter - 2)]; % Vec of y values
for i = 2:(max_num_iter - 1) % Creating the geodesic:
xt = x(i); % Values at point t of x, y and the function:
yt = y(i);
ft = f(xt,yt);
xtm1 = x(i - 1); % Values at t minus 1 (prior point) for x,y,f
ytm1 = y(i - 1);
ftm1 = f(xtm1,ytm1);
xsymp = xt + (xt - xtm1); % Adding the prior difference forward:
ysymp = yt + (yt - ytm1);
fsymp = ft + (ft - ftm1);
df = fsymp - f(xsymp,ysymp); % Is the surface changing? How much?
n = N(xt,yt); % Normal vector at point t
gamma = df * n(3); % Scalar x change f x z value of N
xtp1 = xsymp - gamma * n(1); % Gamma to modulate incre. x & y.
ytp1 = ysymp - gamma * n(2);
x(i + 1) = xtp1;
y(i + 1) = ytp1;
end
P = [x; y; f(x,y)]; % Compiling results into a matrix.
indices = find(abs(P(1,:)) < 1); % Avoiding lines overshooting surface.
P = P(:,indices);
indices = find(abs(P(2,:)) < 1);
P = P(:,indices);
units = 35; % Deternines speed (smaller, faster)
packet = floor(size(P,2)/units);
P = P(:,1: packet * units);
for k = 1:packet:(packet * units)
hold on
plot3(P(1, k:(k+packet-1)), P(2,(k:(k+packet-1))), P(3,(k:(k+packet-1))), '.', 'MarkerSize', 5,'color',C{s})
drawnow
end
end
Some more examples:
x = linspace(-1,1);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = x.^2 - y.^2;
S = [x;y;z];
h = surf(x,y,z)
set(h,'edgecolor','none')
colormap('gray');
f = #(x,y) x.^2 - y.^2; % The actual surface
dfdx = #(x,y) (f(x + eps, y) - f(x - eps, y))/(2 * eps); % ~ partial f wrt x
dfdy = #(x,y) (f(x, y + eps) - f(x, y - eps))/(2 * eps); % ~ partial f wrt y
N = #(x,y) [- dfdx(x,y), - dfdy(x,y), 1]; % Normal vec to surface # any pt.
C = {'b','w','r','g','y','m','c',[0.75, 0.75, 0],[0.9290, 0.6940, 0.1250],[0.3010 0.7450 0.9330],[0.1290 0.6940 0.1250],[0.8500 0.3250 0.0980]}; % Color scheme
for s = 1:11 % No. of lines to be plotted.
start = -5:5; % Distributing the starting points of the lines.
x0 = -start(s)/5; % Fitting the starting pts between -1 and 1 along y axis.
y0 = -1; % Along x axis always starts at 1.
dx0 = 0; % Initial differential increment along x
dy0 = 0.05; % Initial differential increment along y
step_size = 0.00005; % Will determine the progression rate from pt to pt.
eta = step_size / sqrt(dx0^2 + dy0^2); % Normalization.
eps = 0.0001; % Epsilon
max_num_iter = 100000; % Number of dots in each line.
x = [[x0, x0 + eta * dx0], zeros(1,max_num_iter - 2)]; % Vec of x values
y = [[y0, y0 + eta * dy0], zeros(1,max_num_iter - 2)]; % Vec of y values
for i = 2:(max_num_iter - 1) % Creating the geodesic:
xt = x(i); % Values at point t of x, y and the function:
yt = y(i);
ft = f(xt,yt);
xtm1 = x(i - 1); % Values at t minus 1 (prior point) for x,y,f
ytm1 = y(i - 1);
ftm1 = f(xtm1,ytm1);
xsymp = xt + (xt - xtm1); % Adding the prior difference forward:
ysymp = yt + (yt - ytm1);
fsymp = ft + (ft - ftm1);
df = fsymp - f(xsymp,ysymp); % Is the surface changing? How much?
n = N(xt,yt); % Normal vector at point t
gamma = df * n(3); % Scalar x change f x z value of N
xtp1 = xsymp - gamma * n(1); % Gamma to modulate incre. x & y.
ytp1 = ysymp - gamma * n(2);
x(i + 1) = xtp1;
y(i + 1) = ytp1;
end
P = [x; y; f(x,y)]; % Compiling results into a matrix.
indices = find(abs(P(1,:)) < 1); % Avoiding lines overshooting surface.
P = P(:,indices);
indices = find(abs(P(2,:)) < 1);
P = P(:,indices);
units = 45; % Deternines speed (smaller, faster)
packet = floor(size(P,2)/units);
P = P(:,1: packet * units);
for k = 1:packet:(packet * units)
hold on
plot3(P(1, k:(k+packet-1)), P(2,(k:(k+packet-1))), P(3,(k:(k+packet-1))), '.', 'MarkerSize', 5,'color',C{s})
drawnow
end
end
Or this one:
x = linspace(-1,1);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = .07 * (.1 + x.^2 + y.^2).^(-1);
S = [x;y;z];
h = surf(x,y,z)
zlim([0 8])
set(h,'edgecolor','none')
colormap('gray');
axis off
hold on
f = #(x,y) .07 * (.1 + x.^2 + y.^2).^(-1); % The actual surface
dfdx = #(x,y) (f(x + eps, y) - f(x - eps, y))/(2 * eps); % ~ partial f wrt x
dfdy = #(x,y) (f(x, y + eps) - f(x, y - eps))/(2 * eps); % ~ partial f wrt y
N = #(x,y) [- dfdx(x,y), - dfdy(x,y), 1]; % Normal vec to surface # any pt.
C = {'w',[0.8500, 0.3250, 0.0980],[0.9290, 0.6940, 0.1250],'g','y','m','c',[0.75, 0.75, 0],'r',...
[0.56,0,0.85],'m'}; % Color scheme
for s = 1:10 % No. of lines to be plotted.
start = -9:2:9;
x0 = -start(s)/10;
y0 = -1; % Along x axis always starts at 1.
dx0 = 0; % Initial differential increment along x
dy0 = 0.05; % Initial differential increment along y
step_size = 0.00005; % Will determine the progression rate from pt to pt.
eta = step_size / sqrt(dx0^2 + dy0^2); % Normalization.
eps = 0.0001; % EpsilonA
max_num_iter = 500000; % Number of dots in each line.
x = [[x0, x0 + eta * dx0], zeros(1,max_num_iter - 2)]; % Vec of x values
y = [[y0, y0 + eta * dy0], zeros(1,max_num_iter - 2)]; % Vec of y values
for i = 2:(max_num_iter - 1) % Creating the geodesic:
xt = x(i); % Values at point t of x, y and the function:
yt = y(i);
ft = f(xt,yt);
xtm1 = x(i - 1); % Values at t minus 1 (prior point) for x,y,f
ytm1 = y(i - 1);
ftm1 = f(xtm1,ytm1);
xsymp = xt + (xt - xtm1); % Adding the prior difference forward:
ysymp = yt + (yt - ytm1);
fsymp = ft + (ft - ftm1);
df = fsymp - f(xsymp,ysymp); % Is the surface changing? How much?
n = N(xt,yt); % Normal vector at point t
gamma = df * n(3); % Scalar x change f x z value of N
xtp1 = xsymp - gamma * n(1); % Gamma to modulate incre. x & y.
ytp1 = ysymp - gamma * n(2);
x(i + 1) = xtp1;
y(i + 1) = ytp1;
end
P = [x; y; f(x,y)]; % Compiling results into a matrix.
indices = find(abs(P(1,:)) < 1.5); % Avoiding lines overshooting surface.
P = P(:,indices);
indices = find(abs(P(2,:)) < 1);
P = P(:,indices);
units = 15; % Deternines speed (smaller, faster)
packet = floor(size(P,2)/units);
P = P(:,1: packet * units);
for k = 1:packet:(packet * units)
hold on
plot3(P(1, k:(k+packet-1)), P(2,(k:(k+packet-1))), P(3,(k:(k+packet-1))),...
'.', 'MarkerSize', 3.5,'color',C{s})
drawnow
end
end
Or a sinc function:
x = linspace(-10, 10);
y = linspace(-10, 10);
[x,y] = meshgrid(x,y);
z = sin(1.3*sqrt (x.^ 2 + y.^ 2) + eps)./ (sqrt (x.^ 2 + y.^ 2) + eps);
S = [x;y;z];
h = surf(x,y,z)
set(h,'edgecolor','none')
colormap('gray');
axis off
hold on
f = #(x,y) sin(1.3*sqrt (x.^ 2 + y.^ 2) + eps)./ (sqrt (x.^ 2 + y.^ 2) + eps); % The actual surface
dfdx = #(x,y) (f(x + eps, y) - f(x - eps, y))/(2 * eps); % ~ partial f wrt x
dfdy = #(x,y) (f(x, y + eps) - f(x, y - eps))/(2 * eps); % ~ partial f wrt y
N = #(x,y) [- dfdx(x,y), - dfdy(x,y), 1]; % Normal vec to surface # any pt.
C = {'w',[0.8500, 0.3250, 0.0980],[0.9290, 0.6940, 0.1250],'g','y','r','c','m','w',...
[0.56,0,0.85],[0.8500, 0.7250, 0.0980],[0.2290, 0.1940, 0.6250],'w',...
[0.890, 0.1940, 0.4250],'y',[0.2290, 0.9940, 0.3250],'w',[0.1500, 0.7250, 0.0980],...
[0.8500, 0.3250, 0.0980],'m','w'}; % Color scheme
for s = 1:12 % No. of lines to be plotted.
x0 = 10;
y0 = 10; % Along x axis always starts at 1.
dx0 = -0.001*(cos(pi /2 *s/11)); % Initial differential increment along x
dy0 = -0.001*(sin(pi /2 *s/11)); % Initial differential increment along y
step_size = 0.0005; % Will determine the progression rate from pt to pt.
% Making it smaller increases the length of the curve.
eta = step_size / sqrt(dx0^2 + dy0^2); % Normalization.
eps = 0.0001; % EpsilonA
max_num_iter = 500000; % Number of dots in each line.
x = [[x0, x0 + eta * dx0], zeros(1,max_num_iter - 2)]; % Vec of x values
y = [[y0, y0 + eta * dy0], zeros(1,max_num_iter - 2)]; % Vec of y values
for i = 2:(max_num_iter - 1) % Creating the geodesic:
xt = x(i); % Values at point t of x, y and the function:
yt = y(i);
ft = f(xt,yt);
xtm1 = x(i - 1); % Values at t minus 1 (prior point) for x,y,f
ytm1 = y(i - 1);
ftm1 = f(xtm1,ytm1);
xsymp = xt + (xt - xtm1); % Adding the prior difference forward:
ysymp = yt + (yt - ytm1);
fsymp = ft + (ft - ftm1);
df = fsymp - f(xsymp,ysymp); % Is the surface changing? How much?
n = N(xt,yt); % Normal vector at point t
gamma = df * n(3); % Scalar x change f x z value of N
xtp1 = xsymp - gamma * n(1); % Gamma to modulate incre. x & y.
ytp1 = ysymp - gamma * n(2);
x(i + 1) = xtp1;
y(i + 1) = ytp1;
end
P = [x; y; f(x,y)]; % Compiling results into a matrix.
indices = find(abs(P(1,:)) < 10); % Avoiding lines overshooting surface.
P = P(:,indices);
indices = find(abs(P(2,:)) < 10);
P = P(:,indices);
units = 15; % Deternines speed (smaller, faster)
packet = floor(size(P,2)/units);
P = P(:,1: packet * units);
for k = 1:packet:(packet * units)
hold on
plot3(P(1, k:(k+packet-1)), P(2,(k:(k+packet-1))), P(3,(k:(k+packet-1))),...
'.', 'MarkerSize', 3.5,'color',C{s})
drawnow
end
end
And one very last one:
x = linspace(-1.5,1.5);
y = linspace(-1,1);
[x,y] = meshgrid(x,y);
z = 0.5 *y.*sin(5 * x) - 0.5 * x.*cos(5 * y)+1.5;
S = [x;y;z];
h = surf(x,y,z)
zlim([0 8])
set(h,'edgecolor','none')
colormap('gray');
axis off
hold on
f = #(x,y) 0.5 *y.* sin(5 * x) - 0.5 * x.*cos(5 * y)+1.5; % The actual surface
dfdx = #(x,y) (f(x + eps, y) - f(x - eps, y))/(2 * eps); % ~ partial f wrt x
dfdy = #(x,y) (f(x, y + eps) - f(x, y - eps))/(2 * eps); % ~ partial f wrt y
N = #(x,y) [- dfdx(x,y), - dfdy(x,y), 1]; % Normal vec to surface # any pt.
C = {'w',[0.8500, 0.3250, 0.0980],[0.9290, 0.6940, 0.1250],'g','y','k','c',[0.75, 0.75, 0],'r',...
[0.56,0,0.85],'m'}; % Color scheme
for s = 1:11 % No. of lines to be plotted.
start = [0, 0.7835, -0.7835, 0.5877, -0.5877, 0.3918, -0.3918, 0.1959, -0.1959, 0.9794, -0.9794];
x0 = start(s);
y0 = -1; % Along x axis always starts at 1.
dx0 = 0; % Initial differential increment along x
dy0 = 0.05; % Initial differential increment along y
step_size = 0.00005; % Will determine the progression rate from pt to pt.
% Making it smaller increases the length of the curve.
eta = step_size / sqrt(dx0^2 + dy0^2); % Normalization.
eps = 0.0001; % EpsilonA
max_num_iter = 500000; % Number of dots in each line.
x = [[x0, x0 + eta * dx0], zeros(1,max_num_iter - 2)]; % Vec of x values
y = [[y0, y0 + eta * dy0], zeros(1,max_num_iter - 2)]; % Vec of y values
for i = 2:(max_num_iter - 1) % Creating the geodesic:
xt = x(i); % Values at point t of x, y and the function:
yt = y(i);
ft = f(xt,yt);
xtm1 = x(i - 1); % Values at t minus 1 (prior point) for x,y,f
ytm1 = y(i - 1);
ftm1 = f(xtm1,ytm1);
xsymp = xt + (xt - xtm1); % Adding the prior difference forward:
ysymp = yt + (yt - ytm1);
fsymp = ft + (ft - ftm1);
df = fsymp - f(xsymp,ysymp); % Is the surface changing? How much?
n = N(xt,yt); % Normal vector at point t
gamma = df * n(3); % Scalar x change f x z value of N
xtp1 = xsymp - gamma * n(1); % Gamma to modulate incre. x & y.
ytp1 = ysymp - gamma * n(2);
x(i + 1) = xtp1;
y(i + 1) = ytp1;
end
P = [x; y; f(x,y)]; % Compiling results into a matrix.
indices = find(abs(P(1,:)) < 1.5); % Avoiding lines overshooting surface.
P = P(:,indices);
indices = find(abs(P(2,:)) < 1);
P = P(:,indices);
units = 15; % Deternines speed (smaller, faster)
packet = floor(size(P,2)/units);
P = P(:,1: packet * units);
for k = 1:packet:(packet * units)
hold on
plot3(P(1, k:(k+packet-1)), P(2,(k:(k+packet-1))), P(3,(k:(k+packet-1))),...
'.', 'MarkerSize', 3.5,'color',C{s})
drawnow
end
end
On a torus this becomes more complicated, and so far I haven't been able to adapt this code to it. As a reference, the code provided by Paul Chesler in here does provide with an option. It involves saving a file tor.m as
function xp=tor(t,x)
xp=zeros(4,1);
xp(1)=x(2);
xp(2)=-(2+cos(x(1)))*sin(x(1))*x(4)^2;
xp(3)=x(4);
xp(4)=2*(sin(x(1))/(2+cos(x(1))))*x(2)*x(4);
and in another file torus.m plot the torus and call the function above to solve a system of second order differential equations. Here it is for different geodesics:
from below:
with torus.m:
[u,v]=meshgrid(linspace(0,2*pi,100),linspace(0,2*pi,100));
x=(2+cos(u)).*cos(v);
y=(2+cos(u)).*sin(v);
z=sin(u);
h=surf(x,y,z);
set(h,'edgecolor','none');
colormap('gray');
daspect([1 1 1])
tspan = linspace(pi,100*pi,1000000);
[t,X]=ode45('tor',tspan ,[pi,.1,-pi/2,.2]);
u=X(:,1);
v=X(:,3);
x=(2+cos(u)).*cos(v);
y=(2+cos(u)).*sin(v);
z=sin(u);
P=[x y z];
disp(size(P))
units = 10; % Deternines speed (smaller, faster)
packet = floor(size(P,1)/units);
for k = 1:packet:(packet * units)
hold on
plot3(P(k:(k+packet-1),1), P((k:(k+packet-1)),2), P((k:(k+packet-1)),3),...
'.-', 'MarkerSize', 3.5,'color','r', 'LineWidth', 3)
drawnow
pause(2)
end
or
[u,v]=meshgrid(linspace(0,2*pi,100),linspace(0,2*pi,100));
x=(2+cos(u)).*cos(v);
y=(2+cos(u)).*sin(v);
z=sin(u);
h=surf(x,y,z);
set(h,'edgecolor','none');
colormap('gray');
daspect([1 1 1])
tspan = linspace(pi,100*pi,1000000);
[t,X]=ode45('tor',tspan ,[pi/6,.1,-pi/2,.2]);
u=X(:,1);
v=X(:,3);
x=(2+cos(u)).*cos(v);
y=(2+cos(u)).*sin(v);
z=sin(u);
P=[x y z];
disp(size(P))
units = 10; % Deternines speed (smaller, faster)
packet = floor(size(P,1)/units);
for k = 1:packet:(packet * units)
hold on
plot3(P(k:(k+packet-1),1), P((k:(k+packet-1)),2), P((k:(k+packet-1)),3),...
'.-', 'MarkerSize', 3.5,'color','m', 'LineWidth', 3)
drawnow
pause(2)
end
or
from below:
tspan = linspace(pi,100*pi,1000000); defines the limits of integration along 1e^6 pts, and [3/4 * pi,.1,-pi/2,.2], the parametric values for a starting point (on the last example).
On a sphere:
code here.
A geodesic on a surface can be found by the variational Euler equations when minimizing the length integral. This yields a system of two second order ODEs in two unknowns. You can readily solve it by a solver such as Runge-Kutta.
https://proofwiki.org/wiki/Geodesic_Equation/2d_Surface_Embedded_in_3d_Euclidean_Space
I have to thank you guys. I have been studying the theory of geodesics and I have searched online a lot. This thread is pricelss. I have to share my thoughts here:
1- The midpoint method seems a bit iffy.
2- If you have a nicely defined surface you can simply solve the geodesic equation numerically using the method presented in Paul Chesler paper and mentioned by Antoni. If solving the second order is hard for your case you can use COMSOL to directly solve 2nd order equation.
3- The path finding method seems quite efficient and general. Since solving the geodesic equation in general cases can be challenging.
4- Here is the MATLAB code for the egg-carton case. The only chalanging part is clearly writing the ODE function. Read Pauls paper for this purpose.
u0=pi/3;
v0=0;
[u,v]=meshgrid(linspace(-2*pi,2*pi,100),linspace(-2*pi,2*pi,100));
x=v;
y=u;
z=sin(u).*cos(v);
mesh(x,y,z)
hold on
[t,X]=ode23s('tor',[0,2*pi],[u0,1/sqrt(2),v0,1/sqrt(2)]);
u=X(:,1);
v=X(:,3);
x=v;
y=u;
z=sin(u).*cos(v);
plot3(x,y,z,'k','linewidth',2)
Create a separate function for the ODE function as follows:
function xp=tor(t,x)
xp=zeros(4,1);
xp(1)=x(2);
xp(2)=-(cos(x(1))*cos(x(3))*(cos(x(3))*sin(x(1))*x(2)^2 + 2*cos(x(1))*sin(x(3))*x(2)*x(4) + cos(x(3))*sin(x(1))*x(4)^2))/(cos(x(1))^2 + cos(x(3))^2 - 2*cos(x(1))^2*cos(x(3))^2 - 2);
xp(3)=x(4);
xp(4)=(sin(x(1))*sin(x(3))*(cos(x(3))*sin(x(1))*x(2)^2 + 2*cos(x(1))*sin(x(3))*x(2)*x(4) + cos(x(3))*sin(x(1))*x(4)^2))/(sin(x(1))^2 + sin(x(3))^2 - 2*sin(x(1))^2*sin(x(3))^2 - 2);
[https://i.stack.imgur.com/BXrMi.png][1]
Related
Deriving Cubic Bezier Curve control points & handles from series of points in Python
I am trying to find the control points and handles of a Cubic Bezier curve from a series of points. My current code is below (credit to Zero Zero on the Python Discord). The Cubic Spline is creating the desired fit, but the handles (in orange) are incorrect. How may I find the handles of this curve? Thank you! import numpy as np import scipy as sp def fit_curve(points): # Fit a cubic bezier curve to the points curve = sp.interpolate.CubicSpline(points[:, 0], points[:, 1], bc_type=((1, 0.0), (1, 0.0))) # Get 4 control points for the curve p = np.zeros((4, 2)) p[0, :] = points[0, :] p[3, :] = points[-1, :] p[1, :] = points[0, :] + 0.3 * (points[-1, :] - points[0, :]) p[2, :] = points[-1, :] - 0.3 * (points[-1, :] - points[0, :]) return p, curve ypoints = [0.0, 0.03771681353260319, 0.20421680080883106, 0.49896111463402026, 0.7183501026981503, 0.8481517096346528, 0.9256128196832564, 0.9705404287079152, 0.9933297674379904, 1.0] xpoints = [x for x in range(len(ypoints))] points = np.array([xpoints, ypoints]).T from scipy.interpolate import splprep, splev tck, u = splprep([xpoints, ypoints], s=0) #print(tck, u) xnew, ynew = splev(np.linspace(0, 1, 100), tck) # Plot the original points and the Bézier curve import matplotlib.pyplot as plt #plt.plot(xpoints, ypoints, 'x', xnew, ynew, xpoints, ypoints, 'b') plt.axis([0, 10, -0.05, 1.05]) plt.legend(['Points', 'Bézier curve', 'True curve']) plt.title('Bézier curve fitting') # Get the curve p, curve = fit_curve(points) # Plot the points and the curve plt.plot(points[:, 0], points[:, 1], 'o') plt.plot(p[:, 0], p[:, 1], 'o') plt.plot(np.linspace(0, 9, 100), curve(np.linspace(0, 9, 100))) plt.show()
The answer for my case was a Bezier best fit function that accepts an input of point values, fits the points to a Cubic Spline, and outputs the Bézier handles of the curve by finding their coefficients. Here is one such script, fitCurves, which can be used like so: import numpy as np from fitCurve import fitCurve import matplotlib.pyplot as plt y = [0.0, 0.03771681353260319, 0.20421680080883106, 0.49896111463402026, 0.7183501026981503, 0.8481517096346528, 0.9256128196832564, 0.9705404287079152, 0.9933297674379904, 1.0] x = np.linspace(0, 1, len(y)) pts = np.array([x,y]).T bezier_handles = fitCurve(points=pts , maxError=20) x_bez = [] y_bez = [] for bez in bezier_handles: for pt in bez: x_bez.append(pt[0]) y_bez.append(pt[1]) plt.plot(pts[:,0], pts[:,1], 'bo-', label='Points') plt.plot(x_bez[:2], y_bez[:2], 'ro--', label='Handle') # handle 1 plt.plot(x_bez[2:4], y_bez[2:4], 'ro--') # handle 2 plt.legend() plt.show() fitCurve.py from numpy import * """ Python implementation of Algorithm for Automatically Fitting Digitized Curves by Philip J. Schneider "Graphics Gems", Academic Press, 1990 """ # evaluates cubic bezier at t, return point def q(ctrlPoly, t): return (1.0-t)**3 * ctrlPoly[0] + 3*(1.0-t)**2 * t * ctrlPoly[1] + 3*(1.0-t)* t**2 * ctrlPoly[2] + t**3 * ctrlPoly[3] # evaluates cubic bezier first derivative at t, return point def qprime(ctrlPoly, t): return 3*(1.0-t)**2 * (ctrlPoly[1]-ctrlPoly[0]) + 6*(1.0-t) * t * (ctrlPoly[2]-ctrlPoly[1]) + 3*t**2 * (ctrlPoly[3]-ctrlPoly[2]) # evaluates cubic bezier second derivative at t, return point def qprimeprime(ctrlPoly, t): return 6*(1.0-t) * (ctrlPoly[2]-2*ctrlPoly[1]+ctrlPoly[0]) + 6*(t) * (ctrlPoly[3]-2*ctrlPoly[2]+ctrlPoly[1]) # Fit one (ore more) Bezier curves to a set of points def fitCurve(points, maxError): leftTangent = normalize(points[1] - points[0]) rightTangent = normalize(points[-2] - points[-1]) return fitCubic(points, leftTangent, rightTangent, maxError) def fitCubic(points, leftTangent, rightTangent, error): # Use heuristic if region only has two points in it if (len(points) == 2): dist = linalg.norm(points[0] - points[1]) / 3.0 bezCurve = [points[0], points[0] + leftTangent * dist, points[1] + rightTangent * dist, points[1]] return [bezCurve] # Parameterize points, and attempt to fit curve u = chordLengthParameterize(points) bezCurve = generateBezier(points, u, leftTangent, rightTangent) # Find max deviation of points to fitted curve maxError, splitPoint = computeMaxError(points, bezCurve, u) if maxError < error: return [bezCurve] # If error not too large, try some reparameterization and iteration if maxError < error**2: for i in range(20): uPrime = reparameterize(bezCurve, points, u) bezCurve = generateBezier(points, uPrime, leftTangent, rightTangent) maxError, splitPoint = computeMaxError(points, bezCurve, uPrime) if maxError < error: return [bezCurve] u = uPrime # Fitting failed -- split at max error point and fit recursively beziers = [] centerTangent = normalize(points[splitPoint-1] - points[splitPoint+1]) beziers += fitCubic(points[:splitPoint+1], leftTangent, centerTangent, error) beziers += fitCubic(points[splitPoint:], -centerTangent, rightTangent, error) return beziers def generateBezier(points, parameters, leftTangent, rightTangent): bezCurve = [points[0], None, None, points[-1]] # compute the A's A = zeros((len(parameters), 2, 2)) for i, u in enumerate(parameters): A[i][0] = leftTangent * 3*(1-u)**2 * u A[i][1] = rightTangent * 3*(1-u) * u**2 # Create the C and X matrices C = zeros((2, 2)) X = zeros(2) for i, (point, u) in enumerate(zip(points, parameters)): C[0][0] += dot(A[i][0], A[i][0]) C[0][1] += dot(A[i][0], A[i][1]) C[1][0] += dot(A[i][0], A[i][1]) C[1][1] += dot(A[i][1], A[i][1]) tmp = point - q([points[0], points[0], points[-1], points[-1]], u) X[0] += dot(A[i][0], tmp) X[1] += dot(A[i][1], tmp) # Compute the determinants of C and X det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1] det_C0_X = C[0][0] * X[1] - C[1][0] * X[0] det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1] # Finally, derive alpha values alpha_l = 0.0 if det_C0_C1 == 0 else det_X_C1 / det_C0_C1 alpha_r = 0.0 if det_C0_C1 == 0 else det_C0_X / det_C0_C1 # If alpha negative, use the Wu/Barsky heuristic (see text) */ # (if alpha is 0, you get coincident control points that lead to # divide by zero in any subsequent NewtonRaphsonRootFind() call. */ segLength = linalg.norm(points[0] - points[-1]) epsilon = 1.0e-6 * segLength if alpha_l < epsilon or alpha_r < epsilon: # fall back on standard (probably inaccurate) formula, and subdivide further if needed. bezCurve[1] = bezCurve[0] + leftTangent * (segLength / 3.0) bezCurve[2] = bezCurve[3] + rightTangent * (segLength / 3.0) else: # First and last control points of the Bezier curve are # positioned exactly at the first and last data points # Control points 1 and 2 are positioned an alpha distance out # on the tangent vectors, left and right, respectively bezCurve[1] = bezCurve[0] + leftTangent * alpha_l bezCurve[2] = bezCurve[3] + rightTangent * alpha_r return bezCurve def reparameterize(bezier, points, parameters): return [newtonRaphsonRootFind(bezier, point, u) for point, u in zip(points, parameters)] def newtonRaphsonRootFind(bez, point, u): """ Newton's root finding algorithm calculates f(x)=0 by reiterating x_n+1 = x_n - f(x_n)/f'(x_n) We are trying to find curve parameter u for some point p that minimizes the distance from that point to the curve. Distance point to curve is d=q(u)-p. At minimum distance the point is perpendicular to the curve. We are solving f = q(u)-p * q'(u) = 0 with f' = q'(u) * q'(u) + q(u)-p * q''(u) gives u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)| """ d = q(bez, u)-point numerator = (d * qprime(bez, u)).sum() denominator = (qprime(bez, u)**2 + d * qprimeprime(bez, u)).sum() if denominator == 0.0: return u else: return u - numerator/denominator def chordLengthParameterize(points): u = [0.0] for i in range(1, len(points)): u.append(u[i-1] + linalg.norm(points[i] - points[i-1])) for i, _ in enumerate(u): u[i] = u[i] / u[-1] return u def computeMaxError(points, bez, parameters): maxDist = 0.0 splitPoint = len(points)/2 for i, (point, u) in enumerate(zip(points, parameters)): dist = linalg.norm(q(bez, u)-point)**2 if dist > maxDist: maxDist = dist splitPoint = i return maxDist, splitPoint def normalize(v): return v / linalg.norm(v)
progressive iteration approximation(PIA) method for bspline not working?
I am new to this, and tried to implement this algorithm by using uniform B-Spline. And I don't know where I did it wrong, the result just doesn't come out the way it supposed to be. I don't know if the basis is wrong or the procedure of the PIA is wrong. Is there someone that could help me out? Thank you so much!! I use Python to implement all this. In my understanding, the PIA is taking the given point set, P, as control points at first(iteration 0), and use these control points to calculate a b-spline, Q. Then find the difference, d, between the P and Q. Let Q+d in each iteration, until d is small enough as the threshold you set at the beginning. I use deboor-Cox algorithm for generating the basis matrix. def b_spline_basis(i, p, u, nodeVector): # p means the degree of the spline if p == 0: if (nodeVector[i] <= u) & (u <= nodeVector[i + 1]): # if u is between two knots, the basis would be 1 result = 1.0 else: result = 0.0 else: # calculate non-zero intervals length1 = nodeVector[i + p] - nodeVector[i] length2 = nodeVector[i + p + 1] - nodeVector[i + 1] # calculate coefficients for basis functions if length1 == 0: # specifically 0/0 alpha = 0 else: alpha = (u - nodeVector[i]) / length1 if length2 == 0: beta = 0 else: beta = (nodeVector[i + p + 1] - u) / length2 # calculate basis functions recursively result = alpha * b_spline_basis(i, p - 1, u, nodeVector) + beta * b_spline_basis(i + 1, p - 1, u, nodeVector) return result And I tried the lemniscate to test whether my implementation of PIA is okay or not. import numpy as np import math from bspline import b_spline import matplotlib.pyplot as plt import matplotlib from bspline_basis import b_spline_basis matplotlib.use('TkAgg') # lemniscate with 200 points alpha = 1 theta = np.linspace(0, 2 * np.pi, num=200) x_real = alpha * np.sqrt(2) * np.cos(theta) / (np.sin(theta) ** 2 + 1) y_real = alpha * np.sqrt(2) * np.cos(theta) * np.sin(theta) / (np.sin(theta) ** 2 + 1) # draw the real points on lemniscate plt.scatter(x_real, y_real) # degree of bspline is 3, number of control points is 8 degree = 3 n = 8 points = [] delta = np.linspace(0, 2 * np.pi, num=8) # x and y are the x-axis and y-axis for the control points x = alpha * np.sqrt(2) * np.cos(delta) / (np.sin(delta) ** 2 + 1) y = alpha * np.sqrt(2) * np.cos(delta) * np.sin(delta) / (np.sin(delta) ** 2 + 1) plt.scatter(x, y, color='maroon') # calculate bspline basis matrix def bspline_basis(n, degree, knotVector): basis = np.zeros([n, n]) for i in range(n): j = 0 for u in delta: basis[i][j] = b_spline_basis(i, degree, u, knotVector) # print('knot=', knot) # print('basis_i=', basis, 'j=',j) j = j + 1 return basis a = min(delta) b = max(delta) knotVector = [a, a, a, a, *delta[2:-2], b, b, b, b] # basis matrix is stored in bs bs = bspline_basis(n, degree, knotVector) # I think if the basis is right, this plot would be a b-spline curve, but it doesn't turn out that way. I'm also confused by this. plt.plot(np.dot(bs, np.transpose(x)), np.dot(bs, np.transpose(y)), color='red') # the difference between real control points with calculated value dx = x - np.transpose(np.dot(bs, np.transpose(x))) dy = y - np.transpose(np.dot(bs, np.transpose(y))) # norm is going to store the norm of (dx, dy) norm = np.zeros(n) for i in range(len(dx)): norm[i] = math.sqrt(dx[i] ** 2 + dy[i] ** 2) # make the biggest norm to be the error err = max(norm) iteration = 0 print('iteration #', iteration, ', err = ', err) # set the threshold for the algorithm to stop tol = 0.2 # in while loop, calculate the difference in each loop, until error is smaller than the threshold while err > tol: iteration = iteration + 1 x = x + dx y = y + dy dx = x - np.transpose(np.dot(bs, np.transpose(x))) dy = y - np.transpose(np.dot(bs, np.transpose(y))) for i in range(len(dx)): norm[i] = math.sqrt(dx[i] ** 2 + dy[i] ** 2) err = max(norm) print('iteration #', iteration, ', err = ', err) x_inter = np.transpose(np.dot(bs, np.transpose(x))) y_inter = np.transpose(np.dot(bs, np.transpose(y))) plt.show() But the result is not even close. The err printed in each iteration gets bigger and bigger. iteration # 0 , err = 0.8978393078534154 iteration # 1 , err = 0.5572305648715149 iteration # 2 , err = 0.8814649114823587 iteration # 3 , err = 1.406648477874589 iteration # 4 , err = 2.2515402019886657 iteration # 5 , err = 3.610001808299592 iteration # 6 , err = 5.794725750733798 iteration # 7 , err = 9.309544995196921 iteration # 8 , err = 14.966156756400013 iteration # 9 , err = 24.072299683891867 iteration # 10 , err = 38.73507669530552 iteration # 11 , err = 62.34988787737978 iteration # 12 , err = 100.3885976037046 iteration # 13 , err = 161.67015869470632 iteration # 14 , err = 260.40916333350236 iteration # 15 , err = 419.5188341631952 iteration # 16 , err = 675.9369969104991 iteration # 17 , err = 1089.2146572938898 iteration # 18 , err = 1755.3667774904786 iteration # 19 , err = 2829.2109590140344 iteration # 20 , err = 4560.398039137244 iteration # 21 , err = 7351.530766709586 iteration # 22 , err = 11851.91790312345 iteration # 23 , err = 19108.824114848438 iteration # 24 , err = 30811.492573031916 iteration # 25 , err = 49684.87189301904 iteration # 26 , err = 80124.93280280002 iteration # 27 , err = 129223.88403951934 iteration # 28 , err = 208424.68577890267 iteration # 29 , err = 336191.3189164541 iteration # 30 , err = 542318.7082430203 iteration # 31 , err = 874889.5879288138 iteration # 32 , err = 1411504.6936387809 iteration # 33 , err = 2277412.443263706 iteration # 34 , err = 3674778.915040246 ... The printed lines are too long, I won't show them all. But you get the point. Beside, the plot is also wierd. And I just don't know where when wrong, and I upset my for days. Is there someone can help with this? Thank you so so much!! I'm really confused right now, hoping there is someone can help me out. TAT
There are a few things we need to take care of. First, I will put b_spline_basis into a separate file. It was almost correct but there are two changes. The intervals where the degree zero basis functions evaluate to 1 had to be adapted so that the basis functions sum up to one on the entire interval [a, b] (your version evaluated to more in the knots). This problem happens quite often, cf. e.g. here. Also, the 0/0 case needed 1 instead of 0 for alpha and beta: def b_spline_basis(i, p, u, knotVector): # p means the degree of the spline if p == 0: # The support is closed from left but open from the right ... if (i != len(knotVector) - 2): if ((knotVector[i] <= u) & (u < knotVector[i + 1])): result = 1.0 else: result = 0.0 # ... unless it is the last one, which is closed from both sides. else: if ((knotVector[i] <= u) & (u <= knotVector[i + 1])): result = 1.0 else: result = 0.0 else: # calculate non-zero intervals length1 = knotVector[i + p] - knotVector[i] length2 = knotVector[i + p + 1] - knotVector[i + 1] # calculate coefficients for basis functions if length1 == 0: # specifically 0/0 alpha = 1 # You had 0 here. else: alpha = (u - knotVector[i]) / length1 if length2 == 0: beta = 1 # You had 0 here as well. else: beta = (knotVector[i + p + 1] - u) / length2 # calculate basis functions recursively result = alpha * b_spline_basis(i, p - 1, u, knotVector) + beta * b_spline_basis(i + 1, p - 1, u, knotVector) return result Second, I put also bspline_basis [sic] into a separate file. It is almost identical to your version but the matrix is not necessarily square in general. I would also strongly advise to rename the function; the resulting matrix is a transpose of what is usually called collocation matrix. import numpy as np from b_spline_basis import b_spline_basis # calculate bspline basis matrix def bspline_basis(n, degree, knotVector, delta): basis = np.zeros([n, delta.size]) for i in range(n): j = 0 for u in delta: basis[i][j] = b_spline_basis(i, degree, u, knotVector) # print('knot=', knot) # print('basis_i=', basis, 'j=',j) j = j + 1 return basis Finally, I throw in a function for plotting a B-spline (as a curve) given its control points etc. import numpy as np from b_spline_basis import b_spline_basis def plot_bspline(plt, num_samples, degree, knotVector, x_cps, y_cps, color): beg = knotVector[0] end = knotVector[-1] num_cps = len(x_cps) x_curve = np.zeros(num_samples) y_curve = np.zeros(num_samples) for i in range(num_samples): for j in range(num_cps): t_loc = i / (num_samples-1) t = beg * (1 - t_loc) + end * t_loc x_curve[i] += x_cps[j] * b_spline_basis(j, degree, t, knotVector) y_curve[i] += y_cps[j] * b_spline_basis(j, degree, t, knotVector) plt.plot(x_curve, y_curve, color=color) Now we get back to your code, the few corrections are commented. In general, there seemed to be three sources of confusion: The results of bspline_basis had to be transposed, because they are a transposed collocation matrix. Evaluating using bspline_basis does not give you a B-spline as a curve but only its values in delta. It is important to distinguish between x_target, y_target (values of the lemniscate that you want to approximate) and x_cps, y_cps (B-spline control points in the current iteration). You called both of them x, y. import numpy as np import math import matplotlib.pyplot as plt import matplotlib matplotlib.use('TkAgg') from b_spline_basis import b_spline_basis from bspline_basis import bspline_basis from plot_bspline import plot_bspline # lemniscate with 200 points alpha = 1 theta = np.linspace(0, 2 * np.pi, num=200) x_real = alpha * np.sqrt(2) * np.cos(theta) / (np.sin(theta) ** 2 + 1) y_real = alpha * np.sqrt(2) * np.cos(theta) * np.sin(theta) / (np.sin(theta) ** 2 + 1) # draw the real points on lemniscate plt.scatter(x_real, y_real) # degree of bspline is 3, number of control points is 8 degree = 3 n = 8 points = [] delta = np.linspace(0, 2 * np.pi, num=8) # x_target and y_target are the values we want to approximate. # They will be used as starting values for the control points as well. x_target = alpha * np.sqrt(2) * np.cos(delta) / (np.sin(delta) ** 2 + 1) y_target = alpha * np.sqrt(2) * np.cos(delta) * np.sin(delta) / (np.sin(delta) ** 2 + 1) plt.scatter(x_target, y_target, color='maroon') a = min(delta) b = max(delta) knotVector = [a, a, a, a, *delta[2:-2], b, b, b, b] # basis matrix is stored in bs bs = bspline_basis(n, degree, knotVector, delta) # I think if the basis is right, this plot would be a b-spline curve, but it doesn't turn out that way. I'm also confused by this. # The transpositions were wrong. # Also, using bs does not give you a B-spline as a curve but only its values evaluated at delta, i.e., at 8 points. plt.plot(np.dot(np.transpose(bs), x_target), np.dot(np.transpose(bs), y_target), color='red') # If you also plot the B-spline as curve that uses x_target and y_target as control points, you will see that the red curve connects 8 of its values. plot_bspline(plt, 100, 3, knotVector, x_target, y_target, 'green') # Now to PIA. # The control points in the first iteration will be the initial values. x_cps = x_target y_cps = y_target # Then we have a difference between the target values and the corresponding values of our B-spline. dx = x_target - np.transpose(np.dot(np.transpose(bs), x_cps)) dy = y_target - np.transpose(np.dot(np.transpose(bs), y_cps)) # norm is going to store the norm of (dx, dy) norm = np.zeros(n) for i in range(len(dx)): norm[i] = math.sqrt(dx[i] ** 2 + dy[i] ** 2) # make the biggest norm to be the error err = max(norm) iteration = 0 print('iteration #', iteration, ', err = ', err) # set the threshold for the algorithm to stop tol = 1e-5 # in while loop, calculate the difference in each loop, until error is smaller than the threshold while err > tol and iteration < 100: iteration = iteration + 1 # We change the control points ... x_cps = x_cps + dx y_cps = y_cps + dy # ... and compute the difference from the target (which is constant)! dx = x_target - np.transpose(np.dot(np.transpose(bs), x_cps)) dy = y_target - np.transpose(np.dot(np.transpose(bs), y_cps)) for i in range(len(dx)): norm[i] = math.sqrt(dx[i] ** 2 + dy[i] ** 2) err = max(norm) print('iteration #', iteration, ', err = ', err) x_inter = np.transpose(np.dot(np.transpose(bs), x_cps)) y_inter = np.transpose(np.dot(np.transpose(bs), y_cps)) # If I plot the way you did, I will again not get a B-spline as a curve but the values of the B-spline evaluated at delta. Notice that it passes the maroon points. plt.plot(x_inter, y_inter, color='yellow') # Let's now plot the entire B-spline as a curve. Notice that it passes through the maroon points. plot_bspline(plt, 100, 3, knotVector, x_cps, y_cps, 'magenta') plt.show() If we now have a look at the plot, there is quite a lot happening: The blue points are the samples of the lemniscate, i.e., the input. The maroon points are the eight points on the lemniscate that we will be approximating (there seem to be only seven, since the first and last ones coincide). The green curve is the initial guess, i.e., a B-spline that uses the maroon points as its control points. The red polygon uses bspline_basis to connect the values of the green B-spline in the parameter values in delta. This is a corrected version of your red curve. The magenta curve is the final guess, i.e., a B-spline that approximates the maroon points up to tol. The yellow curve uses bspline_basis to connect the values of the magenta B-spline along delta. This is a corrected version of your x_inter, y_inter. I wish you good luck with further B-spline experiments. If you are also into neural networks, you might enjoy a recent paper by my friends that investigated the connection between LSPIA and gradient descent: Dany Rios and Bert Jüttler: LSPIA,(stochastic) gradient descent, and parameter correction. Journal of Computational and Applied Mathematics 406 (2022): 113921 (preprint)
In Python: How to make a bifurcation diagram of the Lorenz system under a varying parameter value?
So, I've seen the coded solution to my question in Mathematica, but with very little understanding of mathematica, I havn't been able to reproduce it yet. This is what I'm trying to do with Python: https://mathematica.stackexchange.com/questions/159211/how-to-make-a-bifurcation-diagram-of-the-lorenz-system-under-a-varying-parameter I'm thinking my errors are in understanding how to calculate what I'm looking for and how to adjust my visualization to make it look just like that in the link, but any ideas are welcome. The code I have so far looks like this: def lorenz_system(x,y,z,r,s=10,b=6): x_dot = s*(y-x) y_dot = r*x-y-x*z z_dot = x*z-b*z return x_dot, y_dot, z_dot dr = 0.1 # parameter step size r=np.arange(40,200,dr) # parameter range dt = 0.001 # time step t = np.arange(0,10,dt) # time range #initialize solution arrays xs = np.empty(len(t) + 1) ys = np.empty(len(t) + 1) zs = np.empty(len(t) + 1) #initial values x0,y0,z0 for the system xs[0], ys[0], zs[0] = (1, 1, 1) for R in r: for i in range(len(t)): #approximate numerical solutions to system x_dot, y_dot, z_dot = lorenz_system(xs[i], ys[i], zs[i],R) xs[i + 1] = xs[i] + (x_dot * dt) ys[i + 1] = ys[i] + (y_dot * dt) zs[i + 1] = zs[i] + (z_dot * dt) #calculate and plot the peak values of the z solution for i in range(0,len(zs)-1): #using only the positive values in the z solution if zs[i]>0: #find the local maxima if (zs[i-1] < zs[i] and zs[i] > zs[i+1]): if (zs[i]<=1000): #plot the local maxima point of the z solution that used the parameter R in r plt.scatter(R,zs[i], color='black') plt.xlim(0,200) plt.ylim(0,400)
There is a bug in the lorenz_system function, it should be z_dot = x * y - b * z. The linked answer also "Uses final values from one run as initial conditions for the next as an easy way to stay near the attractor.", and plots both local minima and local maxima. Here is a way to get a similar plot using your code import numpy as np import matplotlib.pyplot as plt def lorenz_system(x, y, z, r, b=10, s=6): x_dot = b * (y - x) y_dot = r * x - y - x * z z_dot = x * y - s * z return x_dot, y_dot, z_dot dr = 0.1 # parameter step size r = np.arange(40, 200, dr) # parameter range dt = 0.001 # time step t = np.arange(0, 10, dt) # time range # initialize solution arrays xs = np.empty(len(t) + 1) ys = np.empty(len(t) + 1) zs = np.empty(len(t) + 1) # initial values x0,y0,z0 for the system xs[0], ys[0], zs[0] = (1, 1, 1) # Save the plot points coordinates and plot the with a single call to plt.plot # instead of plotting them one at a time, as it's much more efficient r_maxes = [] z_maxes = [] r_mins = [] z_mins = [] for R in r: # Print something to show everything is running print(f"{R=:.2f}") for i in range(len(t)): # approximate numerical solutions to system x_dot, y_dot, z_dot = lorenz_system(xs[i], ys[i], zs[i], R) xs[i + 1] = xs[i] + (x_dot * dt) ys[i + 1] = ys[i] + (y_dot * dt) zs[i + 1] = zs[i] + (z_dot * dt) # calculate and save the peak values of the z solution for i in range(1, len(zs) - 1): # save the local maxima if zs[i - 1] < zs[i] and zs[i] > zs[i + 1]: r_maxes.append(R) z_maxes.append(zs[i]) # save the local minima elif zs[i - 1] > zs[i] and zs[i] < zs[i + 1]: r_mins.append(R) z_mins.append(zs[i]) # "use final values from one run as initial conditions for the next to stay near the attractor" xs[0], ys[0], zs[0] = xs[i], ys[i], zs[i] plt.scatter(r_maxes, z_maxes, color="black", s=0.5, alpha=0.2) plt.scatter(r_mins, z_mins, color="red", s=0.5, alpha=0.2) plt.xlim(0, 200) plt.ylim(0, 400) plt.show() Result:
TypeError: can only concatenate list (not "int") to list 4
I'm required to take a Python module for my course and I get this error for my script. It's plotting the trajectory of a projectile and calculating a few other variables. I've typed the script exactly as in the booklet we are given. Because I am an absolute beginner I can't understand other answers to this error. I would appreciate it an awful lot if someone could give me a quick fix, I don't have time at the moment to learn enough to fix it myself. Code: import matplotlib.pyplot as plt import numpy as np import math # need math module for trigonometric functions g = 9.81 #gravitational constant dt = 1e-3 #integration time step (delta t) v0 = 40 # initial speed at t = 0 angle = math.pi/4 #math.pi = 3.14, launch angle in radians time = np.arange(0,10,dt) #time axis vx0 = math.cos(angle)*v0 # starting velocity along x axis vy0 = math.sin(angle)*v0 # starting velocity along y axis xa = vx0*time # compute x coordinates ya = -0.5*g*time**2 + vy0*time # compute y coordinates fig1 = plt.figure() plt.plot(xa, ya) # plot y versus x plt.xlabel ("x") plt.ylabel ("y") plt.ylim(0, 50) plt.show() def traj(angle, v0): # function for trajectory vx0 = math.cos(angle) * v0 # for some launch angle and starting velocity vy0 = math.sin(angle) * v0 # compute x and y component of starting velocity x = np.zeros(len(time)) #initialise x and y arrays y = np.zeros(len(time)) x[0], y[0], 0 #projecitle starts at 0,0 x[1], y[1] = x[0] + vx0 * dt, y[0] + vy0 * dt # second elements of x and # y are determined by initial # velocity i = 1 while y[i] >= 0: # conditional loop continuous until # projectile hits ground x[i+1] = (2 * x[i] - x[i - 1]) # numerical integration to find x[i + 1] y[i+1] = (2 * y[i] - y[i - 1]) - g * dt ** 2 # and y[i + 1] i = [i + 1] # increment i for next loop x = x[0:i+1] # truncate x and y arrays y = y[0:i+1] return x, y, (dt*i), x[i] # return x, y, flight time, range of projectile x, y, duration, distance = traj(angle, v0) print "Distance:" ,distance print "Duration:" ,duration n = 5 angles = np.linspace(0, math.pi/2, n) maxrange = np.zeros(n) for i in range(n): x,y, duration, maxrange [i] = traj(angles[i], v0) angles = angles/2/math.pi*360 #convert rad to degress print "Optimum angle:", angles[np.where(maxrange==np.max(maxrange))] The error explicitly: File "C:/Users/***** at *****", line 52, in traj x = x[0:i+1] # truncate x and y arrays TypeError: can only concatenate list (not "int") to list
As is pointed out in the comments, this is the offending line i = [i + 1] # increment i for next loop Here, i is not actually being incremented as the comment suggests. When i is 1, it's being set to [1 + 1], which evaluates to [2], the list containing only the number 2. Remove the brackets.
computing smooth color map using interpolation recursively
I am computing the mandelbrot set recursively and attempting to perform linear interpolation using the smooth coloring algorithm. However, this returns floating point RGB values which I can't put into the ppm image I am using so I am having to round off using int(), creating a smoother but yet still banded image. Are there any simpler ways that will produce a better non-banded image? The second function is an extremely bad hack just playing around with ideas as the smooth algorithim seems to be producing rgb values in the range 256**3 Commented out the linear interpolation I was doing. Here are my three functions: def linear_interp(self, color_1, color_2, i): r = (color_1[0] * (1 - i)) + (color_2[0] * i) g = (color_1[1] * (1 - i)) + (color_2[1] * i) b = (color_1[2] * (1 - i)) + (color_2[2] * i) return (int(abs(r)), int(abs(g)), int(abs(b))) def mandel(self, x, y, z, iteration = 0): mod_z = sqrt((z.real * z.real) + (z.imag * z.imag)) #If its not in the set or we have reached the maximum depth if abs(z) >= 2.00 or iteration == DEPTH: if iteration == DEPTH: mu = iteration else: mu = iteration + 1 - log(log(mod_z)) / log(2) else: mu = 0 z = (z * z) + self.c self.mandel(x, y, z, iteration + 1) return mu def create_image(self): begin = time.time() #For computing how long it took (start time) self.rgb.palette = [] for y in range(HEIGHT): self.rgb.palette.append([]) #Need to create the rows of our ppm for x in range(WIDTH): self.c = complex(x * ((self.max_a - self.min_a) / WIDTH) + self.min_a, y * ((self.max_b - self.min_b) / HEIGHT) + self.min_b) z = self.c q = (self.c.real - 0.25)**2 + (self.c.imag * self.c.imag) x = self.c.real y2 = self.c.imag * self.c.imag if not (q*(q + (x - 0.25)) < y2 / 4.0 or (x + 1.0)**2 + y2 <0.0625): mu = self.mandel(x, y, z, iteration = 0) rgb = self.linear_interp((255, 255, 0), (55, 55, 0), mu) self.rgb.palette[y].append(rgb) else: self.rgb.palette[y].append((55, 55, 0)) if self.progress_bar != None: self.progress_bar["value"] = y self.canvas.update() The image I am getting is below:
I think this is the culprit: else: mu = 0 self.mandel(x, y, z, iteration + 1) return mu This isn't passing down the value of mu from the recursive call correctly, so you're getting black for everything that doesn't bottom out after 1 call. Try else: ... mu = self.mandel(x, y, z, iteration + 1) return mu