Plotly: How to plot a tetrahedron volume - python

I have a set of xyz points and a set of tetrahedrons. Where each node of the tetrahedron points to an index in the points table.
I need to plot the tetrahedrons with a corresponding color based on the tag attribute.
points
Index
x
y
z
0
x_1
y_1
z_1
1
x_2
y_2
z_2
...
...
...
...
tetrahedrons
Index
a
b
c
d
tag
0
a_1.pt
b_1.pt
c_1.pt
d_1.pt
9
1
a_2.pt
b_2.pt
c_2.pt
d_2.pt
0
...
...
...
...
...
...
I have tried using the Mesh3d api but it does not allow for a 4th vertex.
I can plot something like the code below but it does not have all the faces of the tetrahedron.
go.Figure(data=[
go.Mesh3d(
x=mesh_pts.x, y=mesh_pts.y, z=mesh_pts.z,
i=tagged_th.a, j=tagged_th.b, k=tagged_th.c,
),
]).show()
I think the Volume or Isosurface plots might work but I'm not sure how to convert my data into a format to be consumed by those apis.

I can't hide the fact that, a few minutes ago, I wasn't even aware of i,j,k parameters. But, still, I know that Mesh3D draws triangles, not tetrahedron. You need to take advantage of those i,j,k parameters to control which triangles are drawn. But it is still your job to tell which triangles need to be drawn to that it look like tetrahedrons.
Yes, there are 4 triangles per tetrahedron. If you wish to draw them four, you need to explicitly pass i,j,k for all 4. Not just pass i,j,k and an nonexistent l and expect plotly to understand that this means 4 triangles.
If a, b, c and d are 4 vertices of a tetrahedron, then the 4 triangles you need to draw are the 4 combinations of 3 of vertices from those. That is bcd, acd, abd and abc.
Let's write this in 4 rows
bcd
acd
abd
abc
^^^
|||
||\------k
|\------ j
\------- i
So, if, now, a, b, c and d are list of n vertices, then i, j, k must be lists 4 times longer
i=b + a + a + a
j=c + c + b + b
k=d + d + d + c
Application: let's define 2 tetrahedrons, one sitting on the spike of the other, using your dataframes format
import plotly.graph_objects as go
import pandas as pd
mesh_pts = pd.DataFrame({'x':[0, 1, 0, 0, 1, 0, 0],
'y':[0, 0, 1, 0, 0, 1, 0],
'z':[0, 0, 0, 1, 1, 1, 2]})
tagged_th = pd.DataFrame({'a':[0,3],
'b':[1,4],
'c':[2,5],
'd':[3,6],
'tag':[0,1]})
# And from there, just create a list of triangles, made of 4 combinations
# of 3 points taken from list of tetrahedron vertices
go.Figure(data=[
go.Mesh3d(
x=mesh_pts.x,
y=mesh_pts.y,
z=mesh_pts.z,
i=pd.concat([tagged_th.a, tagged_th.a, tagged_th.a, tagged_th.b]),
j=pd.concat([tagged_th.b, tagged_th.b, tagged_th.c, tagged_th.c]),
k=pd.concat([tagged_th.c, tagged_th.d, tagged_th.d, tagged_th.d]),
intensitymode='cell',
intensity=pd.concat([tagged_th.tag, tagged_th.tag, tagged_th.tag, tagged_th.tag])
)
]).show()

I don't see what you mean by "does not allow for a 4th vertex". Here is an example with two tetrahedra:
import plotly.graph_objects as go
import plotly.io as pio
import numpy as np
i = np.array([0, 0, 0, 1])
j = np.array([1, 2, 3, 2])
k = np.array([2, 3, 1, 3])
fig = go.Figure(data = [
go.Mesh3d(
x = [0,1,2,0, 4,5,6,4],
y = [0,0,1,2, 0,0,1,2],
z = [0,2,2,3, 4,2,4,1],
i = np.concatenate((i, i+4)),
j = np.concatenate((j, j+4)),
k = np.concatenate((k, k+4)),
facecolor = ["red","red","red","red", "green","green","green","green"]
)
])
pio.write_html(fig, file = "tetrahedra.html", auto_open = True)

Related

Viewing 3d Field Lines of an unstructured dataset

I have tried to view field lines of an uncomplete regular grid vector field with first pyVista Streamlines and then with plotly without success... I have yet good results with other 2d streamplots :
2d streamplot of the data
Could someone help me with this ? I found no answer... Here is my data : https://wetransfer.com/downloads/7f3c4ae01e5922e753ea708134f956e720230214141330/bf11ab
import pandas as pd
import numpy as np
import pyvista as pv
import plotly.graph_objects as go
df = pd.read_csv("mix_griddata.csv")
X = df['X']
Y = df['Y']
Z = df['Z']
Vx = df['Vx']
Vy = df['Vy']
Vz = df['Vz']
fig = go.Figure(data=go.Streamtube(
x = X,
y = Y,
z = Z,
u = Vx,
v = Vy,
w = Vz,
starts = dict(
x = X.sample(frac=0.01,replace=False),
y = Y.sample(frac=0.01,replace=False),
z = Z.sample(frac=0.01,replace=False)
),
sizeref =1,
colorscale = 'Portland',
showscale = False,
maxdisplayed = 30000000
))
fig.update_layout(
scene = dict(
aspectratio = dict(
x = 1,
y = 1,
z = 1
)
),
margin = dict(
t = 10,
b = 10,
l = 10,
r = 10
)
)
fig.show(renderer="browser")
#Streamlines
mix_FD_grid = np.load("C:/Users/hd377/OneDrive - ensam.eu/0-Thesis/Fibres_Direction_in_allvolume/mix/mix_FD_grid.npy")
origin = (0,0,0)
mesh = pv.UniformGrid(dimensions=mix_FD_grid[:,:,:,0].shape, spacing=(1, 1, 1), origin=origin)
vectors = np.empty((mesh.n_points, 3))
vectors[:, 0] = mix_FD_grid[:,:,:,0].flatten()
vectors[:, 1] = mix_FD_grid[:,:,:,1].flatten()
vectors[:, 2] = mix_FD_grid[:,:,:,2].flatten()
mesh['vectors'] = vectors
stream, src = mesh.streamlines(
'vectors', return_source=True, max_steps = 20000, n_points=200, source_radius=25, source_center=(15, 0, 30)
)
p = pv.Plotter()
p.add_mesh(mesh.outline(), color="k")
p.add_mesh(stream.tube(radius=0.1))
p.camera_position = [(182.0, 177.0, 50), (139, 105, 19), (-0.2, -0.2, 1)]
p.show()
The plotly window does appear in my browser but no tube are visible at all, and the axes values are false.
The pyVista does show something, but in the wrong direction, and clearly not what expected (longitudinal flux circumventing a central cone).
I'll only be tackling PyVista. It's hard to say for sure and I'm only guessing, but your data is probably laid out in the wrong order.
For starters, your data is inconsistent to begin with: your CSV has 1274117 rows whereas your multidimensional array has shape (37, 364, 100, 3), for a total of 1346800 vectors. And your question title says "unstructured", but your PyVista attempt uses a uniform grid with.
Secondly, your CSV doesn't correspond to a regular grid in the first place, e.g. at the end of the file you have 15 rows starting with 368.693,36.971999999999994, then 8 rows starting with 369.71999999999997,36.971999999999994, then a single row starting with 370.74699999999996,36.971999999999994. In a regular grid you'd get the same number of items in each block.
Thirdly, your CSV has an unusual (MATLAB-smelling) layout that the order of axes is z-x-y (rather than either x-y-z or z-y-x). This is a strong clue that your data is mangled due to memory layout issues when flattened. But the previous two point mean that I can't verify how your 4d array was created, I have to take it for granted that it's correct.
Just plotting your raw data makes it obvious that the data is mangled in your original version (with some style cleanup):
import numpy as np
import pyvista as pv
mix_FD_grid = np.load("mix_FD_grid.npy")
origin = (0, 0, 0)
mesh = pv.UniformGrid(dimensions=mix_FD_grid.shape[:-1], spacing=(1, 1, 1), origin=origin)
vectors = np.empty_like(mesh.points)
vectors[:, 0] = mix_FD_grid[..., 0].ravel()
vectors[:, 1] = mix_FD_grid[..., 1].ravel()
vectors[:, 2] = mix_FD_grid[..., 2].ravel()
mesh.point_data['vectors'] = vectors
mesh.plot()
The fragmented pattern you can see is a hallmark of data mangling due to mistaken memory layout.
If we assume the layout is more or less sane, trying column-major layout ("F" for "Fortran", also used by MATLAB) seems to make a lot more sense:
vectors[:, 0] = mix_FD_grid[..., 0].ravel('F')
vectors[:, 1] = mix_FD_grid[..., 1].ravel('F')
vectors[:, 2] = mix_FD_grid[..., 2].ravel('F')
mesh.point_data['vectors'] = vectors
mesh.plot()
So we can try using streamlines using that:
stream, src = mesh.streamlines(
'vectors', return_source=True, max_steps=20000, n_points=200, source_radius=25, source_center=(15, 0, 30)
)
p = pv.Plotter()
p.add_mesh(mesh.outline(), color="k")
p.add_mesh(stream.tube(radius=0.1))
p.show()
It doesn't look great:
So, you said that the streamlines should be longitudinal, but here they are clearly transversal. Can it be that the x and y field components are swapped? I can't tell, so let's try!
import numpy as np
import pyvista as pv
mix_FD_grid = np.load("mix_FD_grid.npy")
origin = (0, 0, 0)
mesh = pv.UniformGrid(dimensions=mix_FD_grid.shape[:-1], spacing=(1, 1, 1), origin=origin)
vectors = np.empty_like(mesh.points)
vectors[:, 0] = mix_FD_grid[..., 1].ravel('F') # swap 0 <-> 1
vectors[:, 1] = mix_FD_grid[..., 0].ravel('F') # swap 0 <-> 1
vectors[:, 2] = mix_FD_grid[..., 2].ravel('F')
mesh.point_data['vectors'] = vectors
stream, src = mesh.streamlines(
'vectors', return_source=True, max_steps=20000, n_points=200, source_radius=25, source_center=(15, 0, 30)
)
p = pv.Plotter()
p.add_mesh(mesh.outline(), color="k")
p.add_mesh(stream.tube(radius=0.1))
p.show()
Now we're talking!
Bonus: y field component on a volumetric plot:
mesh.plot(volume=True, scalars=vectors[:, 1], show_scalar_bar=False)

How to make a graph from the produced in python

I have this code, it produces a random matrix of 1s and 0s. I want to create a graph from this matrix where the 1s in the matrix represent a node and each node has a maximum of 3 edges. How can i implement this, please help?
import numpy as np
from random import sample
N = int(input("Enter the number of nodes:"))
my_matrix = np.zeros((N,N), dtype='int8')
If you matrix is just random, probably, you don't need it. Instead, you can create graph from list of edges
import networkx as nx
from random import sample
import numpy as np
from numpy.random import randint
n = 7 # number of nodes in graph
max_connections = int(input("Enter max connections per node:")) # input: 3
nodes = np.arange(n)
# create graph based on list of edges [(0, 1), (0, 4), ...]
gr = nx.Graph([
# for each node select <= 'max_connections' nodes as connections
(i, j) for i in range(n) for j in sample(nodes[nodes != i].tolist(), randint(1, max_connections+1))
])
# check number of connections
for n in gr.nodes():
nei = list(gr.neighbors(n))
while len(nei) > max_connections:
gr.remove_edge(n, random.choice(nei))
nei = list(gr.neighbors(n))
nx.draw_networkx(gr, with_labels=True, node_color='#7d99f5')
Graph:
And you can get adjacency matrix using nx.adjacency_matrix()
nx.adjacency_matrix(gr, nodelist=sorted(gr.nodes())).todense()
matrix([[0, 1, 1, 0, 1, 0, 0],
[1, 0, 0, 0, 0, 1, 1],
[1, 0, 0, 1, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 1],
[0, 1, 1, 0, 1, 0, 0],
[0, 1, 0, 1, 1, 0, 0]])
complete update :
if bi-directional is not important, then the adjM is tri-diagonal
if not more than 3 edges per node are allowed, then each row and each column of adjM has 3 or less "1"
the code follows the tri-diagnoal structure required
step 1: fill in 3 edges starting from each node (follow the rows of adjM)
step 2: but then some nodes may receive more than 3 edges, so remove some of them until there are 3 left only (follow the columnes of adjM)
step 3: remove the self-adjacencies
yes, it might happen that the graph will not be connected due to the random processes. Then repeat the runs till you are happy with.
The structure of adjM looks reasonable now at least. The graph however presents at node 12 more connections than can be seen in the adjM matrix. (so there is still an improvement necessary....)
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
Nnodes = 16
Nedges = 3
#---- 0. to initialize: generate a random adjacency matrix
rng = np.random.default_rng()
adjM = rng.integers(1, size=(Nnodes,Nnodes)) # random adjacency matrix / with upper=1 it is a zero matrix
#---- 1. for eaach node generate randomly Nedges edges ("sending" connections alongs the rows of adjM)
for node in range(Nnodes):
dd = Nnodes-node
rand_ind = np.random.choice(np.arange(node,Nnodes), size=min(dd,Nedges), replace=False, p=None) # generate randomly indexes
# you might use replace=False too with different results
adjM[node, rand_ind] = 1 # insert the connections
#---- 2. for each node eliminate randomly edges that are more than Nedges ("receiving" connections alongs the columns of adjM)
for node in range(Nnodes): # run through the columns of adjM
dd = Nnodes-node
a = adjM[:,node] # select a column = receiving connections
jnz = np.array(a.nonzero()) # indices of the non-zero elements
Nnz = jnz.shape[1] # number of non-zero elements
if Nnz > Nedges: # ...then randomly select Nedges edges only
jchoice = np.random.choice(jnz.ravel(), size=min(Nedges,Nedges), replace=False, p=None)
#print(' jchoice', jchoice)
adjM[:,node] = 0
adjM[jchoice, node] = 1
#---- 3. remove self-adjacency
jDiag = np.arange(Nnodes)
adjM[jDiag, jDiag] = 0 # set the diagonals to zero
print(adjM)
#---- grafics
plt.spy(adjM, precision=0, marker=None, markersize=12, aspect='equal', origin='upper')
plt.show()
gr = nx.from_numpy_matrix(adjM)
nx.draw_networkx(gr, with_labels=True, node_size=400, node_color='#7d99f5', edge_color='orange', width=2, font_weight='bold')
plt.show()
Addition to your code (function check_graph () fix two problems we have discussed). Also, according your way of generation of adjacency matrix, you won't face second problem, so you can comment out second section in function.
def check_graph(graph, max_conn):
# 1) remove self loops
graph.remove_edges_from(nx.selfloop_edges(graph))
# 2) remove random edge(s) if limit of edges per node have been exceeded
for i in graph.nodes():
# list of connections - nodes that are connected to the selected node 'i'
nei = list(graph.neighbors(i))
if len(nei) > max_conn:
graph.remove_edges_from(
# like if len(nei) - max_conn = 5 - 4 = 1, then one random edge will be selected
np.random.choice(nei, size=(len(nei)-max_conn))
)
# <-- insert your code here --> N = 20
gr = nx.from_numpy_matrix(my_matrix)
check_graph(gr, max_conn=N)
nx.draw_networkx(gr, with_labels=True, node_color='#7d99f5')
Result looks a bit strange for me, but I don't know purposes of your graph, probably, it's okay.

Python homogeneous to inhomogeneous plot line

I found an article which is about epipolar geometry.
I calculated the fundamental matrix. Now Iam trying to find the line on which a corresponding point lays as described in the article:
I calculated the line which is in homogeneous coordinates. How could I plot this line into the picture like in the example? I thought about transforming the line from homogeneous to inhomogeneous coordinates. I think this can be achieved by dividing x and y by z
For example, homogeneous:
x=0.0295
y=0.9996
z=-265.1531
to inhomogeneous:
x=0.0295/-265.1531
y=0.9996/-265.1531
so:
x=-0.0001112564778612809
y=0.0037698974667842843
Those numbers seem wrong to me, because theyre so small. Is this the correct approach?
How could I plot my result into an image?
the x, y and z you have are the parameters of the "Epipolar Lines" equation that appear under the "line in the image" formula in the slides, but labelled a, b and c respectively, i.e:
au + bv + c = 0
solutions to this are points on the line. e.g. in Python I'd define a as some points on the picture's x-axis, and solve for b:
import numpy as np
F = np.array([
[-0.00310695, -0.0025646, 2.96584],
[-0.028094, -0.00771621, 56.3813],
[13.1905, -29.2007, -9999.79],
])
p_l = np.array([
[343.53],
[221.70],
[ 1.0],
])
lt = F # p_l
# if you want to normalise
lt /= np.sqrt(sum(lt[:2] ** 2))
# should give your values [0.0295, 0.9996, -265.2]
print(lt)
a, b, c = lt.ravel()
x = np.array([0, 400])
y = -(x*a + c) / b
and then just draw a line between these points

Find the position of a lowest difference between numpy arrays

I've got two musical files: one lossless with little sound gap (at this time it's just silence but it could be anything: sinusoid or just some noise) at the beginning and one mp3:
In [1]: plt.plot(y[:100000])
Out[1]:
In [2]: plt.plot(y2[:100000])
Out[2]:
This lists are similar but not identical so I need to cut this gap, to find the first occurrence of one list in another with lowest delta error.
And here's my solution (5.7065 sec.):
error = []
for i in range(25000):
y_n = y[i:100000]
y2_n = y2[:100000-i]
error.append(abs(y_n - y2_n).mean())
start = np.array(error).argmin()
print(start, error[start]) #23057 0.0100046
Is there any pythonic way to solve this?
Edit:
After calculating the mean distance between special points (e.g. where data == 0.5) I reduce the area of search from 25000 to 2000. This gives me reasonable time of 0.3871s:
a = np.where(y[:100000].round(1) == 0.5)[0]
b = np.where(y2[:100000].round(1) == 0.5)[0]
mean = int((a - b[:len(a)]).mean())
delta = 1000
error = []
for i in range(mean - delta, mean + delta):
...
What you are trying to do is a cross-correlation of the two signals.
This can be done easily using signal.correlate from the scipy library:
import scipy.signal
import numpy as np
# limit your signal length to speed things up
lim = 25000
# do the actual correlation
corr = scipy.signal.correlate(y[:lim], y2[:lim], mode='full')
# The offset is the maximum of your correlation array,
# itself being offset by (lim - 1):
offset = np.argmax(corr) - (lim - 1)
You might want to take a look at this answer to a similar problem.
Let's generate some data first
N = 1000
y1 = np.random.randn(N)
y2 = y1 + np.random.randn(N) * 0.05
y2[0:int(N / 10)] = 0
In these data, y1 and y2 are almost the same (note the small added noise), but the first 10% of y2 are empty (similarly to your example)
We can now calculate the absolute difference between the two vectors and find the first element for which the absolute difference is below a sensitivity threshold:
abs_delta = np.abs(y1 - y2)
THRESHOLD = 1e-2
sel = abs_delta < THRESHOLD
ix_start = np.where(sel)[0][0]
fig, axes = plt.subplots(3, 1)
ax = axes[0]
ax.plot(y1, '-')
ax.set_title('y1')
ax.axvline(ix_start, color='red')
ax = axes[1]
ax.plot(y2, '-')
ax.axvline(ix_start, color='red')
ax.set_title('y2')
ax = axes[2]
ax.plot(abs_delta)
ax.axvline(ix_start, color='red')
ax.set_title('abs diff')
This method works if the overlapping parts are indeed "almost identical". You will have to think of smarter alignment ways if the similarity is low.
I think what you are looking for is correlation. Here is a small example.
import numpy as np
equal_part = [0, 1, 2, 3, -2, -4, 5, 0]
y1 = equal_part + [0, 1, 2, 3, -2, -4, 5, 0]
y2 = [1, 2, 4, -3, -2, -1, 3, 2]+y1
np.argmax(np.correlate(y1, y2, 'same'))
Out:
7
So this returns the time-difference, where the correlation between both signals is at its maximum. As you can see, in the example the time difference should be 8, but this depends on your data...
Also note that both signals have the same length.

Volumetric slice plot of a parallelepiped volume data

I have a parallelepiped volume data defined by three vector:
a 2.468000 0.000000 0.000000
b -1.234000 2.137351 0.000000
c 0.000000 0.000000 32.000000
my grid is described by 40 40 500 points, respectively for the axes a,b,c. As you can see the three vectors are not mutually orthogonal and this causes a lot of problems for the reading of the grid.
My original plan was to read my raw data and then to extract several volumetric slices to be processed with sagemath to produce super nice pictures. Regrettably I looked in the python documentation for something like that and I found several command that can be used for an orthogonal volume (ndgrid, easyviz.slice_) but none for not-mutually orthogonal volume data.
In the Scitools package and numpy I found the following command
Numpy provides:
mgrid
ogrid
meshgrid
Scitools provides:
ndgrid
boxgrid
then I was looking also among the matplotlib functions but they are so many that I simply gave up.
Is there some friendly programmer that can put me in the right way?
What exactly are you after?
You can easily create an array of shape (40, 40, 500, 3) where the item at position [a, b, c] is a 3 element array holding the x, y, z coordinates of the corresponding parallelepiped grid point as follows:
a = np.array([2.468000, 0.000000, 0.000000]).reshape(1, 1, 1, 3)
b = np.array([-1.234000, 2.137351, 0.000000]).reshape(1, 1, 1, 3)
c = np.array([0.000000, 0.000000, 32.000000]).reshape(1, 1, 1, 3)
A = np.linspace(0, 1, num=40).reshape(40, 1, 1, 1)
B = np.linspace(0, 1, num=40).reshape(1, 40, 1, 1)
C = np.linspace(0, 1, num=500).reshape(1, 1, 500, 1)
grid = a * A + b * B + c * C

Categories