Related
I found this great question with some concise code that, with a couple of tweaks, fits a 3D polynomial surface onto a set of points of in space.
Python 3D polynomial surface fit, order dependent
My version is below.
Ultimately, I've realized that I need to fit a surface over time, i.e. I need to solve for a 4 dimensional surface, and I've struggled with it.
I came up with a very hacky and computationally intensive work-around. I create a surface for each time interval. Then I create a grid of points and find the Z value for each point on each surface. So now I have a bunch of x,y points and each one has a list of z values that need to flow smoothly from one interval to the next. So we do a regression on the z values. Now that the z-flow is smooth, I refit a surface for each time interval based on the x,y points and whatever their smoothed Z value is for the relevant time interval.
Its what it sounds like. Clunky and suboptimal. The resulting surfaces flow more smoothly and still perform okay but there's gotta be a way to cut out the middle man and solve for that 4th dimension directly in the fitSurface function.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import itertools
# Start black magic
def xy_powers(order):
powers = itertools.product(range(order + 1), range(order + 1))
return [tup for tup in powers if sum(tup) <= order]
def fitSurface(x, y, z, order):
ncols = (order + 1)**2
G = np.zeros((x.size, ncols))
ij = xy_powers(order)
for k, (i,j) in enumerate(ij):
G[:,k] = x**i * y**j
m, _, _, _ = np.linalg.lstsq(G, z, rcond=None)
return m
def getZValuesForXYInputs(surface, order, x, y):
order = int(np.sqrt(len(surface))) - 1
ij = xy_powers(order)
z = np.zeros_like(x)
for a, (i,j) in zip(surface, ij):
z += a * x**i * y**j
return z
# End black magic
def showRender_3D(x_raw, y_raw, z_raw, xx, yy, zz):
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(x_raw, y_raw, z_raw, color='red', zorder=0)
ax.plot_surface(xx, yy, zz, zorder=10, alpha=0.4)
ax.set_xlabel('X data')
ax.set_ylabel('Y data')
ax.set_zlabel('Z data')
plt.show()
def main(order):
# Make generic data
numdata = 100
x = np.random.random(numdata)
y = np.random.random(numdata)
z = x**2 + y**2 + 3*x**3 + y + np.random.random(numdata)
t = np.random.randint(1, 4, numdata) # Break the data into
# Fit the surface
m = fitSurface(x, y, z, order)
# Sample the surface at regular points so we can more easily plot the surface
nx, ny = 40, 40
xx, yy = np.meshgrid(np.linspace(x.min(), x.max(), nx),
np.linspace(y.min(), y.max(), ny))
zz = getZValuesForXYInputs(m, order, xx, yy)
# Plot it
showRender_3D(x, y, z, xx, yy, zz)
orderForSurfaceFit = 3
main(orderForSurfaceFit)
Alright so I think I got this dialed in. I wont go over the how, other than to say that once you study the code enough the black magic doesn't go away but patterns do emerge. I just extended those patterns and it looks like it works.
End result
Admittedly this is so low res that it look like its not changing from C=1 to C=2 but it is. Load it up and you'll see. The gif should show the flow more cleary now.
First the methodology behind the proof. I found a funky surface equation and added a third input variable C (in-effect creating a 4D surface), then studied the surface shape with fixed C values using the original 3D fitter/renderer as a point of trusted reference.
When C is 1, you get a half pipe from hell. A slightly lopsided downsloping halfpipe.
Whence C is 2, you get much the same but the lopsidedness is even more exaggerated.
When C is 3, you get a very different shape. Like the exaggerated half pipe from above was cut in half, reversed, and glued back together.
When you run the below code, you get a 3D render with a slider that allows you to flow through the C values from 1 to 3. The values at 1, 2, and 3 look like solid matches to the references. I also added a randomizer to the data to see how it would perform at approximating a surface from imperfect noisy data and I like what I see there too.
Props to the below questions for their code and ideas.
Python 3D polynomial surface fit, order dependent
python visualize 4d data with surface plot and slider for 4th variable
import itertools
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.widgets import Slider
class Surface4D:
def __init__(self, order, a, b, c, z):
# Setting initial attributes
self.order = order
self.a = a
self.b = b
self.c = c
self.z = z
# Setting surface attributes
self.surface = self._fit_surface()
self.aa = None
self.bb = None
self._sample_surface_grid()
# Setting graph attributes
self.surface_render = None
self.axis_3d = None
# Start black magic math
def _abc_powers(self):
powers = itertools.product(range(self.order + 1), range(self.order + 1), range(self.order + 1))
return [tup for tup in powers if sum(tup) <= self.order]
def _fit_surface(self):
ncols = (self.order + 1)**3
G = np.zeros((self.a.size, ncols))
ijk = self._abc_powers()
for idx, (i,j,k) in enumerate(ijk):
G[:,idx] = self.a**i * self.b**j * self.c**k
m, _, _, _ = np.linalg.lstsq(G, self.z, rcond=None)
return m
def get_z_values(self, a, b, c):
ijk = self._abc_powers()
z = np.zeros_like(a)
for s, (i,j,k) in zip(self.surface, ijk):
z += s * a**i * b**j * c**k
return z
# End black magic math
def render_4d_flow(self):
# Set up the layout of the graph
fig = plt.figure()
self.axis_3d = Axes3D(fig, rect=[0.1,0.2,0.8,0.7])
slider_ax = fig.add_axes([0.1,0.1,0.8,0.05])
self.axis_3d.set_xlabel('X data')
self.axis_3d.set_ylabel('Y data')
self.axis_3d.set_zlabel('Z data')
# Plot the point cloud and initial surface
self.axis_3d.scatter(self.a, self.b, self.z, color='red', zorder=0)
zz = self.get_z_values(self.aa, self.bb, 1)
self.surface_render = self.axis_3d.plot_surface(self.aa, self.bb, zz, zorder=10, alpha=0.4, color="b")
# Setup the slider behavior
slider_start_step = self.c.min()
slider_max_steps = self.c.max()
slider = Slider(slider_ax, 'time', slider_start_step, slider_max_steps , valinit=slider_start_step)
slider.on_changed(self._update)
plt.show()
input("Once youre done, hit any enter to continue.")
def _update(self, val):
self.surface_render.remove()
zz = self.get_z_values(self.aa, self.bb, val)
self.surface_render = self.axis_3d.plot_surface(self.aa, self.bb, zz, zorder=10, alpha=0.4, color="b")
def _sample_surface_grid(self):
na, nb = 40, 40
aa, bb = np.meshgrid(np.linspace(self.a.min(), self.a.max(), na),
np.linspace(self.b.min(), self.b.max(), nb))
self.aa = aa
self.bb = bb
def noisify_array(one_dim_array, randomness_multiplier):
listOfNewValues = []
range = abs(one_dim_array.min()-one_dim_array.max())
for item in one_dim_array:
# What percentage are we shifting the point dimension by
shift = np.random.randint(0, 101)
shiftPercent = shift/100
shiftPercent = shiftPercent * randomness_multiplier
# Is that shift positive or negative
shiftSignIndex = np.random.randint(0, 2)
shifSignOption = [-1, 1]
shiftSign = shifSignOption[shiftSignIndex]
# Shift it
newNoisyPosition = item + (range * (shiftPercent * shiftSign))
listOfNewValues.append(newNoisyPosition)
return np.array(listOfNewValues)
def generate_data():
# Define our range for each dimension
x = np.linspace(-6, 6, 20)
y = np.linspace(-6, 6, 20)
w = [1, 2, 3]
# Populate each dimension
a,b,c,z = [],[],[],[]
for X in x:
for Y in y:
for W in w:
a.append(X)
b.append(Y)
c.append(W)
z.append((1 * X ** 4) + (2 * Y ** 3) + (X * Y ** W) + (4 * X) + (5 * Y))
# Convert them to arrays
a = np.array(a)
b = np.array(b)
c = np.array(c)
z = np.array(z)
return [a, b, c, z]
def main(order):
# Make the data
a,b,c,z = generate_data()
# Show the pure data and best fit
surface_pure_data = Surface4D(order, a, b, c, z)
surface_pure_data.render_4d_flow()
# Add some noise to the data
a = noisify_array(a, 0.10)
b = noisify_array(b, 0.10)
c = noisify_array(c, 0.10)
z = noisify_array(z, 0.10)
# Show the noisy data and best fit
surface_noisy_data = Surface4D(order, a, b, c, z)
surface_noisy_data.render_4d_flow()
# ----------------------------------------------------------------
orderForSurfaceFit = 5
main(orderForSurfaceFit)
[Edit 1: I've started to incorporate this code into my real projects and I found some tweaks to make things ore sensible. Also there's a scope problem where the runtime needs to be paused while it's still in the scope of the render_4d_flow function in order for the slider to work.]
[Edit 2: Higher resolution gif that shows the flow from c=2 to c=3]
I am working with a projected coordinate dataset that contains x,y,z data (432 line csv with X Y Z headers, not attached). I wish to import this dataset, calculate a new grid based on user input and then start performing some statistics on points that fall within the new grid. I've gotten to the point that I have two lists (raw_lst with 431(x,y,z) and grid_lst with 16(x,y) (calling n,e)) but when I try to iterate through to start calculating average and density for the new grid it all falls apart. I am trying to output a final list that contains the grid_lst x and y values along with the calculated average z and density values.
I searched numpy and scipy libraries thinking that they may have already had something to do what I am wanting but was unable to find anything. Let me know if any of you all have any thoughts.
sample_xyz_reddot_is_newgrid_pictoral_representation
import pandas as pd
import math
df=pd.read_csv("Sample_xyz.csv")
N=df["X"]
E=df["Y"]
Z=df["Z"]
#grid = int(input("Specify grid value "))
grid = float(0.5) #for quick testing the grid value is set to 0.5
#max and total calculate the input area extents
max_N = math.ceil(max(N))
max_E = math.ceil(max(E))
min_E = math.floor(min(E))
min_N = math.floor(min(N))
total_N = max_N - min_N
total_E = max_E - min_E
total_N = int(total_N/grid)
total_E = int(total_E/grid)
#N_lst and E_lst calculate the mid points based on the input file extents and the specified grid file
N_lst = []
n=float(max_N)-(0.5*grid)
for x in range(total_N):
N_lst.append(n)
n=n-grid
E_lst = []
e=float(max_E)-(0.5*grid)
for x in range(total_E):
E_lst.append(e)
e=e-grid
grid_lst = []
for n in N_lst:
for e in E_lst:
grid_lst.append((n,e))
#converts the imported dataframe to list
raw_lst = df.to_records(index=False)
raw_lst = list(raw_lst)
#print(grid_lst) # grid_lst is a list of 16 (n,e) tuples for the new grid coordinates.
#print(raw_lst) # raw_lst is a list of 441 (n,e,z) tuples from the imported file - calling these x,y,z.
#The calculation where it all falls apart.
t=[]
average_lst = []
for n, e in grid_lst:
for x, y, z in raw_lst:
if n >= x-(grid/2) and n <= x+(grid/2) and e >= y-(grid/2) and e <= y+(grid/2):
t.append(z)
average = sum(t)/len(t)
density = len(t)/grid
average_lst = (n,e,average,density)
print(average_lst)
# print("The length of this list is " + str(len(average_lst)))
# print("The length of t is " + str(len(t)))
SAMPLE CODE FOR RUNNING
import random
grid=5
raw_lst = [(random.randrange(0,10), random.randrange(0,10), random.randrange(0,2))for i in range(100)]
grid_lst = [(2.5,2.5),(2.5,7.5),(7.5,2.5),(7.5,7.5)]
t=[]
average_lst = []
for n, e in grid_lst:
for x, y, z in raw_lst:
if n >= x-(grid/2) and n <= x+(grid/2) and e >= y-(grid/2) and e <= y+(grid/2):
t.append(z)
average = sum(t)/len(t)
density = len(t)/grid
average_lst = (n,e,average,density)
print(average_lst)
Some advices
when working with arrays, use numpy. It has more functionalities
when working with grids it's often more handy the use x-coords, y-coords as single arrays
Comments to the solution
obviousley you have a grid, or rather a box, grd_lst. We generate it as a numpy meshgrid (gx,gy)
you have a number of points raw_list. We generate each elemnt of it as 1-dimensional numpy arrays
you want to select the r_points that are in the g_box. We use the percentage formula for that: tx = (rx-gxMin)/(gxMax-gxMin)
if tx, ty are within [0..1] we store the index
as an intermediate result we get all indices of raw_list that are within the g_box
with that index you can extract the elements of raw_list that are within the g_box and can do some statistics
note that I have omitted the z-coord. You will have to improve this solution.
--
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.colors as mclr
from matplotlib import cm
f10 = 'C://gcg//picStack_10.jpg' # output file name
f20 = 'C://gcg//picStack_20.jpg' # output file name
def plot_grid(gx,gy,rx,ry,Rx,Ry,fOut):
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(111)
myCmap = mclr.ListedColormap(['blue','lightgreen'])
ax.pcolormesh(gx, gy, gx, edgecolors='b', cmap=myCmap, lw=1, alpha=0.3)
ax.scatter(rx,ry,s=150,c='r', alpha=0.7)
ax.scatter(Rx,Ry,marker='s', s=150,c='gold', alpha=0.5)
ax.set_aspect('equal')
plt.savefig(fOut)
plt.show()
def get_g_grid(nx,ny):
ix = 2.5 + 5*np.linspace(0,1,nx)
iy = 2.5 + 5*np.linspace(0,1,ny)
gx, gy = np.meshgrid(ix, iy, indexing='ij')
return gx,gy
def get_raw_points(N):
rx,ry,rz,rv = np.random.randint(0,10,N), np.random.randint(0,10,N), np.random.randint(0,2,N), np.random.uniform(low=0.0, high=1.0, size=N)
return rx,ry,rz,rv
N = 100
nx, ny = 2, 2
gx,gy = get_base_grid(nx,ny)
rx,ry,rz,rv = get_raw_points(N)
plot_grid(gx,gy,rx,ry,0,0,f10)
def get_the_points_inside(gx,gy,rx,ry):
#----- run throuh the g-grid -------------------------------
nx,ny = gx.shape
N = len(rx)
index = []
for jx in range(0,nx-1):
for jy in range(0,ny-1):
#--- run through the r_points
for jr in range(N):
test_x = (rx[jr]-gx[jx,jy]) / (gx[jx+1,jy] - gx[jx,jy])
test_y = (ry[jr]-gy[jx,jy]) / (gy[jx,jy+1] - gy[jx,jy])
if (0.0 <= test_x <= 1.0) and (0.0 <= test_y <= 1.0):
index.append(jr)
return index
index = get_the_points_inside(gx,gy,rx,ry)
Rx, Ry, Rz, Rv = rx[index], ry[index], rz[index], rv[index]
plot_grid(gx,gy,rx,ry,Rx,Ry,f20)
I have been working on modeling magnetic fields for research. The code below allows me to calculate correct values of the field for any given point (x,y,z); however, when I pass a np.meshgrid object through the code, the results start to get wonky.
This is my code:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
def normal_vector(u):
return u/np.linalg.norm(u)
class Path:
"""
This defines the Path class which allows for the calculations of the magnetic field.
"""
def __init__(self, xs, ys, zs):
self.points = zip(*[xs, ys, zs]) # defines the points
self.x = xs
self.y = ys
self.z = zs
self.path_vectors = [(self.points[i + 1][0] - self.points[i][0],
self.points[i + 1][1] - self.points[i][1],
self.points[i + 1][2] - self.points[i][2]) for i in range(len(self.x) - 1)]
def get_length(self):
"""
Calculates the path length
:return: returns float length
"""
return sum([np.sqrt(((self.x[i + 1] - self.x[i]) ** 2) + ((self.y[i + 1] - self.y[i]) ** 2) + (
(self.z[i + 1] - self.z[i]) ** 2)) for i in
range(len(self.x) - 1)])
def get_magnetlic_function(self,axes,current=1.0,magnetic_constant = 1.25663706212e-6):
magnetic_parameter = (current*magnetic_constant)/(4*np.pi)
field_function = lambda x,y,z: sum([magnetic_parameter*np.cross(self.path_vectors[j],normal_vector(np.stack([x-self.x[j],y-self.y[j],z-self.z[j]],axis=-1)))/(np.linalg.norm(np.stack([x-self.x[j],y-self.y[j],z-self.z[j]],axis=-1))**2) for j in range(len(self.x)-1)]).swapaxes(0,-1)
return field_function
n = 200
r = 1
h = 5
grid_x,grid_y,grid_z = np.meshgrid(np.linspace(-10,10,5),
np.linspace(-10,10,5),
np.linspace(-10,10,5))
c = h / (2 * n * np.pi)
t = np.linspace(0,2*np.pi, 5000)
xp = 3*np.cos(t)
yp = 3*np.sin(t)
zp = 0*t
p = Path(list(xp), list(yp), list(zp))
func = p.get_magnetlic_function([grid_x,grid_y,grid_z])
u,v,w = func(grid_x,grid_y,grid_z)
r = np.sqrt(u**2+v**2+w**2)
print func(-10.0,00.0,0.0)
ax1 = plt.subplot(111,projection='3d')
ax1.plot(xp,yp,zp,'r-')
ax1.plot([-10],[0],[0],'ro')
ax1.quiver(grid_x,grid_y,grid_z,u/r,v/r,w/r,length=1)
plt.show()
As is clear near the bottom, if the code is run, the direction of the vector at -10.0,00.0,0.0 is not the same as the value that gets printed. Why?
From the code, I recieve the quiver plot here:
It should look like:
When trying to find the magnetic field caused by a current distribution I find it is often much clearer to consider pairwise interactions (although the lambda function is far more pythonic). Consider this approach
class Path:
# ...
def mag_func(self, x, y, z, current = 1.0, mag_const = 1.25663706212e-6):
mag_param = current * mag_const / (4 * np.pi)
s = x.shape
res = np.zeros((s[0],s[1],s[2],3))
for i in range(s[0]):
for j in range(s[1]):
for k in range(s[2]):
for idx, (xc, yc, zc) in enumerate(zip(self.x, self.y, self.z)):
res[i,j,k,:] += mag_param * \
np.cross(self.path_vectors[idx], [x[i,j,k] - xc,
y[i,j,k] - yc, z[i,j,k] - zc]) / \
np.linalg.norm([x[i,j,k] - xc, y[i,j,k] - yc,
z[i,j,k] - zc])**2
return res[:,:,:,0], res[:,:,:,1], res[:,:,:,2]
#...
u, v, w = p.mag_func(grid_x, grid_y, grid_z)
r = np.sqrt(u**2+v**2+w**2)
ax1 = plt.subplot(111,projection='3d')
ax1.plot(xp, yp, zp, 'r-')
ax1.quiver(grid_x, grid_y, grid_z, u/r, v/r, w/r,length=1)
plt.show()
Which will give
Which is the correct representation of the magnetic field surrounding a current carrying wire.
As for the question of why the lambda doesn't work in the first place, I think it is due to the creation of the grid via np.meshgrid such that the outer sum was summing over more points than it should have been. Iterating in the way above remedies that issue. It would be possible to use that lambda function but I think you would have still have to iterate over the grid_x, grid_y, and grid_z in the manner shown.
I was wondering if there is a built-in functionality or at least a 'smart' way of ordering x- and y-labels by their values in combination with seaborn heatmaps.
Let's say the unordered heatmap looks like this:
However, the goal is to reorder the labels having the color shifts 'smoothened'. It should look more like this afterwards:
Thanks for your advice!
Best regards
One needs to somehow quantify "smoothened colorshifts". For that purpose, a cost function may be defined. In the simplest case this could be the sum of the differences between neighboring pixels. If that sum is small, neighboring pixels have a small difference in color.
One may then swap around columns and rows in the matrix at random and check if a smaller cost is produced. Doing this iteratively, would at some point result in a smoothened heatmap. However it will of course depend on the degree of randomness in the initial heatmap. For fully random pixels, not too much of an optimization can be expected.
The following class implements such optimization. It would take nrand different starting permutations and for each, do the swapping niter times. The best result of this is stored and may be retrieved via .get_opt.
import matplotlib.pyplot as plt
import numpy as np
class ReOrder():
def __init__(self, array, nrand=2, niter=800):
self.a = array
self.indi = np.arange(self.a.shape[0])
self.indj = np.arange(self.a.shape[1])
self.i = np.arange(self.a.shape[0])
self.j = np.arange(self.a.shape[1])
self.nrand = nrand
self.niter = niter
def apply(self, a, i, j):
return a[:,j][i,:]
def get_opt(self):
return self.apply(self.a, self.i, self.j)
def get_labels(self, x=None, y=None):
if x is None:
x = self.indj
if y is None:
y = self.indi
return np.array(x)[self.j], np.array(y)[self.i]
def cost(self, a=None):
if a is None:
a = self.get_opt()
m = a[1:-1, 1:-1]
b = 0.5 * ((m - a[0:-2, 0:-2])**2 + \
(m - a[2: , 2: ])**2 + \
(m - a[0:-2, 2: ])**2 + \
(m - a[2: , 0:-2])**2) + \
(m - a[0:-2, 1:-1])**2 + \
(m - a[1:-1, 0:-2])**2 + \
(m - a[2: , 1:-1])**2 + \
(m - a[1:-1, 2: ])**2
return b.sum()
def randomize(self):
newj = np.random.permutation(self.a.shape[1])
newi = np.random.permutation(self.a.shape[0])
return newi, newj
def compare(self, i1, j1, i2, j2, a=None):
if a is None:
a = self.a
if self.cost(self.apply(a,i1,j1)) < self.cost(self.apply(a,i2,j2)):
return i1, j1
else:
return i2, j2
def rowswap(self, i, j):
rows = np.random.choice(self.indi, replace=False, size=2)
ir = np.copy(i)
ir[rows] = ir[rows[::-1]]
return ir, j
def colswap(self, i, j):
cols = np.random.choice(self.indj, replace=False, size=2)
jr = np.copy(j)
jr[cols] = jr[cols[::-1]]
return i, jr
def swap(self, i, j):
ic, jc = self.rowswap(i,j)
ir, jr = self.colswap(i,j)
io, jo = self.compare(ic,jc, ir,jr)
return self.compare(i,j, io,jo)
def optimize(self, nrand=None, niter=None):
nrand = nrand or self.nrand
niter = niter or self.niter
i,j = self.i, self.j
for kk in range(niter):
i,j = self.swap(i,j)
self.i, self.j = self.compare(i,j, self.i, self.j)
print(self.cost())
for ii in range(nrand):
i,j = self.randomize()
for kk in range(niter):
i,j = self.swap(i,j)
self.i, self.j = self.compare(i,j, self.i, self.j)
print(self.cost())
print("finished")
So let's take two starting arrays,
def get_sample_ord():
x,y = np.meshgrid(np.arange(12), np.arange(10))
z = x+y
j = np.random.permutation(12)
i = np.random.permutation(10)
return z[:,j][i,:]
def get_sample():
return np.random.randint(0,120,size=(10,12))
and run it through the above class.
def reorder_plot(nrand=4, niter=10000):
fig, ((ax1, ax2),(ax3,ax4)) = plt.subplots(nrows=2, ncols=2,
constrained_layout=True)
fig.suptitle("nrand={}, niter={}".format(nrand, niter))
z1 = get_sample()
r1 = ReOrder(z1)
r1.optimize(nrand=nrand, niter=niter)
ax1.imshow(z1)
ax3.imshow(r1.get_opt())
xl, yl = r1.get_labels()
ax1.set(xticks = np.arange(z1.shape[1]),
yticks = np.arange(z1.shape[0]),
title=f"Start, cost={r1.cost(z1)}")
ax3.set(xticks = np.arange(z1.shape[1]), xticklabels=xl,
yticks = np.arange(z1.shape[0]), yticklabels=yl,
title=f"Optimized, cost={r1.cost()}")
z2 = get_sample_ord()
r2 = ReOrder(z2)
r2.optimize(nrand=nrand, niter=niter)
ax2.imshow(z2)
ax4.imshow(r2.get_opt())
xl, yl = r2.get_labels()
ax2.set(xticks = np.arange(z2.shape[1]),
yticks = np.arange(z2.shape[0]),
title=f"Start, cost={r2.cost(z2)}")
ax4.set(xticks = np.arange(z2.shape[1]), xticklabels=xl,
yticks = np.arange(z2.shape[0]), yticklabels=yl,
title=f"Optimized, cost={r2.cost()}")
reorder_plot(nrand=4, niter=10000)
plt.show()
The totally random matrix (left column) is only smoothened very little - still it appears a little more sorted. The cost values are pretty high still. A not so random matrix however is perfectly smoothened and the cost is significantly reduced.
That second plot is ordered by the x and y axis labels, not the values. You won't be able to get random data to look like ordered data. You can sort the data by values for one row and one column, but the rest of the data will be fixed. Here is code that plots a heatmap sorted by values for row 0 and column 0. Notice the "cross" in the middle of the plot:
import numpy as np; np.random.seed(0)
import seaborn as sns; sns.set()
uniform_data = np.random.rand(10, 12)
df = pd.DataFrame(uniform_data)
df2 = df.sort_values(by=0).T.sort_values(by=0).T
ax = sns.heatmap(df2)
It is the first time I am trying to write a Poincare section code at Python.
I borrowed the piece of code from here:
https://github.com/williamgilpin/rk4/blob/master/rk4_demo.py
and I have tried to run it for my system of second order coupled odes. The problem is that I do not see what I was expecting to. Actually, I need the Poincare section when x=0 and px>0.
I believe that my implementation is not the best out there. I would like to:
Improve the way that the initial conditions are chosen.
Apply the correct conditions (x=0 and px>0) in order to acquire the correct Poincare section.
Create one plot with all the collected poincare section data, not four separate ones.
I would appreciate any help.
This is the code:
from matplotlib.pyplot import *
from scipy import *
from numpy import *
# a simple Runge-Kutta integrator for multiple dependent variables and one independent variable
def rungekutta4(yprime, time, y0):
# yprime is a list of functions, y0 is a list of initial values of y
# time is a list of t-values at which solutions are computed
#
# Dependency: numpy
N = len(time)
y = array([thing*ones(N) for thing in y0]).T
for ii in xrange(N-1):
dt = time[ii+1] - time[ii]
k1 = dt*yprime(y[ii], time[ii])
k2 = dt*yprime(y[ii] + 0.5*k1, time[ii] + 0.5*dt)
k3 = dt*yprime(y[ii] + 0.5*k2, time[ii] + 0.5*dt)
k4 = dt*yprime(y[ii] + k3, time[ii+1])
y[ii+1] = y[ii] + (k1 + 2.0*(k2 + k3) + k4)/6.0
return y
# Miscellaneous functions
n= 1.0/3.0
kappa1 = 0.1
kappa2 = 0.1
kappa3 = 0.1
def total_energy(valpair):
(x, y, px, py) = tuple(valpair)
return .5*(px**2 + py**2) + (1.0/(1.0*(n+1)))*(kappa1*np.absolute(x)**(n+1)+kappa2*np.absolute(y-x)**(n+1)+kappa3*np.absolute(y)**(n+1))
def pqdot(valpair, tval):
# input: [x, y, px, py], t
# takes a pair of x and y values and returns \dot{p} according to the Hamiltonian
(x, y, px, py) = tuple(valpair)
return np.array([px, py, -kappa1*np.sign(x)*np.absolute(x)**n+kappa2*np.sign(y-x)*np.absolute(y-x)**n, kappa2*np.sign(y-x)*np.absolute(y-x)**n-kappa3*np.sign(y)*np.absolute(y)**n]).T
def findcrossings(data, data1):
# returns indices in 1D data set where the data crossed zero. Useful for generating Poincare map at 0
prb = list()
for ii in xrange(len(data)-1):
if (((data[ii] > 0) and (data[ii+1] < 0)) or ((data[ii] < 0) and (data[ii+1] > 0))) and data1[ii] > 0:
prb.append(ii)
return array(prb)
t = linspace(0, 1000.0, 100000)
print ("step size is " + str(t[1]-t[0]))
# Representative initial conditions for E=1
E = 1
x0=0
y0=0
init_cons = [[x0, y0, np.sqrt(2*E-(1.0*i/10.0)*(1.0*i/10.0)-2.0/(n+1)*(kappa1*np.absolute(x0)**(n+1)+kappa2*np.absolute(y0-x0)**(n+1)+kappa3*np.absolute(y0)**(n+1))), 1.0*i/10.0] for i in range(-10,11)]
outs = list()
for con in init_cons:
outs.append( rungekutta4(pqdot, t, con) )
# plot the results
fig1 = figure(1)
for ii in xrange(4):
subplot(2, 2, ii+1)
plot(outs[ii][:,1],outs[ii][:,3])
ylabel("py")
xlabel("y")
title("Full trajectory projected onto the plane")
fig1.suptitle('Full trajectories E = 1', fontsize=10)
# Plot Poincare sections at x=0 and px>0
fig2 = figure(2)
for ii in xrange(4):
subplot(2, 2, ii+1)
xcrossings = findcrossings(outs[ii][:,0], outs[ii][:,3])
yints = [.5*(outs[ii][cross, 1] + outs[ii][cross+1, 1]) for cross in xcrossings]
pyints = [.5*(outs[ii][cross, 3] + outs[ii][cross+1, 3]) for cross in xcrossings]
plot(yints, pyints,'.')
ylabel("py")
xlabel("y")
title("Poincare section x = 0")
fig2.suptitle('Poincare Sections E = 1', fontsize=10)
show()
You need to compute the derivatives of the Hamiltonian correctly. The derivative of |y-x|^n for x is
n*(x-y)*|x-y|^(n-2)=n*sign(x-y)*|x-y|^(n-1)
and the derivative for y is almost, but not exactly (as in your code), the same,
n*(y-x)*|x-y|^(n-2)=n*sign(y-x)*|x-y|^(n-1),
note the sign difference. With this correction you can take larger time steps, with correct linear interpolation probably even larger ones, to obtain the images
I changed the integration of the ODE to
t = linspace(0, 1000.0, 2000+1)
...
E_kin = E-total_energy([x0,y0,0,0])
init_cons = [[x0, y0, (2*E_kin-py**2)**0.5, py] for py in np.linspace(-10,10,8)]
outs = [ odeint(pqdot, con, t, atol=1e-9, rtol=1e-8) ) for con in init_cons[:8] ]
Obviously the number and parametrization of initial conditions may change.
The computation and display of the zero-crossings was changed to
def refine_crossing(a,b):
tf = -a[0]/a[2]
while abs(b[0])>1e-6:
b = odeint(pqdot, a, [0,tf], atol=1e-8, rtol=1e-6)[-1];
# Newton step using that b[0]=x(tf) and b[2]=x'(tf)
tf -= b[0]/b[2]
return [ b[1], b[3] ]
# Plot Poincare sections at x=0 and px>0
fig2 = figure(2)
for ii in xrange(8):
#subplot(4, 2, ii+1)
xcrossings = findcrossings(outs[ii][:,0], outs[ii][:,3])
ycrossings = [ refine_crossing(outs[ii][cross], outs[ii][cross+1]) for cross in xcrossings]
yints, pyints = array(ycrossings).T
plot(yints, pyints,'.')
ylabel("py")
xlabel("y")
title("Poincare section x = 0")
and evaluating the result of a longer integration interval