-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcustom_functions.py
More file actions
1502 lines (1403 loc) · 80.8 KB
/
custom_functions.py
File metadata and controls
1502 lines (1403 loc) · 80.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import pandas as pd
from xml.etree import ElementTree as ET
from deepdiff import DeepDiff
from deepmerge import Merger
import copy
from ifctester import ids
import itertools
from itertools import product
STRING_ENTITY = 'Entity'
STRING_PREDEFINEDTYPE = 'PredefinedType'
STRING_PROPERTY = 'Property'
STRING_PROPERTYSET = 'PropertySet'
STRING_PROPERTYDATATYPE = 'PropertyDatatype'
STRING_PROPERTYVALUE = 'PropertyValue'
STRING_PROPERTYURI = 'PropertyURI'
STRING_MATERIAL = 'Material'
STRING_MATERIALURI = 'MaterialURI'
STRING_ATTRIBUTE = 'Attribute'
STRING_ATTRIBUTEVALUE = 'AttributeValue'
STRING_CLASSIFICATION = 'Classification'
STRING_CLASSIFICATIONSYSTEM = 'ClassificationSystem'
STRING_CLASSIFICATIONURI = 'ClassificationURI'
STRING_PARTOFRELATION = 'PartOfRelation'
STRING_PARTOFENTITY = 'PartOfEntity'
STRING_PARTOFPREDEFINEDTYPE = 'PartOfPredefinedType'
STRING_DESCRIPTION = 'Description'
STRING_SPECIFICATIONNAME = 'SpecificationName'
STRING_SPECIFICATIONCARDINALITY = 'SpecificationCardinality'
STRING_SPECIFICATIONIFCVERSION = 'SpecificationIfcVersion'
STRING_REQUIREMENTCARDINALITY = 'Cardinality'
KEYWORD_NONE = '_none_'
KEYWORD_MISSING = '_MISSING_'
entity_description_dict = {}
property_description_dict = {}
def excel_to_spec_list(EXCEL_PATH, sheet_name, separate_by, skipped_rows, ifc_versions, is_entity_based_app):
'''Parses excel data from a given file path and sheet name into a list of specifications.
Each specification is represented as dictionary with a given applicability, requirements, specification data, and general data
:param EXCEL_PATH: Path to an excel file
:type EXCEL_PATH: str
:param sheet_name: Name of the relevant sheet
:type sheet_name: str
:param separate_by: List of general data for which specifications must be seperated
:type separate_by: list
:param skipped_rows: Number of skipped rows at the top of the sheet
:type skipped_rows: int
:param ifc_versions: String specifying the default ifc versions for all specifications (are overwritten if the specificationIfcsVersion column is used)
:type ifc_versions: str
:param is_entity_based_app: Boolean specifying if the applicability should be generated only entity-based (with predefiend types)
:type is_entity_based_app: boolean
:return: List of specifications with applicability, requirements, specification data and general data
:rtype: list
'''
###Import data
all_columns = pd.read_excel(EXCEL_PATH, sheet_name=sheet_name, skiprows=skipped_rows, nrows=0).columns.tolist()
dup = {x[:-2] for x in all_columns if x[-2:] == '.1'}
if dup:
raise Exception('Column names must be unique. The following column names occur multiple times: ' + ', '.join(map(str,dup)))
applicability_data = []
requirements_data = []
generaldata = []
specification_data = []
#Organise all relevant available column names according to the facets
prefix = 'A.'
if is_entity_based_app:
removed_columns = [prefix+STRING_PROPERTY,prefix+STRING_PROPERTYSET,prefix+STRING_PROPERTYVALUE,prefix+STRING_PROPERTYDATATYPE,
prefix+STRING_MATERIAL,
prefix+STRING_CLASSIFICATION,prefix+STRING_CLASSIFICATIONSYSTEM,
prefix+STRING_PARTOFENTITY,prefix+STRING_PARTOFRELATION]
all_columns = [x for x in all_columns if x not in removed_columns]
cols_app_entity = [col for col in [prefix+STRING_ENTITY] if col in all_columns]
cols_app_property = [col for col in [prefix+STRING_PROPERTY,prefix+STRING_PROPERTYSET,prefix+STRING_PROPERTYVALUE,prefix+STRING_PROPERTYDATATYPE] if col in all_columns]
cols_app_material = [col for col in [prefix+STRING_MATERIAL] if col in all_columns]
cols_app_attribute = [col for col in [prefix+STRING_ATTRIBUTE,prefix+STRING_ATTRIBUTEVALUE] if col in all_columns]
cols_app_classification = [col for col in [prefix+STRING_CLASSIFICATION,prefix+STRING_CLASSIFICATIONSYSTEM] if col in all_columns]
cols_app_partOf = [col for col in [prefix+STRING_PARTOFENTITY,prefix+STRING_PARTOFRELATION] if col in all_columns]
prefix = 'R.'
cols_req_entity = [col for col in [prefix+STRING_ENTITY,prefix+STRING_DESCRIPTION+'.Entity'] if col in all_columns]
cols_req_property = [col for col in [prefix+STRING_PROPERTY,prefix+STRING_PROPERTYSET,prefix+STRING_PROPERTYVALUE,prefix+STRING_PROPERTYDATATYPE,prefix+STRING_PROPERTYURI,prefix+STRING_REQUIREMENTCARDINALITY,prefix+STRING_DESCRIPTION+'.Property'] if col in all_columns]
cols_req_material = [col for col in [prefix+STRING_MATERIAL,STRING_MATERIALURI,prefix+STRING_REQUIREMENTCARDINALITY] if col in all_columns]
cols_req_attribute = [col for col in [prefix+STRING_ATTRIBUTE,prefix+STRING_ATTRIBUTEVALUE,prefix+STRING_REQUIREMENTCARDINALITY] if col in all_columns]
cols_req_classification = [col for col in [prefix+STRING_CLASSIFICATION,prefix+STRING_CLASSIFICATIONSYSTEM,prefix+STRING_CLASSIFICATIONURI,prefix+STRING_REQUIREMENTCARDINALITY] if col in all_columns]
cols_req_partOf = [col for col in [prefix+STRING_PARTOFENTITY,prefix+STRING_PARTOFRELATION,prefix+STRING_REQUIREMENTCARDINALITY] if col in all_columns]
cols_general = [col for col in ['Phase','Role','Usecase'] if col in all_columns]
cols_specification = [col for col in [STRING_SPECIFICATIONCARDINALITY,STRING_SPECIFICATIONIFCVERSION,STRING_SPECIFICATIONNAME] if col in all_columns]
#Store all relevant column names of the used file
relevant_columns = []
relevant_columns.extend(cols_app_entity)
relevant_columns.extend(cols_app_property)
relevant_columns.extend(cols_app_material)
relevant_columns.extend(cols_app_attribute)
relevant_columns.extend(cols_app_classification)
relevant_columns.extend(cols_app_partOf)
relevant_columns.extend(cols_req_entity)
relevant_columns.extend(cols_req_property)
relevant_columns.extend(cols_req_material)
relevant_columns.extend(cols_req_attribute)
relevant_columns.extend(cols_req_classification)
relevant_columns.extend(cols_req_partOf)
relevant_columns.extend(cols_general)
relevant_columns.extend(cols_specification)
relevant_columns = list(set(relevant_columns))
##Import the relevant columns and merge rows with the same applicability into one row
if relevant_columns:
#Import all relevant columns
df = pd.read_excel(EXCEL_PATH, sheet_name=sheet_name, skiprows=skipped_rows, usecols=relevant_columns, dtype=str)
#Fill empty requirement cardinality with default value
if prefix+STRING_REQUIREMENTCARDINALITY in df.columns:
df[prefix+STRING_REQUIREMENTCARDINALITY] = df[prefix+STRING_REQUIREMENTCARDINALITY].fillna("required")
#Fill empty specification ifc version with default version from IDS4ALL sheet
spec_ifc_version_col_used = False
if STRING_SPECIFICATIONIFCVERSION not in df.columns:
df[STRING_SPECIFICATIONIFCVERSION] = ifc_versions
relevant_columns.append(STRING_SPECIFICATIONIFCVERSION)
cols_specification.append(STRING_SPECIFICATIONIFCVERSION)
else:
df[STRING_SPECIFICATIONIFCVERSION] = df[STRING_SPECIFICATIONIFCVERSION].fillna(ifc_versions)
spec_ifc_version_col_used = True
#Fill NaN values with a placeholder
df_filled = df.fillna(KEYWORD_MISSING)
#Step 1: Merge rows of the columns STRING_PROPERTYVALUE & STRING_ATTRIBUTEVALUE if the values in the other columns are identical
relevant_columns_copy = relevant_columns.copy()
merge_columns_1 = [prefix+STRING_PROPERTYVALUE,
prefix+STRING_ATTRIBUTEVALUE]
removed_items = []
for item in merge_columns_1:
if item in relevant_columns_copy:
relevant_columns_copy.remove(item)
removed_items.append(item)
df_step1 = df_filled.groupby(relevant_columns_copy, dropna=False, sort=False).agg({
col: lambda x: '|'.join(map(str, x.dropna())) for col in removed_items
}).reset_index()
#Step 2: Merge all requirement parameters and general parameters not in seperate_by,
#keeping values of STRING_PROPERTYVALUE & STRING_ATTRIBUTEVALUE as sub-lists
merge_columns_2 = []
merge_columns_2.extend(cols_req_entity)
merge_columns_2.extend(cols_req_property)
merge_columns_2.extend(cols_req_material)
merge_columns_2.extend(cols_req_attribute)
merge_columns_2.extend(cols_req_classification)
merge_columns_2.extend(cols_req_partOf)
merge_columns_2.extend([item for item in cols_general if item not in separate_by])
merge_columns_2 = list(set(merge_columns_2))
for item in merge_columns_2:
if item in relevant_columns_copy:
relevant_columns_copy.remove(item)
removed_items.append(item)
df_final = df_step1.groupby(relevant_columns_copy, as_index=False, sort=False).agg({
col: lambda x: list(x) for col in removed_items # Ensure sublists for dynamic value columns
})
#Create individual dataframes for each facet and store them structured in applicability, requirements, general, and specification
if cols_app_entity:
applicability_data.append(df_final[cols_app_entity])
if cols_app_property:
applicability_data.append(df_final[cols_app_property])
if cols_app_material:
applicability_data.append(df_final[cols_app_material])
if cols_app_attribute:
applicability_data.append(df_final[cols_app_attribute])
if cols_app_classification:
applicability_data.append(df_final[cols_app_classification])
if cols_app_partOf:
applicability_data.append(df_final[cols_app_partOf])
if cols_req_entity:
requirements_data.append(df_final[cols_req_entity])
if cols_req_property:
requirements_data.append(df_final[cols_req_property])
if cols_req_material:
requirements_data.append(df_final[cols_req_material])
if cols_req_attribute:
requirements_data.append(df_final[cols_req_attribute])
if cols_req_classification:
requirements_data.append(df_final[cols_req_classification])
if cols_req_partOf:
requirements_data.append(df_final[cols_req_partOf])
if cols_general:
generaldata.append(df_final[cols_general])
if cols_specification:
specification_data.append(df_final[cols_specification])
###Transform each dataframe row into a dictionary
specs_list = []
for i in range(df_final.index.size):
#General data
generaldata_dict = {}
if generaldata:
row_generaldata = generaldata[0].iloc[i]
generaldata_dict = pandas_row_to_dict(row_generaldata)
#Specification data
specification_data_dict = {}
if specification_data:
row_specification_data = specification_data[0].iloc[i]
specification_data_dict = pandas_row_to_dict(row_specification_data)
if STRING_SPECIFICATIONCARDINALITY in specification_data_dict:
if specification_data_dict[STRING_SPECIFICATIONCARDINALITY][0].lower() not in ['required', 'prohibited']:
specification_data_dict.pop(STRING_SPECIFICATIONCARDINALITY)
if STRING_SPECIFICATIONIFCVERSION in specification_data_dict:
for ifc_version in specification_data_dict[STRING_SPECIFICATIONIFCVERSION]:
if ifc_version.upper() not in ['IFC2X3','IFC4','IFC4X3_ADD2']:
raise Exception('Invalid IFC version used: ' + ifc_version)
#Applicability data
app_list = []
for applicability_facet_df in applicability_data:
row_app = applicability_facet_df.iloc[i]
dict_list_app = pandas_row_to_dict_list(row_app)
for dict_item in dict_list_app:
app_dict = split_OR_AND_values(dict_item, is_entity_based_app)
#Rearrange 'AND' values of the applicability into individual facet dictionaries
app_list.extend(split_AND_values_to_individual_facet_dicts(app_dict))
#Generate possible combinations for all or_values in the applicability and create individual specifications for each.
#This is necessary for assigning generally applicable values to more specific specifications.
#Only if specification cardinality is not "required", because in this case the logic would be lost when seperated
if STRING_SPECIFICATIONCARDINALITY in specification_data_dict and specification_data_dict[STRING_SPECIFICATIONCARDINALITY][0].lower() == 'required':
app_lists = [app_list]
else:
app_lists = generate_combinations(app_list) #split apps
for app_list in app_lists:
#Check if the applicability was splitted in multiple applicabilities due to or_values
app_splitted = True if len(app_lists) > 1 else False
for app_dict in app_list:
#Separate Entity.PredefinedType into two entries
separate_dict_value(app_dict, STRING_ENTITY, STRING_PREDEFINEDTYPE,'.')
separate_dict_value(app_dict, STRING_PARTOFENTITY, STRING_PARTOFPREDEFINEDTYPE,'.')
#Check if a specification with this general data and applicability already exists
req_list = []
diff, diff_generaldata, diff_specification_data, req_list, prev_spec = compare_previous_generaldata_and_applicability(specs_list, generaldata_dict, specification_data_dict, app_list, app_splitted, separate_by)
#Requirements data
for requirement_facet_df in requirements_data:
row_req = requirement_facet_df.iloc[i]
dict_list_req = pandas_row_to_dict_list(row_req)
for dict_item in dict_list_req:
req_dict = split_OR_AND_values(dict_item, is_entity_based_app)
#check if dict only includes the cardinality (invalid); if so, take empty dict.
#Can occur since requirement cardinality column is applied to several facets from which some might be empty in current row
if len(req_dict.keys()) == 1 and STRING_REQUIREMENTCARDINALITY in req_dict:
req_dict = {}
#Rearrange 'AND' values of the requirements into individual facet dictionaries
req_dict_list_arranged = split_AND_values_to_individual_facet_dicts(req_dict)
for req_dict_arranged in req_dict_list_arranged:
#Check whether complex restrictions are included in 'OR' values (this is not possible in IDS4ALL)
for key in req_dict_arranged:
if len(req_dict_arranged[key]) > 1:
j = 0
while j < len(req_dict_arranged[key]):
value = req_dict_arranged[key][j]
if is_complex_restriction(value):
raise Exception('Complex restrictions (pattern=; \\<=; \\<; \\>=; \\>; length=; length<=; length>=) cannot be part of an enumeration (list of "or"-values)')
else: j += 1
#Extract descriptions for properties
#Necessary to add property description to each occurance in the IDS even if it is only one in the Excel
#Not necessary for entity descriptions
if STRING_DESCRIPTION in req_dict_arranged:
if STRING_PROPERTY in req_dict_arranged:
property_description_dict[req_dict_arranged[STRING_PROPERTY][0]] = req_dict_arranged[STRING_DESCRIPTION][0]
#Separate Entity.PredefinedType into two entries
separate_dict_value(req_dict_arranged, STRING_ENTITY, STRING_PREDEFINEDTYPE,'.')
separate_dict_value(req_dict_arranged, STRING_PARTOFENTITY, STRING_PARTOFPREDEFINEDTYPE,'.')
#If the same requirement (except for the values) already exists, merge the values.
#Otherwise, add new requirement
diff_req = True
for j in range(len(req_list)):
diff_req = compare_and_merge_requirement_dicts(req_list[j], req_dict_arranged, [STRING_ATTRIBUTEVALUE,STRING_PROPERTYVALUE], False)
if not diff_req: break
if diff_req: req_list.append(req_dict_arranged)
#if it is a new spec (with different applicability, generaldata, or specificationdata), create a new specification
if diff or diff_generaldata or diff_specification_data:
if app_list or req_list or generaldata_dict:
spec_dict = {}
spec_dict['app'] = app_list
spec_dict['req'] = req_list
spec_dict['general'] = copy.deepcopy(generaldata_dict)
spec_dict['spec'] = copy.deepcopy(specification_data_dict)
spec_dict['app_splitted'] = app_splitted
specs_list.append(spec_dict)
#split and combine specifications according to the different separation categories
#pecifications that belong to several seperation categories are split into indivudual specifications
#if the individual specifications have the same separation category, they are merged again.
#by this specifications with the same applicability are merged in the different separation categories
#only necessary if spearation categories (speparate_by) are used
if separate_by:
specs_list_sep = []
for spec in specs_list:
#generate all combinations of the separation categories for this spec
separation_combinations = [dict(zip(separate_by, values)) for values in product(*(spec['general'][k] if k in spec['general'] else [''] for k in separate_by))]
sep_comb_cleaned = [{k: v for k, v in d.items() if k not in ['MissingKey']} for d in separation_combinations]
#for each combination, check if previous specifications with the same combination have the same applicability, and spec data
#if so, merge them
#if not, add the specification to the new list and set its seperation string
for sep_comb in sep_comb_cleaned:
sep_string = "_".join(v for v in sep_comb.values() if v)
comparison_specs_list = [spec1 for spec1 in specs_list_sep if spec1['sep_string'] == sep_string]
diff, diff_generaldata, diff_specification_data, req_list, prev_spec = compare_previous_generaldata_and_applicability(comparison_specs_list, spec['general'], spec['spec'], spec['app'], spec['app_splitted'], [])
if not diff:
#If the same requirement (except for the values) already exists, merge the values.
#Otherwise, add new requirement
diff_req = True
for req_current_spec in spec['req']:
for req_prev_spec in req_list:
diff_req = compare_and_merge_requirement_dicts(req_prev_spec, req_current_spec, [STRING_ATTRIBUTEVALUE,STRING_PROPERTYVALUE], False)
if not diff_req: break
if diff_req: req_list.append(req_current_spec)
new_spec = prev_spec
else:
new_spec = copy.deepcopy(spec)
new_spec['sep_string'] = sep_string
specs_list_sep.append(new_spec)
for general_key in ['Phase','Role','Usecase']:
if general_key in new_spec['general']:
if general_key in separate_by:
if sep_comb[general_key] != '':
if new_spec != None:
#the general information of the spec must be updated.
#if the old spec had several separation combinations,
#for each a new sepc was created and each should only have one of seperation combination
new_spec['general'][general_key] = [sep_comb[general_key]]
specs_list = specs_list_sep
else:
#if no seperation is used, add the sep_string to all specs as it is needed later for the IDS metadata
for spec in specs_list:
spec['sep_string'] = ''
#organise the specifications according to the ifc versions
#if one specification refers to a subset of ifc versions of another specification with the same applicability and general data,
#the subset of ifc versions is extracted from the more general specification and the requirements are included into the specific specification.
#this allows to merge specifications with the same applicability, general data, and ifc versions
if spec_ifc_version_col_used:
for i in range(len(specs_list)):
specI = specs_list[i]
specI_Ifc_versions = specI['spec'][STRING_SPECIFICATIONIFCVERSION]
if not specI_Ifc_versions: continue
for j in range(i+1,len(specs_list)):
specI_Ifc_versions = specI['spec'][STRING_SPECIFICATIONIFCVERSION]
if not specI_Ifc_versions: break
specJ = specs_list[j]
specJ_Ifc_versions = specJ['spec'][STRING_SPECIFICATIONIFCVERSION]
if not specJ_Ifc_versions: continue
#if all ifc versions of specI are in specJ, specI_Ifc_versions is a subset of specJ_Ifc_versions
#Then all requirements of specJ also apply to specI.
if (all(elem in specJ_Ifc_versions for elem in specI_Ifc_versions)):
structure_specifications_by_Ifc_versions(specI, specJ, specI_Ifc_versions, specJ_Ifc_versions, separate_by)
#if all ifc versions of specJ are in specI, specJ_Ifc_versions is a subset of specI_Ifc_versions
#Then all requirements of specI also apply to specJ.
elif (all(elem in specI_Ifc_versions for elem in specJ_Ifc_versions)):
structure_specifications_by_Ifc_versions(specJ, specI, specJ_Ifc_versions, specI_Ifc_versions, separate_by)
#remove specifications with empty ifc versions (might be created during the re-structuring)
specs_list = [spec for spec in specs_list if spec['spec'][STRING_SPECIFICATIONIFCVERSION]]
#add requirements of specific specifications to more general specifications
add_values_to_general_specs(specs_list,separate_by)
return specs_list
def load_columns(EXCEL_PATH, sheet_name, skipped_rows, columns_to_import, all_columns, dataframe_list):
'''Loads the columns of columns_to_import that are present in the excel sheet and parses them into a pandas dataframe.
The available columns in the excel sheet are given by the all_columns list
:param EXCEL_PATH: Path to an excel file
:type EXCEL_PATH: str
:param sheet_name: Name of the relevant sheet
:type sheet_name: str
:param skipped_rows: Number of skipped rows at the top of the sheet
:type skipped_rows: int
:param columns_to_import: List of column names to import.
:type columns_to_import: List
:param all_columns: List of all column names in the sheet.
:type all_columns: List
:param all_columns: List containing imported data frames
:type all_columns: List
:return: Pandas dataframe containing the data of the imported columns
:rtype: Pandas dataframe
'''
available_columns = [col for col in columns_to_import if col in all_columns]
if available_columns:
dataframe_list.append(pd.read_excel(EXCEL_PATH, sheet_name=sheet_name, skiprows=skipped_rows, usecols=available_columns))
def pandas_row_to_dict(current_row):
'''Stores values of the current row of a pandas dataframe into a dictionary using the column names as keys.
Uses '\\&' and '|' as delimiter but does not differentiate in their meaning. Stores the values in a list and assigns it to a key in the dictionary.
(key = [value, value, value])
:param current_row: Pandas row
:type current_row: Pandas row
:return: Dictionary using the column names as keys and the row values as values (key = [value, value, value])
:rtype: dict
'''
new_dict = {}
for column_name, column_data in current_row.items():
#Cut characters after the first '.'. If duplicate column names exist, pandas numbers them with name.1, name.2 ...
column_name = column_name.split('.')[0]
if isinstance(column_data,list):
column_data = '|'.join(column_data)
#Only use strings and numbers not null
#if isinstance(column_data,str): #or not np.isnan(column_data):
if column_data != KEYWORD_NONE and column_data != KEYWORD_MISSING:
column_data = column_data.strip()
#Use delimiters to distinguish between different values (for general data, the delimiters have the same meaning)
or_values = column_data.replace('\\&','|').split('|')
#Omit KEYWORD_MISSING values
or_values_cleaned = []
values_list = []
for or_value in or_values:
if or_value != KEYWORD_MISSING and or_value not in or_values_cleaned:
or_values_cleaned.append(or_value)
if or_values_cleaned:
values_list.extend(or_values_cleaned)
#If or values except KEYWORD_MISSING exist, add them to the dictionary
if values_list:
new_dict[column_name] = values_list
return new_dict
def pandas_row_to_dict_list(current_row):
'''Creates individual dictionaries for each facet in the current row of the given pandas dataframe using the column names as keys.
Then removes dictionaries if they are subsets of another dictionary.
:param current_row: Pandas row
:type current_row: Pandas row
:return: List of individual dictionaries for each facet
:rtype: list
'''
list_columns = current_row.index # Get all column names
list_values = [current_row[col] if isinstance(current_row[col], list) else [current_row[col]] for col in list_columns] # Ensure lists
num_items = max(len(values) for values in list_values) # Determine max length of lists
# Create dictionaries while handling non-list values and skipping KEYWORD_MISSING
dict_list = [
{
col: values[i] if i < len(values) else values[0] # Use existing value or repeat if shorter
for col, values in zip(list_columns, list_values)
if values[i] != KEYWORD_MISSING # Skip KEYWORD_MISSING values
}
for i in range(num_items)
]
#Remove dictionaries from the list if they are subsets of another dictionary.
filtered_list = []
for d1 in dict_list:
if not any(d1.items() <= d2.items() and d1 != d2 for d2 in dict_list):
filtered_list.append(d1)
return filtered_list
def split_OR_AND_values(input_dict, is_entity_based_app):
'''Transforms the values of the dictionary from strings to list of strings using delimiters.
The delimiter '\\&' is used for 'AND' values and '|' is used for 'OR' values.
The lists of 'OR' values are nested in lists of 'AND' values and then assigned to their original key in a new dictionary.
'AND' values contain lists of 'OR' values:
key: [[OR_value, OR_value],[OR_value, OR_value, OR_value],[OR_value]]
:param input_dict: Dictionary containing values as strings
:type input_dict: dict
:param is_entity_based_app: Boolean specifying if the applicability should be generated only entity-based (with predefiend types)
:type is_entity_based_app: boolean
:return: Dictionary containing nested lists for 'AND' and 'OR' values (key: [[OR_value, OR_value],[OR_value, OR_value, OR_value],[OR_value]])
:rtype: dict
'''
new_dict = {}
number_of_and_values = 0
if input_dict:
#Rearrange 'AND' values into individual facet dictionaries
keylist = input_dict.keys()
for key in keylist:
value = input_dict[key]
key = key.split('.')[1]
values_list = []
if value != KEYWORD_NONE and value != KEYWORD_MISSING:
value = str(value).strip()
#Change relevant values to uppercase
if key in [STRING_PROPERTYDATATYPE,STRING_PARTOFRELATION]:
value = value.upper()
#Use delimiters to distinguish between different 'AND' values
and_values = value.split('\\&')
#Check whether all entries of this facet have the same number of 'AND' values
if number_of_and_values == 0:
number_of_and_values = len(and_values)
if len(and_values) != number_of_and_values:
#Special case 'requirement cardinality'. Since the requirement cardinality is included automatically if not in the excel file,
#the cardinality has no 'and' values. If the other existing columns use 'and' values, then the one cardinality value is applied to all 'and' values.
if key == STRING_REQUIREMENTCARDINALITY and len(and_values) == 1:
and_values = and_values*number_of_and_values
else:
raise Exception('Number of AND values (seperated by \\&) is invalid for ' + key + ' ' + value + '. All columns of one IDS facet require the same number of AND values. Required number of AND values: ' + str(number_of_and_values))
values_list = []
for and_value in and_values:
#if the value is a pattern, do not split the value using the | delimiter, because | already has a meaning in the pattern
if and_value[:8] == 'pattern=':
or_values = [and_value]
else:
#Use delimiters to distinguish between different 'OR' values
or_values = None if and_value == '' else and_value.split('|')
if or_values == None: continue
#Check if complex restrictions were merged due to entity-based applicability. If so, delete them
if is_entity_based_app and len(or_values) > 1:
or_values = list(filter(lambda or_value: not is_complex_restriction(or_value), or_values))
#Omit KEYWORD_MISSING values
or_values_cleaned = []
for or_value in or_values:
if or_value != KEYWORD_MISSING:
or_values_cleaned.append(or_value)
if or_values_cleaned:
values_list.append(or_values_cleaned)
#If values except KEYWORD_MISSING exist, add them to the dictionary
if values_list:
new_dict[key] = values_list
return new_dict
def split_AND_values_to_individual_facet_dicts(input_dict):
'''Creates and individual dictionary for each 'AND' value in the dictionary.
The input dict contains lists of 'AND' values for each key. These are split in individual dictionaries including only one value for each key.
:param input_dict: Dictionary containing lists of 'AND' values for each key
:type input_dict: dict
:return: List of individual dictionaries for each 'AND' value (each facet)
:rtype: list
'''
facet_list = []
if input_dict:
#Rearrange 'AND' values into individual facet dictionaries
keylist = input_dict.keys()
for j in range(len(input_dict[list(keylist)[0]])):
facet_dict = {}
for key in keylist:
facet_dict[key] = input_dict[key][j]
facet_list.append(facet_dict)
return facet_list
def separate_dict_value(current_dict, combined_key, second_key, delimiter):
'''Separates the value of 'combined_key' in the current dict into two values using a delimiter.
The part before the delimiter is stored as value for the 'combined_key' and the part after the delimiter as value for the second_key.
:param current_dict: Used dictionary
:type current_dict: dict
:param combined_key: initial key for the combined value (and key for the first part of the combined value)
:type combined_key: str
:param second_key: new key for the second part of the combined value
:type second_key: str
:param delimiter: delimiter for the separation
:type delimiter: str
:return: None
'''
if combined_key in current_dict:
or_values = current_dict[combined_key]
or_values_first_key = []
or_values_second_key = []
for or_value in or_values:
separate_values = or_value.split(delimiter)
if combined_key == STRING_ENTITY or combined_key == STRING_PARTOFENTITY: separate_values[0] = separate_values[0].upper()
or_values_first_key.append(separate_values[0])
#Only append value to second key if it exists
if len(separate_values) == 2:
or_values_second_key.append(separate_values[1])
if len(set(or_values_first_key)) > 1 and len(or_values_second_key) > 0:
raise Exception("PredefinedTypes cannot be used in an enumeration in a specification applicability with cardinality \"required\" because separating it into two different facet parameters causes the connection between the PredefinedType and the Entity to be lost. Affected Entity Facet: Entities: " + str(or_values_first_key) + " PredefinedTypes: " + str(or_values_second_key))
current_dict[combined_key] = or_values_first_key
if or_values_second_key: current_dict[second_key] = or_values_second_key
def generate_combinations(data):
'''Generate all possible combinations of dictionaries with list values.
This function takes a list of dictionaries, where each dictionary may
contain keys associated with lists of values. It produces all possible
combinations such that for each dictionary, if a key has multiple values
in its list, a separate dictionary is created for each value. The function
ensures that the structure of the input dictionaries is preserved, and
each resulting combination includes one value per list.
:param data: List of dictionaries, where each key maps to a list of values.
:type current_dict: list
:return: List of lists, where each inner list represents a unique combination of dictionaries, preserving the input structure but with one value per list.
:rtype: list
'''
# Collect all possible values for each dictionary in `data`
options = []
for item in data:
item_variants = []
# Create single-value dictionaries for each key in `item`
for key, values in item.items():
# If multiple values, create a separate dictionary for each value
if values is not None and len(values) > 1:
item_variants.append([{key: [value]} for value in values])
else:
# If only one value, use it as-is
item_variants.append([{key: values}])
# Combine the single-value dictionaries for each key in `item`
options.append([dict(sum((list(d.items()) for d in combo), [])) for combo in itertools.product(*item_variants)])
# Cartesian product of all item combinations
combined_variants = [list(variant) for variant in itertools.product(*options)]
return combined_variants
def compare_previous_generaldata_and_applicability(specs_list, generaldata_dict, specification_data_dict, app_list, app_splitted, separate_by):
'''Checks if a specification with the same applicability and general data already exists.
For the general data check only the general data speficied by separate_by is relevant.
Other general data can be merged anyways.
If a specification with the same applicability and general data already exists, the requirements of the new and previous are merged.
:param specs_list: List of all previous specifications
:type specs_list: list
:param generaldata_dict: Dictionary containing lists of 'AND' values for each generaldata key
:type generaldata_dict: dict
:param specification_data_dict: Dictionary containing lists of 'AND' values for each specificationdata key
:type specification_data_dict: dict
:param app_list: List containing dictionaries for each facet in the applicability
:type app_list: list
:param app_spliited: Boolean value that defines whether this app_list was extracted from an applicability with or_values
:type app_list: bool
:param separate_by: List of general data for which specifications must be seperated
:type separate_by: list
:return: Boolean 'diff' specifying whether the applicability is different
:rtype: bool
:return: Boolean 'diff_generaldata' specifying whether the generaldata is different
:rtype: list
:return: List containing the requriements
:rtype: list
'''
req_list = []
diff = True
diff_generaldata = True
diff_specification_data = True
prev_spec = None
for j in range(len(specs_list)-1,-1,-1):
prev_spec = specs_list[j]
#check specification data
spec_included_paths = [STRING_SPECIFICATIONCARDINALITY,STRING_SPECIFICATIONIFCVERSION]
#if the current spec and the previous spec do not have splitted apps, consider also the spec name in the comparison
#if specs with splitted apps are included, the name must not be considered, otherwise the general values are not merged if the general spec has another name
if not prev_spec['app_splitted'] and not app_splitted:
spec_included_paths.append(STRING_SPECIFICATIONNAME)
diff_specification_data = DeepDiff(specification_data_dict, prev_spec['spec'], include_paths=spec_included_paths)
if not diff_specification_data:
#check the general data specified by seperate_by, if it is not empty.
if separate_by:
diff_generaldata = DeepDiff(generaldata_dict, prev_spec['general'], include_paths=separate_by)
else: diff_generaldata = False
if not diff_generaldata:
#check the applicability
diff = app_list != prev_spec['app']
if not diff:
diff = DeepDiff(app_list, prev_spec['app'])
if not diff:
#merge the requirements
req_list = prev_spec['req']
#merge the general data
prev_spec['general'] = merger.merge(prev_spec['general'], generaldata_dict)
#if both specs have a name, use the name of the spec that was not splitted (if new spec was not splitted, update the name of prev_spec)
#specifications with splitted applicabilities are more general, therefore we use the name of the specification without splitted applicability
if STRING_SPECIFICATIONNAME in prev_spec['spec'] and STRING_SPECIFICATIONNAME in specification_data_dict and not app_splitted:
prev_spec['spec'][STRING_SPECIFICATIONNAME][0] = specification_data_dict[STRING_SPECIFICATIONNAME][0]
break
return diff, diff_generaldata, diff_specification_data, req_list, prev_spec
def structure_specifications_by_Ifc_versions(spec1, spec2, spec1_Ifc_versions, spec2_Ifc_versions, separate_by):
'''Includes all requirements of spec2 in spec1 and deletes the ifc versions of spec1 from spec2,
if the two specifications have equal applicability, general data and specification cardinality.
If all ifc versions of spec1 are in spec2, spec1_Ifc_versions is a subset of spec2_Ifc_versions. Then all requirements of spec2 also apply to spec1.
After the requirements are included in spec1, the ifc versions of spec1 are removed spec2.
By this, the two specifications with overlapping ifc-versions are seperated to have one specification for each unique ifc version combination
:param spec1: Specification with more specific ifc version definition
:type spec1: dict
:param spec2: Specification with more general ifc version definition
:type spec2: dict
:param spec1_Ifc_versions: List of all ifc versions spec1 applies to
:type spec1_Ifc_versions: list
:param spec2_Ifc_versions: List of all ifc versions spec2 applies to
:type spec2_Ifc_versions: list
:param separate_by: List of general data for which specifications must be seperated
:type separate_by: list
'''
#check specification data
spec_included_paths = [STRING_SPECIFICATIONCARDINALITY]
#if the spec1 and spec2 do not have splitted apps, consider also the spec name in the comparison
#if specs with splitted apps are included, the name must not be considered, otherwise the general values are not merged if the general spec has another name
if not spec1['app_splitted'] and not spec2['app_splitted']:
spec_included_paths.append(STRING_SPECIFICATIONNAME)
diff_specification_data = DeepDiff(spec1['spec'], spec2['spec'], include_paths=spec_included_paths)
if not diff_specification_data:
#check the general data specified by seperate_by, if it is not empty.
if separate_by:
diff_generaldata = DeepDiff(spec1['general'], spec2['general'], include_paths=separate_by)
else: diff_generaldata = False
if not diff_generaldata:
#check the applicability
diff = spec1['app'] != spec2['app']
if not diff:
diff = DeepDiff(spec1['app'], spec2['app'])
if not diff:
spec1['general'] = merger.merge(spec1['general'], spec2['general'])
for req_dict2 in spec2['req']:
found = False
for req_dict1 in spec1['req']:
found = not compare_and_merge_requirement_dicts(req_dict1, req_dict2, [STRING_ENTITY,STRING_PREDEFINEDTYPE,STRING_ATTRIBUTEVALUE,STRING_PROPERTYVALUE], False, True)
if found == True:
#if both specs have a name, use the name of the spec that was not splitted (if spec2 was not splitted, update the name of spec1)
#specifications with splitted applicabilities are more general, therefore we use the name of the specification without splitted applicability
if STRING_SPECIFICATIONNAME in spec1['spec'] and STRING_SPECIFICATIONNAME in spec2['spec'] and not spec2['app_splitted']:
spec1['spec'][STRING_SPECIFICATIONNAME][0] = spec2['spec'][STRING_SPECIFICATIONNAME][0]
break
if not found:
# Shallow copy to avoid mutating original item2 later
spec1['req'].append({k: v[:] if isinstance(v, list) else v for k, v in req_dict2.items()})
#Since all requirements of spec2 are included in spec1, spec2 does not need to apply to the ifc versions of spec1 anymore
spec2['spec'][STRING_SPECIFICATIONIFCVERSION] = list(set(spec2_Ifc_versions) - set(spec1_Ifc_versions))
def add_values_to_general_specs(specs_list, separate_by):
'''Checks for all specification if a specification with a more general applicability exists.
If so, the requirements of the more specific specification are added to the more general specification.
:param specs_list: List of all specifications
:type specs_list: list
:param separate_by: List of general data for which specifications must be seperated
:type separate_by: list
'''
for i in range(len(specs_list)):
for j in range(i+1,len(specs_list)):
specI = specs_list[i]
specJ = specs_list[j]
#check if specification meta data of both specs is equal
diff_specification_data = False
specdataI = specI['spec']
specdataJ = specJ['spec']
diff_specification_data = DeepDiff(specdataI, specdataJ, ignore_order=True, include_paths=[STRING_SPECIFICATIONCARDINALITY,STRING_SPECIFICATIONIFCVERSION])
#check if general data of both specs is equal
if not diff_specification_data:
diff_generaldata = False
if separate_by:
generaldataI = specI['general']
generaldataJ = specJ['general']
diff_generaldata = DeepDiff(generaldataI, generaldataJ, ignore_order=True, include_paths=separate_by)
#check if spec i has a more general applicability than j.
if not diff_generaldata:
appI = specI['app']
appJ = specJ['app']
if len(appJ) > 0 and len(appI) > 0:
if STRING_ENTITY in appI[0] and STRING_ENTITY in appJ[0]:
if appI[0][STRING_ENTITY] != appJ[0][STRING_ENTITY]: continue
if STRING_PREDEFINEDTYPE in appI[0] and STRING_PREDEFINEDTYPE in appJ[0]:
if appI[0][STRING_PREDEFINEDTYPE] != appJ[0][STRING_PREDEFINEDTYPE]: continue
if len(appJ) > 1 and len(appI) > 1:
if STRING_ATTRIBUTEVALUE in appI[1] and STRING_ATTRIBUTEVALUE in appJ[1]:
if appI[1][STRING_ATTRIBUTEVALUE] != appJ[1][STRING_ATTRIBUTEVALUE]: continue
diff_app = DeepDiff(appI, appJ, ignore_order=True)
removed_dict_items_appJ = diff_app.get('dictionary_item_removed',[])
removed_iterable_items_appJ = diff_app.get('iterable_item_removed',[])
added_dict_items_appJ = diff_app.get('dictionary_item_added',[])
added_iterable_items_appJ = diff_app.get('iterable_item_added',[])
values_changed_appJ = diff_app.get('values_changed',[])
#if general data is equal and app more general in appI, compare/merge the requirements of appJ in appI
if not removed_dict_items_appJ and not removed_iterable_items_appJ and not values_changed_appJ:
#compare the requirements
reqI = specI['req']
reqJ = specJ['req']
for k in range(len(reqI)):
for l in range(len(reqJ)):
diff_req = compare_and_merge_requirement_dicts(reqI[k], reqJ[l], [STRING_ATTRIBUTEVALUE,STRING_PROPERTYVALUE], True)
#if general data is equal and app more general in appJ, compare/merge the requirements of appI in appJ
if not added_dict_items_appJ and not added_iterable_items_appJ and not values_changed_appJ:
#compare the requirements
reqI = specI['req']
reqJ = specJ['req']
for k in range(len(reqI)):
for l in range(len(reqJ)):
diff_req = compare_and_merge_requirement_dicts(reqJ[l], reqI[k], [STRING_ATTRIBUTEVALUE,STRING_PROPERTYVALUE], True)
def compare_and_merge_requirement_dicts(req_dict1, req_dict2, not_compared_keys, merge_only_values, reverse=False):
'''Checks whether the req_dict1 is a subset of req_dict2.
This means, the old requirement must be a subset of the new requirement. The keys of req_dict1 must exist in req_dict2 and the values must be equal.
An exception are lists for property values or attribute values. These are not compared. Here the values can be different because they are merged together, unless one contains a complex restriction. Complex restrictions cannot be merged and thus always indicate a difference.
If req_dict1 is a subset of req_dict2, the information of req_dict2 is merged into req_dict1.
:param req_dict1: dictionary 1 (is altered if dictionaries are subsets)
:type req_dict1: dict
:param req_dict2: dictionary 2 (is not altered)
:type req_dict2: dict
:param merge_only_values: Boolean defining that dicts should only be merged if all keys except Description exist in both dicts
:type merge_only_values: boolean
:param reverse: Boolean defining if the subset logic should be reversed. If true, req_dict2 must be a subset of req_dict1, but still req_dict2 is merged into req_dict1
:type reverse: boolean
:return: boolean specifying whether the dicts were different or not
:rtype: boolean
'''
diff = False
#boolean specifying whether the same type of facet is compared (e.g. property facet with property facet)
same_facet = False
#If property or attribute values are included and are a complex restriction, the requirements must not be merged
#Complex restrictions cannot be in an enumeration
for key in not_compared_keys:
diff = True if key in req_dict1 and req_dict1[key] and is_complex_restriction(req_dict1[key][0]) else diff
diff = True if key in req_dict2 and req_dict2[key] and is_complex_restriction(req_dict2[key][0]) else diff
#Define which dict must be a subset of the other according to the reverse parameter
if not reverse:
looped_dict = req_dict1
reference_dict = req_dict2
else:
looped_dict = req_dict2
reference_dict = req_dict1
for key in looped_dict:
if key == STRING_DESCRIPTION: continue
#if only values should be merged, all keys except STRING_DESCRIPTION must also occur in both dicts
if merge_only_values:
keys1 = set(looped_dict.keys()) - set([STRING_DESCRIPTION])
keys2 = set(reference_dict.keys()) - set([STRING_DESCRIPTION])
if keys1 != keys2:
diff = True
break
#all keys in req_dict1 must be equal in req_dict2
if key in reference_dict:
same_facet = True
#lists for property values and attribute values are not compared
if key in not_compared_keys:
continue
elif req_dict1[key] != req_dict2[key]: diff = True
else: diff = True
if not diff and same_facet:
req_dict1 = merger.merge(req_dict1, req_dict2)
return False
return True
def is_complex_restriction(value):
'''Checks whether the value is a complex restriction (starts with the key character of a complex restriction)
:param value: value to be checked
:type value: string
:return: boolean specifying the or_values list contains a complex restriction
:rtype: boolean
'''
if value[:8] == 'pattern=' or value[:2] == '\\<' or value[:2] == '\\>' or value[:7] == 'length=' or value[:8] == 'length<=' or value[:8] == 'length>=':
return True
return False
def create_ids_specifications(ids_file, spec_list):
'''Creates a new IDS specification for each entry in the spec_list and appends it to the ids_file.
:param ids_file: the used ids_file
:type ids_file: object ids
:param spec_list: list containing all specifications as dictionaries
:type spec_list: list
:param ifc_version: list containing the ifc versions used for all specifications
:type ifc_version: list
:return: None
'''
i = 1
for spec_data in spec_list:
app_data = spec_data['app']
string_instructions = ''
if 'Phase' in spec_data['general']: string_instructions += 'Phase: ' + ', '.join(spec_data['general']['Phase']) + '; '
if 'Role' in spec_data['general']: string_instructions += 'Role: ' + ', '.join(spec_data['general']['Role']) + '; '
if 'Usecase' in spec_data['general']: string_instructions += 'Usecase: ' + ', '.join(spec_data['general']['Usecase']) + '; '
string_instructions = string_instructions[0:len(string_instructions)-2]
#Define specification name
if STRING_SPECIFICATIONNAME in spec_data['spec']: spec_title = spec_data['spec'][STRING_SPECIFICATIONNAME][0]
else:
spec_title = 'Specification ' + str(i)
if len(app_data) > 0:
if STRING_ENTITY in app_data[0]:
spec_title += ':'
for j in range(len(app_data[0][STRING_ENTITY])):
spec_title += ' ' + app_data[0][STRING_ENTITY][j]
if STRING_PREDEFINEDTYPE in app_data[0]:
spec_title += '.' + app_data[0][STRING_PREDEFINEDTYPE][j]
#Define specification cardinality
spec_minOccurs=0
spec_maxOccurs="unbounded"
if STRING_SPECIFICATIONCARDINALITY in spec_data['spec']:
if spec_data['spec'][STRING_SPECIFICATIONCARDINALITY][0].lower() == 'required':
spec_minOccurs = 1
if spec_data['spec'][STRING_SPECIFICATIONCARDINALITY][0].lower() == 'prohibited':
spec_maxOccurs = 0
#Define specification IFC version
ifc_version = spec_data['spec'][STRING_SPECIFICATIONIFCVERSION]
#Create specification
ids_spec = ids.Specification(name=spec_title, ifcVersion=ifc_version, minOccurs=spec_minOccurs, maxOccurs=spec_maxOccurs, instructions=string_instructions if string_instructions != '' else None)
#Append applicability and requirements
append_facets(ids_spec.applicability, app_data)
append_facets(ids_spec.requirements, spec_data['req'])
#Append specification to ids file
ids_file.specifications.append(ids_spec)
i += 1
def append_facets(facets, input_data):
'''Creates a new IDS facet for each entry in the input_data list and and stores it in a list (facets).
It also converts enumerations, patterns, bounds, and lengths into ids resctrictions.
:param facets: list to store the facets
:type facets: list
:param input_data: list containing all facet data as dictionaries
:type input_data: list
:return: None
'''
#prepare the input data for using the ifcopenshell functions
new_input_data = []
for input_dict in input_data:
new_input_dict = {}
for key in input_dict.keys():
or_values = input_dict[key]
if isinstance(or_values, list):
or_values = list(dict.fromkeys(or_values))
restriction_base = 'string'
#Determine the restriction base for complex restrictions depending on the datatype
if key == STRING_PROPERTYVALUE and STRING_PROPERTYDATATYPE in input_dict:
restriction_base = datatype_base_dict[input_dict[STRING_PROPERTYDATATYPE][0]]
#Complex restriction: enumeration
if len(or_values) > 1:
list_value = or_values[0]
if isinstance(list_value, str):
list_as_restriction = ids.Restriction(options={'enumeration': or_values}, base=restriction_base)
new_input_dict[key] = list_as_restriction
elif len(or_values) == 1:
new_value = str(or_values[0])
#Boolean handling
if new_value.lower() == 'true' or new_value.lower() == 'false':
new_value = new_value.lower()
#Complex restriction: pattern
if new_value[:8] == 'pattern=':
new_value = ids.Restriction(options={'pattern': new_value[8:]}, base=restriction_base)
#Complex restriction: bounds
elif new_value[:3] == '\\<=':
if restriction_base == 'string': restriction_base = 'double'
new_value = ids.Restriction(options={'maxInclusive': float(new_value[3:].replace(',','.'))}, base=restriction_base)
elif new_value[:2] == '\\<':
if restriction_base == 'string': restriction_base = 'double'
new_value = ids.Restriction(options={'maxExclusive': float(new_value[2:].replace(',','.'))}, base=restriction_base)
elif new_value[:3] == '\\>=':
if restriction_base == 'string': restriction_base = 'double'
new_value = ids.Restriction(options={'minInclusive': float(new_value[3:].replace(',','.'))}, base=restriction_base)
elif new_value[:2] == '\\>':
if restriction_base == 'string': restriction_base = 'double'
new_value = ids.Restriction(options={'minExclusive': float(new_value[2:].replace(',','.'))}, base=restriction_base)
#Complex restriction: length
elif new_value[:7] == 'length=':
new_value = ids.Restriction(options={'length': int(float(new_value[7:].replace(',','.')))}, base=restriction_base)
elif new_value[:8] == 'length>=':
new_value = ids.Restriction(options={'minLength': int(float(new_value[8:].replace(',','.')))}, base=restriction_base)
elif new_value[:8] == 'length<=':
new_value = ids.Restriction(options={'maxLength': int(float(new_value[8:].replace(',','.')))}, base=restriction_base)
new_input_dict[key] = new_value
else:
#If or_values is None
new_input_dict[key] = or_values
new_input_data.append(new_input_dict)
#Create an ids facet for each prepared input dictionary
for input_dict in new_input_data:
facet = None
incomplete_facet = None
#Entity facet
if STRING_ENTITY in input_dict or STRING_PREDEFINEDTYPE in input_dict:
if STRING_ENTITY not in input_dict: incomplete_facet = 'The Entity facet requires the Entity parameter.'
else:
key = str(input_dict[STRING_ENTITY])
if STRING_PREDEFINEDTYPE in input_dict: key += '.' + str(input_dict[STRING_PREDEFINEDTYPE])
facet = ids.Entity(name=input_dict[STRING_ENTITY] if STRING_ENTITY in input_dict else None,
predefinedType=input_dict[STRING_PREDEFINEDTYPE] if STRING_PREDEFINEDTYPE in input_dict else None,
instructions=input_dict[STRING_DESCRIPTION] if STRING_DESCRIPTION in input_dict else None)
#Property facet