More Pythonic VTK wrapping

Exposing C++ STL classes to the API seems like a bad idea to me. These changes will affect the Java wrappers as well.

Done, I like it! It also looks much better and makes more sense, see here: PythonicAPI on my test site. I’ll do a MR anyway, see MR 322. This means everything should be in place for upgrading/creating examples when it happens.

@ben.boeckel I was uneasy about using Python1 - it had occurred to me that one day there may be a Python 4!

Thanks for the input!

Good point. Although I humbly disagree :slight_smile: I will start another thread to discuss this.

It’s too bad that you can’t do source.filtera().filter(), as that feels the most natural and similar to other “chaining” Python APIs.

While I love the idea of a function that calls .Update() and then returns the output (.GetOutput()), the word “execute” doesn’t convey that concept to me. It sounds like the filter is being run, but there’s no connotation of anything being returned by an “execute” method. What about .generate()? Or maybe .get_result()?

It’s too bad that you can’t do source.filtera().filter() , as that feels the most natural and similar to other “chaining” Python APIs.

Not feasible with > 1000 algorithms :slight_smile: Also, chaining is not as old as people might think. numpy doesn’t let you chain ufuncs for example (while PyTorch lets you chain functions).

While I love the idea of a function that calls .Update() and then returns the output (.GetOutput() ), the word “execute” doesn’t convey that concept to me. It sounds like the filter is being run, but there’s no connotation of anything being returned by an “execute” method. What about .generate() ? Or maybe .get_result() ?

I hear you. I like generate(). Another option would be to use the __call__() method. That would look like vtkContourFilter(input=foo, contour=[10, 20])(). We could also go a more functional route, like:

result = apply(vtkContour(contours=[10, 20]) >> vtkShrink(), input_data)

For full pipelines, this could look like

result = apply(vtkSomeReader(file_name="...") >> vtkContour(contours=[10, 20]) >> vtkShrink())

The rshift operator overload, when combined with initializing properties through constructor allows for very readable code!

If you had to reproduce testCaseManyAlgorithmsWithUserInput in TestAlgorithmNumberProtocol in VTK python right now, one’d have to write a function, instantiate every one of those filters, name the instances and setup connections. In the new code, it’s much more readable and easier to code and customize.

Just for the sake of a visual comparison, here they are:

Future
Lines of code: 12

# A pipeline object can be reused with different input data objects.
pipeline = (
vtkElevationFilter()
  >> vtkShrinkFilter()
  >> vtkGeometryFilter()
  >> vtkPolyDataConnectivityFilter(color_regions=True, extraction_mode=VTK_EXTRACT_ALL_REGIONS)
  >> vtkPolyDataNormals()
)
cone = vtkConeSource(radius=5, resolution=8, height=2).execute()
print(pipeline.execute(cone))
cylinder = vtkCylinderSource(radius=6, resolution=9, height=3).execute()
print(pipeline.execute(cylinder))

Now
Lines of code: 29

def execute(input):
    elevator = vtkElelvationFilter()
    shrinker = vtkShrinkFilter()
    ugToPolyData = vtkGeometryFilter()
    connectivity = vtkPolyDataConnectivityFilter()
    connectivity.SetColorRegions(True)
    connectivity.SetExtractionMode(VTK_EXTRACT_ALL_REGIONS)
    normals = vtkPolyDataNormals()
    # build pipeline
    elevator.SetInputDataObject(input)
    shrinker.SetInputConnection(0, elevator.GetOutputPort(0))
    ugToPolyData.SetInputConnection(0, shrinker.GetOutputPort(0))
    connectivity.SetInputConnection(0, ugToPolyData.GetOutputPort(0))
    normals.SetInputConnection(0, connectivity.GetOutputPort(0))
    # run
    normals.Update()
    return normals.GetOutput()
# create a cone
cone = vtkConeSource()
cone.SetRadius(5)
cone.SetResolution(8)
cone.SetHeight(2)
print(execute(cone))
# create a cylinder
cylinder = vtkCylinderSource()
cylinder.SetRadius(6)
cylinder.SetResolution(9)
cylinder.SetHeight(3)
print(execute(cylinder))

Another benefit is that vtkAlgorithm(s) are constructed only the first time when the pipeline object is defined. Compare this with the below code snippet, where they are recreated. Of course, you could write a class and store references to the algorithms, but that’s just more complicated and annoying to do in the interpreter.

4 Likes

It should also be way easier to mix and match different pipelines. I expect it would be less exhausting to prototype and develop custom modeling applications on top of VTK.

I would have thought this reads better with a left shift operator

pipeline = (vtkPolyDataNormals() << 
vtkPolyDataConnectivityFilter(color_regions=True, extraction_mode=VTK_EXTRACT_ALL_REGIONS) << 
vtkGeometryFilter() << 
vtkShrinkFilter() << 
vtkElevationFilter()
)

Doesn’t that need to be read from right-to-left? I see your point of view because it’s easier to associate pipeline with the last filter in the chain i.e, normals filter.

1 Like

You can still read left to right.
I read the vtkPolyDataNormals filter input depends on vtkPolyDataConnectivityFilter output. If I’m interested in where the poly data filter gets its input I read further to the right.

I’m not a Python expert but it seems more logical to me.

Wow!!! That is so impressive!

I prefer left to right: “>>” since we read left to right and pipes “|” in bash work left to right. For me: “a >> b” means do “a” first then feed the result into “b”, it also makes it easier to just add " >> c" at the end, if “c” is a new filter.

1 Like

You’re right. Pipes push data from left to right. I suppose I was thinking of the pipeline object more like a stream.

Hi folks,

We made some good progress with the wrapped properties and the pipeline connection with >>. See this gist for some examples:

If there are any suggestions for an alternative to execute(), I am all ears. My preference is execute since I have bee using that term for >20 years with VTK.

1 Like

To my mind execute() doesn’t return anything. Methods build() or generate() would be better.

Alternatively a breaking change to
aReader.GetOutput(bool update = false)
or
aFilter.GetOutput(vtkDataObject* data = nullptr)

1 Like

I think I like it. Maybe providing an example with GlyphMapper and its input ports could be interesting.

what about get_output(), or run()

How about eval() or evaluate()? When used in the context of functions, these imply returning a value.

I don’t like overloading get_output any further since it has meant getting the output without execution from the beginning. run() has the same issue as execute() in that it does not imply returning a value.

Perhaps it should have been done the other way around whereby the default behaviour of GetOutput() was to always perform an evaluation but supplemented by a caching mechanism to return a stored value when the filter/reader was unmodified.

I really don’t see any issues with execute() … to my mind the pipelines are normally executed or evaluated. execute() doesn’t imply that no value is returned. I would normally expect a result … Null, Pass, Fail, the pipeline …