Repository: xgi-org/xgi

Issue:
BiGG dataset: reversed directionality of some reactions/hyperedges
Hello, I saw that you implemented a function to read the BiGG dataset as a directed hypergraph. I have been working with this dataset for some months and I have some comments that hopefully could be useful. I believe that some reactions are not being read with the correct direction in the BiGG dataset.
 For example, in the **e_coli_core.json** model, the reaction **“FORt: 1for_c -> 1for_e”** is currently read by putting for_c in the head and for_e in the tail of the hyperedge (you can check the correct directionality of this reaction [here](https://sbrg.github.io/escher-fba/#/app) or [here](http://bigg.ucsd.edu/models/e_coli_core/reactions/FORt).). This happens because xgi relies just on the stoichiometry weights when reading reactions, indeed **r['metabolites']={'for_c': 1.0, 'for_e': -1.0}** for FORt. Xgi should also look at the values of **r["lower_bound"]** and **r["upper_bound"]**. In the case of FORt the **lower_bound= -1000** and **upper_bound = 0**, indicating that the real orientation of the reaction is the reversed one.
Here is a code I wrote to reorder the direction of the reactions: 
```python

def reorder_reactions_df(reactions_df, verbose = True):
    reordered = [False]*len(reactions_df)
    for i in reactions_df.index:
        if reactions_df['lower_bound'].get(i)<0 and reactions_df['upper_bound'].get(i)<=0: # so the direction is <---
            mets = reactions_df['metabolites'][i].keys()
            for met in mets:
                reactions_df['metabolites'][i][met] *= -1  ## now the direction is correct
            reordered[i] = True 
            if verbose:
                print(reactions_df['id'][i], 'has been reordered')
            ## update the bound
            reactions_df.loc[i,'lower_bound'] *= -1
            reactions_df.loc[i,'upper_bound'] *= -1
    return reordered
```
where, I believe, reactions_df is equivalent to the xgi d_model["reactions”].

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/xgi-org/xgi/pull/459?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org) Report
Patch coverage: **`92.30%`** and project coverage change: **`-0.01%`** :warning:
> Comparison is base [(`2962f82`)](https://app.codecov.io/gh/xgi-org/xgi/commit/2962f82e83473bf74ba8a534720a258b4247321b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org) 91.86% compared to head [(`86842b2`)](https://app.codecov.io/gh/xgi-org/xgi/pull/459?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org) 91.85%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #459      +/-   ##
==========================================
- Coverage   91.86%   91.85%   -0.01%     
==========================================
  Files          60       60              
  Lines        4300     4307       +7     
==========================================
+ Hits         3950     3956       +6     
- Misses        350      351       +1     
```


| [Files Changed](https://app.codecov.io/gh/xgi-org/xgi/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org) | Coverage Δ | |
|---|---|---|
| [xgi/readwrite/bigg\_data.py](https://app.codecov.io/gh/xgi-org/xgi/pull/459?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org#diff-eGdpL3JlYWR3cml0ZS9iaWdnX2RhdGEucHk=) | `97.82% <92.30%> (-2.18%)` | :arrow_down: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/xgi-org/xgi/pull/459?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xgi-org).


Patch:
diff --git a/docs/source/contributors.rst b/docs/source/contributors.rst
index 06cf2fd..2d41111 100644
--- a/docs/source/contributors.rst
+++ b/docs/source/contributors.rst
@@ -110,6 +110,13 @@ Contributors
         <a href="https://github.com/marconurisso">@marconurisso</a>
     </p>
 
+    <p>
+        <img src="https://github.com/pietrotraversa.png"
+            width="50" height="50" hspace="5"
+            style="border-radius: 50%; vertical-align: middle;"/>
+        <a href="https://github.com/pietrotraversa">@pietrotraversa</a>
+    </p>
+
     <p>
         <img src="https://github.com/saad1282.png"
             width="50" height="50" hspace="5"
diff --git a/xgi/core/hypergraph.py b/xgi/core/hypergraph.py
index a4aa50a..3d1539b 100644
--- a/xgi/core/hypergraph.py
+++ b/xgi/core/hypergraph.py
@@ -273,11 +273,12 @@ class Hypergraph:
         [{1, 2}, {2, 3}, {1, 3, 4}]
         """
         tempH = Hypergraph()
-        tempH.add_edges_from(zip(self._edge.values(), self._edge_attr.values()))
+
         tempH.add_nodes_from(zip(self._node.keys(), self._node_attr.values()))
+        tempH.add_nodes_from(zip(H2._node.keys(), H2._node_attr.values()))
 
+        tempH.add_edges_from(zip(self._edge.values(), self._edge_attr.values()))
         tempH.add_edges_from(zip(H2._edge.values(), H2._edge_attr.values()))
-        tempH.add_nodes_from(zip(H2._node.keys(), H2._node_attr.values()))
 
         tempH._hypergraph = deepcopy(self._hypergraph)
         tempH._hypergraph.update(deepcopy(H2._hypergraph))
diff --git a/xgi/readwrite/bigg_data.py b/xgi/readwrite/bigg_data.py
index c885bf2..77fb712 100644
--- a/xgi/readwrite/bigg_data.py
+++ b/xgi/readwrite/bigg_data.py
@@ -84,6 +84,15 @@ def _bigg_to_dihypergraph(d_index, d_model):
     -------
     DiHypergraph
         The dihypergraph from the selected BIGG model.
+
+    Notes
+    -----
+    The code for parsing a metabolic reaction is rewritten
+    from a function by @pietrotraversa.
+
+    We use the `lower_bound` and `upper_bound` variables to
+    determine whether a reaction is a forward, reverse,
+    or reversible reaction.
     """
     from .. import DiHypergraph
 
@@ -100,17 +109,54 @@ def _bigg_to_dihypergraph(d_index, d_model):
         DH.add_node(m["id"], name=m["name"])
 
     for r in d_model["reactions"]:
-        head = set()
-        tail = set()
-        for m, val in r["metabolites"].items():
-            if val > 0:
-                head.add(m)
-            else:
-                tail.add(m)
-
-        if not head and not tail:
-            warn(f"{r['id']} is an empty reaction!")
-            continue
-        DH.add_edge((tail, head), id=r["id"], name=r["name"])
+        l = r["lower_bound"]
+        u = r["upper_bound"]
+
+        reactants = set()
+        products = set()
+
+        # forward direction
+        if l >= 0 and u > 0:
+            for m, val in r["metabolites"].items():
+                if val > 0:
+                    products.add(m)
+                elif val <= 0:
+                    reactants.add(m)
+
+            if not reactants and not products:
+                warn(f"{r['id']} is an empty reaction!")
+                continue
+            DH.add_edge((reactants, products), id=r["id"], name=r["name"])
+
+        # reverse direction
+        if l < 0 and u <= 0:
+            for m, val in r["metabolites"].items():
+                if val >= 0:
+                    reactants.add(m)
+                elif val < 0:
+                    products.add(m)
+
+            if not reactants and not products:
+                warn(f"{r['id']} is an empty reaction!")
+                continue
+            DH.add_edge((reactants, products), id=r["id"], name=r["name"])
+
+        # reversible
+        if l < 0 and u > 0:
+            for m, val in r["metabolites"].items():
+                if val > 0:
+                    products.add(m)
+                elif val < 0:
+                    reactants.add(m)
+
+            if not reactants and not products:
+                warn(f"{r['id']} is an empty reaction!")
+                continue
+            # add forward reaction
+            DH.add_edge((reactants, products), id=r["id"], name=r["name"])
+            # add reverse reaction
+            DH.add_edge(
+                (products, reactants), id=str(r["id"]) + "_reverse", name=r["name"]
+            )
 
     return DH


Test patch:
diff --git a/tests/core/test_hypergraph.py b/tests/core/test_hypergraph.py
index 142156d..dcc7f17 100644
--- a/tests/core/test_hypergraph.py
+++ b/tests/core/test_hypergraph.py
@@ -72,7 +72,9 @@ def test_lshift(edgelist1, edgelist2, hyperwithattrs):
     H2 = xgi.Hypergraph(edgelist2)
     H3 = hyperwithattrs
     H = H1 << H2
-    assert set(H.nodes) == {1, 2, 3, 4, 5, 6, 7, 8}
+    nodes1 = list(H1.nodes)
+    assert nodes1 == [1, 2, 3, 4, 5, 6, 8, 7]
+    assert list(H.nodes) == nodes1
     assert H.num_edges == 7
     assert H.edges.members(0) == {1, 2, 3}
 
diff --git a/tests/readwrite/test_bigg_data.py b/tests/readwrite/test_bigg_data.py
index fbaf23d..da8fded 100644
--- a/tests/readwrite/test_bigg_data.py
+++ b/tests/readwrite/test_bigg_data.py
@@ -10,7 +10,7 @@ def test_load_bigg_data(capfd):
     # test loading the online data
     H1 = load_bigg_data("iAF1260", cache=False)
     assert H1.num_nodes == 1668
-    assert H1.num_edges == 2382
+    assert H1.num_edges == 2953
     assert H1["name"] == "iAF1260"
     assert H1["organism"] == "Escherichia coli str. K-12 substr. MG1655"
     assert H1.nodes["2agpg161_c"] == {
@@ -35,4 +35,13 @@ def test_411():
         H = load_bigg_data("iCN718")
     assert H["name"] == "iCN718"
     assert H.num_nodes == 888
-    assert H.num_edges == 1014
+    assert H.num_edges == 1436
+
+
+def test_458():
+    H = load_bigg_data("e_coli_core")
+    assert H.edges.dimembers("FORt") == ({"for_c"}, {"for_e"})
+    assert H.edges.dimembers("PFK") == ({"atp_c", "f6p_c"}, {"adp_c", "fdp_c", "h_c"})
+
+    assert H.edges.dimembers("CO2t") == ({"co2_e"}, {"co2_c"})
+    assert H.edges.dimembers("CO2t_reverse") == ({"co2_c"}, {"co2_e"})