Optimise IfcPatch recipe: make toposort backend configurable

aothms asked for the toposort dependency ordering used by the dedup
walk to try igraph's C-backed topological_sorting() first, since it
should shave off additional time on top of the non-recursive
get_info fix. Falls back to the pure python toposort package with a
warning if igraph is not installed.

Generated with the assistance of an AI coding tool.

(cherry picked from commit 7ebdd046b6)
This commit is contained in:
Petru Conduraru
2026-07-18 23:00:58 +03:00
committed by Dion Moult
parent 4299968b6e
commit 6dba261e1b
2 changed files with 78 additions and 3 deletions
+32 -3
View File
@@ -16,9 +16,40 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
import logging
import ifcopenshell
def _toposort(graph: dict[int, set[int]], logger: logging.Logger) -> list[int]:
"""Flatten a dependency graph of entity ids into dependency order.
Uses igraph's C-backed topological sort when available, otherwise falls
back to the pure python toposort package with a warning.
"""
try:
import igraph
except ImportError:
logger.warning(
"igraph is not installed, falling back to the slower pure python toposort. "
"Install python-igraph for better performance."
)
from toposort import toposort_flatten
return toposort_flatten(graph)
ids = list(graph)
index = {id_: i for i, id_ in enumerate(ids)}
for references in graph.values():
for reference in references:
if reference not in index:
index[reference] = len(ids)
ids.append(reference)
edges = [(index[reference], index[id_]) for id_, references in graph.items() for reference in references]
order = igraph.Graph(n=len(ids), edges=edges, directed=True).topological_sorting(mode="out")
return [ids[i] for i in order]
class Patcher:
def __init__(self, file, logger):
"""Optimise the filesize of an IFC model
@@ -52,8 +83,6 @@ class Patcher:
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
def patch(self):
from toposort import toposort_flatten as toposort
def generate_instances_and_references():
"""
Generator which yields an entity id and
@@ -90,7 +119,7 @@ class Patcher:
info_to_id = {}
for id in toposort(dict(generate_instances_and_references())):
for id in _toposort(dict(generate_instances_and_references()), self.logger):
inst = self.file[id]
info = map_value(inst.get_info(include_identifier=False, recursive=False, return_type=tuple), as_key=True)
if info in info_to_id:
+46
View File
@@ -18,11 +18,18 @@
# This file was generated with the assistance of an AI coding tool.
import logging
import sys
from unittest import mock
import pytest
import ifcopenshell
import ifcopenshell.guid
import ifcpatch
import test.bootstrap
from ifcpatch.recipes.Optimise import _toposort
def add_context(f: ifcopenshell.file) -> ifcopenshell.entity_instance:
@@ -71,3 +78,42 @@ class TestOptimise(test.bootstrap.IFC4):
assert len(output.by_type("IfcPolyline")) == 2
walls_after = output.by_type("IfcWall")
assert curve_of(walls_after[0]) != curve_of(walls_after[1])
def test_folding_still_works_on_the_pure_python_fallback(self):
context = add_context(self.file)
coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
add_wall_with_curve(self.file, context, coords)
add_wall_with_curve(self.file, context, coords)
with mock.patch.dict(sys.modules, {"igraph": None}):
output = ifcpatch.execute({"file": self.file, "recipe": "Optimise", "arguments": []})
assert len(output.by_type("IfcPolyline")) == 1
GRAPH = {4: {2, 3}, 2: {1}, 3: {1}, 1: set()}
def assert_dependency_order(order: list[int], graph: dict[int, set[int]]) -> None:
position = {id_: i for i, id_ in enumerate(order)}
assert sorted(order) == sorted(graph)
for id_, references in graph.items():
for reference in references:
assert position[reference] < position[id_]
class TestToposortBackends:
def test_igraph_backend_orders_dependencies_first(self):
pytest.importorskip("igraph")
assert_dependency_order(_toposort(GRAPH, logging.getLogger(__name__)), GRAPH)
def test_igraph_backend_includes_references_missing_from_the_keys(self):
pytest.importorskip("igraph")
assert _toposort({2: {1}}, logging.getLogger(__name__)) == [1, 2]
def test_pure_python_fallback_warns_when_igraph_is_unavailable(self, caplog):
with mock.patch.dict(sys.modules, {"igraph": None}):
with caplog.at_level(logging.WARNING):
order = _toposort(GRAPH, logging.getLogger(__name__))
assert_dependency_order(order, GRAPH)
assert "python-igraph" in caplog.text