1# Copyright 2013 by Leighton Pritchard.  All rights reserved.
2#
3# This file is part of the Biopython distribution and governed by your
4# choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
5# Please see the LICENSE file that should have been included as part of this
6# package.
7
8"""Classes to represent a KGML Pathway Map.
9
10The KGML definition is as of release KGML v0.7.2
11(http://www.kegg.jp/kegg/xml/docs/)
12
13Classes:
14 - Pathway - Specifies graph information for the pathway map
15 - Relation - Specifies a relationship between two proteins or KOs,
16   or protein and compound. There is an implied direction to the
17   relationship in some cases.
18 - Reaction - A specific chemical reaction between a substrate and
19   a product.
20 - Entry - A node in the pathway graph
21 - Graphics - Entry subelement describing its visual representation
22
23"""
24
25import time
26from itertools import chain
27from xml.dom import minidom
28import xml.etree.ElementTree as ET
29
30
31# Pathway
32class Pathway:
33    """Represents a KGML pathway from KEGG.
34
35    Specifies graph information for the pathway map, as described in
36    release KGML v0.7.2 (http://www.kegg.jp/kegg/xml/docs/)
37
38    Attributes:
39     - name - KEGGID of the pathway map
40     - org - ko/ec/[org prefix]
41     - number - map number (integer)
42     - title - the map title
43     - image - URL of the image map for the pathway
44     - link - URL of information about the pathway
45     - entries - Dictionary of entries in the pathway, keyed by node ID
46     - reactions - Set of reactions in the pathway
47
48    The name attribute has a restricted format, so we make it a property and
49    enforce the formatting.
50
51    The Pathway object is the only allowed route for adding/removing
52    Entry, Reaction, or Relation elements.
53
54    Entries are held in a dictionary and keyed by the node ID for the
55    pathway graph - this allows for ready access via the Reaction/Relation
56    etc. elements.  Entries must be added before reference by any other
57    element.
58
59    Reactions are held in a dictionary, keyed by node ID for the path.
60    The elements referred to in the reaction must be added before the
61    reaction itself.
62
63    """
64
65    def __init__(self):
66        """Initialize the class."""
67        self._name = ""
68        self.org = ""
69        self._number = None
70        self.title = ""
71        self.image = ""
72        self.link = ""
73        self.entries = {}
74        self._reactions = {}
75        self._relations = set()
76
77    def get_KGML(self):
78        """Return the pathway as a string in prettified KGML format."""
79        header = "\n".join(
80            [
81                '<?xml version="1.0"?>',
82                "<!DOCTYPE pathway SYSTEM "
83                '"http://www.genome.jp/kegg/xml/'
84                'KGML_v0.7.2_.dtd">',
85                "<!-- Created by KGML_Pathway.py %s -->" % time.asctime(),
86            ]
87        )
88        rough_xml = header + ET.tostring(self.element, "utf-8").decode()
89        reparsed = minidom.parseString(rough_xml)
90        return reparsed.toprettyxml(indent="  ")
91
92    def add_entry(self, entry):
93        """Add an Entry element to the pathway."""
94        # We insist that the node ID is an integer
95        if not isinstance(entry.id, int):
96            raise TypeError(
97                "Node ID must be an integer, got %s (%s)" % (type(entry.id), entry.id)
98            )
99        entry._pathway = self  # Let the entry know about the pathway
100        self.entries[entry.id] = entry
101
102    def remove_entry(self, entry):
103        """Remove an Entry element from the pathway."""
104        if not isinstance(entry.id, int):
105            raise TypeError(
106                "Node ID must be an integer, got %s (%s)" % (type(entry.id), entry.id)
107            )
108        # We need to remove the entry from any other elements that may
109        # contain it, which means removing those elements
110        # TODO
111        del self.entries[entry.id]
112
113    def add_reaction(self, reaction):
114        """Add a Reaction element to the pathway."""
115        # We insist that the node ID is an integer and corresponds to an entry
116        if not isinstance(reaction.id, int):
117            raise ValueError(
118                "Node ID must be an integer, got %s (%s)"
119                % (type(reaction.id), reaction.id)
120            )
121        if reaction.id not in self.entries:
122            raise ValueError("Reaction ID %d has no corresponding entry" % reaction.id)
123        reaction._pathway = self  # Let the reaction know about the pathway
124        self._reactions[reaction.id] = reaction
125
126    def remove_reaction(self, reaction):
127        """Remove a Reaction element from the pathway."""
128        if not isinstance(reaction.id, int):
129            raise TypeError(
130                "Node ID must be an integer, got %s (%s)"
131                % (type(reaction.id), reaction.id)
132            )
133        # We need to remove the reaction from any other elements that may
134        # contain it, which means removing those elements
135        # TODO
136        del self._reactions[reaction.id]
137
138    def add_relation(self, relation):
139        """Add a Relation element to the pathway."""
140        relation._pathway = self  # Let the reaction know about the pathway
141        self._relations.add(relation)
142
143    def remove_relation(self, relation):
144        """Remove a Relation element from the pathway."""
145        self._relations.remove(relation)
146
147    def __str__(self):
148        """Return a readable summary description string."""
149        outstr = [
150            "Pathway: %s" % self.title,
151            "KEGG ID: %s" % self.name,
152            "Image file: %s" % self.image,
153            "Organism: %s" % self.org,
154            "Entries: %d" % len(self.entries),
155            "Entry types:",
156        ]
157        for t in ["ortholog", "enzyme", "reaction", "gene", "group", "compound", "map"]:
158            etype = [e for e in self.entries.values() if e.type == t]
159            if len(etype):
160                outstr.append("\t%s: %d" % (t, len(etype)))
161        return "\n".join(outstr) + "\n"
162
163    # Assert correct formatting of the pathway name, and other attributes
164    def _getname(self):
165        return self._name
166
167    def _setname(self, value):
168        if not value.startswith("path:"):
169            raise ValueError("Pathway name should begin with 'path:', got %s" % value)
170        self._name = value
171
172    def _delname(self):
173        del self._name
174
175    name = property(_getname, _setname, _delname, "The KEGGID for the pathway map.")
176
177    def _getnumber(self):
178        return self._number
179
180    def _setnumber(self, value):
181        self._number = int(value)
182
183    def _delnumber(self):
184        del self._number
185
186    number = property(_getnumber, _setnumber, _delnumber, "The KEGG map number.")
187
188    @property
189    def compounds(self):
190        """Get a list of entries of type compound."""
191        return [e for e in self.entries.values() if e.type == "compound"]
192
193    @property
194    def maps(self):
195        """Get a list of entries of type map."""
196        return [e for e in self.entries.values() if e.type == "map"]
197
198    @property
199    def orthologs(self):
200        """Get a list of entries of type ortholog."""
201        return [e for e in self.entries.values() if e.type == "ortholog"]
202
203    @property
204    def genes(self):
205        """Get a list of entries of type gene."""
206        return [e for e in self.entries.values() if e.type == "gene"]
207
208    @property
209    def reactions(self):
210        """Get a list of reactions in the pathway."""
211        return self._reactions.values()
212
213    @property
214    def reaction_entries(self):
215        """List of entries corresponding to each reaction in the pathway."""
216        return [self.entries[i] for i in self._reactions]
217
218    @property
219    def relations(self):
220        """Get a list of relations in the pathway."""
221        return list(self._relations)
222
223    @property
224    def element(self):
225        """Return the Pathway as a valid KGML element."""
226        # The root is this Pathway element
227        pathway = ET.Element("pathway")
228        pathway.attrib = {
229            "name": self._name,
230            "org": self.org,
231            "number": str(self._number),
232            "title": self.title,
233            "image": self.image,
234            "link": self.link,
235        }
236        # We add the Entries in node ID order
237        for eid, entry in sorted(self.entries.items()):
238            pathway.append(entry.element)
239        # Next we add Relations
240        for relation in self._relations:
241            pathway.append(relation.element)
242        for eid, reaction in sorted(self._reactions.items()):
243            pathway.append(reaction.element)
244        return pathway
245
246    @property
247    def bounds(self):
248        """Coordinate bounds for all Graphics elements in the Pathway.
249
250        Returns the [(xmin, ymin), (xmax, ymax)] coordinates for all
251        Graphics elements in the Pathway
252        """
253        xlist, ylist = [], []
254        for b in [g.bounds for g in self.entries.values()]:
255            xlist.extend([b[0][0], b[1][0]])
256            ylist.extend([b[0][1], b[1][1]])
257        return [(min(xlist), min(ylist)), (max(xlist), max(ylist))]
258
259
260# Entry
261class Entry:
262    """Represent an Entry from KGML.
263
264    Each Entry element is a node in the pathway graph, as described in
265    release KGML v0.7.2 (http://www.kegg.jp/kegg/xml/docs/)
266
267    Attributes:
268     - id - The ID of the entry in the pathway map (integer)
269     - names - List of KEGG IDs for the entry
270     - type - The type of the entry
271     - link - URL of information about the entry
272     - reaction - List of KEGG IDs of the corresponding reactions
273       (integer)
274     - graphics -    List of Graphics objects describing the Entry's visual
275       representation
276     - components - List of component node ID for this Entry ('group')
277     - alt - List of alternate names for the Entry
278
279    NOTE: The alt attribute represents a subelement of the substrate and
280    product elements in the KGML file
281
282    """
283
284    def __init__(self):
285        """Initialize the class."""
286        self._id = None
287        self._names = []
288        self.type = ""
289        self.image = ""
290        self.link = ""
291        self.graphics = []
292        self.components = set()
293        self.alt = []
294        self._pathway = None
295        self._reactions = []
296
297    def __str__(self):
298        """Return readable descriptive string."""
299        outstr = [
300            "Entry node ID: %d" % self.id,
301            "Names: %s" % self.name,
302            "Type: %s" % self.type,
303            "Components: %s" % self.components,
304            "Reactions: %s" % self.reaction,
305            "Graphics elements: %d %s" % (len(self.graphics), self.graphics),
306        ]
307        return "\n".join(outstr) + "\n"
308
309    def add_component(self, element):
310        """Add an element to the entry.
311
312        If the Entry is already part of a pathway, make sure
313        the component already exists.
314        """
315        if self._pathway is not None:
316            if element.id not in self._pathway.entries:
317                raise ValueError(
318                    "Component %s is not an entry in the pathway" % element.id
319                )
320        self.components.add(element)
321
322    def remove_component(self, value):
323        """Remove the entry with the passed ID from the group."""
324        self.components.remove(value)
325
326    def add_graphics(self, entry):
327        """Add the Graphics entry."""
328        self.graphics.append(entry)
329
330    def remove_graphics(self, entry):
331        """Remove the Graphics entry with the passed ID from the group."""
332        self.graphics.remove(entry)
333
334    # Names may be given as a space-separated list of KEGG identifiers
335    def _getname(self):
336        return " ".join(self._names)
337
338    def _setname(self, value):
339        self._names = value.split()
340
341    def _delname(self):
342        self._names = []
343
344    name = property(
345        _getname, _setname, _delname, "List of KEGG identifiers for the Entry."
346    )
347
348    # Reactions may be given as a space-separated list of KEGG identifiers
349    def _getreaction(self):
350        return " ".join(self._reactions)
351
352    def _setreaction(self, value):
353        self._reactions = value.split()
354
355    def _delreaction(self):
356        self._reactions = []
357
358    reaction = property(
359        _getreaction,
360        _setreaction,
361        _delreaction,
362        "List of reaction KEGG IDs for this Entry.",
363    )
364
365    # We make sure that the node ID is an integer
366    def _getid(self):
367        return self._id
368
369    def _setid(self, value):
370        self._id = int(value)
371
372    def _delid(self):
373        del self._id
374
375    id = property(_getid, _setid, _delid, "The pathway graph node ID for the Entry.")
376
377    @property
378    def element(self):
379        """Return the Entry as a valid KGML element."""
380        # The root is this Entry element
381        entry = ET.Element("entry")
382        entry.attrib = {
383            "id": str(self._id),
384            "name": self.name,
385            "link": self.link,
386            "type": self.type,
387        }
388        if len(self._reactions):
389            entry.attrib["reaction"] = self.reaction
390        if len(self.graphics):
391            for g in self.graphics:
392                entry.append(g.element)
393        if len(self.components):
394            for c in self.components:
395                entry.append(c.element)
396        return entry
397
398    @property
399    def bounds(self):
400        """Coordinate bounds for all Graphics elements in the Entry.
401
402        Return the [(xmin, ymin), (xmax, ymax)] co-ordinates for the Entry
403        Graphics elements.
404        """
405        xlist, ylist = [], []
406        for b in [g.bounds for g in self.graphics]:
407            xlist.extend([b[0][0], b[1][0]])
408            ylist.extend([b[0][1], b[1][1]])
409        return [(min(xlist), min(ylist)), (max(xlist), max(ylist))]
410
411    @property
412    def is_reactant(self):
413        """Return true if this Entry participates in any reaction in its parent pathway."""
414        for rxn in self._pathway.reactions:
415            if self._id in rxn.reactant_ids:
416                return True
417        return False
418
419
420# Component
421class Component:
422    """An Entry subelement used to represents a complex node.
423
424    A subelement of the Entry element, used when the Entry is a complex
425    node, as described in release KGML v0.7.2
426    (http://www.kegg.jp/kegg/xml/docs/)
427
428    The Component acts as a collection (with type 'group', and typically
429    its own Graphics subelement), having only an ID.
430    """
431
432    def __init__(self, parent):
433        """Initialize the class."""
434        self._id = None
435        self._parent = parent
436
437    # We make sure that the node ID is an integer
438    def _getid(self):
439        return self._id
440
441    def _setid(self, value):
442        self._id = int(value)
443
444    def _delid(self):
445        del self._id
446
447    id = property(_getid, _setid, _delid, "The pathway graph node ID for the Entry")
448
449    @property
450    def element(self):
451        """Return the Component as a valid KGML element."""
452        # The root is this Component element
453        component = ET.Element("component")
454        component.attrib = {"id": str(self._id)}
455        return component
456
457
458# Graphics
459class Graphics:
460    """An Entry subelement used to represents the visual representation.
461
462    A subelement of Entry, specifying its visual representation, as
463    described in release KGML v0.7.2 (http://www.kegg.jp/kegg/xml/docs/)
464
465    Attributes:
466     - name         Label for the graphics object
467     - x            X-axis position of the object (int)
468     - y            Y-axis position of the object (int)
469     - coords       polyline co-ordinates, list of (int, int) tuples
470     - type         object shape
471     - width        object width (int)
472     - height       object height (int)
473     - fgcolor      object foreground color (hex RGB)
474     - bgcolor      object background color (hex RGB)
475
476    Some attributes are present only for specific graphics types.  For
477    example, line types do not (typically) have a width.
478    We permit non-DTD attributes and attribute settings, such as
479
480    dash         List of ints, describing an on/off pattern for dashes
481
482    """
483
484    def __init__(self, parent):
485        """Initialize the class."""
486        self.name = ""
487        self._x = None
488        self._y = None
489        self._coords = None
490        self.type = ""
491        self._width = None
492        self._height = None
493        self.fgcolor = ""
494        self.bgcolor = ""
495        self._parent = parent
496
497    # We make sure that the XY coordinates, width and height are numbers
498    def _getx(self):
499        return self._x
500
501    def _setx(self, value):
502        self._x = float(value)
503
504    def _delx(self):
505        del self._x
506
507    x = property(_getx, _setx, _delx, "The X coordinate for the graphics element.")
508
509    def _gety(self):
510        return self._y
511
512    def _sety(self, value):
513        self._y = float(value)
514
515    def _dely(self):
516        del self._y
517
518    y = property(_gety, _sety, _dely, "The Y coordinate for the graphics element.")
519
520    def _getwidth(self):
521        return self._width
522
523    def _setwidth(self, value):
524        self._width = float(value)
525
526    def _delwidth(self):
527        del self._width
528
529    width = property(
530        _getwidth, _setwidth, _delwidth, "The width of the graphics element."
531    )
532
533    def _getheight(self):
534        return self._height
535
536    def _setheight(self, value):
537        self._height = float(value)
538
539    def _delheight(self):
540        del self._height
541
542    height = property(
543        _getheight, _setheight, _delheight, "The height of the graphics element."
544    )
545
546    # We make sure that the polyline co-ordinates are integers, too
547    def _getcoords(self):
548        return self._coords
549
550    def _setcoords(self, value):
551        clist = [int(e) for e in value.split(",")]
552        self._coords = [tuple(clist[i : i + 2]) for i in range(0, len(clist), 2)]
553
554    def _delcoords(self):
555        del self._coords
556
557    coords = property(
558        _getcoords,
559        _setcoords,
560        _delcoords,
561        "Polyline coordinates for the graphics element.",
562    )
563
564    # Set default colors
565    def _getfgcolor(self):
566        return self._fgcolor
567
568    def _setfgcolor(self, value):
569        if value == "none":
570            self._fgcolor = "#000000"  # this default defined in KGML spec
571        else:
572            self._fgcolor = value
573
574    def _delfgcolor(self):
575        del self._fgcolor
576
577    fgcolor = property(_getfgcolor, _setfgcolor, _delfgcolor, "Foreground color.")
578
579    def _getbgcolor(self):
580        return self._bgcolor
581
582    def _setbgcolor(self, value):
583        if value == "none":
584            self._bgcolor = "#000000"  # this default defined in KGML spec
585        else:
586            self._bgcolor = value
587
588    def _delbgcolor(self):
589        del self._bgcolor
590
591    bgcolor = property(_getbgcolor, _setbgcolor, _delbgcolor, "Background color.")
592
593    @property
594    def element(self):
595        """Return the Graphics as a valid KGML element."""
596        # The root is this Component element
597        graphics = ET.Element("graphics")
598        if isinstance(self.fgcolor, str):  # Assumes that string is hexstring
599            fghex = self.fgcolor
600        else:  # Assumes ReportLab Color object
601            fghex = "#" + self.fgcolor.hexval()[2:]
602        if isinstance(self.bgcolor, str):  # Assumes that string is hexstring
603            bghex = self.bgcolor
604        else:  # Assumes ReportLab Color object
605            bghex = "#" + self.bgcolor.hexval()[2:]
606        graphics.attrib = {
607            "name": self.name,
608            "type": self.type,
609            "fgcolor": fghex,
610            "bgcolor": bghex,
611        }
612        for (n, attr) in [
613            ("x", "_x"),
614            ("y", "_y"),
615            ("width", "_width"),
616            ("height", "_height"),
617        ]:
618            if getattr(self, attr) is not None:
619                graphics.attrib[n] = str(getattr(self, attr))
620        if self.type == "line":  # Need to write polycoords
621            graphics.attrib["coords"] = ",".join(
622                [str(e) for e in chain.from_iterable(self.coords)]
623            )
624        return graphics
625
626    @property
627    def bounds(self):
628        """Coordinate bounds for the Graphics element.
629
630        Return the bounds of the Graphics object as an [(xmin, ymin),
631        (xmax, ymax)] tuple.  Co-ordinates give the centre of the
632        circle, rectangle, roundrectangle elements, so we have to
633        adjust for the relevant width/height.
634        """
635        if self.type == "line":
636            xlist = [x for x, y in self.coords]
637            ylist = [y for x, y in self.coords]
638            return [(min(xlist), min(ylist)), (max(xlist), max(ylist))]
639        else:
640            return [
641                (self.x - self.width * 0.5, self.y - self.height * 0.5),
642                (self.x + self.width * 0.5, self.y + self.height * 0.5),
643            ]
644
645    @property
646    def centre(self):
647        """Return the centre of the Graphics object as an (x, y) tuple."""
648        return (
649            0.5 * (self.bounds[0][0] + self.bounds[1][0]),
650            0.5 * (self.bounds[0][1] + self.bounds[1][1]),
651        )
652
653
654# Reaction
655class Reaction:
656    """A specific chemical reaction with substrates and products.
657
658    This describes a specific chemical reaction between one or more
659    substrates and one or more products.
660
661    Attributes:
662     - id             Pathway graph node ID of the entry
663     - names          List of KEGG identifier(s) from the REACTION database
664     - type           String: reversible or irreversible
665     - substrate      Entry object of the substrate
666     - product        Entry object of the product
667
668    """
669
670    def __init__(self):
671        """Initialize the class."""
672        self._id = None
673        self._names = []
674        self.type = ""
675        self._substrates = set()
676        self._products = set()
677        self._pathway = None
678
679    def __str__(self):
680        """Return an informative human-readable string."""
681        outstr = [
682            "Reaction node ID: %s" % self.id,
683            "Reaction KEGG IDs: %s" % self.name,
684            "Type: %s" % self.type,
685            "Substrates: %s" % ",".join([s.name for s in self.substrates]),
686            "Products: %s" % ",".join([s.name for s in self.products]),
687        ]
688        return "\n".join(outstr) + "\n"
689
690    def add_substrate(self, substrate_id):
691        """Add a substrate, identified by its node ID, to the reaction."""
692        if self._pathway is not None:
693            if int(substrate_id) not in self._pathway.entries:
694                raise ValueError(
695                    "Couldn't add substrate, no node ID %d in Pathway"
696                    % int(substrate_id)
697                )
698        self._substrates.add(substrate_id)
699
700    def add_product(self, product_id):
701        """Add a product, identified by its node ID, to the reaction."""
702        if self._pathway is not None:
703            if int(product_id) not in self._pathway.entries:
704                raise ValueError(
705                    "Couldn't add product, no node ID %d in Pathway" % product_id
706                )
707        self._products.add(int(product_id))
708
709    # The node ID is also the node ID of the Entry that corresponds to the
710    # reaction; we get the corresponding Entry when there is an associated
711    # Pathway
712    def _getid(self):
713        return self._id
714
715    def _setid(self, value):
716        self._id = int(value)
717
718    def _delid(self):
719        del self._id
720
721    id = property(_getid, _setid, _delid, "Node ID for the reaction.")
722
723    # Names may show up as a space-separated list of several KEGG identifiers
724    def _getnames(self):
725        return " ".join(self._names)
726
727    def _setnames(self, value):
728        self._names.extend(value.split())
729
730    def _delnames(self):
731        del self.names
732
733    name = property(
734        _getnames, _setnames, _delnames, "List of KEGG identifiers for the reaction."
735    )
736
737    # products and substrates are read-only properties, returning lists
738    # of Entry objects
739    @property
740    def substrates(self):
741        """Return list of substrate Entry elements."""
742        return [self._pathway.entries[sid] for sid in self._substrates]
743
744    @property
745    def products(self):
746        """Return list of product Entry elements."""
747        return [self._pathway.entries[pid] for pid in self._products]
748
749    @property
750    def entry(self):
751        """Return the Entry corresponding to this reaction."""
752        return self._pathway.entries[self._id]
753
754    @property
755    def reactant_ids(self):
756        """Return a list of substrate and product reactant IDs."""
757        return self._products.union(self._substrates)
758
759    @property
760    def element(self):
761        """Return KGML element describing the Reaction."""
762        # The root is this Relation element
763        reaction = ET.Element("reaction")
764        reaction.attrib = {"id": str(self.id), "name": self.name, "type": self.type}
765        for s in self._substrates:
766            substrate = ET.Element("substrate")
767            substrate.attrib["id"] = str(s)
768            substrate.attrib["name"] = self._pathway.entries[s].name
769            reaction.append(substrate)
770        for p in self._products:
771            product = ET.Element("product")
772            product.attrib["id"] = str(p)
773            product.attrib["name"] = self._pathway.entries[p].name
774            reaction.append(product)
775        return reaction
776
777
778# Relation
779class Relation:
780    """A relationship between to products, KOs, or protein and compound.
781
782    This describes a relationship between two products, KOs, or protein
783    and compound, as described in release KGML v0.7.2
784    (http://www.kegg.jp/kegg/xml/docs/)
785
786    Attributes:
787     - entry1 - The first Entry object node ID defining the
788       relation (int)
789     - entry2 - The second Entry object node ID defining the
790       relation (int)
791     - type - The relation type
792     - subtypes - List of subtypes for the relation, as a list of
793       (name, value) tuples
794
795    """
796
797    def __init__(self):
798        """Initialize the class."""
799        self._entry1 = None
800        self._entry2 = None
801        self.type = ""
802        self.subtypes = []
803        self._pathway = None
804
805    def __str__(self):
806        """Return a useful human-readable string."""
807        outstr = [
808            "Relation (subtypes: %d):" % len(self.subtypes),
809            "Entry1:",
810            str(self.entry1),
811            "Entry2:",
812            str(self.entry2),
813        ]
814        for s in self.subtypes:
815            outstr.extend(["Subtype: %s" % s[0], str(s[1])])
816        return "\n".join(outstr)
817
818    # Properties entry1 and entry2
819    def _getentry1(self):
820        if self._pathway is not None:
821            return self._pathway.entries[self._entry1]
822        return self._entry1
823
824    def _setentry1(self, value):
825        self._entry1 = int(value)
826
827    def _delentry1(self):
828        del self._entry1
829
830    entry1 = property(_getentry1, _setentry1, _delentry1, "Entry1 of the relation.")
831
832    def _getentry2(self):
833        if self._pathway is not None:
834            return self._pathway.entries[self._entry2]
835        return self._entry2
836
837    def _setentry2(self, value):
838        self._entry2 = int(value)
839
840    def _delentry2(self):
841        del self._entry2
842
843    entry2 = property(_getentry2, _setentry2, _delentry2, "Entry2 of the relation.")
844
845    @property
846    def element(self):
847        """Return KGML element describing the Relation."""
848        # The root is this Relation element
849        relation = ET.Element("relation")
850        relation.attrib = {
851            "entry1": str(self._entry1),
852            "entry2": str(self._entry2),
853            "type": self.type,
854        }
855        for (name, value) in self.subtypes:
856            subtype = ET.Element("subtype")
857            subtype.attrib = {"name": name, "value": str(value)}
858            relation.append(subtype)
859        return relation
860