Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IVS-80/GRD000 - Grid Information #257

Draft
wants to merge 2 commits into
base: development
Choose a base branch
from
Draft
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
26 changes: 26 additions & 0 deletions features/GRD000_Grid-information.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@implementer-agreement
@GRD
@POS
@version1
@E00020

Feature: GRD000 - Grid Information
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Feature: GRD000 - Grid Information
Feature: GRD000 - Grid Placement

@evandroAlfieri Why did we pick "Information" instead of "Placement"? Grid Information to me hints at information on grids in the model, not other elements placed according to grid.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, I thought moving the pr back to draft was enough to save you from reviewing it. It's about this rule that we (Geert and I) wanted to talk with you on Monday

The rule verifies the presence of IFC entities used to define a design grid to be used as reference for object placement.
https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/concepts/Product_Shape/Product_Placement/Product_Grid_Placement/content.html


Scenario: Check for activation

Given an IfcGridPlacement
Given its attribute PlacesObject
Given its entity type is 'IfcProduct' including subtypes
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is governed by the schema.

PlacesObject: SET [0:?] OF IfcProduct FOR ObjectPlacement

But what is not governed is that there is at least one Product being placed by the Placement, which I think is a good requirement to add here as an activation criterion.

Given return to IfcGridPlacement
Given its attribute PlacementRelTo
Copy link
Collaborator

Choose a reason for hiding this comment

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

PlacementRelTo does not exist on schemas pre IFC4.3. I think this must be reflected in the scenario.

Given its attribute PlacesObject
Given its entity type is 'IfcGrid'
Comment on lines +18 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.

These three lines should actually not be a given statement, but a then statement.

Given an IfcGridPlacement Then PlacementRelTo/PlacesObject **must be** IfcGrid

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This makes it a normative rule, and not a rule to check for activation.
Which is part of the reason why this PR is de-activated. It would probably be better to create two new normative rules and then skip the 000-rule. Based on this issue

Given return to IfcGridPlacement
Given its attribute PlacementLocation
Given IntersectingAxes = not empty
Comment on lines +22 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is given by the schema:

IntersectingAxes: LIST [2:2] OF UNIQUE IfcGridAxis

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I was wondering how close we should stay to the relating concept template.


Then The IFC model contains information on quantities of elements
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy pasta

Copy link
Collaborator

Choose a reason for hiding this comment

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

Speaking of which, maybe this is too error prone and we can rather easily replace this with:

Suggested change
Then The IFC model contains information on quantities of elements
Then The IFC model contains information on the selected functional part

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Undercooked pasta, in this case ..

It's indeed very easy to miss, and maybe not such an important part. We have the description in the feature file to provide context after all.


Copy link
Collaborator

Choose a reason for hiding this comment

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

So all in all, you could consider rewriting this scenario as:

Given an IfcProduct
And its attribute ObjectPlacement
And its entity type is IfcGridPlacement

Then The IFC model contains information on ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But would this be close enough to the relating concept template?
Would it be nice if we could state that some software supports a functional part based on these templates?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, it goes back to the age old question that we have to second guess the intention behind the template, and this case I'd say it's twofold:

  • [GRD000] Describe grid placement functionality: IfcProduct ----ObjectPlacement---> GridPlacement
  • [GRD001-a] Constrain placement of grid "a grid placement should be defined relatively to a grid": GridPlacement ----PlacementRelTo----> IfcObjectPlacement ---PlacesObject---> IfcGrid

And then a third component that is important, but is not in the template:

  • [GRD001-b] "The grid to which the placement is relative should contain the axes that are used for the intersection"

afbeelding

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's phrase it differently, as soon as the model contains:

IfcProduct ----ObjectPlacement---> GridPlacement

The file contains a "grid placement" according to the functional part.

But if it's not according to the 2 further specifications I just highlighted, it's not that the model doesn't contain it, it's that the usage is invalid. So yes, these are additional normative parts and we need to create this separation.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@evandroAlfieri @Ghesselink Annother rule for the backlog is this informal proposition from the docs:

IntersectingAxes[1] and IntersectingAxes[2] shall not be part of the same row of grid axes, i.e. both shall not be within the same set of IfcGrid.UAxes or IfcGrid.VAxes of the corresponding IfcGrid.

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

23 changes: 1 addition & 22 deletions features/steps/givens/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ifcopenshell
from behave import register_type
from utils import geometry, ifc, misc
from utils.subtype_handling import SubTypeHandling, check_entity_type
from parse_type import TypeBuilder
from validation_handling import gherkin_ifc
from . import ValidationOutcome, OutcomeSeverity
Expand All @@ -17,9 +18,6 @@ class ComparisonOperator (Enum):
EQUAL = auto()
NOT_EQUAL = auto()

class SubTypeHandling (Enum):
INCLUDE = auto()
EXCLUDE = auto()

register_type(include_or_exclude_subtypes=TypeBuilder.make_enum({"including subtypes": SubTypeHandling.INCLUDE, "excluding subtypes": SubTypeHandling.EXCLUDE }))
register_type(first_or_final=TypeBuilder.make_enum({"first": FirstOrFinal.FIRST, "final": FirstOrFinal.FINAL }))
Expand All @@ -30,25 +28,6 @@ class SubTypeHandling (Enum):
"is": ComparisonOperator.EQUAL,
}))

def check_entity_type(inst: ifcopenshell.entity_instance, entity_type: str, handling: SubTypeHandling) -> bool:
"""
Check if the instance is of a specific entity type or its subtype.
INCLUDE will evaluate to True if inst is a subtype of entity_type while the second function for EXCLUDE will evaluate to True only for an exact type match

Parameters:
inst (ifcopenshell.entity_instance): The instance to check.
entity_type (str): The entity type to check against.
handling (SubTypeHandling): Determines whether to include subtypes or not.

Returns:
bool: True if the instance matches the entity type criteria, False otherwise.
"""
handling_functions = {
SubTypeHandling.INCLUDE: lambda inst, entity_type: inst.is_a(entity_type),
SubTypeHandling.EXCLUDE: lambda inst, entity_type: inst.is_a() == entity_type,
}
return handling_functions[handling](inst, entity_type)

@gherkin_ifc.step("{attribute} {comparison_op:equal_or_not_equal} {value}")
@gherkin_ifc.step("{attribute} {comparison_op:equal_or_not_equal} {value} {tail:include_or_exclude_subtypes}")
def step_impl(context, inst, comparison_op, attribute, value, tail=SubTypeHandling.EXCLUDE):
Expand Down
2 changes: 1 addition & 1 deletion features/steps/steps.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from givens import attributes, entities, relationships, values
from thens import alignment, attributes, existance, geometry, nesting, reference, relations, values
from steps import attributes
from steps import attributes, return_to
50 changes: 50 additions & 0 deletions features/steps/steps/return_to.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from utils import misc, ifc

import ifcopenshell
from validation_handling import gherkin_ifc, global_rule

from behave import register_type
from parse_type import TypeBuilder
from enum import Enum, auto

from utils.subtype_handling import SubTypeHandling, check_entity_type

register_type(include_or_exclude_subtypes=TypeBuilder.make_enum({"including subtypes": SubTypeHandling.INCLUDE, "excluding subtypes": SubTypeHandling.EXCLUDE }))

@global_rule
@gherkin_ifc.step("Return to {entity}")
@gherkin_ifc.step("Return to {entity} {include_or_exclude_subtypes:include_or_exclude_subtypes}")
def step_impl(context, entity, include_or_exclude_subtypes=SubTypeHandling.EXCLUDE):
"""
Disclaimer: Currently untested for normative rules

| Feature Step | Context Stack |
|----------------------------|-------------------------------------|
| Given an IfcEntity | [entity1, entity2, entity3] |
| Given its attribute X | [attr, None, attr] |
| Given Y is attr | [True, False, True] |
| Given return to IfcEntity | [entity1, entity2, entity3] |
For rules used for activation, this is not a problem (one is sufficient to activate the rule).
However, for normative rules, we do not want to consider entity2.
Simply using indexes would not work as the stack is often nested.
"""
context.include_or_exclude_subtypes = include_or_exclude_subtypes
def filter_stack_tree(layer):
def check_inclusion_criteria(input):
"""
Verifies if layer includes a boolean variable or instance of {entity}
"""
is_bool = isinstance(input, bool)
correct_entity = False
if isinstance(input, ifcopenshell.entity_instance):
correct_entity = check_entity_type(input, entity, context.include_or_exclude_subtypes)
context.include_layer = is_bool or correct_entity
layer = layer.get('instances')
misc.map_state(layer, check_inclusion_criteria)
return layer if context.include_layer else None

#ensure the stack does not get pupulated when nothing was yielded in the last step
if (lambda f: f(f))(lambda f: lambda data: bool(data) and (not isinstance(data, (list, tuple)) or any(f(f)(item) for item in data)))(context.instances):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again this is rocket science to me. Can we clear this up first in the other PR

Copy link
Contributor Author

@Ghesselink Ghesselink Aug 10, 2024

Choose a reason for hiding this comment

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

Let's bring the rocket science back to building information science then 🚀
See #258 (comment)
In short, it ensures that context.instances is meaningful. In this case, when the result of the last Given statement is an empty set of instances, we don't want to go back. For instance;

| Step  ->  context.instances after step |
Given An IfcEntity -> [Entity, Entity, Entity]
Given its attribute A -> [SomeAttr, SomeAttr, SomeOtherAttr]
Given its attribute is SomeAttr ->  [SomeAttr, SomeAttr, ()]
Given its attribute B ->  [(), (), False]
Given its attribute IfcEntity -> What do we return here?

In case we don't check whether context.instances is meaningful, we'd return the context.instances as was the case after the initial Given step. This is not the preferred behavior, since the preoccuring Given statements determined that the rule would not be applicable.

The handle_given section in the decorator would probably be a better location for this logic.

stack_tree_filtered = list(
filter(None, list(map(filter_stack_tree, context._stack))))
context.instances = stack_tree_filtered
26 changes: 26 additions & 0 deletions features/steps/utils/subtype_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ifcopenshell

from enum import Enum, auto

class SubTypeHandling (Enum):
INCLUDE = auto()
EXCLUDE = auto()

def check_entity_type(inst: ifcopenshell.entity_instance, entity_type: str, handling: SubTypeHandling) -> bool:
"""
Check if the instance is of a specific entity type or its subtype.
INCLUDE will evaluate to True if inst is a subtype of entity_type while the second function for EXCLUDE will evaluate to True only for an exact type match

Parameters:
inst (ifcopenshell.entity_instance): The instance to check.
entity_type (str): The entity type to check against.
handling (SubTypeHandling): Determines whether to include subtypes or not.

Returns:
bool: True if the instance matches the entity type criteria, False otherwise.
"""
handling_functions = {
SubTypeHandling.INCLUDE: lambda inst, entity_type: inst.is_a(entity_type),
SubTypeHandling.EXCLUDE: lambda inst, entity_type: inst.is_a() == entity_type,
}
return handling_functions[handling](inst, entity_type)
7 changes: 2 additions & 5 deletions features/steps/validation_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def handle_given(context, fn, **kwargs):
context.instances = list(map(attrgetter('instance_id'), filter(lambda res: res.severity == OutcomeSeverity.PASSED, insts)))
pass
else:
pass # (1) -> context.applicable is set within the function ; replace this with a simple True/False and set applicability here?
pass # (1) -> context.applicable is set within the function, e.g. implementations with the 'global_rule' tag
else:
context._push('attribute') # for attribute stacking
if 'at depth 1' in context.step.name:
Expand Down Expand Up @@ -132,10 +132,7 @@ def apply_then_operation(fn, inst, context, current_path, depth=0, **kwargs):
if inst is None:
return
top_level_index = current_path[0] if current_path else None
activation_inst = inst if not current_path or activation_instances[top_level_index] is None else activation_instances[top_level_index]
#TODO: refactor into a more general solution that works for all rules
if "GEM051" in context.feature.name and context.is_global_rule:
activation_inst = activation_instances[0]
activation_inst = misc.do_try(lambda: activation_instances[0] if not current_path else misc.do_try(lambda: activation_instances[top_level_index], activation_instances[0]), None)
if isinstance(activation_inst, ifcopenshell.file):
activation_inst = None # in case of blocking IFC101 check, for safety set explicitly to None

Expand Down
92 changes: 92 additions & 0 deletions test/files/grd000/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import ifcopenshell
import ifcopenshell.template
import os

rule_code = "grd000"

abs_path = os.path.join(os.getcwd(), "test", "files", rule_code)
save_ifc_file = lambda file, filename: file.write(os.path.join(abs_path, filename))

f = ifcopenshell.template.create(schema_identifier="IFC4X3_ADD2")

placement = f.createIfcLocalPlacement()
O = 0., 0., 0.
X = 1., 0., 0.
Y = 0., 1., 0.
Z = 0., 0., 1.
grid = f.createIfcGrid(
GlobalId = ifcopenshell.guid.new(),
ObjectPlacement = placement,
UAxes = [f.createIfcGridAxis(
AxisTag = "1",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(X)]),
SameSense = True
)],
VAxes = [f.createIfcGridAxis(
AxisTag = "2",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(Y)]),
SameSense = True
)],
WAxes = [f.createIfcGridAxis(
AxisTag = "3",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(Z)]),
SameSense = True
)]
)

save_ifc_file(f, f'pass-{rule_code}-not_activated_no_placement.ifc')


grid_axis_1 = f.createIfcGridAxis(
AxisTag = "Axis 1",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(X)]),
SameSense = True
)

grid_axis_2 = f.createIfcGridAxis(
AxisTag = "Axis 2",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(Y)]),
SameSense = True
)

grid_placement = f.createIfcGridPlacement(
PlacementRelTo = placement,
)

column = f.createIfcColumn(
GlobalId = ifcopenshell.guid.new(),
ObjectPlacement = grid_placement,
)

grid.ObjectPlacement = placement

grid_placement.PlacementLocation = f.createIfcVirtualGridIntersection(
[grid_axis_1, grid_axis_2],
(0.0, 0.0, 0.0)
)
Comment on lines +63 to +66
Copy link
Collaborator

Choose a reason for hiding this comment

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

This violates the informal propositions here: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcVirtualGridIntersection.htm

It's also a really creative grid.

I would define:

  U axes:
    (0, 0) -> (0, 2)
    (1, 0) -> (1, 2)
    (2, 0) -> (2, 2)
  V axes:
    (0, 0) -> (2, 0)
    (0, 1) -> (2, 1)
    (0, 2) -> (2, 2)
  W:
    <empty>

And then you intersect e.g UAxes[1] with VAxes[1], but they need to be instances from the grid itself.

Also you don't actually reference this variable, but redefined sth on L 777

Also the polyline grid axes are best 2d instead of 3d.


save_ifc_file(f, f'pass-{rule_code}-activated_valid_grid_placement.ifc')

f = ifcopenshell.template.create(schema_identifier="IFC4X3_ADD2")

column = f.createIfcColumn(
GlobalId = ifcopenshell.guid.new(),
ObjectPlacement = f.createIfcGridPlacement(
PlacementLocation = f.createIfcVirtualGridIntersection(
[
f.createIfcGridAxis(
AxisTag = "Axis 1",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(X)]),
SameSense = True
),
f.createIfcGridAxis(
AxisTag = "Axis 2",
AxisCurve = f.createIfcPolyLine([f.createIfcCartesianPoint(O), f.createIfcCartesianPoint(Y)]),
SameSense = True
)
]
),
),
)

save_ifc_file(f, f'pass-{rule_code}-not_activated_no_grid.ifc')
Loading