Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 11 additions & 22 deletions src/ifcopenshell-python/ifcopenshell/util/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -1753,27 +1753,21 @@ def remove_deep2(
# ifc_file.batch()
if not ifc_file:
ifc_file = element.file
total_inverses = ifc_file.get_total_inverses(element)
if total_inverses > 0:

def are_inverses_contained() -> bool:
also_considered_inverses = 0

for considered_element in also_consider:
traverse = ifc_file.traverse(considered_element, max_levels=1)
if element in traverse:
also_considered_inverses += 1
if total_inverses == also_considered_inverses:
return True
return False

if not are_inverses_contained():
return
# The start element may only be referenced from also_consider; decided in
# C++ without traversing each considered element.
if not ifc_file._all_inverses_within(element, [e.id() for e in also_consider if e.id()]):
return

to_delete: set[ifcopenshell.entity_instance] = set()
subgraph = list(ifc_file.traverse(element, breadth_first=True))
subgraph.extend(also_consider)
subgraph_set = set(subgraph)
# Which subgraph members are referenced only from inside the subgraph,
# decided once in C++ without materializing any inverse list. Clearing
# large aggregates below only removes references whose source is inside
# the subgraph, so this doesn't change while the loop runs.
subgraph_ids = [e.id() for e in subgraph_set if e.id()]
referenced_only_within = set(ifc_file._ids_referenced_only_within(subgraph_ids))
subelement_queue = [element]

# Cache already processed entities to avoid traversing them multiple time.
Expand All @@ -1787,12 +1781,7 @@ def are_inverses_contained() -> bool:
subelement_id
and subelement_id not in processed_ids
and subelement not in do_not_delete
and (
# 0 or 1 inverses guarantees that the subelement only exists in this subgraph
ifc_file.get_total_inverses(subelement) < 2
# Alternatively, let's ensure all inverses are within the subgraph
or len(set(ifc_file.get_inverse(subelement)) - subgraph_set) == 0
)
and subelement_id in referenced_only_within
):
to_delete.add(subelement)
subelement_queue.extend(ifc_file.traverse(subelement, max_levels=1)[1:])
Expand Down
4 changes: 4 additions & 0 deletions src/ifcparse/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ class IFC_PARSE_API file {
/// Returns all entities in the file that reference the id
std::vector<express::base> instances_by_reference(int reference_id);

/// Returns whether pred accepts the id of every instance that references
/// instance_id, stopping at the first one it rejects.
bool all_referencing_instances(int instance_id, const std::function<bool(uint32_t)>& pred);

/// Returns the entity with the specified id
express::base instance_by_id(int instance_id);

Expand Down
30 changes: 30 additions & 0 deletions src/ifcparse/parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3805,6 +3805,36 @@ std::vector<express::base> file::instances_by_reference(int t) {
return ret;
}

bool file::all_referencing_instances(int instance_id, const std::function<bool(uint32_t)>& pred) {
return std::visit([instance_id, &pred](auto& x) -> bool {
if constexpr (std::is_same_v<std::decay_t<decltype(x)>, impl::in_memory_file_storage>) {
return x.byref_excl_.all_sources((uint32_t)instance_id, pred);
}
#ifdef IFOPSH_WITH_ROCKSDB
else if constexpr (std::is_same_v<std::decay_t<decltype(x)>, impl::rocks_db_file_storage>) {
// @todo no lower/upper_bounds() implemented yet
auto prefix = "v|" + std::to_string(instance_id) + "|";
auto it = std::unique_ptr<rocksdb::Iterator>(x.db->NewIterator(rocksdb::ReadOptions()));
it->Seek(prefix);
while (it->Valid() && it->key().starts_with(prefix)) {
std::vector<uint32_t> vals(it->value().size() / sizeof(uint32_t));
memcpy(vals.data(), it->value().data(), it->value().size());
for (auto& v : vals) {
if (!pred(v)) {
return false;
}
}
it->Next();
}
return true;
}
#endif
else {
throw std::runtime_error("Storage not initialized");
}
}, storage_);
}

express::base file::instance_by_id(int id) {
return std::visit([id](auto& x) {
if constexpr (std::is_same_v<std::decay_t<decltype(x)>, std::monostate>) {
Expand Down
21 changes: 21 additions & 0 deletions src/ifcparse/storage.h
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,27 @@ namespace ifcopenshell {
return n;
}

// True iff pred accepts the source of every live record
// referencing referenced_id. Stops at the first rejection.
template <typename Pred>
bool all_sources(uint32_t referenced_id, Pred&& pred) const {
auto range = base_range(referenced_id);
for (auto it = range.first; it != range.second; ++it) {
if (!is_dead(*it) && !pred(it->source_id)) {
return false;
}
}
auto bucket = delta_.find(referenced_id);
if (bucket != delta_.end()) {
for (const auto& record : bucket->second) {
if (!pred(record.source_id)) {
return false;
}
}
}
return true;
}

bool empty() const {
return size() == 0;
}
Expand Down
28 changes: 28 additions & 0 deletions src/ifcwrap/IfcParseWrapper.i
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -316,6 +317,33 @@ 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());
return $self->all_referencing_instances(e_.id(), [&allowed](uint32_t source_id) {
return allowed.count(source_id) != 0;
});
}

// The subset of ids whose every referencing instance is itself in ids:
// one crossing in, one crossing out, early exit per id.
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;
for (int id : ids) {
if ($self->all_referencing_instances(id, within)) {
contained.push_back(id);
}
}
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
Expand Down
Loading