mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
committed by
Dion Moult
parent
4299968b6e
commit
6dba261e1b
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user