Repository: pybel/pybel

Issue:
Build metagraph from nested statements
Though a graph data structure doesn't directly enable edges to be treated as nodes, edges can carry additional information about transitivity. For example, `A -> (B -> C)` is added to the graph as `A -> B` and `B -> C`, but both edges could include information about inwards transitivity and outwards transitivity.

Each BELGraph could have an additional directed *metagraph* associated with it, where nodes are the hashes of edges in the original graph and edges denote the transitivity between them

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`develop@d4264e3`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pybel/pybel/pull/385/graphs/tree.svg?width=650&token=J7joRTRygG&height=150&src=pr)](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             develop     #385   +/-   ##
==========================================
  Coverage           ?   87.14%           
==========================================
  Files              ?      133           
  Lines              ?     6403           
  Branches           ?      850           
==========================================
  Hits               ?     5580           
  Misses             ?      637           
  Partials           ?      186
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/parser/parse\_bel.py](https://codecov.io/gh/pybel/pybel/pull/385/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9iZWwucHk=) | `96.35% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=footer). Last update [d4264e3...9420979](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`develop@d4264e3`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pybel/pybel/pull/385/graphs/tree.svg?width=650&token=J7joRTRygG&height=150&src=pr)](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             develop     #385   +/-   ##
==========================================
  Coverage           ?   87.14%           
==========================================
  Files              ?      133           
  Lines              ?     6403           
  Branches           ?      850           
==========================================
  Hits               ?     5580           
  Misses             ?      637           
  Partials           ?      186
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/parser/parse\_bel.py](https://codecov.io/gh/pybel/pybel/pull/385/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9iZWwucHk=) | `96.35% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=footer). Last update [d4264e3...9420979](https://codecov.io/gh/pybel/pybel/pull/385?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/pybel/parser/parse_bel.py b/src/pybel/parser/parse_bel.py
index 15af30bd..16f53149 100644
--- a/src/pybel/parser/parse_bel.py
+++ b/src/pybel/parser/parse_bel.py
@@ -262,6 +262,7 @@ class BELParser(BaseParser):
         :param required_annotations: Optional list of required annotations
         """
         self.graph = graph
+        self.metagraph = set()
 
         self.allow_nested = allow_nested
         self.disallow_unqualified_translocations = disallow_unqualified_translocations
@@ -637,17 +638,18 @@ class BELParser(BaseParser):
         if not self.allow_nested:
             raise NestedRelationWarning(self.get_line_number(), line, position)
 
-        self._handle_relation_harness(line, position, {
+        subject_hash = self._handle_relation_checked(line, position, {
             SUBJECT: tokens[SUBJECT],
             RELATION: tokens[RELATION],
             OBJECT: tokens[OBJECT][SUBJECT],
         })
 
-        self._handle_relation_harness(line, position, {
+        object_hash = self._handle_relation_checked(line, position, {
             SUBJECT: tokens[OBJECT][SUBJECT],
             RELATION: tokens[OBJECT][RELATION],
             OBJECT: tokens[OBJECT][OBJECT],
         })
+        self.metagraph.add((subject_hash, object_hash))
         return tokens
 
     def check_function_semantics(self, line: str, position: int, tokens: ParseResults) -> ParseResults:
@@ -784,6 +786,10 @@ class BELParser(BaseParser):
 
         Note: this can't be changed after instantiation!
         """
+        self._handle_relation_checked(line, position, tokens)
+        return tokens
+
+    def _handle_relation_checked(self, line, position, tokens):
         if not self.control_parser.citation:
             raise MissingCitationException(self.get_line_number(), line, position)
 
@@ -794,8 +800,7 @@ class BELParser(BaseParser):
         if missing_required_annotations:
             raise MissingAnnotationWarning(self.get_line_number(), line, position, missing_required_annotations)
 
-        self._handle_relation(tokens)
-        return tokens
+        return self._handle_relation(tokens)
 
     def handle_unqualified_relation(self, _, __, tokens: ParseResults) -> ParseResults:
         """Handle unqualified relations."""


Test patch:
diff --git a/tests/test_parse/test_parse_bel_relations.py b/tests/test_parse/test_parse_bel_relations.py
index 3246177f..03b2102d 100644
--- a/tests/test_parse/test_parse_bel_relations.py
+++ b/tests/test_parse/test_parse_bel_relations.py
@@ -575,6 +575,7 @@ class TestRelations(TestTokenParserBase):
 
         self.assert_has_edge(cat, h2o2)
         self.assert_has_edge(h2o2, apoptosis)
+        self.assertEqual(1, len(self.parser.metagraph))
 
         self.parser.lenient = False