There are currently two ways to create and build Energy Systems with Python. The first is to use the XSD created from the Ecore model and use an XSD to Python class generator to generate python classes. There are a few generators available, but we have useful result with generateDS. You can read here more for this approach.
An alternative is using pyEcore. PyEcore is the python equivalent of the Eclipse EMF Ecore implementation. This means that the ESDL meta-model is at runtime available to you for reading, writing and changing ESDL instances. This section provides some examples to use pyEcore to do just that. PyEcore offers two ways:
Using a dynamic instance of the model. This means that at runtime the Ecore-model is read and Python classes are dynamically instantiated.
Using a static instance of the model. This means that you first generate Python classes from the Ecore model (similar to the XSD-approach) and import these in your project.
The pyEcore approach provides some benefits above the XSD approach:
In the transformation from Ecore to XSD, some information is lost, which makes validation more difficult.
Assigning a value to a cross-reference will automatically set the eOpposite() of it, e.g. in the case of the connectedTo reference of a Port
Type checking when assigning values to e.g. a reference or a primitive type.
Since pyEcore is using the ESDL Ecore model as a basis and can create a dynamic instance of the model at runtime, there is no need for generating files/classes before you can use a new version of the core model. Especially during development of the ESDL ecore model this approach is convenient. The downside is that auto completion in the IDE (e.g. PyCharm) is not available.
All Python objects created for ESDL (both dynamic and static) inherit from the EClass class (similar to the Java generated files). This means that you can query the meta model at runtime and get e.g. the contents of the class using eclass.eContents. Also notification support is available, if your code needs to know when parts of the model are changed. See the pyEcore documentation at https://pyecore.readthedocs.io/en/latest/user/quickstart.html for more information and features.
Currently pyEcore support the XMI format for storing models and model instances. As ESDL is currently not using the XMI features, but only the XMLResource in the plugins, you need to register esdl-files with the XMLResource, otherwise the XMI namespace and the XMI version information is added to the ESDL-file when saving, giving errors when loading this file in Eclipse with the ESDL plugins. To use XMLResources you need to download the file attached at the end of this page and import it in your project (as done in all the examples below).
Prerequisites
You can install pyecore and pyecoregen using pip
pip install pyecore
pip install pyecoregen
Using dynamic instances to work with ESDL files
Creating and storing an ESDL file using dynamic instances can then be done as follows.
Note that there is no need to generated classes first, nor clone the ESDL-repository on github, as the ESDL Ecore model is automatically downloaded from github.
Creating and storing Energy Systems
from pyecore.resources import ResourceSet, URIfrom pyecore.utils import DynamicEPackagefrom pyecore.resources.resource import HttpURIfrom xmlresource import XMLResourcedefmain():# create a resourceSet that hold the contents of the esdl.ecore model and the instances we use/create rset =ResourceSet()# Assign files with the .esdl extension to the XMLResource instead of default XMI rset.resource_factory['esdl']=lambdauri: XMLResource(uri)# Read the lastest esdl.ecore from github resource = rset.get_resource(HttpURI('https://raw.githubusercontent.com/EnergyTransition/ESDL/master/esdl/model/esdl.ecore'))
esdl_model = resource.contents[0] rset.metamodel_registry[esdl_model.nsURI]= esdl_model# Create a dynamic model from the loaded esdl.ecore model, which we can use to build Energy Systems esdl =DynamicEPackage(esdl_model) es = esdl.EnergySystem(name="Test energy system") es.instance.append( esdl.Instance(name="My new instance")) es.instance[0].area = esdl.Area(name="My area") pvpark = esdl.PVPark(name="PV park") ed = esdl.ElectricityDemand(name="E demand") es.instance[0].area.asset.append(pvpark) es.instance[0].area.asset.append(ed) inPort = esdl.InPort(id='inPort1') ed.port.append(inPort) outPort = esdl.OutPort(id='outPort1') outPort.connectedTo.append(inPort) pvpark.port.append(outPort)print("Energy system(name={}), {}".format(es.name, dir(es)))print("OutPort connectedTo: {}".format(outPort.connectedTo))print("InPort connectedTo: {}".format(inPort.connectedTo)) # Create a new resource to hold the Energy System instance we just created, in the context of the previous created resourceSet that holds our ESDL model
resource = rset.create_resource(URI('Dynamic_instance.esdl'))# Append the energy system to the resource resource.append(es)# Save the resource resource.save()if__name__=='__main__':main()
Loading ESDL models
Loading an ESDL instance is as follows:
from pyecore.resources import ResourceSet, URIfrom pyecore.utils import DynamicEPackagefrom pyecore.resources.resource import HttpURIfrom xmlresource import XMLResourcedefmain():# create a resourceSet that hold the contents of the esdl.ecore model and the instances we use/create rset =ResourceSet()# Assign files with the .esdl extension to the XMLResource instead of default XMI rset.resource_factory['esdl']=lambdauri: XMLResource(uri)# Read the lastest esdl.ecore from github resource = rset.get_resource(HttpURI('https://raw.githubusercontent.com/EnergyTransition/ESDL/master/esdl/model/esdl.ecore'))
esdl_model = resource.contents[0]print('Namespace: {}'.format(esdl_model.nsURI)) rset.metamodel_registry[esdl_model.nsURI]= esdl_model# Create a dynamic model from the loaded esdl.ecore model, which we can use to build Energy Systems esdl =DynamicEPackage(esdl_model) resource = rset.get_resource(URI('test.esdl')) es = resource.contents[0]# At this point, the model instance is loaded!# Now the model can be manipulated, e.g. add a PowerToX asset to the area.print(es.name)print(es.instance[0].area.asset) es.instance[0].area.asset.append(esdl.PowerToX(name='Power to Heat', id='powerToH'))print(es.instance[0].area.asset)# print a list of all the classes of the instancefor child in es.eAllContents():print(attr_to_dict(child))# Save the result in the same file resource.save()if__name__=='__main__':main()
Using the generated classes to create ESDL
To have the Python classes available offline and use auto-completion in your favorite Python Editor, PyEcoreGen can be used to generate the ESDL-classes from the esdl.ecore model. On the command line do the following:
$ pyecoregen -e https://raw.githubusercontent.com/EnergyTransition/ESDL/master/esdl/model/esdl.ecore -o . --auto-register-package
This creates an esdl/folder and and esdl.py that contain all the classes from the ESDL model. More information about pyEcoreGen can be found here.
There is also an alternative way to generate classes programmatically by executing the following code:
from pyecoregen.ecore import EcoreGeneratorfrom pyecore.resources import ResourceSetfrom pyecore.resources.resource import HttpURI# Load the ESDL model from GitHubrset =ResourceSet()resource = rset.get_resource(HttpURI('https://raw.githubusercontent.com/EnergyTransition/ESDL/master/esdl/model/esdl.ecore'))
esdl_model = resource.contents[0]# Generate the classesgenerator =EcoreGenerator()generator.generate(esdl_model, ".")
Loading ESDL files using the generated Python classes
Use the following code to use the generated classes in your Python code:
from pyecore.resources import ResourceSet, URI from esdl.esdl import*import esdl from xmlresource import XMLResourcedefmain():# create a resourceSet that hold the contents of the esdl.ecore model and the instances we use/create rset =ResourceSet()# Assign files with the .esdl extension to the XMLResource instead of default XMI rset.resource_factory['esdl'] = lambda uri: XMLResource(uri) # we register the factory for '.esdl' extension and XML serialization
# Load the ESDL model in memory resource = rset.get_resource(HttpURI('https://raw.githubusercontent.com/EnergyTransition/ESDL/master/esdl/model/esdl.ecore'))
# and register that model in the meta model registry rset.metamodel_registry[esdl.nsURI]= esdl# load the ESDL file resource = rset.get_resource(HttpURI('https://github.com/EnergyTransition/ESDL-municipality-example/raw/master/ESDL_example_with_municipality_potential_and_measures-new.xmi'))
# Get the Energy System as root object from the resource es = resource.contents[0]# EnergySystem is now loaded into 'es' and can be manipulated, e.g. print it.print(attr_to_dict(es))if__name__=='__main__':main()
Show attributes and their values of Python classes
When you try to print() e.g. an Energy System, you see something like:
print(es) you get the following information that does not show the attributes of the EnergySystem, but that it is an object of type esdl.esdl.EnergySystem at a certain memory location:
<esdl.esdl.EnergySystem object at 0x000002A66FE60390>
A convenient method to print the results of an object is the following code. That code creates a dictionary from the EClass with all its attributes and their values.
defattr_to_dict(eobj): d =dict() d['eClass']= eobj.eClass.__name__for attr indir(eobj): attr_value = eobj.eGet(attr)if attr_value isnotNone: d[attr]= eobj.eGet(attr)return d
{'eClass': 'EnergySystem','description': 'Voorbeeld voor ESDL beschrijving van een gemeente op een redelijk hoog abstractieniveau','energySystemInformation': <esdl.esdl.EnergySystemInformationobjectat 0x0000013E54788D68>, 'instance': EOrderedSet([<esdl.esdl.Instance objectat 0x0000013E5474EF98>]), 'measures': <esdl.esdl.Measures objectat 0x0000013E5474EDD8>, 'name': 'ESDL Voorbeeld Gemeente XXX', 'parties': <esdl.esdl.Parties objectat 0x0000013E54792E80>, 'potentials': <esdl.esdl.Potentials objectat 0x0000013E54755978>, 'sector': EOrderedSet([GEBOUWDE_OMGEVING=1])}
You can also change the __repr__ of the classes, e.g. by changing the __repr__ of the python_class attribute, e.g. EnergySystem.python_class.__repr__ = lambda x: 'EnergySystem(name={})'.format(x.name)
Manipulating and saving ESDL files
The following example lets you create and save Energy Systems using the generated ESDL classes:
from pyecore.resources import ResourceSet, URIfrom esdl.esdl import*import esdlfrom xmlresource import XMLResourceimport datetime# insert attr_to_dict() function from above defmain():# create a resourceSet that hold the contents of the esdl.ecore model and the instances we use/create rset =ResourceSet()# register the metamodel (available in the generated files) rset.metamodel_registry[esdl.nsURI]= esdl rset.resource_factory['esdl'] = lambda uri: XMLResource(uri) # we register the factory for '.esdl' extension and XML serialization
# Create a new EnergySystem es =EnergySystem(name="test") instance =Instance(name="test instance")# example of using an Enum instance.aggrType = AggrTypeEnum.PER_COMMODITY es.instance.append( instance ) es.instance[0].area =Area(name="test area") pvpark =PVPark(name="PV park") pvpark.numberOfPanels =10# Use datatime to set dates and times now = datetime.datetime.now() pvpark.commissioningDate = now ed =ElectricityDemand(name="E demand") es.instance[0].area.asset.append(pvpark) es.instance[0].area.asset.append(ed) inPort =InPort(id='InPort1') ed.port.append(inPort) outPort =OutPort(id='OutPort1', connectedTo=[inPort]) pvpark.port.append(outPort)print("Energy system: {}".format(attr_to_dict(es)))print("OutPort connectedTo: {}".format(outPort.connectedTo))print("InPort connectedTo: {}".format(inPort.connectedTo)) resource = rset.create_resource(URI('Static_model_test.esdl')) resource.append(es) resource.save()if__name__=='__main__':main()
XMLResource support for PyEcore
Use the following file to support XMLResources instead of the default XMIResource support of PyEcore
from pyecore.resources.xmi import XMIResource, XMIOptions, XMI_URL, XSI_URL, XSIfrom lxml.etree import QName, Element, ElementTreeimport logginglogger = logging.getLogger(__name__)"""Extension of pyecore's XMIResource to support the XMLResource in EMF.It basically removes the xmi:version stuff from the serialization."""classXMLResource(XMIResource):def__init__(self,uri=None,use_uuid=False):super().__init__(uri, use_uuid) self._later = [] self.prefixes ={} self.reverse_nsmap ={} self.parse_information = []defget_parse_information(self):return self.parse_informationdefsave(self,output=None,options=None): self.options = options or{} output = self.open_out_stream(output) self.prefixes.clear() self.reverse_nsmap.clear() serialize_default =\ self.options.get(XMIOptions.SERIALIZE_DEFAULT_VALUES,False) nsmap ={XSI: XSI_URL}# remove XMI for XML serializationiflen(self.contents)==1: root = self.contents[0] self.register_eobject_epackage(root) tmp_xmi_root = self._go_across(root, serialize_default)else:# this case hasn't been verified for XML serialization tag =QName(XMI_URL, 'XMI') tmp_xmi_root =Element(tag)for root in self.contents: root_node = self._go_across(root, serialize_default) tmp_xmi_root.append(root_node)# update nsmap with prefixes register during the nodes creation nsmap.update(self.prefixes) xmi_root =Element(tmp_xmi_root.tag, nsmap=nsmap) xmi_root[:]= tmp_xmi_root[:] xmi_root.attrib.update(tmp_xmi_root.attrib)#xmi_version = etree.QName(XMI_URL, 'version') # remove XMI version in XML serialization#xmi_root.attrib[xmi_version] = '2.0' tree =ElementTree(xmi_root) tree.write(output, pretty_print=True, xml_declaration=True, encoding=tree.docinfo.encoding) output.flush()return self.uri.close_stream()""" This function has been overriden XMIResource, to make it a little more robust for ESDL's that are 'older' and do not have a certain feature. By default XMIResource throws an exception when an unknown attribute is found for a class. This version prints a warning and continues. """def_decode_attribute(self,owner,key,value): namespace, att_name = self.extract_namespace(key) prefix = self.reverse_nsmap[namespace]if namespace elseNone# This is a special case, we are working with uuidsif key == self.xmiid: owner._internal_id = value self.uuid_dict[value]= ownerelif prefix in ('xsi','xmi') and att_name =='type':# type has already been handledpass# elif namespace:# passelifnot namespace:if att_name =='href':return feature = self._find_feature(owner.eClass, att_name)ifnot feature:#raise ValueError('Feature {0} does not exists for type {1}'# .format(att_name, owner.eClass.name)) s ='Attribute {0} does not exists for type {1} and is ignored.'.format(att_name, owner.eClass.name) logger.warning(s) self.parse_information.append(s)return feature