geom: expose a reusable kernel object to Python (prototype for #9417)

ifcopenshell.geom.create_shape constructs a geometry kernel, converter and
mapping on every call and discards them afterwards. For hybrid kernels the
construction alone rescans the plugin directory per component, which #9417
addresses on the C++ side by caching the resolved component ids. This
prototypes the alternative aothms suggested in the #9417 review: reuse the
constructed kernel itself from Python.

The wrapper gains a geometry_kernel class holding the converter (and through
it the kernel, mapping and conversion caches) built once per
(geometry_library, file, settings) triple, with create_shape(instance[,
representation]) delegating to the same helper the free create_shape uses,
now split so both paths share one body. Python gains
ifcopenshell.geom.kernel(settings, file, geometry_library) mirroring the
iterator constructor signature. The binding is fixed at construction:
settings are copied the way converter already copies them, and a guard
rejects instances from a different file because the mapping is file-bound.

On PGSuper_Import_Model.ifc (322 products, single-threaded, arm64 Linux),
hybrid-cgal-simple-opencascade drops from 71.97 ms/call with per-call
create_shape to 23.35 ms/call through the reused kernel, matching the
iterator (22.74 ms/call) without needing the #9417 cache; plain opencascade
is unaffected (23.30 vs 22.28 ms/call).
This commit is contained in:
Petru Conduraru
2026-09-01 13:29:21 +03:00
committed by Thomas Krijnen
parent e713fdb17e
commit 66bcddc277
2 changed files with 95 additions and 4 deletions
@@ -549,6 +549,63 @@ def create_shape(
)
class kernel:
"""A reusable geometry kernel bound to a (geometry library, file, settings) triple.
``ifcopenshell.geom.create_shape`` constructs a new geometry kernel on every
call, which repeats the backend resolution (including plugin discovery for
hybrid kernels) and discards the mapping and conversion caches afterwards.
This class performs that construction once so that converting many products
one by one reuses the same kernel, mapping and caches, similar to what
``ifcopenshell.geom.iterator`` does internally.
The kernel is bound at construction: the settings are copied and the file
reference is kept, so later changes to the settings object do not affect an
existing kernel and instances passed to :meth:`create_shape` must belong to
the bound file.
Example:
.. code:: python
settings = ifcopenshell.geom.settings()
k = ifcopenshell.geom.kernel(settings, ifc_file, geometry_library="hybrid-cgal-simple-opencascade")
for product in ifc_file.by_type("IfcProduct"):
if product.Representation:
shape = k.create_shape(product)
"""
def __init__(
self,
settings: settings,
file: file,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
logger: Optional[ifcopenshell.logger] = None,
):
self.settings = settings
self.file = file
self.wrapped = ifcopenshell_wrapper.geometry_kernel(
geometry_library, file, settings, *ifcopenshell.optional_logger_args(logger)
)
def create_shape(
self,
inst: entity_instance,
repr: Optional[entity_instance] = None,
) -> Union[
ShapeType, ShapeElementType, ifcopenshell_wrapper.transformation, utils.shape_tuple, TopoDS.TopoDS_Shape
]:
"""Identical to :func:`create_shape` but reuses this kernel across calls.
See :func:`create_shape` for the possible return types; the settings and
geometry library bound at construction are used for every call.
"""
return wrap_shape_creation(
self.settings,
self.wrapped.create_shape(inst, repr) if repr else self.wrapped.create_shape(inst),
)
def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper.item:
"""
Returns an interpretation of the geometry encoded as per IfcOpenShell's taxonomy layer.
+38 -4
View File
@@ -990,10 +990,7 @@ struct shape_rtti : public boost::static_visitor<PyObject*>
return oss.str();
}
static std::variant<ifcopenshell::geom::element*, ifcopenshell::geom::representation*, ifcopenshell::geom::transformation*> helper_fn_create_shape(ifcopenshell::logger& logger, const std::string& geometry_library, ifcopenshell::geom::settings& settings, const express::base& instance, const express::base& representation = express::base()) {
ifcopenshell::file* file = instance.file();
ifcopenshell::geom::converter kernel(ifcopenshell::geom::kernels::construct(file, geometry_library, settings, logger), file, settings, logger);
static std::variant<ifcopenshell::geom::element*, ifcopenshell::geom::representation*, ifcopenshell::geom::transformation*> helper_fn_create_shape_with_converter(ifcopenshell::geom::converter& kernel, ifcopenshell::geom::settings& settings, const express::base& instance, const express::base& representation) {
if (instance.declaration().is("IfcProduct")) {
if (representation && !representation.declaration().is("IfcRepresentation")) {
throw ifcopenshell::exception("Supplied representation not of type IfcRepresentation");
@@ -1069,6 +1066,12 @@ struct shape_rtti : public boost::static_visitor<PyObject*>
}
return std::variant<ifcopenshell::geom::element*, ifcopenshell::geom::representation*, ifcopenshell::geom::transformation*>();
}
static std::variant<ifcopenshell::geom::element*, ifcopenshell::geom::representation*, ifcopenshell::geom::transformation*> helper_fn_create_shape(ifcopenshell::logger& logger, const std::string& geometry_library, ifcopenshell::geom::settings& settings, const express::base& instance, const express::base& representation = express::base()) {
ifcopenshell::file* file = instance.file();
ifcopenshell::geom::converter kernel(ifcopenshell::geom::kernels::construct(file, geometry_library, settings, logger), file, settings, logger);
return helper_fn_create_shape_with_converter(kernel, settings, instance, representation);
}
%}
%typemap(out) ifcopenshell::geom::taxonomy::item::ptr {
@@ -1116,6 +1119,37 @@ ifcopenshell::geom::taxonomy::item::ptr try_upcast(PyObject* obj0, swig_type_inf
}
%}
%inline %{
// Reusable geometry kernel handle bound to a (geometry_library, file,
// settings) triple. Constructing it resolves the kernel once and reuses
// the converter, mapping and caches for every create_shape call.
class geometry_kernel {
public:
geometry_kernel(const std::string& geometry_library, ifcopenshell::file* file, ifcopenshell::geom::settings& settings, ifcopenshell::logger* logger = nullptr)
: file_(file)
, settings_(settings)
, converter_(ifcopenshell::geom::kernels::construct(file, geometry_library, settings_, ifcopenshell::logger_or_root(logger)), file, settings_, ifcopenshell::logger_or_root(logger))
{}
std::variant<ifcopenshell::geom::element*, ifcopenshell::geom::representation*, ifcopenshell::geom::transformation*> create_shape(const express::base& instance, const express::base& representation) {
if (instance.file() != file_) {
throw ifcopenshell::exception("Instance does not belong to the file this kernel was constructed for");
}
return helper_fn_create_shape_with_converter(converter_, settings_, instance, representation);
}
// Manual definition of overload without representation argument
std::variant<ifcopenshell::geom::element*, ifcopenshell::geom::representation*, ifcopenshell::geom::transformation*> create_shape(const express::base& instance) {
return create_shape(instance, express::base());
}
private:
ifcopenshell::file* file_;
ifcopenshell::geom::settings settings_;
ifcopenshell::geom::converter converter_;
};
%}
// @todo bring back serialization OCCT -> IFC by means of opencascade_geometry_ifc_writer_registry
%template(OpaqueCoordinate_3) ifcopenshell::geom::opaque_coordinate<3>;