Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Models containing IfcFacility must contain a IfcProjectedCRS or IfcGeographicCRS
Given a model with Schema 'IFC4'
Given an .IfcBuilding.

Then There must be at least 1 instance(s) of IfcProjectedCRS
Then There must be at least 1 instance(s) of .IfcProjectedCRS.

Scenario: CRS required when IfcFacility is present

Given a model with Schema 'IFC4.3'
Given an .IfcFacility.

Then There must be at least 1 instance(s) of IfcCoordinateReferenceSystem
Then There must be at least 1 instance(s) of .IfcCoordinateReferenceSystem.
26 changes: 26 additions & 0 deletions features/rules/GRF/GRF005_CRS-unit-type-differences.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@industry-practice
@GRF
@version1
@E00100
Feature: GRF005 - CRS unit type differences
The rule verifies that the Scale attribute of IfcMapConversion is used when the units of the CRS are not identical to the units of the engineering coordinate system.
If omitted, the value of 1.0 is assumed.
If the units of the referenced source location engineering coordinate system are different from the units of the referenced target coordinate system,
then this attribute must be included and must have the value of the scale from the source to the target units


Scenario Outline: When the length unit of the Local CRS (from IfcProject) is not equal to the length unit of the Projected CRS, then the IfcMapCOnversion.Scale must be provided and cannot be 1.0

Given A model with Schema 'IFC4.3'
Given An .IfcMapConversion.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, MapConversion is a bit strange in the sense the SourceSRC is a SELECT between (CoordinateRef, RepresentationContext).

I think this allows us to workaround the by_type(IfcProject)[0], because we can do MapConv --Source--> RepContext -> Project/Context -> Unit.

So I think we should also condition this check to only execute when MapConv.Source is the RepContext (I'm not aware of any scenario in which this is not the case, but still).

Given There must be at least 1 instance(s) of .<IfcCoordinateReferenceSystem>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bit fan of "Given There must be ..." can we rephrase this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IfcMapConversion.TargetCRS = (IfcCoordinateReferenceSystem) is also a mandatory attribute so I don't see how this could be different (or at least.. it's already handled by the schema check).

Given The <unit> unit of the project ^is not^ equal to the <unit> unit(s) of the .<IfcCoordinateReferenceSystem>.

Then .Scale. ^is not^ empty
Then .Scale. ^is not^ 1.0
Comment on lines +19 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than requiring not empty and not 1.0, can we require the actual value that is needed? I.e the one conversion factor divide by the other.


Examples:
| unit | IfcCoordinateReferenceSystem |
| length | IfcProjectedCRS |
| angle | IfcGeographicCRS |
Comment on lines +24 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I look at the mapconv docs I don't see any reference to scaling angular units and I don't think this actually is a thing.

NOTE The Scale can be used when the length unit for the 3 axes of the map coordinate system are not identical with the length unit established for this project (see IfcProject.UnitsInContext) - for example to convert feet into metres. If omitted, the scale factor 1.0 is assumed.

https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcMapConversion.htm

So I think the rule only has to check length


Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ IFC4: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/
Given A model with Schema 'IFC4.3'
Given An IFC model

Then There must be less than 1 instance(s) of <Entity> ^excluding subtypes^
Then There must be less than 1 instance(s) of .<Entity>. ^excluding subtypes^

Examples:
| Entity |
Expand Down Expand Up @@ -51,7 +51,7 @@ IFC4: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/
Given An IFC model
Given A model with Schema 'IFC4'

Then There must be less than 1 instance(s) of <Entity> ^excluding subtypes^
Then There must be less than 1 instance(s) of .<Entity>. ^excluding subtypes^

Examples:
| Entity |
Expand Down Expand Up @@ -91,7 +91,7 @@ IFC4: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/
Given An IFC model
Given A model with Schema 'IFC2X3'

Then There must be less than 1 instance(s) of <Entity> ^excluding subtypes^
Then There must be less than 1 instance(s) of .<Entity>. ^excluding subtypes^

Examples:
| Entity |
Expand Down
2 changes: 1 addition & 1 deletion features/rules/PJS/PJS101_Project-presence.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ For example, project libraries, including type libraries or property definition
Scenario: Check project existence
Given an IFC Model

Then There must be exactly 1 instance(s) of IfcProject
Then There must be exactly 1 instance(s) of .IfcProject.
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ The rule verifies that there's maximum one instance of IfcSite and at least one

Scenario: Agreement141 - Agreement on having maximum of one instance of IfcSite

Then There must be at most 1 instance(s) of IfcSite
Then There must be at most 1 instance(s) of .IfcSite.

Scenario: Agreement142(1) - Agreement on having at least one instance of IfcBuilding as part of the spatial structure

Then There must be at least 1 instance(s) of IfcBuilding
Then There must be at least 1 instance(s) of .IfcBuilding.

Scenario: Agreement142(2) - Agreement on having at least one instance of IfcBuilding as part of the spatial structure

Given An .IfcSite.
Given an .IfcBuilding.
Then It must be assigned to the IfcSite
Then It must be assigned to the .IfcSite.


Scenario: Agreement142(3) - Agreement on having at least one instance of IfcBuilding as part of the spatial structure

Given no .IfcSite.
Given An .IfcBuilding.

Then It must be assigned to the IfcProject
Then It must be assigned to the .IfcProject.
3 changes: 1 addition & 2 deletions features/steps/givens/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from validation_handling import gherkin_ifc
from . import ValidationOutcome, OutcomeSeverity


@gherkin_ifc.step("Its values")
@gherkin_ifc.step("Its values excluding {excluding}")
def step_impl(context, inst, excluding=None):
Expand Down Expand Up @@ -32,4 +31,4 @@ def step_impl(context, 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)
yield ValidationOutcome(instance_id=d, severity=OutcomeSeverity.PASSED)
3 changes: 2 additions & 1 deletion features/steps/registered_type_definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"include_or_exclude_subtypes": ["including subtypes", "excluding subtypes"],
"first_or_final": ["first", "final"],
"absence_or_presence": ["absence", "presence"],
"equal_or_not_equal": ["=", "!=", "is not", "is"],
"equal_or_not_equal": ["=", "!=", "is not", "is", "must be", "must be not"],
"length_and_or_angle_units": ["length", "length and angle", "angle"],
"nested_sentences": ["must nest only 1", "must not a list of only", "is nested by only 1", "is nested by a list of only"],
"aggregated_or_contained_or_positioned": ["aggregated", "contained", "positioned"],
"prefix_condition": ["starts", "does not start", "must not start"],
Expand Down
9 changes: 2 additions & 7 deletions features/steps/steps/attribute_value.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ast
import operator

from utils import geometry, ifc, misc
from utils import misc
from validation_handling import gherkin_ifc
from . import ValidationOutcome, OutcomeSeverity

Expand All @@ -24,11 +24,6 @@ def step_impl(context, inst, comparison_operator, attribute, value, subtype_hand
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':
Expand All @@ -43,7 +38,7 @@ def inner(*args):
pred = operator.contains

if comparison_operator in {"is not", "!="}: # avoid using != together with (not)empty stmt
pred = negate(pred)
pred = misc.negate(pred)

observed_v = ()
if attribute.lower() in ['its type', 'its entity type']: # it's entity type is a special case using ifcopenshell 'is_a()' func
Expand Down
85 changes: 84 additions & 1 deletion features/steps/steps/crs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pyproj.database import query_crs_info
from pyproj import CRS
from validation_handling import gherkin_ifc
from utils import misc
import operator

from . import ValidationOutcome, OutcomeSeverity

Expand All @@ -22,4 +24,85 @@ def step_impl(context, inst):
yield ValidationOutcome(instance_id=inst, severity=OutcomeSeverity.PASSED)
else:
yield ValidationOutcome(inst=inst, severity=OutcomeSeverity.ERROR)



@gherkin_ifc.step("The {unit_types} unit(s) of the project ^{comparison_operator:equal_or_not_equal}^ equal to the {crs_unit_types} unit(s) of the .{crs_entity_type}.")
def step_impl(context, inst, unit_types, comparison_operator, crs_unit_types, crs_entity_type):
assert unit_types == crs_unit_types, "it's only possible to compare equal unit types"
pred = misc.negate(operator.eq) if comparison_operator in {"is not", "!="} else operator.eq
unit_types = unit_types.split(' and ')

conversion_based = False

unit_type_attr_map= {
'length' : 'LENGTHUNIT',
'area': 'AREAUNIT',
'volume': 'VOLUMEUNIT',
'angle': 'PLANEANGLEUNIT',
}

def map_units(units):
map = {}
for unit in units:
if unit.is_a('IfcNamedUnit')
unit_type = unit.UnitType
if unit_type and unit_type not in map:
map[unit_type] = unit
return map

# determine the project unit values
project = context.model.by_type("IfcProject")[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] is always scary of course. Maybe we can work our way around this...

project_unit_map = map_units(getattr(project.UnitsInContext, 'Units', []))
project_units = {utype : project_unit_map.get(unit_type_attr_map[utype]) for utype in unit_types}


if 'length' in unit_types and (length := project_units.get('length')):
prefix = getattr(length, 'Prefix', '')
name = getattr(length, 'Name', '').lower()
project_units['length'] = (f"{prefix}{name}" if prefix else name).lower()

if 'angle' in unit_types and (angle := project_units.get('angle')):
if length.is_a('IfcSIUnit'):
project_units['angle'] = {
'name': (f"{prefix}{name}" if prefix else name).lower(),
'conversion_factor' : None
}
conversion_based=True
else:
project_units['angle'] = {
'name': getattr(angle, 'Name', '').lower(),
'conversion_factor': misc.do_try(lambda: angle.ConversionFactor.ValueComponent.wrappedValue, '')
}

# determine the crs unit values
crs = context.model.by_type(crs_entity_type)[0]
epsg_crs = CRS.from_string(crs.Name)
unit_attrs = [attr for attr in dir(crs) if attr.endswith('Unit')]
crs_unit_map = map_units([getattr(crs, attr) for attr in unit_attrs if getattr(crs, attr, None) is not None])
crs_units = {utype: crs_unit_map.get(unit_type_attr_map[utype]) for utype in unit_types}

if 'length' in unit_types:
if length := crs_units.get('length'):
prefix = getattr(length, 'Prefix', '')
name = getattr(length, 'Name', '').lower()
crs_units['length'] = (f"{prefix}{name}" if prefix else name).lower()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of focussing on names not being equal, i'd rather focus on the conversion factors

I.e https://github.com/IfcOpenShell/IfcOpenShell/blob/7b1aa207d842af2edc8d5dd5daa921a96d69ea91/src/ifcopenshell-python/ifcopenshell/util/unit.py#L680

Divide that with the unit scale reported by proj and that's the value of Scale you expect.

else:
crs_units['length'] = epsg_crs.coordinate_system.axis_list[0].unit_name

if 'angle' in unit_types:
if angle := crs_units.get('angle'):
crs_units['angle'] = {
'name': getattr(angle, 'Name').lower(),
'conversion_factor': misc.do_try(lambda : project_units['angle'].ConversionFactor.ValueComponent.wrappedValue, '')
}
else:
crs_units['angle'] = {
'name' : epsg_crs.coordinate_system.axis_list[0].unit_name,
'conversion_factor': misc.do_try(lambda : epsg_crs.coordinate_system.axis_list[0].unit_conversion_factor, None) if conversion_based else None
}


if pred(project_units, crs_units):
yield ValidationOutcome(instance_id=inst, severity=OutcomeSeverity.PASSED)
else:
yield ValidationOutcome(inst=inst, severity=OutcomeSeverity.ERROR)
18 changes: 16 additions & 2 deletions features/steps/thens/existence.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def get_entities_in_model(context, constraint, entity, include_or_exclude_subtyp


@gherkin_ifc.step(
"There must be {constraint} {num:d} instance(s) of {entity} ^{subtype_handling:include_or_exclude_subtypes}^"
"There must be {constraint} {num:d} instance(s) of .{entity}. ^{subtype_handling:include_or_exclude_subtypes}^"
)
@global_rule
def step_impl(context, inst, constraint, num, entity, subtype_handling="including subtypes"):
Expand All @@ -40,15 +40,29 @@ def step_impl(context, inst, constraint, num, entity, subtype_handling="includin
)


@gherkin_ifc.step("There must be {constraint} {num:d} instance(s) of {entity}")
@gherkin_ifc.step("There must be {constraint} {num:d} instance(s) of .{entity}.")
@global_rule
def step_impl(context, inst, constraint, num, entity, include_or_exclude_subtypes="including subtypes"):
"""
The Given step_impl in combination with 'at least 1' is the equivalent of 'Given an IfcEntity', but without setting new applicable instances.
For example:
Given an IfcWall -> insts = [IfcWall, IfcWall]
Given an IfcRoof -> inst = [IfcRoof, IfcRoof]

However,
Given an IfcWall -> insts = [IfcWall, IfcWall]
Given there must be at least 1 instance(s) of IfcRoof -> inst = [IfcWall, IfcWall]
"""
op = misc.stmt_to_op(constraint)
instances_in_model = get_entities_in_model(context, constraint, entity, include_or_exclude_subtypes)
if not op(len(instances_in_model), num):
yield ValidationOutcome(
inst=inst, observed=instances_in_model, severity=OutcomeSeverity.ERROR
)
else:
yield ValidationOutcome(
instance_id=inst, severity=OutcomeSeverity.PASSED
)


@gherkin_ifc.step(
Expand Down
2 changes: 1 addition & 1 deletion features/steps/thens/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def step_impl(context, inst, relationship, table):
yield ValidationOutcome(inst=inst, expected={"oneOf": expected_relationship_objects, "context": context}, observed=relationship_object, severity=OutcomeSeverity.ERROR)


@gherkin_ifc.step("It must be assigned to the {relating}")
@gherkin_ifc.step("It must be assigned to the .{relating}.")
def step_impl(context, inst, relating):
for rel in getattr(inst, 'Decomposes', []):
if not rel.RelatingObject.is_a(relating):
Expand Down
22 changes: 16 additions & 6 deletions features/steps/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ def inner(*args):
return fn(*reversed(args))
return inner

def negate(fn):
"""
Returns a function that negates the result of the given predicate function.

Example:
>>> import operator
>>> not_equal = negate(operator.eq)
>>> not_equal(1, 2)
True
>>> not_equal(1, 1)
False
"""
def inner(*args):
return not fn(*args)
return inner


def recursive_flatten(lst):
flattened_list = []
Expand Down Expand Up @@ -150,12 +166,6 @@ def clean(s):




def unpack_sequence_of_entities(instances):
# in case of [[inst1, inst2], [inst3, inst4]]
return [do_try(lambda: unpack_tuple(inst), None) for inst in instances]


def unpack_tuple(tup):
for item in tup:
if isinstance(item, tuple):
Expand Down
4 changes: 2 additions & 2 deletions features/steps/validation_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def global_rule(func):
"""
Use this decorator when the rule applies to the whole stack instead of a single instance.
For instance
@gherkin_ifc.step('There must be {constraint} {num:d} instance(s) of {entity}')
@gherkin_ifc.step('There must be {constraint} {num:d} instance(s) of {entity} {tail:include_or_exclude_subtypes}')
@gherkin_ifc.step('There must be {constraint} {num:d} instance(s) of .{entity}.')
@gherkin_ifc.step('There must be {constraint} {num:d} instance(s) of .{entity}. {tail:include_or_exclude_subtypes}')
@global_rule
"""
@functools.wraps(func)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
ISO-10303-21;
/* IFC units are in metres, ESPG:2277 uses survey foot and the map conversion scaled is not set */
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView]'),'2;1');
FILE_NAME('fail-grf005-project_meters_crs_foot_no_map_conversion_scaled.ifc','2023-01-25T18:40:40',(''),(''),'','IfcOpenShell contributors - IfcOpenShell - v0.7.0+6180d73f','');
FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6180d73f','IfcOpenShell-v0.7.0-6180d73f','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOCHANGE.,$,#3,#4,1674672040);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_FOOT.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_FOOT.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('2X9wjf5oPB4PQjenCYrhHZ',#5,'',$,$,$,$,(#11),#19)
#21=IFCPROJECTEDCRS('EPSG:2277',$,'NAD83',$,'','3',$);
#22=IFCMAPCONVERSION(#11,#21,316131.64,5690966.11,1.,1.,0.,$);
#23=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#24=IFCMAPCONVERSION(#23,#21,316131.64,5690966.11,1.,1.,0.,$);
ENDSEC;
END-ISO-10303-21;
Loading
Loading