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

Feature/feature concept templates #171

Draft
wants to merge 2 commits into
base: main
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
echo $CONDA/bin >> $GITHUB_PATH
- name: Install dependencies
run: |
pip install behave pytest tabulate pyparsing
pip install behave pytest tabulate pyparsing markdown networkx
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.7.0-d1a40d1-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
51 changes: 51 additions & 0 deletions features/ABC999_Alignment-main-points.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@implementer-agreement
@ALB
Feature: Alignment main points

Background: Linear elements with main point referents

Given the template "Alignment Main Points"
And any "ReferentObjectType" equals "MainPoint"

Scenario: Main point relationship names

Then all "PositionsName" equals "StartsAt" or "EndsAt"

Scenario: Segment to main point relationship cardinality

Given an IfcAlignmentSegment
Then number of values for "PositionsName" should be "2"
And a value for "PositionsName" should be "StartsAt"
And a value for "PositionsName" should be "EndsAt"

Scenario: Main point to segment relationship cardinality - first

Given an IfcAlignmentSegment
And it is [first] in relationship "Nests"
And any "PositionsName" equals "StartsAt"
And any "Referent"
Then number of values for "PositionsName" should be "1"

Scenario: Main point to segment relationship cardinality - intermediate

Given an IfcAlignmentSegment
And it is [neither first nor last] in relationship "Nests"
And any "PositionsName" equals "StartsAt"
And any "Referent"
Then number of values for "PositionsName" should be "1"

Scenario: Main point to segment relationship cardinality - intermediate

Given an IfcAlignmentSegment
And it is [neither first nor last] in relationship "Nests"
And any "PositionsName" equals "EndsAt"
And any "Referent"
Then number of values for "PositionsName" should be "1"

Scenario: Main point to segment relationship cardinality - end

Given an IfcAlignmentSegment
And it is [last] in relationship "Nests"
And any "PositionsName" equals "StartsAt"
And any "Referent"
Then number of values for "PositionsName" should be "1"
31 changes: 31 additions & 0 deletions features/resources/templates/main_points.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Alignment Main Points
=====================


```
concept {
IfcLinearElement:IsNestedBy -> IfcRelNests:RelatingObject
IfcRelNests:RelatedObjects -> IfcReferent
IfcReferent:Name -> IfcLabel_0
IfcReferent:PredefinedType -> IfcReferentTypeEnum
IfcReferent:ObjectType -> IfcLabel_5
IfcReferent:Positions -> IfcRelPositions:RelatingPositioningElement
IfcRelPositions:Name -> IfcLabel_1
IfcRelPositions:RelatedProducts -> IfcAlignmentSegment
IfcAlignmentSegment:DesignParameters -> IfcAlignmentParameterSegment
IfcAlignmentSegment:Name -> IfcLabel_4
IfcAlignmentParameterSegment:StartTag -> IfcLabel_2
IfcAlignmentParameterSegment:EndTag -> IfcLabel_3
IfcRelNests:RelatedObjects[binding="Referent"]
IfcReferent:Name[binding="ReferentName"]
IfcReferent:PredefinedType[binding="ReferentPredefinedType"]
IfcReferent:ObjectType[binding="ReferentObjectType"]
IfcReferent:Positions[binding="Positions"]
IfcRelPositions:Name[binding="PositionsName"]
IfcRelPositions:RelatedProducts[binding="RelatedProducts"]
IfcAlignmentSegment:DesignParameters[binding="DesignParameters"]
IfcAlignmentParameterSegment:StartTag[binding="StartTag"]
IfcAlignmentParameterSegment:EndTag[binding="EndTag"]
IfcAlignmentSegment:Name[binding="SegmentName"]
}
```
9 changes: 9 additions & 0 deletions features/steps/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,12 @@ class RepresentationTypeError:

def __str__(self):
return f"On instance {misc.fmt(self.inst)} the {self.representation_id} shape representation does not have {self.representation_type} as RepresentationType"


@dataclass
class TemplateValuationError:
inst: ifcopenshell.entity_instance
message : str

def __str__(self):
return f"On instance {misc.fmt(self.inst)} there is a template error. {self.message}"
14 changes: 14 additions & 0 deletions features/steps/givens/relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ def step_impl(context, relationship_type, entity):
assert relationship_type in reltype_to_extr
extr = reltype_to_extr[relationship_type]
context.instances = list(filter(lambda inst: misc.do_try(lambda: getattr(getattr(inst, extr['attribute'])[0], extr['object_placement']).is_a(entity), False), context.instances))


@given('it is {first_last_or_neither} in relationship "{attr}"')
def step_impl(context, first_last_or_neither, attr):
valid_preds = {"first": slice(0,1), "last": slice(-1,None), "neither first nor last": slice(1,-1)}

assert (first_last_or_neither[0], first_last_or_neither[-1]) == ('[', ']')
assert (slc := valid_preds.get(first_last_or_neither[1:-1]))

filename_related_attr_matrix = system.get_abs_path(f"resources/**/related_entity_attributes.csv")
related_attr_matrix = system.get_csv(filename_related_attr_matrix, return_type='dict')[0]

rel_to_related = lambda rel: getattr(rel, related_attr_matrix[rel.is_a()])
context.instances = [inst for inst in context.instances if inst in rel_to_related(getattr(inst, attr)[0])[slc]]
46 changes: 46 additions & 0 deletions features/steps/givens/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from collections import defaultdict
import markdown

from behave import *

from utils import concept_template, system, markdown

@given('the template "{template_name}"')
def step_impl(context, template_name):
for fn in system.get_abs_path('resources/templates/*.md'):
md = open(fn, encoding='utf-8').read()
if markdown.get_heading(md) == template_name:
break
else:
raise NotImplementedError(f"Template {template_name} not found")

context.template_tuples = concept_template.query(
context.model,
concept_template.from_graphviz(filecontent=md)
)


@given('any "{key}" equals "{value}"')
def step_impl(context, key, value):
context.template_tuples = [d for d in context.template_tuples if d.get(key) == value]


def group_template_tuples(context):
if instances := getattr(context, 'instances', None):
values = [[d for d in context.template_tuples if inst in d.values()] for inst in instances]
else:
values = [context.template_tuples]
return values


@given('any "{key}"')
def step_impl(context, key):
context.instances = [d.get(key) for d in context.template_tuples]


@given(u'a value for "{key}"')
def step_impl(context, key):
grouping = defaultdict(list)
for d in context.template_tuples:
grouping[d.get(key)].append(d)
context.template_tuples = list(grouping.values())
4 changes: 2 additions & 2 deletions features/steps/steps.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from givens import attributes, entities, relationships, values
from thens import attributes, existance, geometry, nesting, reference, relations, values
from givens import attributes, entities, relationships, values, templates
from thens import attributes, existance, geometry, nesting, reference, relations, values, templates
6 changes: 3 additions & 3 deletions features/steps/thens/nesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ def step_impl(context, entity, fragment, other_entity):
if condition == 'only 1' and len(correct_elements) > 1:
errors.append(err.InstanceStructureError(inst, correct_elements, f'{error_log_txt}'))
if condition == 'a list of only':
if len(getattr(inst, extr['attribute'], [])) > 1:
errors.append(err.InstanceStructureError(f'{error_log_txt} more than 1 list, including'))
elif len(false_elements):
# if len(getattr(inst, extr['attribute'], [])) > 1:
# errors.append(err.InstanceStructureError(inst, [], f'{error_log_txt} more than 1 list'))
if len(false_elements):
errors.append(err.InstanceStructureError(inst, false_elements, f'{error_log_txt} a list that includes'))
if condition == 'only' and len(false_elements):
errors.append(err.InstanceStructureError(inst, correct_elements, f'{error_log_txt}'))
Expand Down
82 changes: 82 additions & 0 deletions features/steps/thens/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from collections import defaultdict
import functools
import pprint
from behave import *

import errors as err
from features.steps.utils import misc

def group_template_tuples(context):
if instances := getattr(context, 'instances', None):
values = [[d for d in context.template_tuples if inst in d.values()] for inst in instances]
else:
values = [context.template_tuples]
return values

def group_template_tuples_by_root(context):
grouping = defaultdict(list)
for d in context.template_tuples:
grouping[d.get('_root')].append(d)
return [(vs[0].get('_root'), vs) for vs in grouping.values()]


def grouped_templates_tuples(func):
@functools.wraps(func)
def inner(context, *args, **kwargs):
if hasattr(context, 'instances'):
grouping = zip(context.instances, group_template_tuples(context))
else:
grouping = group_template_tuples_by_root(context)

func(context, grouping, *args, **kwargs)
return inner

@then('all "{key}" equals "{value}"')
@then('all "{key}" equals "{value}" or "{value_2}"')
@grouped_templates_tuples
def step_impl(context, grouping, key, value, value_2 = None):
required = {value}
if value_2 is not None:
required.add(value_2)

errors = []

if context.template_tuples:
for inst, vs in grouping:
if vs:
values = set(d.get(key) for d in vs)
invalid = values - required
if invalid:
errors.append(err.TemplateValuationError(inst, f"{' '.join(invalid)} do not match {' or '.join(required)}"))

misc.handle_errors(context, errors)


@then('number of values for "{key}" should be "{num:d}"')
@grouped_templates_tuples
def step_impl(context, values, key, num):
errors = []

if context.template_tuples:
for inst, vs in values:
if vs:
filtered = [v for d in vs if (v := d.get(key))]
if len(filtered) != num:
errors.append(err.TemplateValuationError(inst, f"has {len(filtered)} values for {key}"))

misc.handle_errors(context, errors)


@then('{article} value for "{key}" should be "{value}"')
@grouped_templates_tuples
def step_impl(context, values, article, key, value):
errors = []

if context.template_tuples:
for inst, vs in values:
if vs:
filtered = [v for d in vs if (v := d.get(key))]
if value not in filtered:
errors.append(err.TemplateValuationError(inst, f"{' '.join(filtered)} do not contain {value}"))

misc.handle_errors(context, errors)