Skip to content

Commit

Permalink
Merge branch 'development' into ivs158-planar-ifcface
Browse files Browse the repository at this point in the history
  • Loading branch information
aothms committed Jan 23, 2025
2 parents 1b03da1 + 397dc9f commit 27b96b9
Show file tree
Hide file tree
Showing 27 changed files with 1,519 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
echo $CONDA/bin >> $GITHUB_PATH
- name: Install dependencies
run: |
pip install behave pytest tabulate pyparsing sqlalchemy numpy pydantic pydot sqlalchemy_utils django python-dotenv deprecated pandas pyspellchecker rtree lark-parser mpmath
pip install behave pytest tabulate pyparsing sqlalchemy numpy pydantic pydot sqlalchemy_utils django python-dotenv deprecated pandas pyspellchecker rtree lark-parser networkx mpmath
wget -O /tmp/ifcopenshell_python.zip https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-`python3 -c 'import sys;print("".join(map(str, sys.version_info[0:2])))'`-v0.8.1-1d27161-linux64.zip
mkdir -p `python3 -c 'import site; print(site.getusersitepackages())'`
unzip -d `python3 -c 'import site; print(site.getusersitepackages())'` /tmp/ifcopenshell_python.zip
Expand Down
12 changes: 12 additions & 0 deletions features/BRP002_Single-component-in-connected-faceset.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@informal-proposition
@GEM
@version1
@E00050
Feature: BRP002 - Single component in connected faceset
The rule verifies that for connected facesets (open- and closed shells) their union of the domains of the faces and their bounding loops shall be arcwise connected.

Scenario: IfcConnectedFaceSet components

Given An IfcConnectedFaceSet

Then all edges must form a single connected component
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@implementer-agreement
@GEM
@version1
Feature: GEM113 - Indexed poly curve arcs must not be defined using colinear points
The rule verifies that all the three points of any IfcArcIndex segment of an IfcIndexedPolyCurve are not colinear after taking the Precision factor into account

@E00050
Scenario: No poly curve arcs using colinear points

Given An IfcIndexedPolyCurve
Then It must have no arc segments that use colinear points after taking the Precision factor into account
6 changes: 3 additions & 3 deletions features/IFC102_Absence-of-deprecated-entities.feature
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ IFC4: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/
Given A model with Schema "IFC4.3"
Given an <Entity>

Then its type is not <Entity> excluding subtypes
Then its type is not "<Entity>" excluding subtypes

Examples:
| Entity |
Expand All @@ -270,7 +270,7 @@ IFC4: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/
Given A model with Schema "IFC4"
Given an <Entity>

Then its type is not <Entity> excluding subtypes
Then its type is not "<Entity>" excluding subtypes

Examples:
| Entity |
Expand All @@ -289,7 +289,7 @@ IFC4: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/
Given A model with Schema "IFC2X3"
Given an <Entity>

Then its type is not <Entity> excluding subtypes
Then its type is not "<Entity>" excluding subtypes

Examples:
| Entity |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@informal-proposition
@SWE
@version1
@E00050
Feature: SWE002 - Mirroring within IfcDerivedProfileDef shall not be used

The rule verifies that the transformation defined in IfcDerivedProfileDef.Operator does not introduce mirroring.
The subtype IfcMirroredProfileDef should be used for that. For the tapered sweeps, which tend to rely on
IfcDerivedProfileDef, by expressing the tapering operation as a change in profile, mirroring should not be used
altogether.

Scenario: IfcDerivedProfileDef must not use mirroring as there is a dedicated subtype for that

Given An IfcDerivedProfileDef without subtypes
Given Its attribute Operator
Given The determinant of the placement matrix

Then The resulting value must be greater than 0

Scenario Outline: Tapered sweeps must not use mirroring altogether

Given An <entity>
Given Its attribute <attribute>
Given Its entity type is 'IfcDerivedProfileDef' or 'IfcMirroredProfileDef'
Given Its attribute Operator
Given The determinant of the placement matrix

Then The resulting value must be greater than 0

Examples:
| entity | attribute |
| IfcExtrudedAreaSolidTapered | SweptArea |
| IfcExtrudedAreaSolidTapered | EndSweptArea |
| IfcRevolvedAreaSolidTapered | SweptArea |
| IfcRevolvedAreaSolidTapered | EndSweptArea |
32 changes: 16 additions & 16 deletions features/steps/givens/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,39 +75,39 @@ def step_impl(context, inst, comparison_op, attribute, value, tail=SubTypeHandli
"""
start_value = value
pred = operator.eq

def negate(fn):
def inner(*args):
return not fn(*args)
return inner

if value == 'empty':
value = ()
elif value == 'not empty':
value = ()
pred = operator.ne
elif comparison_op == ComparisonOperator.NOT_EQUAL: # avoid using != together with (not)empty stmt
pred = operator.ne
try:
value = set(map(ast.literal_eval, map(str.strip, value.split(' or '))))
except ValueError:
print('ValueError: entity must be typed in quotes')
else:
try:
value = ast.literal_eval(value)
except ValueError:
# Check for multiple values, for example `PredefinedType = 'POSITION' or 'STATION'`.
value = set(map(ast.literal_eval, map(str.strip, value.split(' or '))))
pred = misc.reverse_operands(operator.contains)
pred = operator.contains

if comparison_op == ComparisonOperator.NOT_EQUAL: # avoid using != together with (not)empty stmt
pred = negate(pred)

entity_is_applicable = False
observed_v = ()
if attribute.lower() in ['its type', 'its entity type']: # it's entity type is a special case using ifcopenshell 'is_a()' func
observed_v = misc.do_try(lambda : inst.is_a(), ())
values = {value} if isinstance(value, str) else value
if any(pred(check_entity_type(inst, v, tail), True) for v in values):
entity_is_applicable = True
if isinstance(value, set):
values = [check_entity_type(inst, v, tail) for v in value]
else:
values = check_entity_type(inst, value, tail)
entity_is_applicable = pred(values, True)
else:
observed_v = getattr(inst, attribute, ()) or ()
if comparison_op.name == 'NOT_EQUAL':
if all(pred(observed_v, v) for v in value):
entity_is_applicable = True
elif pred(observed_v, value):
entity_is_applicable = True
entity_is_applicable = pred(value, observed_v)

if entity_is_applicable:
yield ValidationOutcome(instance_id=inst, severity = OutcomeSeverity.PASSED)
Expand Down
18 changes: 18 additions & 0 deletions features/steps/givens/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,21 @@ def step_impl(context, inst, excluding=None):
def step_impl(context, inst):
inst = itertools.pairwise(inst)
yield ValidationOutcome(instance_id=inst, severity=OutcomeSeverity.PASSED)

@gherkin_ifc.step("The determinant of the placement matrix")
def step_impl(context, inst):
import numpy as np
import ifcopenshell.ifcopenshell_wrapper

if inst.wrapped_data.file_pointer() == 0:
# In some case we're processing operations on attributes that are 'derived in subtype', for
# example the Operator on an IfcMirroredProfileDef. Derived attribute values are generated
# on the fly and are not part of a file. Due to a limitation on the mapping expecting a file
# object, such instances can also not be mapped. Therefore in such case we create a temporary
# file to add the instance to.
f = ifcopenshell.file(schema=context.model.schema_identifier)
inst = f.add(inst)

shp = ifcopenshell.ifcopenshell_wrapper.map_shape(ifcopenshell.geom.settings(), inst.wrapped_data)
d = np.linalg.det(np.array(shp.components))
yield ValidationOutcome(instance_id=d, severity=OutcomeSeverity.PASSED)
28 changes: 20 additions & 8 deletions features/steps/thens/attributes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import operator
import re
import ifcopenshell

from utils import misc, system, geometry
Expand Down Expand Up @@ -63,7 +64,8 @@ def accumulate_errors(i):
@gherkin_ifc.step('The value of attribute {attribute} must be {value_or_comparison_op}')
@gherkin_ifc.step('The value of attribute {attribute} must be {value_or_comparison_op} {display_entity:display_entity}')
@gherkin_ifc.step('The value of attribute {attribute} must be {value_or_comparison_op} the expression: {expression}')
def step_impl(context, inst, attribute:str, value_or_comparison_op:str, expression:str=None, display_entity=0):
@gherkin_ifc.step('The resulting value must be {value_or_comparison_op}')
def step_impl(context, inst, value_or_comparison_op:str, attribute:str=None, expression:str=None, display_entity=0):
"""
Compare an attribute to an expression based on attributes.
Expand All @@ -78,19 +80,22 @@ def step_impl(context, inst, attribute:str, value_or_comparison_op:str, expressi
** : exponentiation.
"""

binary_operators = {
'equal to' : operator.eq,
'not equal to' : operator.ne,
'greater than' : operator.gt,
'less than' : operator.lt,
'greater than or equal to' : operator.ge,
'less than or equal to' : operator.le,
}
operators = {
'+' : operator.add,
'-' : operator.sub,
'*' : operator.mul,
'/' : operator.truediv,
'%' : operator.mod,
'**' : operator.pow,
'equal to' : operator.eq,
'not equal to' : operator.ne,
'greater than' : operator.gt,
'less than' : operator.gt,
'greater than or equal to' : operator.ge,
'less than or equal to' : operator.le,
**binary_operators
}

if expression is not None:
Expand Down Expand Up @@ -162,10 +167,17 @@ def step_impl(context, inst, attribute:str, value_or_comparison_op:str, expressi
opts = value_or_comparison_op.split(' or ')
value_or_comparison_op = tuple(opts)
pred = misc.reverse_operands(operator.contains)
elif m := re.match(rf"^({'|'.join(binary_operators.keys())})\s+(\d+(\.\d+)?)$", value_or_comparison_op):
pred_str, val_str, *_ = m.groups()
value_or_comparison_op = float(val_str)
pred = binary_operators[pred_str]

if isinstance(inst, (tuple, list)):
inst = inst[0]
attribute_value = getattr(inst, attribute, 'Attribute not found')
if attribute is None:
attribute_value = inst
else:
attribute_value = getattr(inst, attribute, 'Attribute not found')
if attribute_value is None:
attribute_value = ()
if inst is None:
Expand Down
27 changes: 27 additions & 0 deletions features/steps/thens/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ifcopenshell.geom
import numpy as np
import rtree.index
import networkx as nx

# 'Mapping' is new functionality in IfcOpenShell v0.8 that allows us to inspect interpreted
# segments without depending on OpenCASCADE. Hypothetically using Eigen with an arbitrary
Expand Down Expand Up @@ -233,8 +234,34 @@ def step_impl(context, inst: ifcopenshell.entity_instance, clause: str):
yield ValidationOutcome(inst=inst, observed=(points_coordinates[i], points_coordinates[j]),
severity=OutcomeSeverity.ERROR)

@gherkin_ifc.step("It must have no arc segments that use colinear points after taking the Precision factor into account")
def step_impl(context, inst: ifcopenshell.entity_instance):
import mpmath as mp
mp.prec = 128

representation_context = geometry.recurrently_get_entity_attr(context, inst, 'IfcRepresentation', 'ContextOfItems')
precision = mp.mpf(geometry.get_precision_from_contexts(representation_context))

for seg in (inst.Segments or ()):
ps = inst.Points.CoordList
if seg.is_a('IfcArcIndex') and len(seg[0]) == 3 and all((i >= 1) and ((i - 1) < len(ps)) for i in seg[0]):
a, b, c = (ps[i-1] for i in seg[0])
l = geometry.Line.from_points(a, c)
if l.distance(b) < precision:
yield ValidationOutcome(inst=inst, observed=str(seg),
severity=OutcomeSeverity.ERROR)


@gherkin_ifc.step("all edges must form a single connected component")
def step_impl(context, inst: ifcopenshell.entity_instance):
G = nx.Graph()
G.add_edges_from(geometry.get_edges(
context.model, inst
))
n_components = len(list(nx.connected_components(G)))
if n_components != 1:
yield ValidationOutcome(inst=inst, observed=n_components, severity=OutcomeSeverity.ERROR)


@gherkin_ifc.step("the boundaries of the face must conform to the implicit plane fitted through the boundary points")
def step_impl(context, inst: ifcopenshell.entity_instance):
Expand Down
26 changes: 26 additions & 0 deletions features/steps/utils/geometry.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from dataclasses import dataclass
import operator
import math
<<<<<<< HEAD
from typing import Dict, Optional
=======
from typing import Dict, Tuple
>>>>>>> development

import numpy as np
import mpmath as mp
Expand Down Expand Up @@ -379,3 +383,25 @@ def estimate_plane_through_points(points) -> Optional[Plane]:
d = -(Nx*x_avg + Ny*y_avg + Nz*z_avg)

return Plane(Nx, Ny, Nz, d)


class Line:
"""
Represents a line a + d*b where a is a position and b a normalized unit vector
"""
a: Tuple[mp.mpf]
b: Tuple[mp.mpf]

def distance(self, point: Tuple[mp.mpf]) -> mp.mpf:
v = [p - ai for p, ai in zip(point, self.a)]
dot_prod = mp.fsum([x * y for x, y in zip(v, self.b)])
proj = [dot_prod * bi for bi in self.b]
dist_vec = [vi - pi for vi, pi in zip(v, proj)]
return mp.sqrt(mp.fsum([x*x for x in dist_vec]))

@staticmethod
def from_points(a, b):
a, b = (tuple(map(mp.mpf, p)) for p in (a,b))
l = mp.sqrt(mp.fsum([x*x for x in b]))
b = [x / l for x in b]
return Line(a, b)
Loading

0 comments on commit 27b96b9

Please sign in to comment.