Files
IfcOpenShell/src/exterior-shell-extractor/main.py
T

1001 lines
41 KiB
Python

import argparse
import json
import math
import os
import sys
import time
import operator
import itertools
import functools
import threading
import concurrent.futures
import multiprocessing
from collections import defaultdict
from functools import reduce
from dataclasses import dataclass, field, fields
try:
import igraph as graph
has_igraph = True
except:
import networkx as graph
print("Warning: networkx uses considerable amounts of memory consider install igraph")
has_igraph = False
import numpy
from scipy.spatial import KDTree
from scipy.spatial import ConvexHull
import voxec
import ifcopenshell
import ifcopenshell.geom
from ifcopenshell.util.unit import calculate_unit_scale
import utils
# numpy.seterr(all='raise')
to_str = lambda eq: tuple(x.to_string() for x in utils.to_tuple(eq))
@dataclass
class settings:
debug : bool = False
verbose : bool = False
resolution : float = 1.e-5
voxel_prefiltering : bool = True
detailed_element_substitution : bool = True
element_categories : list = None
element_guids : list = None
store_mapping : bool = False
existing_mapping : bool = False
@dataclass
class model_geometry:
"""
Stores the extracted geometric detail for a certain set of elements, including the arbitrarily precise plain equations and their correspondence to polyhedral facets.
"""
# list[list[int]]
# ~^ non_convex_halfspace_facets_equations[...]~
# ^ non_convex_halfspace_facets_equations[n][...]
#
# used to map after finding clusters on plane equations
# float_facet_normals[i] -> non_convex_halfspace_facets_equations[i][j]
epeck_equation_idxs: list = field(default_factory=list)
# list[pair[str, tuple[halfspacetree]]]
# used to apply mapping to
convex_halfspace_trees: list = field(default_factory=list)
# halfspaces > facets > plane_equation
# list[list[plane]]
non_convex_halfspace_facets_equations: list = field(default_factory=list)
# normalized list[ndarray[N, 3]]
float_facet_normals: list = field(default_factory=list)
# list[ndarray[N, 3]]
float_facet_centroids: list = field(default_factory=list)
def __add__(self, other):
"""Concatenate two model_geometry objects
Args:
other (model_geometry): Other set of interpreted geometries
Returns:
_type_: model_geometry
"""
return model_geometry(
self.epeck_equation_idxs + other.epeck_equation_idxs,
self.convex_halfspace_trees + other.convex_halfspace_trees,
self.non_convex_halfspace_facets_equations + other.non_convex_halfspace_facets_equations,
self.float_facet_normals + other.float_facet_normals,
self.float_facet_centroids + other.float_facet_centroids,
)
class context:
def __init__(self, fns : list, output : str, st : settings):
self.fns = fns
self.is_substituted = False
self.settings = st
self.fs = []
if self.settings.detailed_element_substitution:
for fn in fns:
bfn = os.path.basename(fn)
substituted_fn = bfn + ".substituted.ifc"
if os.path.exists(substituted_fn):
self.is_substituted = True
self.fs.append(ifcopenshell.open(substituted_fn))
else:
self.fs.append(ifcopenshell.open(fn))
if self.settings.voxel_prefiltering:
if self.settings.element_categories:
self.elems = self.prefilter_elements_using_voxelization(exclude=('IfcOpeningElement', 'IfcSpace'))
else:
self.elems = self.prefilter_elements_using_voxelization(include=self.settings.element_categories)
elif self.settings.element_categories:
self.elems = reduce(operator.add, itertools.chain.from_iterable((map(f.by_type, self.settings.element_categories) for f in self.fs)))
elif self.settings.element_guids:
def wrap_try(fn, default = None):
def inner():
try:
return fn()
except:
return default
return inner
self.elems = sum((list(map(wrap_try(f.by_guid), self.settings.element_guids)) for f in self.fs), [])
else:
self.elems = [inst for f in self.fs for inst in f.by_type('IfcProduct') if not inst.is_a('IfcOpeningElement') or inst.is_a('IfcSpace')]
if not self.is_substituted and self.settings.detailed_element_substitution:
substituted_files = []
for fn, f in zip(self.fns, self.fs):
bfn = os.path.basename(fn)
substituted_fn = bfn + ".substituted.ifc"
substituted_files.append((self.substitute_detailed_elements(f, include=self.elems), f))
substituted_files[-1][0].write(substituted_fn)
self.fs, self.orig_files = zip(*substituted_files)
self.opening_elems = list(itertools.chain.from_iterable([rel.RelatedOpeningElement for rel in getattr(el, "HasOpenings", ())] for el in self.elems))
openings = self.extract_geometry(include=self.opening_elems)
data = self.extract_geometry(include=self.elems)
openings = self.remove_narrow(openings)
data = self.remove_narrow(data)
all_geom = openings + data
if self.settings.existing_mapping:
my_mapping = json.load(open('epeck_mapping.json'))
def deser(strs):
return tuple(utils.to_opaque(tuple(map(utils.create_epeck, st.split(' ')))) for st in strs)
my_mapping = {k: list(map(list, zip(*map(deser, vs)))) for k, vs in my_mapping.items() if k in map(operator.attrgetter('GlobalId'), self.elems)}
else:
my_mapping = self.create_mapping(all_geom)
self.apply_mapping(all_geom, my_mapping, from_disk=self.settings.existing_mapping)
del my_mapping
new_data = utils.make_default(self.apply_openings(data, openings))
del data
del openings
result = self.union(itertools.chain.from_iterable(new_data.values()))
with open(output, "w") as ff:
ff.write(result.serialize_obj())
@staticmethod
def definition_is_convex(repitem):
if repitem.is_a('IfcExtrudedAreaSolid'):
if repitem.SweptArea.is_a('IfcRectangleProfileDef'):
return True
if repitem.SweptArea.is_a() == 'IfcArbitraryClosedProfileDef':
crv = repitem.SweptArea.OuterCurve
if crv.is_a('IfcPolyline'):
points = numpy.array([p.Coordinates for p in crv.Points])[:-1, :]
elif crv.is_a('IfcIndexedPolyCurve'):
points = numpy.array(crv.Points.CoordList)
if crv.Segments:
if any(seg.is_a('IfcArcIndex') for seg in crv.Segments):
return False
idxs = numpy.array(seg[0][0] for seg in crv.Segments) - 1
points = points[idxs]
else:
# ?
points = points[:, :-1]
else:
return False
return len(ConvexHull(points[:, 0:2]).vertices) == len(points)
def substitute_with_box(self, file, elem, min_thickness=0.01, force=False):
"""
Computes a (somewhat) optimal oriented bounding box around the triangulated geometry described in elem by constructing a local reference frame based on the prevalent triangle normals
Args:
file (ifcopenshell.file): file containing elem
elem (TriangulationElement): triangulated geometry
min_thickness (float, optional): minimal thickness of the oriented bounding box to create around elem
Returns:
tuple: <guid, <3x4 matrix, min, max>> with min and max being the local coords in the matrix
"""
vs = numpy.array(elem.geometry.verts).reshape((-1, 3))
fs = numpy.array(elem.geometry.faces).reshape((-1, 3))
def _():
for f in fs:
p, q, r = vs[f]
pq = q - p
pr = r - p
pq /= numpy.linalg.norm(pq)
pr /= numpy.linalg.norm(pr)
pqr = numpy.cross(pq, pr)
pqr /= numpy.linalg.norm(pqr)
yield pqr
tri_norms = numpy.array(list(_()))
def _():
for f in fs:
p, q, r = vs[f]
pq = q - p
pr = r - p
pqr = numpy.cross(pq, pr)
yield numpy.linalg.norm(pqr) / 2.
tri_areas = numpy.array(list(_()))
_, inv, cnts = numpy.unique(numpy.int_(tri_norms * 1000), return_counts=True, return_inverse=True, axis=0)
di = utils.make_default(sorted((j, i) for i, j in enumerate(inv)))
summed_area = [v[1] for v in sorted((k, sum(tri_areas[v])) for k, v in di.items())]
sorted_summed_areas = numpy.argsort(summed_area)
V = numpy.average(tri_norms[di[sorted_summed_areas[-1]]], axis=0)
candidates = []
for i in range(1, min(10, len(cnts))):
ref = numpy.average(tri_norms[di[sorted_summed_areas[-i]]], axis=0)
candidates.append((abs(ref @ V), ref))
if not candidates:
refs = [(0, 0, 1), (1, 0, 0)]
for ref in refs:
candidates.append((abs(ref @ V), ref))
ref = min(candidates, key=operator.itemgetter(0))[1]
Y = numpy.cross(V, ref)
X = numpy.cross(V, Y)
M = numpy.array((X, -Y, V))
Mi = numpy.linalg.inv(M)
vsi = numpy.array([v @ Mi for v in vs])
vsimi = vsi.min(axis=0)
vsima = vsi.max(axis=0)
for i in range(3):
d = vsima[i] - vsimi[i]
if d < min_thickness:
dd = (min_thickness - d) / 2.0
vsima[i] += dd
vsimi[i] -= dd
def norm(v):
return v / numpy.linalg.norm(v)
def approx_diff():
for tri in vsi[fs]:
e1, e2 = tri[1:] - tri[0]
c = numpy.cross(e1, e2)
a = numpy.linalg.norm(c) / 2.
n = norm(c)
cent = numpy.average(tri, axis=0)
def distances():
for bnd in (vsimi, vsima):
for v in numpy.diag(cent - bnd):
if numpy.linalg.norm(v) < 1.e-9:
yield numpy.inf, 0.
else:
yield norm(v) @ n, numpy.linalg.norm(v)
yield max(distances())[1] * a
if not force:
bbox_dim = functools.reduce(operator.mul, vsima - vsimi)
approx_volume_diff = sum(approx_diff())
volume_factor = approx_volume_diff / bbox_dim
if volume_factor >= 0.25:
return None
vsimi = vsimi / calculate_unit_scale(file)
vsima = vsima / calculate_unit_scale(file)
return (elem.id,) + tuple(x.tolist() for x in (M, vsimi, vsima))
@utils.trace
def prefilter_elements_using_voxelization(self, **kwargs):
"""Uses a course voxelization (5cm) to quickly detect the likely subset
of elements participating in the building exterior. In case of small cavities
protruding into the building, bounding elements may be omitted from the
return list of elements.
Returns:
list[ifcopenshell.entity_instance]
"""
if all(os.path.exists(bfn + ".elements.json") for bfn in map(os.path.basename, self.fns)):
return sum(([f[i] for i in json.load(open(bfn + ".elements.json"))] for f, bfn in zip(self.fs, map(os.path.basename, self.fns))), [])
results = []
s = ifcopenshell.geom.settings(
USE_WORLD_COORDS=True,
WELD_VERTICES=False,
DISABLE_OPENING_SUBTRACTIONS=True,
ITERATOR_OUTPUT=ifcopenshell.ifcopenshell_wrapper.SERIALIZED,
)
building_elements_union = None
building_elements = []
for bfn, f in zip(map(os.path.basename, self.fns), self.fs):
result = []
it = ifcopenshell.geom.iterator(s, f, geometry_library="opencascade", **kwargs)
if not it.initialize():
# print(ifcopenshell.get_log())
# exit(1)
return result
while True:
elem = it.get()
geom = elem.geometry.brep_data
if f[int(elem.geometry.id.split("-")[0])].RepresentationIdentifier != "Box":
# breakpoint()
vox = voxec.run("voxelize", geom, method="volume")
building_elements.append((f[elem.id], vox))
if building_elements_union is None:
building_elements_union = vox
else:
building_elements_union = building_elements_union.boolean_union(vox)
if not it.next():
break
exterior = voxec.run("exterior", building_elements_union)
exterior_shell = [voxec.run("offset", exterior)]
for i in range(1):
exterior_shell.append(voxec.run("offset", exterior_shell[-1]))
exterior_shell_thick = reduce(lambda a, b: a.boolean_union(b), exterior_shell)
for elem, vox in building_elements:
if exterior_shell_thick.boolean_intersection(vox).count():
result.append(elem)
json.dump([i.id() for i in result], open(bfn + ".elements.json", "w"))
results.extend(result)
return results
def substitute_detailed_elements(self, file=None, force=False, **kwargs):
"""Substitute elements with a high vertex count with an
oriented bounding box.
Args:
force (bool, optional): Substitute regardless of vertex count. Defaults to False.
Returns:
ifcopenshell.file: file with substitutions made to the representation items
"""
s = ifcopenshell.geom.settings(
USE_WORLD_COORDS=True,
# ITERATOR_OUTPUT=ifcopenshell.ifcopenshell_wrapper.NATIVE,
ITERATOR_OUTPUT=ifcopenshell.ifcopenshell_wrapper.TRIANGULATED,
DISABLE_OPENING_SUBTRACTIONS=True,
)
it = ifcopenshell.geom.iterator(s, file, geometry_library="cgal", **kwargs)
if not it.initialize():
return
substitutions = []
while True:
nat = it.get_native()
elem = it.get()
num_verts = len(elem.geometry.verts) // 3
num_faces = len(elem.geometry.faces) // 3
volume = sum(nat.geometry.item(i).volume().to_double() for i in range(nat.geometry.size()))
if force or num_verts > 128 or ((num_verts / volume) > 2000 and num_faces > 12):
subs_result = self.substitute_with_box(f, elem, force=force)
if subs_result:
substitutions.append(subs_result)
if not it.next():
break
f = file
for elid, m3, mi, ma in substitutions:
elem = f[elid]
elem.ObjectPlacement = f.createIfcLocalPlacement(
RelativePlacement=f.createIfcAxis2Placement3D(
f.createIfcCartesianPoint((0.0, 0.0, 0.0)),
f.createIfcDirection(m3[2]),
f.createIfcDirection(m3[0]),
)
)
rep = [rep for rep in elem.Representation.Representations if rep.RepresentationIdentifier == "Body"][0]
elem.Representation = f.createIfcProductDefinitionShape(
None,
None,
[
f.createIfcShapeRepresentation(
rep[0],
rep[1],
"SweptSolid",
Items=[
f.createIfcExtrudedAreaSolid(
f.createIfcRectangleProfileDef(
"AREA",
None,
f.createIfcAxis2Placement2D(f.createIfcCartesianPoint(((ma[0] - mi[0]) / 2.0, (ma[1] - mi[1]) / 2.0))),
ma[0] - mi[0],
ma[1] - mi[1],
),
f.createIfcAxis2Placement3D(f.createIfcCartesianPoint(mi)),
f.createIfcDirection((0.0, 0.0, 1.0)),
ma[2] - mi[2],
)
],
)
],
)
return f
@utils.trace
# @profile
def extract_geometry(self, **kwargs):
# not only align facets part of the (potentially concave) input polyhedron, but also align facets resulting from the convex decomposition
ALIGN_INNER = True
s = ifcopenshell.geom.settings(
USE_WORLD_COORDS=False,
# ITERATOR_OUTPUT=ifcopenshell.ifcopenshell_wrapper.NATIVE,
ITERATOR_OUTPUT=ifcopenshell.ifcopenshell_wrapper.TRIANGULATED,
DISABLE_OPENING_SUBTRACTIONS=True,
)
its = []
fffs = []
data = model_geometry()
for f in self.fs:
if kwargs.keys() == {'include'}:
kwargs2 = {'include': [e for e in kwargs['include'] if e.wrapped_data.file == f]}
else:
kwargs2 = kwargs
it = ifcopenshell.geom.iterator(s, f, geometry_library="cgal", **kwargs2)
if not it.initialize():
# print(ifcopenshell.get_log())
# exit(1)
continue
# convex decomposition is expensive, geometries can be shared, apply product-level transformations after CD and cache results pre-transform
cd_cache = {}
while True:
elem = it.get()
elem_g_id = elem.geometry.id
if f[int(elem_g_id.split("-")[0])].RepresentationIdentifier != "Box":
print(f"[{utils.get_mem()} MB]", "reading", f[elem.id])
elem = it.get_native()
elem2 = None
for i in range(elem.geometry.size()):
elem_i = elem.geometry.item(i)
repitem = f[elem.geometry.item_id(i)]
if elem_i.num_vertices() < 6:
# try and detect single faces used sometime for glass panes which can't
# be represented as halfspace intersection and need to be 'solidified'
fs = elem_i.facets()
axes_ = [f.axis() for f in fs]
axes = list(map(utils.to_tuple, axes_))
if all(ax == axes[0] for ax in axes):
ff = ifcopenshell.file(schema=f.schema)
ff.add(*f.by_type("IfcProject"))
nelem = ff.add(f[elem.id])
body = [rep for rep in nelem.Representation.Representations if rep.RepresentationIdentifier == "Body"][0]
while body.Items[0].is_a("IfcMappedItem"):
body = body.Items[0].MappingSource.MappedRepresentation
body.Items = [body.Items[i]]
ff.write("temp.ifc")
fff = ifcopenshell.open("temp.ifc")
fffs.append(fff)
its.append(ifcopenshell.geom.iterator(s, fff, geometry_library="cgal"))
assert its[-1].initialize()
elem2 = its[-1].get_native()
elem_i = elem2.geometry.item(0)
repitem = body.Items[0]
assert not its[-1].next()
if ALIGN_INNER:
ke = elem_g_id, elem.geometry.item_id(i)
parts = cd_cache.get(ke)
if parts is None:
# @todo reuse decomp on shape instances
if self.definition_is_convex(repitem):
# convex decomposition is expensive, figure out the
# convexity from a 2d extrusion basis where possible
parts = [elem_i]
parts[0].convex_tag(True)
else:
try:
parts = elem_i.convex_decomposition()
except:
# @todo likely due to self-intersections
parts = []
cd_cache[ke] = parts
else:
parts = [elem_i]
parts = [p.moved((elem2 if elem2 else elem).transformation.matrix) for p in parts]
for poly in parts:
if ALIGN_INNER:
cd = [poly]
else:
cd = poly.convex_decomposition()
for p in cd:
# print('part volume', p.volume().to_double())
# print('part area ', p.area().to_double())
pass
assert len(cd) == 1
fs = poly.facets()
phfs = poly.halfspaces().facets()
if len(phfs) == 0:
# @todo investigate why two cases of 0-length checks needed
continue
data.non_convex_halfspace_facets_equations.append(list(map(lambda f: f.plane_equation(), phfs)))
ns = [f.axis() for f in fs]
ps_ = [f.position() for f in fs]
# without this weird results on linux
ps = [tuple(ifcopenshell.ifcopenshell_wrapper.create_epeck(x.to_string()) for x in utils.to_tuple(t)) for t in ps_]
ds = list(map(utils.dot, ns, ps))
nsd = numpy.array(list(map(utils.to_double, ns)))
if nsd.size == 0:
continue
nsd /= numpy.linalg.norm(nsd, axis=1).reshape((-1, 1))
data.float_facet_normals.append(nsd)
data.float_facet_centroids.append(numpy.array(list(map(utils.to_double, ps))))
data.epeck_equation_idxs.append([])
last_hs_tups = tuple(
map(
lambda x: tuple(x.get(i) for i in range(4)),
data.non_convex_halfspace_facets_equations[-1],
)
)
data.convex_halfspace_trees.append((f[elem.id], tuple(p.halfspaces() for p in cd)))
# correlate halfspace planes back to polyhedral facets
for d, n1, n2 in zip(ds, ns, nsd.tolist()):
abcd = tuple(-n1.get(i) for i in range(3)) + (d,)
# @todo unable to find probably due to triangulation?
# ... yes it seems that triangulation has solved this (but only to a large extent)
# @todo should we divide by largest component?
try:
j = last_hs_tups.index(abcd)
except:
# breakpoint()
enumerated_plane_eq_diff = lambda t: reduce(
operator.add,
((abcd[i] - t[1][i]) * (abcd[i] - t[1][i]) for i in range(4)),
).to_double()
if (
min(
map(
enumerated_plane_eq_diff,
enumerate(last_hs_tups),
)
)
> 0.1
):
print(">", *(x.to_double() for x in abcd))
for h in last_hs_tups:
print(*(x.to_double() for x in h))
breakpoint()
j = min(enumerate(last_hs_tups), key=enumerated_plane_eq_diff)[0]
data.epeck_equation_idxs[-1].append(j)
if not it.next():
break
return data
@utils.trace
def remove_narrow(self, data):
negate = lambda x: utils.to_opaque(utils.negate(-1)(x))
astuple_nocopy = lambda dc: list(map(functools.partial(getattr, dc), map(operator.attrgetter('name'), fields(dc))))
datas = [model_geometry(*map(lambda x: [x], xs)) for xs in zip(*astuple_nocopy(data))]
by_elem_id = lambda i_d: i_d[1].convex_halfspace_trees[0][0].id()
datas2 = [(k, list(vs)) for k, vs in itertools.groupby(sorted(enumerate(datas), key=by_elem_id), key=by_elem_id)]
to_remove = []
for i, rest in datas2:
decomps = list(map(lambda d_i: d_i[1].convex_halfspace_trees[0][1], rest))
orig_ids = list(map(lambda d_i: d_i[0], rest))
# only tested on align inner
assert all(len(parts) == 1 for parts in decomps)
internal_mapping = []
for j, parts in zip(orig_ids, decomps):
hs = parts[0]
epecks = [h.plane_equation() for h in hs.facets()]
# print('I', original_index)
# for eq in epecks:
# print('eq', *(x.to_string() for x in utils.to_tuple(eq)))
rounded_negated = [tuple(-int(round(v * 10000)) for v in utils.to_double(eq)) for eq in epecks]
# for v in rounded_negated:
# print('ap', *v)
for eq in epecks:
try:
abcd_idx = rounded_negated.index(tuple(int(round(v * 10000)) for v in utils.to_double(eq)))
except ValueError as e:
continue
internal_mapping.append((eq, negate(epecks[abcd_idx])))
internal_mapping.append((negate(eq), epecks[abcd_idx]))
to_remove.append(j)
# print('removing', original_index)
break
for j, parts in zip(orig_ids, decomps):
if j in to_remove:
continue
hs = parts[0]
for ab in internal_mapping:
hs.map(*ab)
datas_filtered = [d for i, d in enumerate(datas) if i not in to_remove]
if not datas_filtered:
return model_geometry()
else:
return reduce(operator.add, datas_filtered)
@utils.trace
def create_mapping(self, data):
"""Finds groups of halfspace plane equations that are within a certain
angular and linear deviation, computes the average and construct a
mapping from original to cluster average.
"""
if self.settings.verbose and self.settings.debug:
for ii, eqs in enumerate(data.non_convex_halfspace_facets_equations):
print('ELEMENT', ii)
for i, eq in enumerate(eqs):
print(i, *to_str(eq))
epeck_equation_list_idx = numpy.cumsum([0] + list(map(len, data.epeck_equation_idxs)))
# epeck_equation_idxs_flat = list(itertools.chain.from_iterable(data.epeck_equation_idxs))
mapping = []
# First use a kd-tree to find planes with similar normals (the first three) components
# of the plane equations. Note that we search also for the opposite.
# A single float64 vector might be associated to multiple distinct epeck equations.
# in our kd-tree we store unique float64 coordinates and maintain a mapping back to
# indices into the original epeck equations.
vecs = numpy.concatenate(data.float_facet_normals)
vecs_unique, vecs_inverse = numpy.unique(vecs, return_inverse=True, axis=0)
vecs_dict = utils.make_default(sorted((j, i) for i, j in enumerate(vecs_inverse)))
points = numpy.concatenate(data.float_facet_centroids)
kdtree = KDTree(vecs_unique)
G = graph.Graph()
if has_igraph:
# @todo write a proper adaptor. igraph only supports integer vertex ids, so we
# need a separate mapping
# vertices = [(+1, i) for i in range(len(vecs_unique))] + [(-1, i) for i in range(len(vecs_unique))]
G.add_vertices(len(vecs_unique))
vidx = lambda x: x
getv = lambda x: x
add_edges = lambda g, es: g.add_edges(es)
components = lambda g: list(g.connected_components())
else:
vidx = lambda x: x
getv = lambda x: x
add_edges = lambda g, es: g.add_edges_from(es)
components = lambda g: list(graph.connected_components(g))
def yield_edges():
for i, p in enumerate(vecs_unique):
# @todo if i in G.nodes: continue?
for sign in (+1, -1):
yield from ((i,j) for j in kdtree.query_ball_point(p * sign, r=0.2))
# for i in range(len(vecs_unique)):
# yield (vidx((+1, i)), vidx((-1, i)))
add_edges(G, yield_edges())
for comp in components(G):
print(f"[{utils.get_mem()} MB]", "component size", len(comp))
comp = list(map(getv, comp))
# construct the average plane normal (keeping in mind the sign)
# to within the component create a sorted sequence based on the dot
# product with the polyhedral facet centroid
# @todo should be weighted based on vecs_count?
# idx_pos = sorted(i for s, i in comp if s == +1)
# idx_neg = sorted(i for s, i in comp if s == -1)
signs = numpy.sign(vecs_unique[comp] @ vecs_unique[comp][0]).reshape((-1,1))
avgv = numpy.average(vecs_unique[comp] * signs, axis=0)
avgv /= numpy.linalg.norm(avgv)
def augment(c):
for i in c:
for j in vecs_dict[i]:
yield j
comp = list(augment(comp))
# the original facet centroids
pts = points[comp]
ds = pts @ avgv
shuff = numpy.argsort(pts @ avgv)
srted = ds[shuff]
diff = numpy.diff(srted)
# cluster based on jumps in sorted array
chunks = numpy.split(shuff, numpy.where(diff > 2 * self.settings.resolution)[0] + 1)
for chunk in chunks:
comp_subset = [comp[c] for c in chunk]
Gcomp = graph.Graph()
if has_igraph:
Gcomp_vs = dict(map(reversed, enumerate(comp_subset)))
Gcomp.add_vertices(len(comp_subset))
Gcomp_vidx = lambda x: Gcomp_vs[x]
Gcomp_getv = lambda x: comp_subset[x]
else:
Gcomp_vidx = lambda x: x
Gcomp_getv = lambda x: x
def _():
# Project facet centroid onto plane both sides and compare
for a, b in itertools.combinations(comp_subset, 2):
d = abs((points[b] - points[a]) @ vecs[a]) + abs((points[a] - points[b]) @ vecs[b])
if d < self.settings.resolution:
yield Gcomp_vidx(a), Gcomp_vidx(b)
# This becomes the final connected component of plane equations to be averaged
add_edges(Gcomp, _())
for comp2 in components(Gcomp):
comp2 = list(map(Gcomp_getv, comp2))
signs = list(map(int, numpy.sign(vecs[comp2] @ vecs[comp2][0])))
eqt = []
idxs = set()
listidxs = [numpy.searchsorted(epeck_equation_list_idx, c, side='right')-1 for c in comp2]
modelo = [(j - epeck_equation_list_idx[i]) for i, j in zip(listidxs, comp2)]
eqs = [data.non_convex_halfspace_facets_equations[a][data.epeck_equation_idxs[a][b]] for a, b in zip(listidxs, modelo)]
idxs.update(listidxs)
# tuples
for a in map(lambda sign, tup: utils.negate(sign)(tup), signs, map(utils.to_tuple, eqs)):
if a not in eqt:
eqt.append(a)
N = ifcopenshell.ifcopenshell_wrapper.create_epeck(len(eqt))
# transpose
eqtt = list(zip(*eqt))
# sum and divide components
avg = tuple(
map(
functools.partial(utils.reserialize, to_double=False),
[reduce(operator.add, comps) / N for comps in eqtt],
)
)
avgs = tuple(map(lambda s: utils.to_opaque(utils.negate(s)(avg)), (+1, -1)))
for sign, pl in zip(signs, eqs):
mapping.append((pl, avgs[sign == -1], idxs))
return mapping
@utils.trace
def apply_mapping(self, data, mapping, from_disk=False):
if from_disk:
by_id = mapping
else:
by_id = defaultdict(lambda: (list(), list()))
for a, b, idxs in mapping:
fr, to = (" ".join(map(lambda n: n.to_string(), utils.to_tuple(x))) for x in (a,b))
if fr == to:
continue
for idx in idxs:
by_id[idx][0].append(a)
by_id[idx][1].append(b)
if self.settings.store_mapping:
# can be used to store global mapping and apply to individually extracted elements
mapping = defaultdict(list)
for k, vs in by_id.items():
guid = data.convex_halfspace_trees[k][0].GlobalId
for ab in zip(*vs):
from_to = tuple(" ".join(map(lambda n: n.to_string(), utils.to_tuple(x))) for x in ab)
mapping[guid].append(from_to)
json.dump(mapping, open('epeck_mapping.json', 'w'))
for i, (elem, ps) in enumerate(data.convex_halfspace_trees):
if from_disk:
maps = by_id[elem.GlobalId]
else:
maps = by_id[i]
for j, p in enumerate(ps):
if self.settings.verbose:
pps = p.solid()
old_area = pps.area().to_double()
old_volume = pps.volume().to_double()
open(f'{i}_{j}_before.obj', 'w').write(pps.serialize_obj())
p.map(*maps)
if self.settings.verbose:
for ab in zip(*maps):
c, d = map(utils.to_double, ab)
print(*c, '->', *d)
c, d = map(to_str, ab)
print(*c, '->', *d)
pps = p.solid()
new_area = pps.area().to_double()
new_volume = pps.volume().to_double()
open(f'{i}_{j}_after.obj', 'w').write(pps.serialize_obj())
if new_area:
print(i, j, new_area / old_area, old_area, new_area, old_volume, new_volume)
if new_area / old_area > 100:
breakpoint()
@staticmethod
def write_obj(ofn, *, elem=None, item=None):
s = ifcopenshell.geom.settings(USE_WORLD_COORDS=True, WELD_VERTICES=False)
if item:
geom = item.Triangulate(s)
else:
geom = elem.geometry
vs_fs = geom.verts, geom.faces
vs, fs = map(lambda tup: numpy.array(tup).reshape((-1, 3)), vs_fs)
with open(ofn, "w") as obj:
for v in vs:
print('v', *v, file=obj)
for f in fs + 1:
print('f', *f, file=obj)
@utils.trace
def evaluate_st(self, data):
def inner():
for i, (elem, ps) in enumerate(data.convex_halfspace_trees):
print("Evaluating", elem)
solids = [p.solid() for p in ps]
# @todo use union()
if len(solids) == 0:
continue
elif len(solids) == 1:
v = solids[0]
else:
v = ifcopenshell.ifcopenshell_wrapper.nary_union(solids)
if self.settings.debug:
self.write_obj(f"{elem.GlobalId}_{i}.obj", item=v)
yield elem, v
return list(inner())
def evaluate_mt(self, data):
def ev(i_elem_ps):
i, (elem, ps) = i_elem_ps
v = ps[0].solid_mt()
if self.settings.debug:
self.write_obj(f"{elem.GlobalId}_{i}.obj", item=v)
return (elem, v)
# yield from map(ev, data.convex_halfspace_trees)
# return
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# futures = (executor.submit(ev, el) for el in data.convex_halfspace_trees)
# yield from map(lambda f: f.result(), concurrent.futures.as_completed(futures))
return executor.map(ev, enumerate(data.convex_halfspace_trees))
@utils.trace
def apply_openings(self, data, openings):
def inner():
opgeom = utils.make_default(self.evaluate_mt(openings))
for k, v in self.evaluate_mt(data):
for el in getattr(k, "HasOpenings", ()):
print("opening", k, el.RelatedOpeningElement)
for p in opgeom[el.RelatedOpeningElement]:
v = v.subtract(p)
# print('v.volume', v.volume().to_double())
yield k, v
return list(inner())
@staticmethod
@utils.trace
def union_mt(shapes):
shps = list(shapes)
n = int(math.ceil(len(shps) / 4))
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# futures = (executor.submit(ev, el) for el in data.convex_halfspace_trees)
# yield from map(lambda f: f.result(), concurrent.futures.as_completed(futures))
return ifcopenshell.ifcopenshell_wrapper.nary_union(list(executor.map(ifcopenshell.ifcopenshell_wrapper.nary_union, (shps[i*n:i*n+n] for i in range(4)))))
@staticmethod
@utils.trace
def union(shapes):
return ifcopenshell.ifcopenshell_wrapper.nary_union(list(shapes))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("files", type=str, nargs="+")
for field in fields(settings):
if field.type == bool:
parser.add_argument("--" + field.name.replace("_", "-"), dest=field.name, action="store_true")
parser.add_argument("--no-" + field.name.replace("_", "-"), dest=field.name, action="store_false")
parser.set_defaults(**{field.name: field.default})
else:
if field.type is list:
parser.add_argument(
"--" + field.name.replace("_", "-"), dest=field.name, type=lambda s: s.split(','), default=field.default
)
else:
parser.add_argument(
"--" + field.name.replace("_", "-"), dest=field.name, type=field.type, default=field.default
)
args = vars(parser.parse_args(sys.argv))
files = args.pop("files")
if os.path.basename(__file__) == os.path.basename(files[0]):
files = files[1:]
output = files.pop()
assert files
settings = settings(**args)
context(files, output, settings)