Skip to content

Commit

Permalink
refactor alignment continuity checks for better error reporting
Browse files Browse the repository at this point in the history
ALS016 warnings are currently rather hard to troubleshoot.
This revision provides a dataclass that carries the associated information about the pairwise alignment segments
so that they can be reported in more detail.

(IVS-138)
  • Loading branch information
civilx64 committed Oct 14, 2024
1 parent 49465b8 commit 41d8573
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 61 deletions.
21 changes: 10 additions & 11 deletions features/steps/thens/alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import ifcopenshell.util.unit

from utils import ifc43x_alignment_validation as ifc43
from utils import geometry
from utils.geometry import AlignmentSegmentContinuityCalculation
from utils import ifc
from validation_handling import gherkin_ifc
from . import ValidationOutcome, OutcomeSeverity
Expand Down Expand Up @@ -377,6 +377,12 @@ def step_impl(context, inst, continuity_type):
for previous, current in inst:
entity_contexts = ifc.recurrently_get_entity_attr(context, current, 'IfcRepresentation', 'ContextOfItems')
precision = ifc.get_precision_from_contexts(entity_contexts)
continuity_calc = AlignmentSegmentContinuityCalculation(
previous_segment=previous,
segment_to_analyze=current,
length_unit_scale_factor=length_unit_scale_factor,
)
continuity_calc.run()

# calculate number of significant figures to display
# use the precision of the geometric context plus one additional digit to accommodate rounding
Expand All @@ -387,19 +393,11 @@ def step_impl(context, inst, continuity_type):

if (continuity_type == "position") and (current.Transition in position_transition_codes):
expected = precision
observed = geometry.alignment_segment_positional_difference(
length_unit_scale_factor,
previous,
current
)
observed = continuity_calc.positional_difference

elif (continuity_type == "tangency") and (current.Transition in tangency_transition_codes):
expected = math.atan2(precision, current.SegmentLength.wrappedValue)
observed = geometry.alignment_segment_angular_difference(
length_unit_scale_factor,
previous,
current
)
observed = continuity_calc.directional_difference
else:
return

Expand All @@ -415,5 +413,6 @@ def step_impl(context, inst, continuity_type):
"observed": observed,
"num_digits": display_sig_figs,
"context": f"calculated deviation in {continuity_type.lower()}",
"continuity_details": continuity_calc.to_dict(),
},
severity=OutcomeSeverity.WARNING)
124 changes: 74 additions & 50 deletions features/steps/utils/geometry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
import operator
import math
from typing import Dict

import numpy as np

Expand All @@ -14,6 +15,7 @@
GEOM_TOLERANCE = 1E-12



def get_edges(file, inst, sequence_type=frozenset, oriented=False):
edge_type = tuple if oriented else frozenset

Expand Down Expand Up @@ -162,12 +164,11 @@ def evaluate_segment(segment: ifcopenshell.entity_instance, dist_along: float) -

return np.array(prev_trans_matrix, dtype=np.float64).T


def alignment_segment_positional_difference(
length_unit_scale_factor: float, previous_segment: ifcopenshell.entity_instance,
segment_to_analyze: ifcopenshell.entity_instance):
@dataclass
class AlignmentSegmentContinuityCalculation:
"""
Use ifcopenshell to determine the difference in cartesian position between segments of an IfcAlignment.
Use ifcopenshell to determine the difference in cartesian position and tangent direction
between segments of an IfcAlignment.
The expected entity type is either `IfcCurveSegment` or `IfcCompositeCurveSegment`.
:param length_unit_scale_factor: Scale factor between the project units and metric units used internally by
Expand All @@ -177,48 +178,71 @@ def alignment_segment_positional_difference(
:param segment_to_analyze: The segment under analysis. The calculated end point of the previous segment will be
compared to the calculated start point of this segment.
"""

u = abs(previous_segment.SegmentLength.wrappedValue) * length_unit_scale_factor
prev_end_transform = evaluate_segment(segment=previous_segment, dist_along=u)
current_start_transform = evaluate_segment(segment=segment_to_analyze, dist_along=0.0)

e0 = prev_end_transform[3][0] / length_unit_scale_factor
e1 = prev_end_transform[3][1] / length_unit_scale_factor
preceding_end = (e0, e1)

s0 = current_start_transform[3][0] / length_unit_scale_factor
s1 = current_start_transform[3][1] / length_unit_scale_factor
current_start = (s0, s1)

return math.dist(preceding_end, current_start)


def alignment_segment_angular_difference(
length_unit_scale_factor: float, previous_segment: ifcopenshell.entity_instance,
segment_to_analyze: ifcopenshell.entity_instance):
"""
Use ifcopenshell to determine the difference in tangent direction angle between segments of an IfcAlignment.
The expected entity type is either `IfcCurveSegment` or `IfcCompositeCurveSegment`.
:param length_unit_scale_factor: Scale factor between the project units and metric units used internally by
ifcopenshell
:param previous_segment: The segment that precede the segment being analyzed. The ending direction of this segment
will be determined via ifcopenshell geometry calculations.
:param segment_to_analyze: The segment under analysis. The calculated ending direction of the previous segment
will be compared to the calculated starting direction of this segment.
"""
u = abs(float(previous_segment.SegmentLength.wrappedValue)) * length_unit_scale_factor
prev_end_transform = evaluate_segment(segment=previous_segment, dist_along=u)
current_start_transform = evaluate_segment(segment=segment_to_analyze, dist_along=0.0)

prev_i = prev_end_transform[0][0]
prev_j = prev_end_transform[0][1]
preceding_end_direction = math.atan2(prev_j, prev_i)

curr_i = current_start_transform[0][0]
curr_j = current_start_transform[0][1]
current_start_direction = math.atan2(curr_j, curr_i)

delta = abs(current_start_direction - preceding_end_direction)

return delta
previous_segment: ifcopenshell.entity_instance
segment_to_analyze: ifcopenshell.entity_instance
length_unit_scale_factor: float
preceding_end_point: tuple = None
preceding_end_direction: float = None
current_start_point: tuple = None
current_start_direction: float = None
positional_difference: float = None
directional_difference: float = None

def _calculate_positional_difference(self) -> None:

u = abs(self.previous_segment.SegmentLength.wrappedValue) * self.length_unit_scale_factor
prev_end_transform = evaluate_segment(segment=self.previous_segment, dist_along=u)
current_start_transform = evaluate_segment(segment=self.segment_to_analyze, dist_along=0.0)

e0 = prev_end_transform[3][0] / self.length_unit_scale_factor
e1 = prev_end_transform[3][1] / self.length_unit_scale_factor
self.preceding_end_point = (e0, e1)

s0 = current_start_transform[3][0] / self.length_unit_scale_factor
s1 = current_start_transform[3][1] / self.length_unit_scale_factor
self.current_start_point = (s0, s1)

self.positional_difference = math.dist(
self.preceding_end_point, self.current_start_point)


def _calculate_directional_difference(self) -> None:
u = abs(float(self.previous_segment.SegmentLength.wrappedValue)) * self.length_unit_scale_factor
prev_end_transform = evaluate_segment(segment=self.previous_segment, dist_along=u)
current_start_transform = evaluate_segment(segment=self.segment_to_analyze, dist_along=0.0)

prev_i = prev_end_transform[0][0]
prev_j = prev_end_transform[0][1]
self.preceding_end_direction = math.atan2(prev_j, prev_i)

curr_i = current_start_transform[0][0]
curr_j = current_start_transform[0][1]
self.current_start_direction = math.atan2(curr_j, curr_i)

self.directional_difference = abs(self.current_start_direction - self.preceding_end_direction)

def run(self) -> None:
"""
Run the calculation
"""
self._calculate_positional_difference()
self._calculate_directional_difference()

def to_dict(self) -> Dict:
"""
Serialize dataclass to a dictionary
This method is required because dataclasses.asdict() will fail
because ifcopenshell.entity_instances are of type SwigPyObject which cannot be pickled
"""
return {
"previous_segment": str(self.previous_segment),
"segment_to_analyze": str(self.segment_to_analyze),
"length_unit_scale_factor": self.length_unit_scale_factor,
"preceding_end_point": tuple(self.preceding_end_point),
"preceding_end_direction": self.preceding_end_direction,
"current_start_point": tuple(self.current_start_point),
"current_start_direction": self.current_start_direction,
"positional_difference": self.positional_difference,
"directional_difference": self.directional_difference,
}

0 comments on commit 41d8573

Please sign in to comment.