I know that we were almost done deciding on a method to update the pipeline and get the result, but there have been 2 good suggestions that we should consider:
pipeline.update().data
implementing the __call__ method so we can do: vtkContourFilter(contour_values=[100])(input)
My two cents on these: I like them both and I wouldn’t mind having both implemented. (1) is a shorter version of what we do in VTK, (2) would allow us to treat the filters as functors and pass them where functions/lambdas/callable objects are used such as map.
PS: I would suggest using the Update() method and having it return the algorithm rather than implementing a new Python method.
Ideally this would be “optional” and live under something like vtkmodules.util.networkx_support (a sibling of numpy_support. I don’t think import vtkmodules.vtkCommonCore should require networkx.
It took a bit but I now have an implementation of both update() and __call__. update() is equivalent to Update(). It does not take any input. It just updates the pipeline. I will add support for specifying things like pieces and time later. __call__ is for using an algorithm or a pipeline as a function. It will take either no arguments (sources), one data object (regular filters), or a sequence of data objects (repeatable inputs like append). Things that are possible:
aSource.update().output
(aSource >> aFilter).update().output # will return a tuple if aFilter has multiple outputs
aFilter(inpData)
(aFilter >> bFilter)(inpData)
aSource()
(aSource >> aFilter)()
app = vtk.vtkAppendPolyData()
app((poly1, poly2))
There are still a few things that I would like to implement such as specifying pieces etc. in update() and using sequences with >> for connecting pipelines to append etc. but I am going to focus on writing tests for what is there and getting it committed in the next few weeks and do those later.
(aSource >> aFilter)() is equivalent to (aSource >> aFilter).update().output so it does not have a lot of value. It was also easy to implement so I did it.
The main value of __call__ is that it can take a data object as argument so you can do this: output = aFilter(inpData)
The equivalent of this with >> is output = (inpData >> aFilter).update.output
which is not as concise AND it leaves aFilter in a state of pipeline connection. The () version lets you use pipelines and filters as functions (without hooking up the input AND splitting out the output through a shallow copy).
Hrm. While it is the most common case, does this mean we’ll have an ambiguity if we return some algorithm result as a tuple itself in the future (say we start using std::tuple in VTK)? Do the bindings know this that they can write it into the .pyi or __doc__ for the attribute/method? I know dealing with (vtkobject,) in the vastly common one-object case is annoying, but I do want to note this; I think mentioning the data in the generated docs is sufficient though.
Yeah, good point. I ran into this. The clip filter has two outputs, even though the second one is turned on with a flag. I was expecting () to return the data object, not a tuple, so my code was broken. I’ll do my best to document this. It is not always straightforward to document some of these methods (for example, I can’t find a way to document methods that have their slots such as tp_call (__call__).
Might need to change how we define objects. There is PyMethodDef which supports docstrings on slots. See this MR for work towards supporting the stable ABI (which would eventually use PyMethodDef).
I love being able to specify keyword arguments in the constructor!!!
Note also that you can call other functions to set a keyword in the constructor, see ren = vtkRenderer... line 43.
Ideas for improvement welcome.
#!/usr/bin/env python3
# This simple example shows how to do basic rendering and pipeline creation.
# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkFiltersSources import vtkCylinderSource
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkPolyDataMapper,
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkRenderer
)
def main():
colors = vtkNamedColors()
# Set the background color.
bkg = map(lambda x: x / 255.0, [26, 51, 102, 255])
colors.SetColor('BkgColor', *bkg)
# This creates a polygonal cylinder model with eight circumferential facets.
# We can initialize the properties of a wrapped VTK class by specifying keyword arguments in the constructor.
cylinder = vtkCylinderSource(resolution=8)
# We can also print the properties of a VTK object in a more pythonic way.
print(
f'Cylinder properties:\n height: {cylinder.height}, radius: {cylinder.radius},'
f' center: {cylinder.center} resolution: {cylinder.resolution} capping: {cylinder.capping == 1}')
cm = vtkPolyDataMapper(input_connection=cylinder.output_port)
ca = vtkActor(mapper=cm)
ca.RotateX(30)
ca.RotateY(-45)
ca.GetProperty().SetColor(colors.GetColor3d('Tomato'))
# Note the setting of the background by calling GetColor()
ren = vtkRenderer(background=colors.GetColor3d('BkgColor'))
ren.AddActor(ca)
ren_win = vtkRenderWindow(size=[300, 300], window_name='CylinderExample')
ren_win.AddRenderer(ren)
iren = vtkRenderWindowInteractor()
iren.SetRenderWindow(ren_win)
iren.Initialize()
ren.ResetCamera()
ren.GetActiveCamera().Zoom(1.5)
ren_win.Render()
# Start the event loop.
iren.Start()
if __name__ == '__main__':
main()
Generates an: AttributeError: attribute 'actors' of 'vtkmodules.vtkRenderingCore.vtkRenderer' objects is not writable
I actually tried renderers= however this error is generated: AttributeError: attribute 'renderers' of 'vtkmodules.vtkRenderingCore.vtkRenderWindow' objects is not writable
Works but it is flagged as an unexpected argument in PyCharm.
Properties are only generated when there are Set and/or Get methods. If we want renderers to be a property, we need a SetRenderers method. Which is doable with std::vector argument now that we are allowing those. Same with actors.