mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +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.
This commit is contained in:
committed by
Thomas Krijnen
parent
57cfd9d1fd
commit
7ebdd046b6
@@ -16,9 +16,40 @@
|
|||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcPatch. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import ifcopenshell
|
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:
|
class Patcher:
|
||||||
def __init__(self, file, logger):
|
def __init__(self, file, logger):
|
||||||
"""Optimise the filesize of an IFC model
|
"""Optimise the filesize of an IFC model
|
||||||
@@ -52,8 +83,6 @@ class Patcher:
|
|||||||
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
|
self.optimized_file = ifcopenshell.file(schema=self.file.schema)
|
||||||
|
|
||||||
def patch(self):
|
def patch(self):
|
||||||
from toposort import toposort_flatten as toposort
|
|
||||||
|
|
||||||
def generate_instances_and_references():
|
def generate_instances_and_references():
|
||||||
"""
|
"""
|
||||||
Generator which yields an entity id and
|
Generator which yields an entity id and
|
||||||
@@ -90,7 +119,7 @@ class Patcher:
|
|||||||
|
|
||||||
info_to_id = {}
|
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]
|
inst = self.file[id]
|
||||||
info = map_value(inst.get_info(include_identifier=False, recursive=False, return_type=tuple), as_key=True)
|
info = map_value(inst.get_info(include_identifier=False, recursive=False, return_type=tuple), as_key=True)
|
||||||
if info in info_to_id:
|
if info in info_to_id:
|
||||||
|
|||||||
@@ -18,11 +18,18 @@
|
|||||||
|
|
||||||
# This file was generated with the assistance of an AI coding tool.
|
# 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
|
||||||
import ifcopenshell.guid
|
import ifcopenshell.guid
|
||||||
|
|
||||||
import ifcpatch
|
import ifcpatch
|
||||||
import test.bootstrap
|
import test.bootstrap
|
||||||
|
from ifcpatch.recipes.Optimise import _toposort
|
||||||
|
|
||||||
|
|
||||||
def add_context(f: ifcopenshell.file) -> ifcopenshell.entity_instance:
|
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
|
assert len(output.by_type("IfcPolyline")) == 2
|
||||||
walls_after = output.by_type("IfcWall")
|
walls_after = output.by_type("IfcWall")
|
||||||
assert curve_of(walls_after[0]) != curve_of(walls_after[1])
|
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