I'm wondering if something is possible using VisPy, or if I should start looking for other alternatives.
Here's what's going on - I'm writing an undergraduate thesis on some of the paradox-like situations of Special Relativity. What I am doing is this essentially this, I'll have some script in python producing arrays of ordered triplets (points in 3D space) that change with a time like variable (the "time like" variable is actually velocity, but it will count on just like time). I need to animate these points, which in the simpler cases will form rods, 2D squares, and 3D cubes, and of course produce .gif or similar.
Coding is definitely not my strong suit, but I have been using Python for a while. I have looked into VisPy and like that it uses OpenGL capability, and generally like how nice the various examples are (which I can get to work).
My question is: Is VisPy the best thing for what I need to do? I am having a hard time figuring out how to make VisPy make individual points into 3D objects and such - I have played around with the various Geometry things like create_cube, but it doesn't look like I can shift just the vertices around.
If anyone has any suggestions on where to start, or if Mayavi or another thing would be easier please let me know.
Update: I did figure out how to make a really nice (and simple) 3D cube outline, which is exactly what I'm wanting. I am still not sure how to get it to animate, as examples using VisPy for animation are quite different.
Does anyone have any direction? In the code, what needs to happen is that initial array of points needs to update each frame (the points can be imported or calculated in the script, either way)
import numpy as np
import vispy
import vispy.scene
from vispy.scene import visuals
from vispy import app
canvas = vispy.scene.SceneCanvas(keys='interactive', show=True)
view = canvas.central_widget.add_view()
# generate data
pos = np.array([[0, 0, 0], [0.5, 0.5, 0.5], [0, 0.5, 0.5], [0.5, 0, 0.5],
[0.5, 0.5, 0], [0, 0, 0.5], [0, 0.5, 0], [0.5, 0, 0]])
# These are the data that need to be updated each frame --^
scatter = visuals.Markers()
scatter.set_data(pos, edge_color=None, face_color=(1, 1, 1, .5), size=10)
view.add(scatter)
view.camera = 'turntable'
# just makes the axes
axis = visuals.XYZAxis(parent=view.scene)
if __name__ == '__main__':
import sys
if sys.flags.interactive != 1:
vispy.app.run()
I have the same problem and it though to seek a solution. I checked the Vispy API and changed the codes, it works well.
Hope this is helpful for you and the others.
import numpy as np
import vispy
import vispy.scene
from vispy.scene import visuals
from vispy import app
canvas = vispy.scene.SceneCanvas(keys='interactive', show=True)
view = canvas.central_widget.add_view()
view.camera = 'turntable'
# generate data
def solver(t):
pos = np.array([[0.5 + t/10000, 0.5, 0], [0, 0, 0.5], [0, 0.5, 0], [0.5, 0, 0]])
return pos
# These are the data that need to be updated each frame --^
scatter = visuals.Markers()
view.add(scatter)
#view.camera = scene.TurntableCamera(up='z')
# just makes the axes
axis = visuals.XYZAxis(parent=view.scene)
t = 0.0
def update(ev):
global scatter
global t
t += 1.0
scatter.set_data(solver(t), edge_color=None, face_color=(1, 1, 1, .5), size=10)
timer = app.Timer()
timer.connect(update)
timer.start(0)
if __name__ == '__main__':
canvas.show()
if sys.flags.interactive == 0:
app.run()
Related
I am working on a manual implementation of Ruppert's algorithm for my final project. The algorithm requires a constrained delaunay diagram as an input, which PyVista can produce by using the delaunay_2d() function. However, I am finding some inconsistency between how they managed to produce a mesh with hollowed out shapes inside, while I am unable to make my own custom inputs that achieve the same results.
For some context, here is the documentation on delaunay_2d() in PyVista: https://docs.pyvista.org/api/core/_autosummary/pyvista.PolyData.delaunay_2d.html
The code and image of the final example of this page, modified slightly for my own purposes (I turned the circles into squares), is shown below:
import pyvista as pv
squar = pv.Polygon(n_sides=4, radius=8, fill=False)
squar = squar.rotate_z(45, inplace=False)
hole1 = pv.Polygon(center=(2,3,0), n_sides=4, radius=1)
hole2 = pv.Polygon(center=(-2,-3,-0), n_sides=4, radius=0.5)
comb = hole1 + hole2 + squar
comb.plot(cpos='xy',show_edges=True)
tess = comb.delaunay_2d(edge_source=comb)
tess.plot(cpos='xy', show_edges=True)
The plot of comb, before the triangulation
The resulting constrained delaunay diagram
I tried to implement my own custom edge constraints as follows:
points1 = [[1,0,0],[1,1,0],[0,1,0],[0,0,0]]
points2 = [[0.25,0.5,0],[0.25,0.25,0],[0.5,0.25,0],[0.5,0.5,0]]
faces2 = [4, 0, 1, 2, 3]
lines1 = [5, 0, 1, 2, 3, 0]
lines2 = [5, 0, 1, 2, 3, 0]
rect1 = pv.PolyData(points1, lines=lines1)
rect2 = pv.PolyData(points2, faces2, lines=lines2)
PSLG = rect2 + rect1
PSLG.plot(cpos='xy', show_edges=True)
tess = PSLG.delaunay_2d(edge_source=PSLG)
tess.plot(cpos='xy', show_edges=True)
The plot of the PSLG constraints
The resulting constrained delaunay diagram
For some reason, my input does not recognize the inside square as a hole and it only uses the outer edges for the delaunay diagram. I am unsure why this is the case as from my understanding, I should have followed how the API example performed the operation to the letter. Please let me know if you have any insight to this matter.
Gary Lucas pointed out how other APIs use the order of the polygon to indicate "enclosing" or "excluding." For PyVista, apparently that order is determined by the input given to the face.
In this case, reversing the face input to [4, 0, 3, 2, 1] is sufficient.
The result after reversing the point order of the face I wish to exclude
Goal
I want to plot a large number of cubes (arranged in a 3D grid) with different colors and opacities.
Current State and question
I have come up with a solution using vispy, but the performance is very poor - drawing takes very long and the window is very unresponsive. Also, there seem to be some glitches in the visualization, but I could live with those.
Is there a more efficient/elegant way to implement that? I am open to using other packages (I have tried open3d but found it difficult to specify colors and opacities - the documentation is not very verbose). However, I need to use python.
What I did so far
The first problem I had to solve with vispy was that I was unable to create cubes at custom positions. I therefore wrote a subclass that can do that:
import vispy.visuals
from vispy.geometry import create_box
class PositionedCubeVisual(vispy.visuals.BoxVisual):
def __init__(self, size=1, position=(0, 0, 0), width_segments=1,
height_segments=1, depth_segments=1, planes=None,
vertex_colors=None, face_colors=None,
color=(0.5, 0.5, 1, 1), edge_color=None, **kwargs):
vertices, filled_indices, outline_indices = create_box(
size, size, size, width_segments, height_segments,
depth_segments, planes)
for column, pos in zip(vertices['position'].T, position):
column += pos
self._mesh = vispy.visuals.MeshVisual(vertices['position'], filled_indices,
vertex_colors, face_colors, color)
if edge_color:
self._border = vispy.visuals.MeshVisual(vertices['position'], outline_indices,
color=edge_color, mode='lines')
else:
self._border = vispy.visuals.MeshVisual()
vispy.visuals.CompoundVisual.__init__(self, [self._mesh, self._border], **kwargs)
self.mesh.set_gl_state(polygon_offset_fill=True,
polygon_offset=(1, 1), depth_test=True)
PositionedCube = vispy.scene.visuals.create_visual_node(PositionedCubeVisual)
I then plot the cubes as follows:
import numpy as np
import vispy.scene
def plot_grid_cubes(x, y, z, c=None, size=1, alpha=0.1, edge_color="black",
cmap="viridis", bgcolor="#FFFFFF"):
canvas = vispy.scene.SceneCanvas(keys='interactive', show=True)
view = canvas.central_widget.add_view()
view.bgcolor = bgcolor
view.camera = 'turntable'
c = get_color_array(c, alpha, min(len(x), len(y), len(z)), cmap)
for xx, yy, zz, cc in zip(x, y, z, c):
cube = PositionedCube(size, (xx, yy, zz), color=cc, edge_color=edge_color, parent=view.scene)
canvas.app.run()
def get_color_array(c, alpha, size, cmap):
if c is not None:
cmap = cm.get_cmap(cmap)
if hasattr(c, "__iter__"):
c = np.array(c, copy=True, dtype=float)
c -= c.min()
c *= 255/c.max()
return cmap(c.astype(int), alpha)
else:
color = np.ones((size, 4))
color[:, 3] = alpha
return color
This can then be applied as follows:
plot_grid_cubes([0, 1], [0, 1], [0, 1], c=[0.3, 0.5], alpha=[0.3, 0.8])
The example above works great, but it becomes poor if I plot thousands of cubes.
Regarding performance on vispy, you may want to read this:
Each Visual object in VisPy is an OpenGL Program consisting of at least a vertex shader and a fragment shader (see Modern OpenGL). In general, except for some very specific cases, OpenGL Programs can only be executed one at a time by a single OpenGL context. This means that in your VisPy visualization each Visual object you tell VisPy to draw will extend how long each update (draw) takes. When frames per second (FPS) or responsiveness are a concern, this means each Visual you add reduces the performance of your visualization.
While VisPy is constantly striving to improve performance, there are things that you can do in the mean time (depending on your particular case). The most important change that you can make is to lower the number of Visual objects you have. For a lot of Visuals it is possible to combine them into one by putting a little extra work into the data you provide them. For example, instead of creating 10 separate LineVisuals, create 1 LineVisual that draws 10 lines. While this is a simple example, the same concept applies to other types of Visuals in VisPy and more complex use cases. As a last resort for the most complex cases, a custom Visual (custom shader code) may be necessary. Before writing a custom Visual, check with VisPy maintainers by asking a question on gitter or creating a question as a GitHub issue.
Now for the BoxVisual this is a little difficult because as far as I can tell this "convenience" class doesn't allow you to make many boxes with a single BoxVisual instance. Since you are already comfortable making a Visual subclass I would recommend making the MeshVisuals yourself and providing the vertices for each box as one single position array.
As for not being able to specify position, this won't apply to your custom Visual class that will use the all-in-one array since you'll be providing each position at the beginning, but I thought I should still describe it. It is unfortunate that the BoxVisual is trying to be so convenient that it isn't helpful in this case since other Visuals allow you to pass your vertex positions on creation. In other cases or when you only want to make small modifications, typically what is done in VisPy is to use one or more "transforms" added to the Visual to shift (transform) the positions passed to the Visual. For example:
from vispy.visuals.transforms import STTransform
cube = ... create a cube visual ...
cube.transform = STTransform(scale=(1.0, 1.0, 1.0), translate=(0.0, 0.0, 0.0))
where you change the scale and translate values as needed to effect (X, Y, Z) coordinate values. After this, if you modify the cube.transform.translate = (new_x, new_y, new_z) property (or .scale or use another transform class) directly this has the benefit of only modifying that property on the GPU and not needing to recompute and resend the vertex positions (better performance).
I'm trying to combine Holoviews' Pointdraw functionality with its Sample functionality (I couldn't find a specific page, but it is shown in action here http://holoviews.org/gallery/demos/bokeh/mandelbrot_section.html)
Specifically, I want to have two subplots with interactivity. The one on the left shows a colormap, and the one on the right shows a sample (a linecut) of the colormap. This is achieved with .sample. Inside this right plot I'd like to have points that can be drawn, moved, and removed, typically done with pointdraw. I'd then also like to access their coordinates once I am done moving, which is possible when following the example from the documentation.
Now, I've got the two working independently, following the examples above. But when combined in the way that I have, this results in a plot that looks like this:
It has the elements I am looking for, except the points cannot be interacted with. This is somehow related to Holoviews' streams, but I am not sure how to solve it. Would anyone be able to help out?
The code that generates the above:
%%opts Points (color='color' size=10) [tools=['hover'] width=400 height=400]
%%opts Layout [shared_datasource=True] Table (editable=True)
import param
import numpy as np
import holoviews as hv
hv.extension('bokeh', 'matplotlib')
from holoviews import streams
def lorentzian(x, x0, gamma):
return 1/np.pi*1/2*gamma/((x-x0)**2+(1/2*gamma)**2)
xs = np.arange(0,4*np.pi,0.05)
ys = np.arange(0,4*np.pi,0.05)
data = hv.OrderedDict({'x': [2., 2., 2.], 'y': [0.5, 0.4, 0.2], 'color': ['red', 'green', 'blue']})
z = lorentzian(xs.reshape(len(xs),1),2*np.sin(ys.reshape(1,len(ys)))+5,1) + lorentzian(xs.reshape(len(xs),1),-2*np.sin(ys.reshape(1,len(ys)))+5,1)
def dispersions(f0):
points = hv.Points(data, vdims=['color']).redim.range(x=(xs[0], xs[-1]), y=(np.min(z), np.max(z)))
point_stream = streams.PointDraw(data=points.columns(), source=points, empty_value='black')
image = hv.Image(z, bounds=(xs[0], ys[0], xs[-1], ys[-1]))
return image* hv.VLine(x=f0) + image.sample(x=f0)*points
dmap = hv.DynamicMap(dispersions, kdims=['f0'])
dmap.redim.range(f0=(0,10)).redim.step(f0=(0.1))
I apologize for the weird function that we are plotting, I couldn't immediately come up with a simple one.
Based on your example it's not yet quite clear to me what you will be doing with the points but I do have some suggestions on structuring the code better.
In general it is always better to compose plots from several separate DynamicMaps than creating a single DynamicMap that does everything. Not only is it more composable but you also get handles on the individual objects allowing you to set up streams to listen to changes on each component and most importantly it's more efficient, only the plots that need to be updated will be updated. In your example I'd split up the code as follows:
def lorentzian(x, x0, gamma):
return 1/np.pi*1/2*gamma/((x-x0)**2+(1/2*gamma)**2)
xs = np.arange(0,4*np.pi,0.05)
ys = np.arange(0,4*np.pi,0.05)
data = hv.OrderedDict({'x': [2., 2., 2.], 'y': [0.5, 0.4, 0.2], 'color': ['red', 'green', 'blue']})
points = hv.Points(data, vdims=['color']).redim.range(x=(xs[0], xs[-1]), y=(np.min(z), np.max(z)))
image = hv.Image(z, bounds=(xs[0], ys[0], xs[-1], ys[-1]))
z = lorentzian(xs.reshape(len(xs),1),2*np.sin(ys.reshape(1,len(ys)))+5,1) + lorentzian(xs.reshape(len(xs),1),-2*np.sin(ys.reshape(1,len(ys)))+5,1)
taps = []
def vline(f0):
return hv.VLine(x=f0)
def sample(f0):
return image.sample(x=f0)
dim = hv.Dimension('f0', step=0.1, range=(0,10))
vline_dmap = hv.DynamicMap(vline, kdims=[dim])
sample_dmap = hv.DynamicMap(sample, kdims=[dim])
point_stream = streams.PointDraw(data=points.columns(), source=points, empty_value='black')
(image * vline_dmap + sample_dmap * points)
Since the Image and Points are not themselves dynamic there is no reason to put them inside the DynamicMap and the VLine and the sampled Curve are easily split out. The PointDraw stream doesn't do anything yet but you can now set that up as yet another DynamicMap which you can compose with the rest.
In pyqtgraph you can scatterplot each item for itself or a whole bunch of them as bulk (using spots). working with large datasets i prefer the last method since the figure stays light and is movable without lagging all over the screen.
my problem
some of my symbols i need an angle... that isn't that much of a problem, however if i add them separately to the plot it results in a laggy figure. so my problem is that i am currently unable to find a suitable way to subclass the whole thing and implement a small method for the keyword argument "rotation"/"angle". has anyone finished this task already or has someone an idea?
thank you very much in advance!
After another look today I finally found that it was way too simple: Just rotating my symbol before adding it to the ScatterPlotItem did the trick. For the sake of documentation and maybe some other struggling programmers, a snippet:
import numpy as np
import pyqtgraph as pg
# define a symbol bowtie style
_mos = np.asarray([
[0.5, 0.25],
[0.5, -0.25],
[-0.5, 0.25],
[-0.5, -0.25],
[0.5, 0.25]
])
my_symbol = pg.arrayToQPath(_mos[:, 0], _mos[:, 1], connect='all')
# define color and stuff for your items
exit_item = pg.ScatterPlotItem(
size=20,
pen=pg.mkPen(128, 128, 128, 255),
brush=pg.mkBrush(255, 255, 255, 255),
)
# calculate angle between two sets of points
angle = np.arctan2(np.asarray(y1-y0), np.asarray(x1-x0)) * 180/np.pi
# rotate symbol with that angle
tr = QTransform()
angle_rot = tr.rotate(angle)
my_rotated_symbol = angle_rot.map(my_symbol)
# may be a whole list of spots with different angles and positions
exit_spots = []
exit_spots.append({
'pos': (0, 0),
'symbol': my_rotated_symbol
})
# add the spots to the item
exit_item.addPoints(exit_spots)
# create a plot and add the content
win = pg.GraphicsWindow()
plot = win.addPlot()
plot.addItem(exit_item)
I would like to set individual properties (zorder and label for example) for a specific element of a matplotlib.collections.PathCollection. I couldn't find a way in the documentation.
Here I jot down a user case.
Let say we have the following snippet, and we'd like to change the zorder of the red ball, bringing it to the top, by using the balls handle, which is a matplotlib.collections.PathCollection.
balls = plt.scatter([-1, 1], [0, 0], c = ['r', 'b'], s = 4e4)
plt.axis([-5, 5, -5, 5])
Does anyone have any idea about how to tweak individual paths of a PathCollection?
The alternative would be using plt.plot('o'), which actually returns a list of handles. Unfortunately the plt.plot('o') solution won't allow me to set a different colour per ball, since they would all belong to the same chart. So a for loop would be required.
The drastic solution, which I bet I'll go for, since my deadline, is going of Inkscape :/
Not sure if this is the best solution, but it might help you.
From what I can see, the paths in the PathCollection are always plotted in the order they are created. So in your case, the path with the x-position of -1 is created first, then the one with 1.
You can switch that order after initially plotting them, by changing the offsets, in your case using balls.set_offsets():
In [4]: balls = plt.scatter([-1, 1], [0, 0], c = ['r', 'b'], s = 4e4)
In [5]: plt.axis([-5, 5, -5, 5])
This creates the following figure:
In [42]: print balls.get_offsets()
[[-1. 0.]
[ 1. 0.]]
On [43]: balls.set_offsets([[1,0],[-1,0]])
Now, this has plotted the left-hand ball on top of the right-hand ball:
But as you can see, this has also switched the facecolors around (since we set the order of that in the call to plt.scatter as ['r','b']. There's a solution to this, which is to also switch the facecolors around:
In [46]: balls.set_facecolors(['b','r'])
Great, so putting that all together, we can define a function to switch the offsets and facecolors of any two arbitrary paths in the PathCollection.
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
balls = ax.scatter([-3, -1, 1, 3], [0, 0, 0, 0], c = ['r', 'b', 'g', 'm'], s = 4e4)
ax.set_xlim(-6,6)
ax.set_ylim(-6,6)
plt.savefig('balls_01.png')
def switch_scatter(pathcoll,a,b):
# Switch offsets
offsets = pathcoll.get_offsets()[:]
offsets[[a,b]] = offsets[[b,a]]
# Switch facecolors
facecolors = pathcoll.get_facecolors()
facecolors[[a,b]] = facecolors[[b,a]]
# Switch sizes
sizes = pathcoll.get_sizes()
sizes[[a,b]] = sizes[[b,a]]
# Set the new offsets, facecolors and sizes on the PathCollection
pathcoll.set_offsets(offsets)
pathcoll.set_facecolors(facecolors)
pathcoll.set_sizes(sizes)
switch_scatter(balls,2,1)
plt.savefig('balls_02.png')
Heres balls_01.png:
And here is balls_02.png (where we switch ball 1 and ball 2 (the blue and green balls)
A final note: if you have other properties varying in your scatter plot (e.g. linecolor), you will also need to switch them around in the function I defined above.