Skip to content

Commit 330a4ff

Browse files
authored
Feats: extract additional beam/joints data for results csv export (#143)
* FIX: got rid of cloudtocloud away from publication * FIX:catch up with main * ADD-WIP: unsure about this commit, leftover mods * FIX-ADD: no more distances in csv export, added distances export option as separate * ADD: extra commodity for automatic added bool toggles * MILESTONE-ADD: new category results with import/export/inspect results + load cloud fix * FIX: convert pickle to JSON serialization * FIX: missing import * FIX: correct exception RML message * FIRST COMMIT: for next implementation * ADD: axis for beam * FIX: faster dfassembly preview * FIX: add output to component for better debug * CAP-ADD: implemented extra info geometries * CAP-TYPO: cleaning * WIP: committing before solving conflicts * FIX: add axis compatibility for elements with no joints
1 parent 797e912 commit 330a4ff

File tree

7 files changed

+317
-68
lines changed

7 files changed

+317
-68
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Open your Grasshopper canvas and search for the `DF` components!
5454
The full documentation, with tutorials, automatic documentation for GHComponents and PythonAPI is available [here](https://diffcheckorg.github.io/diffCheck/).
5555

5656

57+
5758
## Roadmap
5859

5960
```mermaid

src/gh/components/DF_csv_exporter/code.py

+37-37
Original file line numberDiff line numberDiff line change
@@ -66,44 +66,29 @@ def __init__(self):
6666
"export_dist",
6767
input_indx, X_cord, Y_cord)
6868

69-
def _get_id(self,
70-
idx: int,
71-
i_result: DFVizResults
72-
) -> str:
73-
""" Get the ID of the element """
74-
counter = 0
75-
76-
if self.prefix == "beam":
77-
return idx
78-
elif self.prefix == "joint":
79-
for idx_b, beam in enumerate(i_result.assembly.beams):
80-
for idx_j, joint in enumerate(beam.joints):
81-
if counter == idx:
82-
return f"{idx_b}--{idx_j}--{0}"
83-
counter += 1
84-
elif self.prefix == "joint_face":
85-
for idx_b, beam in enumerate(i_result.assembly.beams):
86-
for idx_j, joint in enumerate(beam.joints):
87-
for idx_f, face in enumerate(joint.faces):
88-
if counter == idx:
89-
return f"{idx_b}--{idx_j}--{idx_f}"
90-
counter += 1
91-
9269
def _prepare_row(self,
9370
idx: int,
9471
i_result: DFVizResults
95-
) -> typing.Dict:
72+
) -> typing.Dict[str, typing.Any]:
9673
"""
9774
Convert the results contained in the DFVizResults object to a dict to be written in the CSV file
9875
9976
:param idx: Index of the element
10077
:param i_result: DFVizResults object containing all the values
10178
102-
:return: Dict of values containng as keys the header and as items the values to be written in the CSV file
79+
:return: Dict of values containing as keys the header and as items the values to be written in the CSV file
10380
"""
10481
if i_result.sanity_check[idx].value != DFInvalidData.VALID.value:
10582
invalid_type = i_result.sanity_check[idx].name
106-
return [self._get_id(idx, i_result), invalid_type, invalid_type, invalid_type, invalid_type, invalid_type, invalid_type]
83+
return {
84+
f"{self.prefix} id": i_result.find_id(idx),
85+
"invalid_type": invalid_type,
86+
"min_deviation": invalid_type,
87+
"max_deviation": invalid_type,
88+
"std_deviation": invalid_type,
89+
"rmse": invalid_type,
90+
"mean": invalid_type
91+
}
10792

10893
distances = [round(value, 4) for value in i_result.distances[idx]]
10994
min_dev = round(i_result.distances_min_deviation[idx], 4)
@@ -112,27 +97,47 @@ def _prepare_row(self,
11297
rmse = round(i_result.distances_rmse[idx], 4)
11398
mean = round(i_result.distances_mean[idx], 4)
11499

115-
row: typing.Dict = {
116-
f"{self.prefix} id": self._get_id(idx, i_result),
100+
row: typing.Dict[str, typing.Any] = {
101+
f"{self.prefix} id": i_result.find_id(idx),
117102
"distances": distances,
118103
"min_deviation": min_dev,
119104
"max_deviation": max_dev,
120105
"std_deviation": std_dev,
121106
"rmse": rmse,
122107
"mean": mean
123108
}
109+
110+
# Add extra geometric info based on analysis type here:
111+
if i_result.analysis_type == "beam":
112+
row.update({
113+
"beam_length": i_result.assembly.beams[idx].length
114+
})
115+
elif i_result.analysis_type == "joint":
116+
# NB:: for conviniency, if there is only one beam, we add the lenght of the beam i nthe joint csv analysis output
117+
if i_result.assembly.has_only_one_beam:
118+
row.update({
119+
"beam_length": i_result.assembly.beams[0].length
120+
})
121+
row.update({
122+
"joint_distance_to_beam_midpoint": i_result.assembly.compute_all_joint_distances_to_midpoint()[idx]
123+
})
124+
elif i_result.analysis_type == "joint_face":
125+
row.update({
126+
"jointface_angle": i_result.assembly.compute_all_joint_angles()[idx]
127+
})
128+
124129
return row
125130

126131
def _write_csv(self,
127132
csv_path: str,
128-
rows: typing.List[typing.Dict],
133+
rows: typing.List[typing.Dict[str, typing.Any]],
129134
is_writing_only_distances: bool = False
130135
) -> None:
131136
"""
132137
Write the CSV file
133138
134139
:param csv_path: Path of the CSV file
135-
:param rows: Dict of values to be written in the CSV file
140+
:param rows: List of dictionaries containing values to be written in the CSV file
136141
:param is_writing_only_distances: Flag to check if to write ONLY distances or the whole analysis
137142
138143
:return: None
@@ -157,20 +162,15 @@ def RunScript(self,
157162
i_file_name: str,
158163
i_export_seperate_files: bool,
159164
i_export_distances: bool,
160-
i_result):
165+
i_result: DFVizResults) -> None:
161166

162167
csv_analysis_path: str = None
163168
csv_distances_path: str = None
164169

165170
if i_dump:
166171
os.makedirs(i_export_dir, exist_ok=True)
167172

168-
if len(i_result.assembly.beams) == len(i_result.source):
169-
self.prefix = "beam"
170-
elif len(i_result.assembly.all_joints) == len(i_result.source):
171-
self.prefix = "joint"
172-
elif len(i_result.assembly.all_joint_faces) == len(i_result.source):
173-
self.prefix = "joint_face"
173+
self.prefix = i_result.analysis_type
174174

175175
if i_export_seperate_files:
176176
for idx in range(len(i_result.source)):

src/gh/components/DF_export_results/code.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import System
44

55
from ghpythonlib.componentbase import executingcomponent as component
6-
import Grasshopper as gh
6+
import Grasshopper
77

88

99
def add_button(self,
@@ -24,7 +24,7 @@ def add_button(self,
2424
"""
2525
param = ghenv.Component.Params.Input[indx] # noqa: F821
2626
if param.SourceCount == 0:
27-
button = gh.Kernel.Special.GH_ButtonObject()
27+
button = Grasshopper.Kernel.Special.GH_ButtonObject()
2828
button.NickName = ""
2929
button.Description = ""
3030
button.CreateAttributes()
@@ -33,7 +33,7 @@ def add_button(self,
3333
Y_param_coord - (button.Attributes.Bounds.Height / 2 - 0.1)
3434
)
3535
button.Attributes.ExpireLayout()
36-
gh.Instances.ActiveCanvas.Document.AddObject(button, False)
36+
Grasshopper.Instances.ActiveCanvas.Document.AddObject(button, False)
3737
ghenv.Component.Params.Input[indx].AddSource(button) # noqa: F821
3838

3939
class DFExportResults(component):
@@ -52,7 +52,8 @@ def RunScript(self, i_dump: bool, i_export_dir: str, i_results):
5252
if i_dump is None or i_export_dir is None or i_results is None:
5353
return None
5454

55+
o_path = None
5556
if i_dump:
56-
i_results.dump_serialization(i_export_dir)
57+
o_path = i_results.dump_serialization(i_export_dir)
5758

58-
return None
59+
return o_path

src/gh/components/DF_export_results/metadata.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@
5050
"typeHintID": "ghdoc"
5151
}
5252
],
53-
"outputParameters": []
53+
"outputParameters": [
54+
{
55+
"name": "o_path",
56+
"nickname": "o_path",
57+
"description": "The file path of the generated .diffCheck files.",
58+
"optional": false,
59+
"sourceCount": 0,
60+
"graft": false
61+
}
62+
]
5463
}
5564
}

src/gh/components/DF_preview_assembly/code.py

+1-14
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import System
44

5-
import typing
6-
75
import Rhino.Geometry as rg
86

97
from ghpythonlib.componentbase import executingcomponent as component
@@ -93,18 +91,7 @@ def DrawViewportWires(self, args):
9391
## DFBeams
9492
#######################################
9593
if len(self._dfassembly.beams) > 1:
96-
# beams' obb
97-
df_cloud = diffCheck.diffcheck_bindings.dfb_geometry.DFPointCloud()
98-
vertices_pt3d_rh : typing.List[rg.Point3d] = [vertex.to_rg_point3d() for vertex in beam.vertices]
99-
df_cloud.points = [np.array([vertex.X, vertex.Y, vertex.Z]).reshape(3, 1) for vertex in vertices_pt3d_rh]
100-
obb: rg.Brep = diffCheck.df_cvt_bindings.cvt_dfOBB_2_rhbrep(df_cloud.get_tight_bounding_box())
101-
# args.Display.DrawBrepWires(obb, System.Drawing.Color.Red) ## keep for debugging
102-
103-
# axis arrow
104-
obb_faces = obb.Faces
105-
obb_faces = sorted(obb_faces, key=lambda face: rg.AreaMassProperties.Compute(face).Area)
106-
obb_endfaces = obb_faces[:2]
107-
beam_axis = rg.Line(obb_endfaces[0].GetBoundingBox(True).Center, obb_endfaces[1].GetBoundingBox(True).Center)
94+
beam_axis = beam.axis
10895
extension_length = 0.5 * diffCheck.df_util.get_doc_2_meters_unitf()
10996
beam_axis.Extend(extension_length, extension_length)
11097
args.Display.DrawArrow(beam_axis, System.Drawing.Color.Magenta)

src/gh/diffCheck/diffCheck/df_error_estimation.py

+55-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class NumpyEncoder(json.JSONEncoder):
2626
def default(self, obj):
2727
if isinstance(obj, np.ndarray):
2828
return obj.tolist()
29+
elif isinstance(obj, Enum):
30+
return obj.value # or use obj.name if you prefer
2931
return super().default(obj)
3032

3133
class DFInvalidData(Enum):
@@ -41,7 +43,7 @@ class DFInvalidData(Enum):
4143

4244
class DFVizResults:
4345
"""
44-
This class compiles the resluts of the error estimation into one object
46+
This class compiles the results of the error estimation into one object
4547
"""
4648
__serial_file_extenion: str = ".diffCheck"
4749

@@ -61,6 +63,8 @@ def __init__(self, assembly):
6163
self.distances_sd_deviation = []
6264
self.distances = []
6365

66+
self._analysis_type: str = None
67+
6468
def __repr__(self):
6569
return f"DFVizResults of({self.assembly})"
6670

@@ -109,7 +113,8 @@ def dump_serialization(self, dir: str) -> str:
109113

110114
timestamp: str = datetime.now().strftime("%Y%m%d_%H%M%S")
111115
assembly_name: str = self.assembly.name
112-
serial_file_path = os.path.join(dir, f"{assembly_name}_{timestamp}{self.__serial_file_extenion}")
116+
result_type: str = self.analysis_type
117+
serial_file_path = os.path.join(dir, f"{assembly_name}_{result_type}_{timestamp}{self.__serial_file_extenion}")
113118

114119
try:
115120
with open(serial_file_path, "w") as f:
@@ -135,6 +140,49 @@ def load_serialization(file_path: str) -> 'DFVizResults':
135140
raise e
136141
return obj
137142

143+
def _compute_dfresult_type(self):
144+
"""
145+
Detect if the DFVizResults object contains results of beam, joint of joint_face level analysis
146+
"""
147+
# check that source and target have the same length
148+
if len(self.source) != len(self.target):
149+
raise ValueError("Source and target have different length, cannot determine the type of analysis")
150+
if len(self.assembly.beams) == len(self.source):
151+
self._analysis_type = "beam"
152+
elif len(self.assembly.all_joints) == len(self.source):
153+
self._analysis_type = "joint"
154+
elif len(self.assembly.all_joint_faces) == len(self.source):
155+
self._analysis_type = "joint_face"
156+
return self._analysis_type
157+
158+
def find_id(self, idx: int,) -> str:
159+
"""
160+
Return the ID in str format of the element. This func is used during
161+
the csv export. With the following format:
162+
- beam: idx
163+
- joint: idx_b--idx_j--0
164+
- joint_face: idx_b--idx_j--idx_f
165+
166+
:param idx: the index of the element
167+
"""
168+
counter = 0
169+
170+
if self.analysis_type == "beam":
171+
return str(idx)
172+
elif self.analysis_type == "joint":
173+
for idx_b, beam in enumerate(self.assembly.beams):
174+
for idx_j, joint in enumerate(beam.joints):
175+
if counter == idx:
176+
return f"{idx_b}--{idx_j}--{0}"
177+
counter += 1
178+
elif self.analysis_type == "joint_face":
179+
for idx_b, beam in enumerate(self.assembly.beams):
180+
for idx_j, joint in enumerate(beam.joints):
181+
for idx_f, face in enumerate(joint.faces):
182+
if counter == idx:
183+
return f"{idx_b}--{idx_j}--{idx_f}"
184+
counter += 1
185+
return ""
138186

139187
def add(self, source, target, distances, sanity_check: DFInvalidData = DFInvalidData.VALID):
140188

@@ -215,6 +263,11 @@ def filter_values_based_on_valuetype(self, settings):
215263
def is_source_cloud(self):
216264
return type(self.source[0]) is diffcheck_bindings.dfb_geometry.DFPointCloud
217265

266+
@property
267+
def analysis_type(self):
268+
self._analysis_type = self._compute_dfresult_type()
269+
return self._analysis_type
270+
218271
# FIXME: ths is currently broken, we need to fix it
219272
def df_cloud_2_df_cloud_comparison(
220273
assembly: DFAssembly,

0 commit comments

Comments
 (0)