pyOCCT — Python bindings for OpenCASCADE¶
About¶
The pyOCCT project provides Python bindings to the OpenCASCADE geometry kernel via pybind11. Together, this technology stack enables rapid CAD/CAE/CAM application development in the popular Python programming language.
## Enabling technology The pyOCCT core technology stack includes:
- OpenCASCADE: Open CASCADE Technology (OCCT) is an object-oriented C++ class library designed for rapid production of sophisticated domain-specific CAD/CAM/CAE applications.
- pybind11: A lightweight header-only library that exposes C++ types in Python and vice versa, mainly to create Python bindings of existing C++ code.
Design Considerations¶
This section describes some fundamental assumptions and implementation details for the pyOCCT project, most of which relate to pybdind11 usage. Feedback is welcomed on these topics based on usage experience and expertise.
Organization¶
The pyOCCT project, source code, and binaries are generally organized by OpenCASCADE packages (over three hundred so far), which eventually get organized into separate Python modules. Each pyOCCT source file represents an OpenCASCADE package and gets compiled into a Python module and contains all the entities of that package. Import statements and call guards in the pyOCCT source are used as needed. This results in a general structure of:
from OCCT.PackageName import ClassName
to be used in Python when importing the various pyOCCT modules. One major advantage of this approach has been the ability to modify and compile each module separately rather than a single large binary, reducing build time and improving maintainability.
Static Methods¶
To avoid naming conflicts with regular instance methods, the names of static methods are modified by appending a trailing underscore. For example, the code
edge = TopoDS.Edge_(shape)
uses the static method TopoDS::Edge
to downcast a shape to an edge if
possible. The trailing underscore was selected as a global rule to avoid naming
conflicts and inform the user they are calling a static method.
Templates¶
The OpenCASCADE library is a large and complex codebase that makes use of modern C++ features including templates. Compared to earlier versions, the latest releases of OpenCASCADE define a number of different types by instantiating a set of core class templates. To more closely follow the OpenCASCADE architecture and avoid repetitive code, function templates are used whenever possible to bind types derived from an OpenCASCADE class template. In the appropriate module, the function template can be called with essentially the same parameters as its OpenCASCADE counterpart, with the addition of the current pybind11 module and its desired name.
As an example, binding the type TopoDS_ListOfShape
in the TopoDS
module
source code is:
bind_NCollection_List<TopoDS_Shape>(mod, "TopoDS_ListOfShape");
where mod
is the pybind11 module. In the NCollection
template header
file included in the TopoDS
module source code, the function template looks
similar to:
template <typename TheItemType>
void bind_NCollection_List(py::object &mod, std::string const &name) {
py::class_<NCollection_List<TheItemType>, NCollection_BaseList> cls(mod, name.c_str());
cls.def(py::init<>());
// continued in source...
};
This seems to be an efficient and maintainable implementation, but feedback and suggestions are welcomed.
Smart Pointers¶
The first and most critical decision was what smart pointer
to use for binding Python classes. Both std::unique_ptr
and
std::shared_ptr
are supported out of the box by pybind11, with
std::unique_ptr
being used by default.
OpenCASCADE also implements its own custom smart pointer referred to as a
handle in the library. This opencascade::handle<T>
class template is their
own implementation of an intrusive smart pointer used with the
Standard_Transient
class and its descendants.
The following approach was taken for smart pointer selection:
- Use
std::unique_ptr
for all types if not a descendant ofStandard_Transient
- Use
opencascade::handle
for all descendants ofStandard_Transient
To enable the use of opencascade::hande
the following macro is applied:
PYBIND11_DECLARE_HOLDER_TYPE(T, opencascade::handle<T>, true);
where true
is used since it uses intrusive reference counting. So far this
seems to be a workable and convenient approach, but again feedback is welcomed.
Non-public Destructors¶
One reason the std::unique_ptr
was chosen as described above is the ability
to handle types non-public destructors. This is described here
in the pybind11 documentation. A number of OpenCASCADE types make use of
non-public destructors and the pybind11 helper class py::nodelete
is used
when binding these types.
As a result of using py::nodelete
in some types, it was found that types
derived from those with non-public destructors must have some type of helper
class in the std::unique_ptr
instantiation otherwise a compile error would
result. It was unclear whether this was a compiler or pybind11 issue, but the
remedy at the time was to implement a “dummy” helper class as:
template<typename T> struct Deleter { void operator() (T *o) const { delete o; } };
and use in binding source like:
// Base type with non-public destructor
py::class_<Base, std::unique_ptr<Base, py::nodelete>>
// Derived type with public destructor
py::class_<Foo, std::unique_ptr<Foo, Deleter<Foo>>, Base>
This Deleter
template pattern was applied to all types with public
destructors to better support the automation of the binder generation tool.
Early tests seemed to indicate that this worked as expected (i.e., instances
were deleted as the Python reference count dropped to zero), but the
implications of this approach may not be entirely understood and feedback
is welcomed.
Iterators¶
Some types support iteration like NCollection_List<TheItemType>
which is
used as the template for the TopoDS_ListOfShape
type. So now the user can
do something like:
from OCCT.TopoDS import TopoDS_ListOfShape
shape_list = TopoDS_ListOfShape()
shape_list.Append(item1)
shape_list.Append(item2)
for item in shape_list:
do something...
Enabling iterators is done by defining a __iter__
method for the type if
the type also has begin
and end
methods, the assumption here being that
this type is an iterator. For the example above, both
NCollection_List<TheItemType>::begin
and
NCollection_List<TheItemType>::end
are present so the binder generation
tool automatically implement the method:
cls.def("__iter__", [](const NCollection_List<TheItemType> &s) { return py::make_iterator(s.begin(), s.end()); }, py::keep_alive<0, 1>());
This seems to be a useful approach but it dependent on function names.
Overriding Virtual Functions¶
The capability to override virtual functions defined in a C++ class in Python is provided by pybind11 and described here. Initial attempts to provide this functionality to pyOCCT were made using trampoline classes but proved to be difficult and complex to implement via the automated generation tool. Therefore, this capability is not provided in pyOCCT and typical usage thus far has not required it.
Reference Arguments¶
Passing arguments by mutable references and pointers is common in C++, but
certain Python basic types (str
, int
, bool
, float
, etc.) are
immutable and will not behave the same way. This is described in detail in the
pybind11 docs.
For example,
this
method passes in the First
and Last
arguments by reference and are floats
which are modified in place while the method returns the underlying curve. In
Python, providing these last two parameters will have no affect. To remedy
this, some logic is built into the binding generation tool that attempts to
recognize Python immutable types that are passed by reference (and without
const
) and instead return them in a tuple along with the regular return
argument. To maintain overload resolution order, “dummy” parameters are
still required to be input. The example in Python now becomes something like:
curve, first, last = BRep_Tool.Curve_(edge, 0. ,0.)
So far this has proven to be a reliable approach but is dependent on the logic and assumptions described above.
Exceptions¶
Exception handling is supported by pybind11 and described here in the pybind11 documentation. How to best handle exceptions raised by the OpenCASCADE library on the Python side has not yet been fully explored. A minimal attempt can be found at the bottom of the Standard.cpp source file and is also shown below.
// Register Standard_Failure as Python RuntimeError.
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) std::rethrow_exception(p);
}
catch (const Standard_Failure &e) {
PyErr_SetString(PyExc_RuntimeError, e.GetMessageString());
}
});
This seems to catch and report some errors in Python but not all. Alternative
approaches are improvements are needed. This small implementation was placed
in the Standard
module since most, if not all, modules import this module
at some level.
Known Issues¶
This is a summary of some known issues:
- Methods like
ShapeAnalysis_FreeBounds::ConnectEdgesToWires
take in aTopTools_HSequenceOfShape
which is modified on the C++ side to contain the resulting wires. In the source, they useowires = new TopTools_HSequenceOfShape
to I think clear the list. At this point I think this breaks the associativity to the Python variable as the provided variable is not changed. For now, this is avoided by using a lambda function in the bindings and the resulting wires are returned rather than modified as an input. So far only trial and error has detected these issues and they are usually fixed on a case-by-case basis. - While pyOCCT provides coverage for a significant amount of the OpenCASCADE codebase, there are exceptions. An error will be thrown if a needed type is not registered by pybind11. Sometimes it’s just a small matter of patching the source to expose a type, function, or attribute. It could also be omitted for a reason and the user is encouraged to investigate the issue and determine the root cause. Issues (and hopefully resolutions) can be submitted using GitHub Issues and Pull Requests.
- Arrays are not supported but have not been encountered during typical usage. Resolving this mostly just requires a better understanding of how to handle arrays within pybind11.
- Support for nested classes and types is mixed. This is well supported in pybind11 but its a matter of implementation detail and complexity in the automated binding generation tool. Things can be fixed manually as needed but another pass at this is needed in the binding generator tool.
Overview¶
For now, extensions are relatively small, lightweight modules intended to streamline basic OpenCASCADE functionality and make it more pythonic. They should be small in scope and provide relatively generic capability to enable users to more quickly develop their own applications. Development of large-scale, special purpose toolkits or applications is outside the scope of native pyOCCT functionality.
Exchange¶
The Exchange
extension provides tools for data exchange including reading
and writing BREP, STEP, and/or IGES files. The tools can be imported as:
from OCCT.Exchange import *
shape = ExchangeBasic.read_step('model.step')
The following tools are available:
Name | Description |
---|---|
ExchangeBasic |
Basic read/write static methods. |
Visualization¶
A minimal viewing tool is provided in the Visualization
extension. It can
be imported as:
from OCCT.Visualization import WxViewer
v = ViewerWx()
v.add(*args)
v.start()
This is intended to provide only a minimum capability to display shapes to the screen. Examine the source further for other methods and properties.
The following tools are available:
How to Cite pyOCCT¶
The following Bibtex template can be used to cite the pyOCCT project in scientific publications:
@misc{pyOCCT,
author = {Trevor Laughlin},
year = {2020},
note = {https://github.com/trelau/pyOCCT},
title = {pyOCCT -- Python bindings for OpenCASCADE via pybind11}
}
PythonOCC Comparison¶
The overall organization between pyOCCT and PythonOCC is very similar. The
most noticeable difference is that the installed package is called OCCT
instead of OCC
and the concept of handles as described below.
Static Methods¶
In OCC, static methods are converted to module level methods with their
name following the format modulename_MethodName()
. In pyOCCT, static
methods are within the class but have a trailing underscore. The trailing
underscore was needed to avoid naming conflicts with regular class methods.
For example, the method to convert a generic TopoDS_Shape
to a
TopoDS_Edge
in PythonOCC is:
from OCC.TopoDS import topods_Edge
In pyOCCT, this is now:
from OCCT.TopoDS import TopoDS
and the method is called as:
edge = TopoDS.Edge_(shape)
GetHandle() and GetObject()¶
In PythonOCC, a Python object wrapping an OpenCASCADE type usually had a
method called GetHandle()
which would return a Handle_*
instance (e.g.,
Handle_Geom_Curve
), or a GetObject()
method to return the underlying
object if you have a Handle_*
instance on the Python side. The OpenCASCADE
opencascade::handle<Type>
is their own implementation of a smart pointer
for memory management. In pyOCCT, the binding technology actually uses
the OpenCASCADE handle as a custom smart pointer (everything is wrapped by a
smart pointer in pybind11) so on the Python side the wrapped type actually
serves as both the object and the handle. Methods that returned a
Handle_*
instance in PythonOCC will now return the specific type (i.e.,
Handle_Geom_Curve
now just comes back as a Geom_Curve
). There is no
more GetHandle()
or GetObject()
methods. Methods and/or classes that
require a handle as an input can now just be supplied the pyOCCT instance.
Return Types¶
In pybind11, return types are resolved to their most specific type before being returned to Python. This is not the case in C++ where a type may be returned and then require additional downcasting to get a more specific type. This may provide a more pythonic interface, but the user should be aware that the return types may not exactly much the C++ documentation, although since they will be a sub-class they should have the same functionality. For example, copying a line in PythonOCC may have looked like:
handle_geom = line.Copy()
new_line = Handle_Geom_Line.Downcast(handle_geom).GetObject()
where in pyOCCT it will now look like:
new_line = line.Copy()
with new_line
being of type Geom_Line
. There are no more Handle_*
types available to import or use.