First Commit
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import sys
|
||||
from itertools import accumulate
|
||||
from typing import IO, Optional
|
||||
|
||||
from . import ifcopenshell_wrapper
|
||||
|
||||
|
||||
class TransformDefaultDict:
|
||||
def __init__(self, keyfunc=lambda x: x, default_factory=None):
|
||||
"""
|
||||
Similar to collections.defaultdict, but allows for storing
|
||||
non-hashable keys by means of a transform function
|
||||
"""
|
||||
self.keyfunc = keyfunc
|
||||
self.default_factory = default_factory
|
||||
self._data = {}
|
||||
self._original_keys = {}
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
k = self.keyfunc(key)
|
||||
self._data[k] = value
|
||||
self._original_keys[k] = key
|
||||
|
||||
def __getitem__(self, key):
|
||||
k = self.keyfunc(key)
|
||||
if k in self._data:
|
||||
return self._data[k]
|
||||
if self.default_factory is not None:
|
||||
value = self.default_factory()
|
||||
self._data[k] = value
|
||||
self._original_keys[k] = key
|
||||
return value
|
||||
raise KeyError(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
return self.keyfunc(key) in self._data
|
||||
|
||||
def __delitem__(self, key):
|
||||
k = self.keyfunc(key)
|
||||
del self._data[k]
|
||||
del self._original_keys[k]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._original_keys.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def items(self):
|
||||
return ((self._original_keys[k], v) for k, v in self._data.items())
|
||||
|
||||
def keys(self):
|
||||
return self._original_keys.values()
|
||||
|
||||
def values(self):
|
||||
return self._data.values()
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._data.get(self.keyfunc(key), default)
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
k = self.keyfunc(key)
|
||||
if k not in self._data:
|
||||
self._data[k] = default
|
||||
self._original_keys[k] = key
|
||||
return self._data[k]
|
||||
|
||||
def update(self, other):
|
||||
for k, v in other.items():
|
||||
self[k] = v
|
||||
|
||||
def clear(self):
|
||||
self._data.clear()
|
||||
self._original_keys.clear()
|
||||
|
||||
def __repr__(self):
|
||||
items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
|
||||
return f"{self.__class__.__name__}({{{items}}})"
|
||||
|
||||
|
||||
class ProductCounter:
|
||||
def __init__(self, only_with_representation=True):
|
||||
self.total = 0
|
||||
transform_func = lambda decl: decl.name() if hasattr(decl, "name") else decl
|
||||
self.counts = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
|
||||
self.counts_excl = TransformDefaultDict(keyfunc=transform_func, default_factory=int)
|
||||
self.only_with_representation = only_with_representation
|
||||
|
||||
def count(self, decl, values):
|
||||
if decl._is("IfcProduct"):
|
||||
if self.only_with_representation and values.get("Representation") is None:
|
||||
return
|
||||
self.counts_excl[decl] += 1
|
||||
while decl.name() != "IfcProduct":
|
||||
self.counts[decl] += 1
|
||||
decl = decl.supertype()
|
||||
self.total += 1
|
||||
|
||||
def print(self, out=sys.stdout):
|
||||
if self.counts:
|
||||
results = []
|
||||
|
||||
def process(ent, level=-1):
|
||||
if ent in self.counts:
|
||||
results.append((level, ent.name(), self.counts[ent]))
|
||||
for subtype in ent.subtypes():
|
||||
process(subtype, level=level + 1)
|
||||
|
||||
prod = next(iter(self.counts.keys())).schema().declaration_by_name("ifcproduct")
|
||||
process(prod)
|
||||
max_width = max(level * 2 + len(name) for level, name, _ in results)
|
||||
for level, name, count in results:
|
||||
print(" " * (level * 2), f"{name:<{max_width - level * 2}}", ": ", count, file=out, sep="")
|
||||
|
||||
|
||||
class BooleanResultCounter:
|
||||
def __init__(self, exclude_union=True):
|
||||
self.total = 0
|
||||
self.exclude_union = exclude_union
|
||||
|
||||
def count(self, decl, values):
|
||||
if decl._is("IfcBooleanResult"):
|
||||
if self.exclude_union and values.get("Operator") == "UNION":
|
||||
return
|
||||
self.total += 1
|
||||
|
||||
def print(self, out=sys.stdout):
|
||||
print("IfcBooleanResult:", self.total, file=out)
|
||||
|
||||
|
||||
class StatsCollector:
|
||||
streamer: ifcopenshell_wrapper.InstanceStreamer
|
||||
page_size: Optional[int]
|
||||
file_stream: Optional[IO[str]]
|
||||
|
||||
finalized: bool = False
|
||||
needs_data: bool = False
|
||||
|
||||
counters: list
|
||||
|
||||
num_semis: int = 0
|
||||
|
||||
def __init__(self):
|
||||
self.streamer = ifcopenshell_wrapper.InstanceStreamer()
|
||||
self.needs_data = True
|
||||
self.counters = [ProductCounter(), BooleanResultCounter()]
|
||||
|
||||
def feedFromFile(self, f: Optional[IO[str]] = None):
|
||||
if f:
|
||||
self.file_stream = f
|
||||
self.feed(self.file_stream.read(self.page_size))
|
||||
|
||||
def feed(self, data: str):
|
||||
self.streamer.pushPage(data)
|
||||
self.needs_data = False
|
||||
self.num_semis = self.streamer.semicolonCount()
|
||||
|
||||
@staticmethod
|
||||
def fromFilePath(fn, page_size: int = 102400):
|
||||
collector = StatsCollector()
|
||||
collector.page_size = page_size
|
||||
collector.feedFromFile(open(str(fn), encoding="ascii"))
|
||||
return collector
|
||||
|
||||
def next(self):
|
||||
if self.num_semis > 0:
|
||||
if inst := self.streamer.readInstancePy(True):
|
||||
self.num_semis -= 1
|
||||
return inst["type"], dict(list(inst.items())[2:])
|
||||
else:
|
||||
self.finalized = True
|
||||
return None
|
||||
elif self.file_stream:
|
||||
self.feedFromFile()
|
||||
return self.next()
|
||||
else:
|
||||
self.needs_data = True
|
||||
return None
|
||||
|
||||
def process(self):
|
||||
if n := self.next():
|
||||
decl, values = n
|
||||
else:
|
||||
return
|
||||
if decl.schema().name() == "HEADER_SECTION_SCHEMA":
|
||||
return
|
||||
for cnt in self.counters:
|
||||
cnt.count(decl, values)
|
||||
|
||||
def print(self, out=sys.stdout):
|
||||
for cnt in self.counters:
|
||||
print(type(cnt).__name__, file=out)
|
||||
print("=" * len(type(cnt).__name__), file=out)
|
||||
cnt.print(out)
|
||||
|
||||
def includeElementTypesBasedOnBudget(self, priorities: dict, budget):
|
||||
def specificity(decl, target):
|
||||
if decl._is(target):
|
||||
i = 0
|
||||
while decl.name() != target:
|
||||
decl = decl.supertype()
|
||||
i += 1
|
||||
return i
|
||||
return None
|
||||
|
||||
def calc_prio(ty):
|
||||
# find most specific priority directive for the given type in ty
|
||||
return min(((specificity(ty, k), v) for k, v in priorities.items() if ty._is(k)), default=(0, 0))[1]
|
||||
|
||||
sorted_types = sorted(self.counters[0].counts_excl.keys(), key=calc_prio, reverse=True)
|
||||
sorted_types = [s for s in sorted_types if not s._is("IfcFeatureElement")]
|
||||
counts = [self.counters[0].counts_excl[v] for v in sorted_types]
|
||||
|
||||
ccounts = list(accumulate(counts))
|
||||
|
||||
if ccounts[0] >= budget:
|
||||
# At the very least include one type
|
||||
return sorted_types[0:1]
|
||||
|
||||
for i, s in enumerate(ccounts):
|
||||
if s >= budget:
|
||||
over = s - budget
|
||||
under = budget - ccounts[i - 1]
|
||||
k = i if over <= under else i - 1
|
||||
return sorted_types[0 : k + 1]
|
||||
|
||||
# We're lucky we can render everything in budget
|
||||
return sorted_types[:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
collector = StatsCollector.fromFilePath(sys.argv[1])
|
||||
while not collector.finalized:
|
||||
collector.process()
|
||||
collector.print()
|
||||
print(
|
||||
*collector.includeElementTypesBasedOnBudget(
|
||||
{
|
||||
"IfcWall": 3,
|
||||
"IfcSlab": 3,
|
||||
"IfcWindow": 2,
|
||||
"IfcDoor": 2,
|
||||
"IfcBuildingElement": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user