ifcopenshell-python: decide remove_deep2 containment in C++ without materializing inverses

remove_deep2 decides whether a subelement may be purged by checking that
every instance referencing it lies inside the subgraph. It did so with
set(ifc_file.get_inverse(subelement)) - subgraph_set, which wraps every
referencing instance into a Python object first. For shared instances
that is enormous: a representation context is referenced once per
representation in the file, so purging one representation wrapped
~100k instances just to conclude "not exclusively mine". The same
question was asked once more for the start element against
also_consider, answered by traversing each considered element and
scanning the result with entity_instance.__eq__.

Add inverse_index::all_sources(id, pred), which visits the live records
referencing id and stops at the first source pred rejects, and two
wrapper helpers on file built on it: _all_inverses_within(e, ids) for
one instance, and _ids_referenced_only_within(ids) which returns the
subset of ids referenced only from within ids. remove_deep2 calls the
latter once per invocation, before the loop: clearing large aggregates
inside the loop only removes references whose source is inside the
subgraph, so "referenced only from inside" cannot change while it
runs. Both call sites are decision-identical to the old checks: the
set of surviving instances after root.remove_product on 300 products
of a 155 MB model is unchanged, and so is the element/api test suite.

root.remove_product per call, 300 products, on top of open-perf-v2:
see the pull request for the per-model table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNrXDmR88wKPCYwGE21SyH
This commit is contained in:
Dion Moult
2026-09-15 16:54:45 +10:00
parent 788d25767f
commit 4efa21cd0a
3 changed files with 73 additions and 22 deletions
+41
View File
@@ -148,6 +148,7 @@ PyObject* get_feature(const std::string& x) {
#include <fstream>
#include <random>
#include <unordered_set>
// Atomic IFC/STEP write (issue #4797): serialize to a temporary file next to
// the destination, then atomically rename it onto the destination. If the
@@ -316,6 +317,46 @@ private:
throw ifcopenshell::exception("Only entities with ids are supported for get_total_inverses. Provided entity: '" + e.declaration().name() + "'.");
}
// True iff every instance referencing e has an id in ids. Stops at the
// first referencing instance outside the set, without materializing any.
bool _all_inverses_within(const express::base& e, const std::vector<int>& ids) {
auto e_ = e.as<express::entity>();
if (!e_) {
throw ifcopenshell::exception("Only entities with ids are supported for _all_inverses_within. Provided entity: '" + e.declaration().name() + "'.");
}
const std::unordered_set<uint32_t> allowed(ids.begin(), ids.end());
const auto referenced_id = (uint32_t)e_.id();
return std::visit([referenced_id, &allowed](auto& x) -> bool {
if constexpr (std::is_same_v<std::decay_t<decltype(x)>, ifcopenshell::impl::in_memory_file_storage>) {
return x.byref_excl_.all_sources(referenced_id, [&allowed](uint32_t source_id) {
return allowed.count(source_id) != 0;
});
} else {
throw ifcopenshell::exception("_all_inverses_within is only implemented for in-memory storage");
}
}, $self->storage_);
}
// The subset of ids whose every referencing instance is itself in ids:
// one crossing in, one crossing out, early exit per id in C++.
std::vector<int> _ids_referenced_only_within(const std::vector<int>& ids) {
const std::unordered_set<uint32_t> allowed(ids.begin(), ids.end());
const auto within = [&allowed](uint32_t source_id) { return allowed.count(source_id) != 0; };
std::vector<int> contained;
std::visit([&](auto& x) {
if constexpr (std::is_same_v<std::decay_t<decltype(x)>, ifcopenshell::impl::in_memory_file_storage>) {
for (int id : ids) {
if (x.byref_excl_.all_sources((uint32_t)id, within)) {
contained.push_back(id);
}
}
} else {
throw ifcopenshell::exception("_ids_referenced_only_within is only implemented for in-memory storage");
}
}, $self->storage_);
return contained;
}
void _write(const std::string& fn) {
// Atomic write: serialize to a temp file next to the target, then
// atomically rename it into place, so an interrupted write can never