cimexport.py 20 KB
Newer Older
1
2
3
4
import os
import importlib
import chevron
from datetime import datetime
5
from enum import Enum
6
7
from time import time
import logging
8

9
logger = logging.getLogger(__name__)
10

11

12
# This function gets all attributes of an object and resolves references to other objects
13
14
15
16
17
18
19
20
21
22
def _get_class_attributes_with_references(res, version):
    class_attributes_list = []

    for key in res.keys():
        class_dict = dict(name=res[key].__class__.__name__)
        class_dict['mRID'] = key
        # array containing all attributes, attribute references to objects
        attributes_dict = _get_attributes(res[key])
        # change attribute references to mRID of the object, res needed because classes like SvPowerFlow does not have
        # mRID as an attribute. Therefore the corresponding class has to be searched in the res dictionary
23
        class_dict['attributes'] = _get_reference_uuid(attributes_dict, version, res, key)
24
25
26
27
        class_attributes_list.append(class_dict)
        del class_dict

    return class_attributes_list
28
29


30
31
# This function resolves references to objects
def _get_reference_uuid(attr_dict, version, res, mRID):
32
33
34
35
36
    reference_list = []
    base_class_name = 'cimpy.' + version + '.Base'
    base_module = importlib.import_module(base_class_name)
    base_class = getattr(base_module, 'Base')
    for key in attr_dict:
37
        if key in ['serializationProfile', 'possibleProfileList']:
38
39
40
            reference_list.append({key: attr_dict[key]})
            continue

41
        attributes = {}
42
        if isinstance(attr_dict[key], list):  # many
43
44
45
            array = []
            for elem in attr_dict[key]:
                if issubclass(type(elem), base_class):
46
47
48
                    # classes like SvVoltage does not have an attribute called mRID, the mRID is only stored as a key
                    # for this object in the res dictionary
                    # The % added before the mRID is used in the lambda _set_attribute_or_reference
49
                    if not hasattr(elem, 'mRID'):
50
                        # search for the object in the res dictionary and return the mRID
51
52
                        UUID = '%' + _search_mRID(elem, res)
                        if UUID == '%':
Philipp Reuber's avatar
Philipp Reuber committed
53
                            logger.warning('Object of type {} not found as reference for object with UUID {}.'.format(
54
                                elem.__class__.__name__, mRID))
55
56
57
                    else:
                        UUID = '%' + elem.mRID

58
59
                    array.append(UUID)
                else:
Philipp Reuber's avatar
Philipp Reuber committed
60
                    logger.warning('Reference object not subclass of Base class for object with UUID {}.'.format(mRID))
61
62
63
64
            if len(array) == 1:
                attributes['value'] = array[0]
            else:
                attributes['value'] = array
65
        elif issubclass(type(attr_dict[key]), base_class):  # 0..1, 1..1
66
            # resource = key + ' rdf:resource='
67
            if not hasattr(attr_dict[key], 'mRID'):
68
69
                # search for object in res dict and return mRID
                # The % added before the mRID is used in the lambda _set_attribute_or_reference
70
71
                UUID = '%' + _search_mRID(attr_dict[key], res)
                if UUID == '%':
Philipp Reuber's avatar
Philipp Reuber committed
72
                    logger.warning('Object of type {} not found as reference for object with UUID {}.'.format(
73
                        elem.__class__.__name__, mRID))
74
75
            else:
                UUID = '%' + attr_dict[key].mRID
76
77
78
79
80
81
82
83
84
85
            attributes['value'] = UUID
        elif attr_dict[key] == "" or attr_dict[key] is None:
            pass
        else:
            attributes['value'] = attr_dict[key]

        attributes['attr_name'] = key
        if 'value' in attributes.keys():
            if isinstance(attributes['value'], list):
                for reference_item in attributes['value']:
86
87
                    # ignore default values
                    if reference_item not in ['', None, 0.0, 0]:
88
                        reference_list.append({'value': reference_item, 'attr_name': key})
89
90
            # ignore default values
            elif attributes['value'] not in ['', None, 0.0, 0, 'many']:
91
92
                reference_list.append(attributes)

93
    return reference_list
94
95


96
97
# This function searches a class_object in the res dictionary and returns the corresponding key (the mRID). Necessary
# for classes without mRID as attribute like SvVoltage
98
99
100
101
102
103
104
def _search_mRID(class_object, res):
    for mRID, class_obj in res.items():
        if class_object == class_obj:
            return mRID
    return ""


105
# Lambda function for chevron renderer to decide whether the current element is a reference or an attribute
106
107
108
109
110
111
112
113
114
115
116
117
def _set_attribute_or_reference(text, render):
    result = render(text)
    result = result.split('@')
    value = result[0]
    attr_name = result[1]
    if '%' in value:
        reference = value.split('%')[1]
        return ' rdf:resource="#' + reference + '"/>'
    else:
        return '>' + value + '</cim:' + attr_name + '>'


118
# Lambda function for chevron renderer to set an attribute or a reference in the model description.
119
120
121
122
123
124
125
126
127
128
129
130
def _set_attribute_or_reference_model(text, render):
    result = render(text)
    result = result.split('@')
    value = result[0]
    attr_name = result[1]
    if '%' in value:
        reference = value.split('%')[1]
        return ' rdf:resource="' + reference + '"/>'
    else:
        return '>' + value + '</md:Model.' + attr_name + '>'


131
# Restructures the namespaces dict into a list. The template engine writes each entry in the RDF header
132
133
134
135
def _create_namespaces_list(namespaces_dict):
    namespaces_list = []

    for key in namespaces_dict:
136
        namespace = dict(key=key, url=namespaces_dict[key])
137
138
139
140
141
        namespaces_list.append(namespace)

    return namespaces_list


142
# This function sorts the classes and their attributes to the corresponding profiles. Either the classes/attributes are
143
144
# imported or they are set afterwards. In the first case the serializationProfile is used to determine from which
# profile this class/attribute was read. If an entry exists the class/attribute is added to this profile. In the
145
146
147
# possibleProfileList dictionary the possible origins of the class/attributes is stored. All profiles have a different
# priority which is stored in the enum cgmesProfile. As default the smallest entry in the dictionary is used to
# determine the profile for the class/attributes.
148
def _sort_classes_to_profile(class_attributes_list, activeProfileList):
149
150
151
152
153
154
155
156
    export_dict = {}
    export_about_dict = {}

    # iterate over classes
    for klass in class_attributes_list:
        same_package_list = []
        about_dict = {}

157
158
159
        # store serializationProfile and possibleProfileList
        # serializationProfile class attribute, same for multiple instances of same class, only last origin of variable stored
        serializationProfile = klass['attributes'][0]['serializationProfile']
160
161
        possibleProfileList = klass['attributes'][1]['possibleProfileList']

162
163
        class_serializationProfile = ''

164
        if 'class' in serializationProfile.keys():
165
            # class was imported
166
            if serializationProfile['class'] in activeProfileList:
167
                # else: class origin profile not active for export, get active profile from possibleProfileList
168
                if cgmesProfile[serializationProfile['class']].value in possibleProfileList[klass['name']]['class']:
169
170
171
                    # profile active and in possibleProfileList
                    # else: class should not have been imported from this profile, get allowed profile
                    # from possibleProfileList
172
                    class_serializationProfile = serializationProfile['class']
Philipp Reuber's avatar
Philipp Reuber committed
173
174
                else:
                    logger.warning('Class {} was read from profile {} but this profile is not possible for this class'
175
                                   .format(klass['name'], serializationProfile['class']))
Philipp Reuber's avatar
Philipp Reuber committed
176
177
            else:
                logger.info('Class {} was read from profile {} but this profile is not active for the export. Use'
178
                            'default profile from possibleProfileList.'.format(klass['name'], serializationProfile['class']))
179
180

        if class_serializationProfile == '':
181
            # class was created
182
183
            if klass['name'] in possibleProfileList.keys():
                if 'class' in possibleProfileList[klass['name']].keys():
184
                    possibleProfileList[klass['name']]['class'].sort()
185
186
                    for klass_profile in possibleProfileList[klass['name']]['class']:
                        if cgmesProfile(klass_profile).name in activeProfileList:
187
                            # active profile for class export found
188
                            class_serializationProfile = cgmesProfile(klass_profile).name
189
190
191
192
193
194
                            break
                    if class_serializationProfile == '':
                        # no profile in possibleProfileList active
                        logger.warning('All possible export profiles for class {} not active. Skip class for export.'
                                       .format(klass['name']))
                        continue
195
                else:
196
                    logger.warning('Class {} has no profile to export to.'.format(klass['name']))
197
            else:
198
                logger.warning('Class {} has no profile to export to.'.format(klass['name']))
199
200
201
202
203
204

        # iterate over attributes
        for attribute in klass['attributes']:
            if 'attr_name' in attribute.keys():
                attribute_class = attribute['attr_name'].split('.')[0]
                attribute_name = attribute['attr_name'].split('.')[1]
205
206
207
208
209

                # IdentifiedObject.mRID is not exported as an attribute
                if attribute_name == 'mRID':
                    continue

210
                attribute_serializationProfile = ''
211

212
                if attribute_name in serializationProfile.keys():
213
                    # attribute was imported
214
215
216
217
                    if serializationProfile[attribute_name] in activeProfileList:
                        attr_value = cgmesProfile[serializationProfile[attribute_name]].value
                        if attr_value in possibleProfileList[attribute_class][attribute_name]:
                            attribute_serializationProfile = serializationProfile[attribute_name]
218

219
                if attribute_serializationProfile == '':
220
                    # attribute was added
221
222
                    if attribute_class in possibleProfileList.keys():
                        if attribute_name in possibleProfileList[attribute_class].keys():
223
                            possibleProfileList[attribute_class][attribute_name].sort()
224
225
                            for attr_profile in possibleProfileList[attribute_class][attribute_name]:
                                if cgmesProfile(attr_profile).name in activeProfileList:
226
                                    # active profile for class export found
227
                                    attribute_serializationProfile = cgmesProfile(attr_profile).name
228
229
230
231
232
233
234
                                    break
                            if attribute_serializationProfile == '':
                                # no profile in possibleProfileList active, skip attribute
                                logger.warning('All possible export profiles for attribute {}.{} of class {} '
                                               'not active. Skip attribute for export.'
                                               .format(attribute_class, attribute_name, klass['name']))
                                continue
235
                        else:
236
237
                            logger.warning('Attribute {}.{} of class {} has no profile to export to.'.
                                           format(attribute_class, attribute_name, klass['name']))
238
                    else:
239
                        logger.warning('The class {} for attribute {} is not in the possibleProfileList'.format(
240
                            attribute_class, attribute_name))
241

242
                if attribute_serializationProfile == class_serializationProfile:
243
                    # class and current attribute belong to same profile
244
245
                    same_package_list.append(attribute)
                else:
246
247
                    # class and current attribute does not belong to same profile -> rdf:about in
                    # attribute origin profile
248
249
                    if attribute_serializationProfile in about_dict.keys():
                        about_dict[attribute_serializationProfile].append(attribute)
250
                    else:
251
                        about_dict[attribute_serializationProfile] = [attribute]
252

253
        # add class with all attributes in the same profile to the export dict sorted by the profile
254
        if class_serializationProfile in export_dict.keys():
255
            export_class = dict(name=klass['name'], mRID=klass['mRID'], attributes=same_package_list)
256
            export_dict[class_serializationProfile]['classes'].append(export_class)
257
258
259
            del export_class
        else:
            export_class = dict(name=klass['name'], mRID=klass['mRID'], attributes=same_package_list)
260
            export_dict[class_serializationProfile] = {'classes': [export_class]}
261

262
        # add class with all attributes defined in another profile to the about_key sorted by the profile
263
264
265
266
267
268
269
270
271
272
273
        for about_key in about_dict.keys():
            if about_key in export_about_dict.keys():
                export_about_class = dict(name=klass['name'], mRID=klass['mRID'], attributes=about_dict[about_key])
                export_about_dict[about_key]['classes'].append(export_about_class)
            else:
                export_about_class = dict(name=klass['name'], mRID=klass['mRID'], attributes=about_dict[about_key])
                export_about_dict[about_key] = {'classes': [export_about_class]}

    return export_dict, export_about_dict


274
def cim_export(res, namespaces_dict, file_name, version, activeProfileList):
275
276
    """Function for serialization of cgmes classes

277
278
    This function serializes cgmes classes with the template engine chevron. The classes are separated by their profile
    and one xml file for each profile is created. The package name is added after the file name. The
279
280
281
282
283
284
    set_attributes_or_reference function is a lamda function for chevron to decide whether the value of an attribute is
    a reference to another class object or not.

    :param res: a dictionary containing the cgmes classes accessible via the mRID
    :param namespaces_dict: a dictionary containing the RDF namespaces used in the imported xml files
    :param file_name: a string with the name of the xml files which will be created
285
    :param version: cgmes version, e.g. version = "cgmes_v2_4_15"
286
    :param activeProfileList: a list containing the strings of all short names of the profiles used for serialization
287
    """
288

289
290
    cwd = os.getcwd()
    os.chdir(os.path.dirname(__file__))
291
292
    t0 = time()
    logger.info('Start export procedure.')
293

294
    # returns all classes with their attributes and resolved references
295
296
    class_attributes_list = _get_class_attributes_with_references(res, version)

297
298
299
    # determine class and attribute export profiles. The export dict contains all classes and their attributes where
    # the class definition and the attribute definitions are in the same profile. Every entry in about_dict generates
    # a rdf:about in another profile
300
    export_dict, about_dict = _sort_classes_to_profile(class_attributes_list, activeProfileList)
301
302
303

    namespaces_list = _create_namespaces_list(namespaces_dict)

304
    # get information for Model header
305
306
307
    created = {'attr_name': 'created', 'value': datetime.now().strftime("%d/%m/%Y %H:%M:%S")}
    authority = {'attr_name': 'modelingAuthoritySet', 'value': 'www.acs.eonerc.rwth-aachen.de'}

308
309
    # iterate over all profiles
    for profile_name, short_name in short_profile_name.items():
310
311
312
313
314
        model_name = {'mRID': file_name, 'description': []}
        model_description = {'model': [model_name]}
        model_description['model'][0]['description'].append(created)
        model_description['model'][0]['description'].append(authority)

315
316
317
        if short_name not in export_dict.keys() and short_name not in about_dict.keys():
            # nothing to do for current profile
            continue
318
        else:
319
320
321
322
323
            # extract class lists from export_dict and about_dict
            if short_name in export_dict.keys():
                classes = export_dict[short_name]['classes']
            else:
                classes = False
324

325
326
327
328
            if short_name in about_dict.keys():
                about = about_dict[short_name]['classes']
            else:
                about = False
329

330
331
        # File name
        full_file_name = file_name + '_' + profile_name + '.xml'
332
333
334

        full_path = os.path.join(cwd, full_file_name)

335
        profile = {'attr_name': 'profile', 'value': profile_name}
336
337
338
339
        model_description['model'][0]['description'].append(profile)

        if not os.path.exists(full_path):
            with open(full_path, 'w') as file:
340
341
                logger.info('Write file \"%s\"', full_path)

342
343
344
345
346
347
348
349
                with open('export_template.mustache') as f:
                    output = chevron.render(f, {"classes": classes,
                                                "about": about,
                                                "set_attributes_or_reference": _set_attribute_or_reference,
                                                "set_attributes_or_reference_model": _set_attribute_or_reference_model,
                                                "namespaces": namespaces_list,
                                                "model": model_description['model']})
                file.write(output)
350
351
352
        else:
            logger.warning('File {} already exists in path {}. Delete file or change file name to serialize CGMES '
                           'classes.'.format(full_file_name, cwd))
353
354
        del model_description, model_name
    os.chdir(cwd)
355
    logger.info('End export procedure. Elapsed time: {}'.format(time() - t0))
356
357


358
# This function extracts all attributes from class_object in the form of Class_Name.Attribute_Name
359
360
361
362
def _get_attributes(class_object):
    inheritance_list = [class_object]
    class_type = type(class_object)
    parent = class_object
363
364

    # get parent classes
365
366
367
368
369
370
371
    while 'Base.Base' not in str(class_type):
        parent = parent.__class__.__bases__[0]()
        # insert parent class at beginning of list, classes inherit from top to bottom
        inheritance_list.insert(0, parent)
        class_type = type(parent)

    # dictionary containing all attributes with key: 'Class_Name.Attribute_Name'
372
    attributes_dict = dict(serializationProfile=class_object.serializationProfile, possibleProfileList={})
373

374
375
376
    # __dict__ of a subclass returns also the attributes of the parent classes
    # to avoid multiple attributes create list with all attributes already processed
    attributes_list = []
377

378
    # iterate over parent classes from top to bottom
379
380
381
382
    for parent_class in inheritance_list:
        # get all attributes of the current parent class
        parent_attributes_dict = parent_class.__dict__
        class_name = parent_class.__class__.__name__
383
384

        # check if new attribute or old attribute
385
386
387
388
389
390
391
392
        for key in parent_attributes_dict.keys():
            if key not in attributes_list:
                attributes_list.append(key)
                attributes_name = class_name + '.' + key
                attributes_dict[attributes_name] = getattr(class_object, key)
            else:
                continue

393
        # get all possibleProfileLists from all parent classes except the Base class (no attributes)
394
395
        # the serializationProfile from parent classes is not needed because entries in the serializationProfile
        # are only generated for the inherited class
396
397
398
        if class_name is not 'Base':
            attributes_dict['possibleProfileList'][class_name] = parent_class.possibleProfileList

399
    return attributes_dict
400
401


402
403
# Mapping between the profiles and their short names
short_profile_name = {
404
405
406
407
408
409
410
411
412
    "DiagramLayout": 'DI',
    "Dynamics": "DY",
    "Equipment": "EQ",
    "GeographicalLocation": "GL",
    "StateVariables": "SV",
    "SteadyStateHypothesis": "SSH",
    "Topology": "TP"
}

413
414
# Enum containing all profiles and their export priority
cgmesProfile = Enum("cgmesProfile", {"EQ": 0, "SSH": 1, "TP": 2, "SV": 3, "DY": 4, "GL": 5, "DI": 6})