Explore LipiNet#

What this notebook does

Explore the combined LipiNet graph, which currently includes:

  • SwissLipids

  • Rhea

You’ll use OnionNet to load preprocessed nodes/edges, build a multilayer graph, and interactively inspect how layers connect and how information flows through the network.

In this notebook you will:

  • Load LipiNet data via build_lipinet_data() (with optional caching).

  • Build a multilayer graph in OnionNet from the nodes/edges tables.

  • Summarize the network with layer counts and a MetaGraph of layer–layer links.

  • Verify the SwissLipids ↔ Rhea bridge via ChEBI mappings.

  • Run topology-first exploration: filtering by layer, targeted neighborhood search, and shortest paths between nodes of interest.

  • Produce exportable, publication-quality SVG figures.

Tip: Set use_cache=True in build_lipinet_data() to reuse processed data between runs.
Prereqs: graph-tool, onionnet, and network access on the first run to fetch sources.

Setup#

Load Dependencies#

from graph_tool.all import Graph, GraphView, graph_draw
import graph_tool as gt

from onionnet import OnionNet
import onionnet.visualisation

import pandas as pd

from onionnet.property_manager import OnionNetPropertyManager
from onionnet.analytics import layer_stats, plot_layer_metagraph, _infer_family_basic

import onionnet
# … make changes to onionnet/core.py, builder.py, etc. on disk …
import importlib
importlib.reload(onionnet)
# If you also need to reload its submodules:
import onionnet.core, onionnet.builder, onionnet.searcher, onionnet.property_manager
importlib.reload(onionnet.core)
importlib.reload(onionnet.builder)
importlib.reload(onionnet.searcher)
importlib.reload(onionnet.property_manager)
<module 'onionnet.property_manager' from '/Users/agjanyunlu/Documents/Metabolomics/onionnet/onionnet/property_manager.py'>

ℹ️ Note on visualisation if running locally: Note that ReadTheDocs doesn’t always render plots very well, for example scaling png width depending on the view. This can distort the view of plots in the documentation. To overcome this and keep plots high resolution, some graphs in this notebook have been saved as SVGs then rendered in the cells. But on the flip side, this doesn’t always render well locally as some small details on the plots collapse or overlay eachother in Jupyter notebooks. In any case, if you need to view high-resolution, please zoom in, view the original svg file, or if running locally - delete the SVG lines and render with graph_draw() defaults.

import cairo
from IPython.display import SVG

Load LipiNet data#

First, we begin by loading the necessary data for LipiNet, available in a function that compiles the node and edge dataframes from each of the parsed input sources. Currently:

  • SwissLipids

  • Rhea

If this is your first time running this it may take a few minutes to download from each of the sources and parse the data. 🐢

We recommend you set use_cache=True so that subsequent runs can load the preprocessed data much faster (should load in ~5 seconds). 🐇

Optionally, you can force_download from the original sources, but we recommend only doing this every now and then, when you want the latest resources.

from lipinet.build_lipinet import build_lipinet_data

lipinet_data = build_lipinet_data(verbose=True, use_cache=True, force_download=False)
df_nodes = lipinet_data['df_nodes']
df_edges = lipinet_data['df_edges'] 
↪ Loading LipiNet (combined) from cache

Now simply build the network from the node and edges, a super easy process with OnionNet. This should only take just over a minute.

onion = OnionNet()

onion.grow_onion(df_nodes=df_nodes,
           df_edges=df_edges,
           node_prop_cols=df_nodes.columns.to_list(),
           edge_prop_cols=df_edges.columns.to_list(),
           drop_na=True,
           drop_duplicates=True)
Nodes: in=2817072, dropped_na=0, deduped=0 → final=2817072
Edges: in=7002161, dropped_invalid=10222, deduped=0 → final=6991939

That’s it, now we have loaded the processed LipiNet data and created a multilayered network from all the resources using OnionNet. Let’s dig in! 🤓

Explore LipiNet#

Inspecting the LipiNet MetaGraph#

Here we will begin our exploration by creating a MetaGraph of LipiNet, from the layers.

To do this we will first inspect node and edge stats using the OnionNet layer_stats() function.

nodes_by_layer, edges_by_pair = layer_stats(
    df_nodes=df_nodes,
    df_edges=df_edges,
    print_tables=True
)
Node counts by layer:
count
layer
swisslipids 779312
sl_abbreviation 736949
sl_synonyms 534781
sl_metanetx 504880
sl_parent 184620
rhea_reactionid 17783
sl_hmdb 17232
rhea_chebiid 13723
sl_lipidmaps 12112
rhea_ec 6489
sl_chebi 4277
sl_components 1708
sl_components_parsed 1677
sl_pmid 1529
Interlayer edge count: 6114475
Edge counts by (source_layer, target_layer):
edges
source_layer target_layer
swisslipids sl_components 1852844
sl_components_parsed 1852844
sl_abbreviation 786750
swisslipids 779247
sl_synonyms 568257
sl_metanetx 505003
sl_parent 493491
rhea_reactionid rhea_chebiid 83885
swisslipids sl_hmdb 26026
rhea_reactionid rhea_ec 18072
swisslipids sl_lipidmaps 12117
sl_pmid 10109
rhea_ec rhea_ec 6482
swisslipids sl_chebi 4278
sl_chebi rhea_chebiid 2756

Now we will use the OnionNet plot_layer_metagraph() function to get the MetaGraph.

As a small helper function we will colour the MetaGraph by swisslipids or rhea layers.

def my_family(name: str) -> str:
    n = (name or "").lower()
    if n.startswith(("sl_", "swisslipids")): return "sl"
    if n.startswith("rhea"): return "rhea"
    return _infer_family_basic(n)
mg, mg_pos = plot_layer_metagraph(
    edges_by_pair,
    nodes_by_layer=nodes_by_layer,
    node_size_range=(5, 7),
    node_text_size_range=(8, 12),
    edge_width_range=(4, 10),
    node_scaler="log",
    edge_scaler="log",
    show_labels=True,
    # output_size=(1200, 1200),
    node_text_position=-1,
    return_graph=True,
    # pad_label_string=True,
    # vertex_font='consolas',
    family_extractor=my_family,
    family_colors={"sl": (0.2,0.6,0.9,0.9), "rhea": (0.9,0.4,0.1,0.9)}
)

pos_sfdp_metagraph = onionnet.visualisation.load_or_compute_layout(mg, filename='.data/.explore_lipinet_pos_sfdp_metagraph.tsv', override=False) #, inject=mg_pos
../_images/5b439edcdc188b546236493c6b1416d4fc76f758423d26412e4941ee82012fef.png
[load]   Loaded layout for 14 rows from .data/.explore_lipinet_pos_sfdp_metagraph.tsv

Okay, so here we see the different kinds of relationships between layers in LipiNet. Some things we can note:

  • the SwissLipids based layers currently contain many more nodes and data than those from Rhea

  • the SwissLipids ontology includes intra-layer links (within itself) and inter-layer links to lipidmap, HMDB, ChEBI IDs etc, but those layers only have inter-layer links to the SL ontology (except for ChEBI)

  • ChEBI identifiers act as the bridge between SwissLipids and Rhea

  • LipiNet represents Rhea as Reaction IDs that are linked to ChEBI ID participants, and the Enzyme Commission ontology (rhea_ec)

  • Rhea EC is an ontology developed by IUBMB with intra-layer links, for more info see the rhea documentation and ExplorEnz

We can also get more granular details such as node and edge counts for each layer.

plot_layer_metagraph(
    edges_by_pair, nodes_by_layer,
    node_scaler="log", node_size_range=(14, 30),
    show_edge_counts=True,
    show_node_counts=True,
    node_text_position=3, #'centered', #"centered",
    node_label_fmt= "{layer} ({count})",
    edge_scaler="linear", edge_width_range=(3, 10),
    return_graph=False,
    pos=pos_sfdp_metagraph,
    layout=None,
    show_labels=True,
    family_extractor=my_family,
    family_colors={"sl": (0.2,0.6,0.9,0.9), "rhea": (0.9,0.4,0.1,0.9)}
)

This is admittedly a bit cluttered given the number of different layers. Nonetheless, in combination with the results from the layer_stats() function run earlier, this MetaGraph should help get a better understanding of the overall composition of LipiNet, including the number of nodes and edges in or between each layer and the relationships between them.

Filtering LipiNet#

The network we just built contains millions of nodes and edges.

str(onion.g)
'<Graph object, directed, with 2817072 vertices and 6991939 edges, 43 internal vertex properties, 8 internal edge properties, at 0x3feef3e30>'

Let’s use the OnionNet filtering functionality to confirm what we see above and double check how many nodes there are in different layers. To do this, let’s first decode the vertex node_id and layer that were encoded behind the scenes for efficiency when OnionNet built the network.

onion.decode_property_labels_bulk(df_nodes[['node_id','layer']], encoded_prop_type='v')
V property 'node_id_decoded' created successfully.
V property 'layer_decoded' created successfully.
pd.Series(onion.g.vp['layer_decoded']).value_counts()
swisslipids             779312
sl_abbreviation         736949
sl_synonyms             534781
sl_metanetx             504880
sl_parent               184620
rhea_reactionid          17783
sl_hmdb                  17232
rhea_chebiid             13723
sl_lipidmaps             12112
rhea_ec                   6489
sl_chebi                  4277
sl_components             1708
sl_components_parsed      1677
sl_pmid                   1529
Name: count, dtype: int64

We see that there are:

  • 13723 chebi IDs from Rhea

  • 4277 chebi IDs from SwissLipids

We can also filter our network to only include these layers.

First for just swisslipid ChEBIs:

onion.view_layers('sl_chebi')
<GraphView object, directed, with 4277 vertices and 0 edges, 45 internal vertex properties, 8 internal edge properties, edges filtered by (<EdgePropertyMap object with value type 'bool', for Graph 0x178d210a0, at 0x3e04c5bb0>, False), vertices filtered by (<VertexPropertyMap object with value type 'bool', for Graph 0x178d210a0, at 0x3e04c5c70>, False), at 0x178d210a0>

Then for just Rhea ChEBIs:

onion.view_layers('rhea_chebiid')
<GraphView object, directed, with 13723 vertices and 0 edges, 45 internal vertex properties, 8 internal edge properties, edges filtered by (<EdgePropertyMap object with value type 'bool', for Graph 0x3e04c5430, at 0x84afcbfb0>, False), vertices filtered by (<VertexPropertyMap object with value type 'bool', for Graph 0x3e04c5430, at 0x84afc84d0>, False), at 0x3e04c5430>

Then for both SwissLipids or Rhea ChEBIs:

onion.view_layers(['sl_chebi','rhea_chebiid'])
<GraphView object, directed, with 18000 vertices and 2756 edges, 45 internal vertex properties, 8 internal edge properties, edges filtered by (<EdgePropertyMap object with value type 'bool', for Graph 0x16591e780, at 0x448e194c0>, False), vertices filtered by (<VertexPropertyMap object with value type 'bool', for Graph 0x16591e780, at 0x448e19400>, False), at 0x16591e780>

But what if we want to get only the nodes from each of these layers that have connections between them? i.e. the shared connections?

To do this, we can use the filter_edges_between_categories function with mode=both if we don’t mind which direction the connection is coming from, which in this case is arbitrary for LipiNet connections between layers.

onion.filter_edges_between_categories(source_label='sl_chebi', target_label='rhea_chebiid', mode='both')
<GraphView object, directed, with 5512 vertices and 2756 edges, 45 internal vertex properties, 8 internal edge properties, edges filtered by (<EdgePropertyMap object with value type 'bool', for Graph 0x448e1b830, at 0x84afcb0e0>, False), vertices filtered by (<VertexPropertyMap object with value type 'bool', for Graph 0x448e1b830, at 0x178ef7980>, False), at 0x448e1b830>

It looks like there are 2756 edges, and exactly double the number of nodes, which is a pretty strong indication that each node is probably only connected by one edge to a single other node.

The below plot result further helps show us that there are no apparent multi-mapping issues between SwissLipid and Rhea ChEBIs.

graph_draw(onion.filter_edges_between_categories(source_label='sl_chebi', target_label='rhea_chebiid', mode='both'), output_size=(400,400),)
../_images/d80f8dac6055f8a9667201f771e8b8656f3bb447bc7c86f5cadcea985c987fd9.png
<VertexPropertyMap object with value type 'vector<double>', for Graph 0x3feef3e30, at 0x16ac6f260>

This example has helped uncover a helpful insight into the linkage between SL and Rhea using ChEBI. However we should note that we could have easily done the same using a dataframe.

Now will move to more complex examples where network representations are much more useful; this is where LipiNet’s integration of prior knowledge networks starts to shine.

Node traversal#

Often times we may want to identify a node in LipiNet, then find it’s parents or children, and their connections, etc. Or we may want to identify all shortest paths between two or more nodes of interest.

We will consider these two scenarios as:

  1. Searching for a specific node

  2. Calculating shortest paths between two nodes

In the following subsections we will deal with each of these. But before we do, we will define a function here that we can use to quickly plot stylised graphs with custom graphics for the edge and node properties.

def style_graph(graph_to_style, halo_nodes=[], colors=[(1, 1, 0, 0.6)]):
    origin_colors = {
        "swisslipids": (0.988, 0.733, 0.298, 1.0),   # light golden orange
        "rhea": (0.651, 0.807, 0.890, 1.0),          # pale sky blue
        "lipinet": (1.0, 0.4, 0.4, 1.0),             # red
    }
    layer_shapes = {
        'swisslipids': 'circle',
        'rhea_reactionid': 'square', # 'triangle',
        'sl_chebi': 'pentagon',
        'rhea_chebiid': 'pentagon',
        'sl_hmdb': 'triangle', #non-essential
        'sl_pmid': 'triangle', #non-essential
        'sl_synonyms': 'hexagon', #non-essential
    }

    pm = OnionNetPropertyManager(onion.core)
    pm.decode_property_labels_bulk(
        df=df_nodes[['layer', 'origin_vertex', 'node_id']],
        encoded_prop_type='v',
        g=graph_to_style
    )
    pm.decode_property_labels_bulk(
        df=df_edges[['interlayer', 'origin_edge']],
        encoded_prop_type='e',
        g=graph_to_style
    )

    # Nodes
    color_result = onionnet.visualisation.color_nodes(g=graph_to_style, prop_name="origin_vertex_decoded", method="categorical", generate_legend=True, custom_color_dict=origin_colors) # can also just use 'origin_vertex' if you want to use the encoded property
    shape_result = onionnet.visualisation.shape_nodes(g=graph_to_style, prop_name="layer_decoded", shape_method="categorical", generate_legend=True, custom_shape_dict=layer_shapes)
    halo_result  = onionnet.visualisation.add_halos_to_nodes(g=onion.core.graph, nodes=halo_nodes, colors=colors)

    # Edges
    # edges_interlayer_col = onionnet.visualisation.color_edges(g=graph_to_style, prop_name='interlayer', method='boolean')
    edges_origin_col = onionnet.visualisation.color_edges(g=graph_to_style, prop_name='origin_edge_decoded', method='categorical', generate_legend=True, custom_color_dict=origin_colors) # can also just use 'origin_edge' if you want to use the encoded property

    # Create summary dict for convenience
    graphic_styles = {**color_result, **shape_result, **edges_origin_col, **halo_result}

    # Assign some of the properties that we will likely be using often back to the graph
    graph_to_style.vp['v_color_level'] = graphic_styles['v_color']
    graph_to_style.vp['v_shape_layer'] = graphic_styles['v_shape']
    graph_to_style.ep['e_color_inter'] = graphic_styles['e_color']
    graph_to_style.vp['v_halo'] = graphic_styles['v_halo']
    graph_to_style.vp['v_halo_color'] = graphic_styles['v_halo_color']

    return graphic_styles

1. Searching for a specific node (and parents/children)#

Motivation

Let’s say we have a molecule of interest to us: Vitamin A.

More specifically, we are interested in a certain isomer of this: all-trans-retinol. In this form, all exocyclic double bonds are in the E configuration (i.e. trans).

We’ve also know that it is a lipid, and we have found online that it has a CHEBI identifier: CHEBI:17336.

But now we want to answer the following questions:

  • What reactions does this lipid participate in?

  • Does it have a connection in SwissLipids?

  • And if so, where does it sit in the SL hierarchical ontology?

Approach

LipiNet has both SwissLipids and Rhea already integrated, so we will be well equipped to answer these questions.

To do so, we will use OnionNet to:

  1. isolate the node of interest (noi), CHEBI:17336 in this case, which is part of the rhea_chebiid layer

  2. use the OnionNet searcher functionality, with upstream directionality, so that we can get all participating reactions and traverse up the SL ontology

  3. style the graph using our previous helper function, and set the halo to be the noi for clear visualisation

  4. compute and save a reproducible layout for the graph

  5. plot the search results and visualise along with a legend

# search for a specific node (noi = node of interest)
noi = onion.get_vertex_by_name_tuple(layer_name='rhea_chebiid', node_id_str='CHEBI:17336')

search_res = onion.searcher.search(start_node_idx=noi, 
                        max_dist=6, 
                        node_text_prop='node_id_decoded',
                        vertex_text_position=-1,
                        vertex_size=14, 
                        output_size=(1000,1000),
                        direction='upstream',
                        show_plot=False
                        )

graphic_styles = style_graph(search_res, halo_nodes=[noi])
pos_sfdp_searchres_CHEBI17336_traverse = onionnet.visualisation.load_or_compute_layout(search_res, filename='.data/.explore_lipinet_pos_sfdp_searchres_CHEBI17336_traverse.tsv', override=False) # note: this needs to be run _after_ the style_graph function to ensure that the vertex properties like node_id_decoded and layer_decoded are set correctly

graph_draw(search_res,
            pos=pos_sfdp_searchres_CHEBI17336_traverse,
            vertex_text=search_res.vp['node_id_decoded'],
            vertex_text_position=-1,
            vertex_size=5,
            # output_size=(1500,1500),
            # vertex_text_color='black',
            output=".data/fig/explore_lipinet_searchres_CHEBI17336_traverse.svg",            # vector output
            output_size=(500, 400),           # pick the page aspect you want
            antialias=cairo.ANTIALIAS_BEST,   # nicer edges
            ink_scale=1.2,                     # slightly thicker strokes for readability
            # here we add the graphics from above
            vertex_fill_color=search_res.vp['v_color_level'],
            vertex_shape=search_res.vp['v_shape_layer'],
            edge_color=search_res.ep['e_color_inter'],
            vertex_halo=search_res.vp['v_halo'],
            vertex_halo_color=search_res.vp['v_halo_color'], 
)

# Note since we used a custom color dict, we can also use the legend function to get the legend for the node and edge colors, whcih should both be the same
# onionnet.visualisation.get_legend(source=graphic_styles['legend_node_color'], title='Legend: Node Origin Colors')
# onionnet.visualisation.get_legend(source=graphic_styles['legend_edge_color'], title='Legend: Edge Origin Colors')
onionnet.visualisation.get_legend(source=graphic_styles['legend_edge_color'], title='Color Legend:\nNode & Edge Origin')
onionnet.visualisation.get_legend(source=graphic_styles['legend_node_shape'], title='Shape Legend:\nNode Layer')
SVG(".data/fig/explore_lipinet_searchres_CHEBI17336_traverse.svg")
Filtered graph contains 38 vertices and 38 edges.
V property 'layer_decoded' created successfully.
V property 'origin_vertex_decoded' created successfully.
V property 'node_id_decoded' created successfully.
interlayer prop left as is, no decoding needed (not an object type)
E property 'origin_edge_decoded' created successfully.
[load]   Loaded layout for 38 rows from .data/.explore_lipinet_pos_sfdp_searchres_CHEBI17336_traverse.tsv
../_images/a7cf6a290f6a73273a290dcb0298750675f349faae1ec4a52c35b6acb18b05dc.png ../_images/b6b1f429a25eb993e51c8ba6870abcbef5306d5be734714cc3d653220564047e.png ../_images/1ccf0c20acc9dad91c8f0a41d9ae03a84bd992f6276a6ba061fa4391a958649f.svg

Results

With this quick search and plot we can now answer each of our original questions

  • What reactions does this lipid participate in?

    • CHEBI:17336 (i.e. all-trans-retinol) participates in a large number of different reactions (see plot for exact Rhea identifiers).

    • For example RHEA:13933, one of the nodes towards the bottom of the Rhea hub, which is the undirectional reaction for:

      all-trans-retinyl hexadecanoate + H₂O = all-trans-retinol + hexadecanoate + H⁺

  • Does it have a connection in SwissLipids?

    • Yes, CHEBI:17336 is linked to both SLM:000000511 and SLM:000598072 from SL.

  • And if so, where does it sit in the SL hierarchical ontology?

    • If we look this up online we can see that SLM:000000511 (all-trans-retinol) is an isomeric subspecies. Whereas SLM:000598072 (all-trans-retinol–[retinol-binding protein]) does not have a SL associated with it.

Advanced insights

This example also hints at why making this connection using LipiNet can be helpful to trace from nodes higher in the hierarchy to those lower, since higher levels are less likely to be directly associated with a ChEBI ID.

For instance, if we had experimentally measured a higher class such as SLM:000508854 (Retinoids), which when we look up online on SL does actually have a CHEBI ID too (CHEBI:26537), it does not have any interactions/reactions linked to that entity.

Screenshot from SwissLipids SLM000508854 search, for comparative purposes. 13 Aug 2025.

However if we search Rhea directly for CHEBI:26537 we currently get 86 reactions in our results (as of 12 Aug 2025). And furthermore, these results appear to be for specific isomers or forms of Retinoids.

So what is going on here? Why does SwissLipids not have any links to Rhea for the lipid class with that ChEBI ID, but Rhea is returning results for that same ChEBI ID?

Screenshot from Rhea CHEBI26537 search, for comparative purposes. 13 Aug 2025.

The reason for this discrepancy is that technically Rhea is computed at the exact level, i.e. the major microspecies at pH 7.3, to make it more biologically realistic.

But often, the measurements we have of lipidomics in real world settings are not at the isomeric level.

Rhea acknowledges this complexity and deals with it by using the ChEBI ontology to find specific reactions and participants from more general terms. For instance, the Rhea search functionality can use lipid to find all reactions involving lipids, by using the ChEBI ontology.

Screenshot from Rhea search chebi about, for comparative purposes. 13 Aug 2025.

In contrast, it appears the SwissLipids browser does not (currently) have this built in capability on their web browser (and it could be argued that it could be needlessly inefficient in that scenario anyway).

Nonetheless, this is exactly the point: LipiNet can, and should, be used to make possible connections between higher levels of the ontology and the prior knowledge. Without making these kind of connections and inferring such links, we will often miss plausible biological links for our analysis.

This is further exemplified in the plot below.

# search for a specific node (noi = node of interest)
noi = onion.get_vertex_by_name_tuple(layer_name='swisslipids', node_id_str='SLM:000508854') # higher level node, SL Retinoid entry
noi2 = onion.get_vertex_by_name_tuple(layer_name='rhea_chebiid', node_id_str='CHEBI:17336') # Rhea CHEBI ID with connection to lower level SL CHEBI ID (noi3)
noi3 = onion.get_vertex_by_name_tuple(layer_name='sl_chebi', node_id_str='17336') # SL CHEBI ID with connection to lower level SL all-trans-retinol entry
noi4 = onion.get_vertex_by_name_tuple(layer_name='sl_chebi', node_id_str='26537') # SL CHEBI ID with connection to SL Retinoid entry (noi)


search_res = onion.searcher.search(start_node_idx=noi, 
                        max_dist=6, 
                        node_text_prop='node_id_decoded',
                        vertex_text_position=-1,
                        vertex_size=14, 
                        output_size=(1000,1000),
                        direction='downstream',
                        show_plot=False
                        )

graphic_styles = style_graph(search_res, halo_nodes=[noi, noi2, noi3, noi4], colors=[(1, 0, 0, 0.6), (1, 1, 0, 0.6), (1, 1, 0, 0.6), (1, 0, 1, 0.6)])
pos_sfdp_searchres_SLM000508854_traverse = onionnet.visualisation.load_or_compute_layout(search_res, filename='.data/.explore_lipinet_pos_sfdp_searchres_SLM000508854_traverse.tsv', override=False) # note: this needs to be run _after_ the style_graph function to ensure that the vertex properties like node_id_decoded and layer_decoded are set correctly

graph_draw(search_res,
            pos=pos_sfdp_searchres_SLM000508854_traverse,
            vertex_text=search_res.vp['node_id_decoded'],
            vertex_text_position=-1,
            vertex_size=3.5,
            # output_size=(1500,1500),
            # vertex_text_color='black',
            output=".data/fig/explore_lipinet_searchres_SLM000508854_traverse.svg",            # vector output
            output_size=(500, 400),           # pick the page aspect you want
            antialias=cairo.ANTIALIAS_BEST,   # nicer edges
            ink_scale=1.2,                     # slightly thicker strokes for readability
            # here we add the graphics from above
            vertex_fill_color=search_res.vp['v_color_level'],
            vertex_shape=search_res.vp['v_shape_layer'],
            edge_color=search_res.ep['e_color_inter'],
            vertex_halo=search_res.vp['v_halo'],
            vertex_halo_color=search_res.vp['v_halo_color'], 
)

onionnet.visualisation.get_legend(source=graphic_styles['legend_edge_color'], title='Color Legend:\nNode & Edge Origin')
onionnet.visualisation.get_legend(source=graphic_styles['legend_node_shape'], title='Shape Legend:\nNode Layer')
SVG(".data/fig/explore_lipinet_searchres_SLM000508854_traverse.svg")
Filtered graph contains 142 vertices and 231 edges.
V property 'layer_decoded' created successfully.
V property 'origin_vertex_decoded' created successfully.
V property 'node_id_decoded' created successfully.
interlayer prop left as is, no decoding needed (not an object type)
E property 'origin_edge_decoded' created successfully.
[load]   Loaded layout for 142 rows from .data/.explore_lipinet_pos_sfdp_searchres_SLM000508854_traverse.tsv
../_images/a7cf6a290f6a73273a290dcb0298750675f349faae1ec4a52c35b6acb18b05dc.png ../_images/b6b1f429a25eb993e51c8ba6870abcbef5306d5be734714cc3d653220564047e.png ../_images/ea4785714c4287a1624ceaa60e73dc315e6b8b3d51e8b4dfb20032e129d565b7.svg

Conclusion

This plot above confirms that the SL Retinoid class (SLM:000508854)(red halo), connects directly to the SL CHEBI:26537 ID. However the CHEBI:26537 ID (purple halo) does not directly link to any downstream entities or reactions, etc. When we search for it in the graph for the rhea chebi ID layer, it does not return anything.

try:
    onion.get_vertex_by_name_tuple(layer_name='rhea_chebiid', node_id_str='CHEBI:26537')
except Exception as e:
    print("❗ Could not find the node with id 'CHEBI:26537' on the `rhea_chebiid` layer in the graph. Please check the node id and layer name.")
    # raise e
❗ Could not find the node with id 'CHEBI:26537' on the `rhea_chebiid` layer in the graph. Please check the node id and layer name.

Whereas in contrast, the more specific SL all-trans-retinol isomeric subspecies (SLM:000000511) links directly to the SL CHEBI:17336 ID (yellow halo). This in turn is linked to the Rhea CHEBI:17336 entry, and from there to all the downstream Rhea reactions.

This highlights the importance of using ontologies such as ChEBI or SwissLipids to link terms. Some tools already have this functionality baked in to their back-ends, but LipiNet aims to support this more broadly across various ontologies, for direct use in analyses and visualisations.

2. Calculating shortest paths between two nodes of interest#

Motivation

In the first plot from our previous example, we saw how our node of interest (CHEBI:17336), eventually connected back to the root ‘Lipid’ node from SwissLipids (SLM:000389145).

Along the path, we saw that it was also connected to other nodes in the SL hierarchy, like all-trans-retinol, the retinoid class, and other in the hierarchy.

But what if we also wanted to find other possible connections back to the root node?

Approach

We can find all possible shortest paths between two nodes of interest in LipiNet by using the OnionNet searcher.compute_on_shortest() function.

  1. Isolate the nodes of interest

  2. Pass these to the compute_on_shortest method

noi_a = onion.get_vertex_by_name_tuple(layer_name='swisslipids', node_id_str='SLM:000389145')
noi_b = onion.get_vertex_by_name_tuple(layer_name='rhea_chebiid', node_id_str='CHEBI:17336')

search_res = onion.searcher.compute_on_shortest(g=onion.g, source=noi_a, targets=[noi_b], return_gv=True, directed=False)
print(search_res)

graphic_styles = style_graph(search_res, halo_nodes=[noi_a, noi_b], colors=[(1, 1, 0, 0.6), (0, 1, 1, 0.6)])
pos_sfdp_searchres_SLM000389145_CHEBI17336 = onionnet.visualisation.load_or_compute_layout(search_res, filename='.data/.explore_lipinet_pos_sfdp_searchres_SLM000389145_CHEBI17336.tsv', override=False) # note: this needs to be run _after_ the style_graph function to ensure that the vertex properties like node_id_decoded and layer_decoded are set correctly

graph_draw(search_res,
            pos = pos_sfdp_searchres_SLM000389145_CHEBI17336,
            vertex_text=search_res.vp['node_id_decoded'],
            vertex_text_position=-1,
            vertex_size=5, #17
            # output_size=(1500,1500),
            vertex_text_color='black',
            # Using SVG function output for docs
            output=".data/fig/explore_lipinet_searchres_SLM000389145_CHEBI17336.svg",            # vector output
            output_size=(500, 400),           # pick the page aspect you want
            antialias=cairo.ANTIALIAS_BEST,   # nicer edges
            ink_scale=1.2,                     # slightly thicker strokes for readability
            # here we add the graphics from above
            vertex_fill_color=search_res.vp['v_color_level'],
            vertex_shape=search_res.vp['v_shape_layer'],
            edge_color=search_res.ep['e_color_inter'],
            vertex_halo=search_res.vp['v_halo'],
            vertex_halo_color=search_res.vp['v_halo_color'], 
)
onionnet.visualisation.get_legend(source=graphic_styles['legend_edge_color'], title='Color Legend:\nNode & Edge Origin')
onionnet.visualisation.get_legend(source=graphic_styles['legend_node_shape'], title='Shape Legend:\nNode Layer')
SVG(".data/fig/explore_lipinet_searchres_SLM000389145_CHEBI17336.svg")
<GraphView object, directed, with 16 vertices and 17 edges, 47 internal vertex properties, 8 internal edge properties, edges filtered by (<EdgePropertyMap object with value type 'bool', for Graph 0x16ac6d5e0, at 0x13dfc8dd0>, False), vertices filtered by (<VertexPropertyMap object with value type 'bool', for Graph 0x16ac6d5e0, at 0x13dfcb1d0>, False), at 0x16ac6d5e0>
V property 'layer_decoded' created successfully.
V property 'origin_vertex_decoded' created successfully.
V property 'node_id_decoded' created successfully.
interlayer prop left as is, no decoding needed (not an object type)
E property 'origin_edge_decoded' created successfully.
[load]   Loaded layout for 16 rows from .data/.explore_lipinet_pos_sfdp_searchres_SLM000389145_CHEBI17336.tsv
../_images/a7cf6a290f6a73273a290dcb0298750675f349faae1ec4a52c35b6acb18b05dc.png ../_images/b6b1f429a25eb993e51c8ba6870abcbef5306d5be734714cc3d653220564047e.png ../_images/dd9dfc62d81303e56e6d989d179b0cf9df07c47ff2904ccfdce9646bc597c4a5.svg

From this graph we can make the following observations:

  • CHEBI:17336 (all-trans-retinol) has only 1 direct connection back to SwissLipids (which was already confirmed in our earlier analysis)

  • two of the other shortest paths back to the SL root node are formed from lipids that are also co-participants in reactions with CHEBI:17336

These two other shortest paths aren’t all that informative in this case, but highlight an interesting point: many lipid reactions (or reactions in general) involve products that are extremely unspecific and/or cannot be easily measured in reactions. Running shortest paths in this case is also more likely to return these kind of linkages passing through unspecific molecules to the user because these link to very high levels of the corresponding swisslipids ontology. This isn’t a bug, but is something we might want to consider when evaluating results. In the future, LipiNet may also support the optional removal of these kinds of unspecific molecules - however knowing where to draw the line could be impossible to determine, and for now we leave it to user discretion to keep this in mind when analysing their data.

We can also find shortest paths between say a reaction and the lipid root node, in a similar way to before.

noi_a =  onion.get_vertex_by_name_tuple(layer_name='swisslipids', node_id_str='SLM:000389145')
noi_b = onion.get_vertex_by_name_tuple(layer_name='rhea_reactionid', node_id_str='RHEA:19193')

search_res = onion.searcher.compute_on_shortest(g=onion.g, source=noi_a, targets=[noi_b], return_gv=True, directed=False)
print(search_res)

style_graph(search_res, halo_nodes=[noi_a, noi_b], colors=[(1, 1, 0, 0.6), (0, 1, 1, 0.6)])
pos_sfdp_searchres_SLM000389145_RHEA19193 = onionnet.visualisation.load_or_compute_layout(search_res, filename='.data/.explore_lipinet_pos_sfdp_searchres_SLM000389145_RHEA19193.tsv', override=False)


graph_draw(search_res,
            pos = pos_sfdp_searchres_SLM000389145_RHEA19193,
            vertex_text=search_res.vp['node_id_decoded'],
            vertex_text_position=-1,
            vertex_size=5, #17,
            # output_size=(1500,1500),
            vertex_text_color='black',
            # Using SVG function output for docs
            output=".data/fig/explore_lipinet_searchres_SLM000389145_RHEA19193.svg",            # vector output
            output_size=(500, 400),           # pick the page aspect you want
            antialias=cairo.ANTIALIAS_BEST,   # nicer edges
            ink_scale=1.2,                     # slightly thicker strokes for readability
            # here we add the graphics from above
            vertex_fill_color=search_res.vp['v_color_level'],
            vertex_shape=search_res.vp['v_shape_layer'],
            edge_color=search_res.ep['e_color_inter'],
            vertex_halo=search_res.vp['v_halo'],
            vertex_halo_color=search_res.vp['v_halo_color'], 
)
onionnet.visualisation.get_legend(source=graphic_styles['legend_edge_color'], title='Color Legend:\nNode & Edge Origin')
onionnet.visualisation.get_legend(source=graphic_styles['legend_node_shape'], title='Shape Legend:\nNode Layer')
SVG(".data/fig/explore_lipinet_searchres_SLM000389145_RHEA19193.svg")
<GraphView object, directed, with 24 vertices and 27 edges, 47 internal vertex properties, 8 internal edge properties, edges filtered by (<EdgePropertyMap object with value type 'bool', for Graph 0x448d87c80, at 0x13e028830>, False), vertices filtered by (<VertexPropertyMap object with value type 'bool', for Graph 0x448d87c80, at 0x13e02bb90>, False), at 0x448d87c80>
V property 'layer_decoded' created successfully.
V property 'origin_vertex_decoded' created successfully.
V property 'node_id_decoded' created successfully.
interlayer prop left as is, no decoding needed (not an object type)
E property 'origin_edge_decoded' created successfully.
[load]   Loaded layout for 24 rows from .data/.explore_lipinet_pos_sfdp_searchres_SLM000389145_RHEA19193.tsv
../_images/a7cf6a290f6a73273a290dcb0298750675f349faae1ec4a52c35b6acb18b05dc.png ../_images/b6b1f429a25eb993e51c8ba6870abcbef5306d5be734714cc3d653220564047e.png ../_images/d9d1c884ed1e28e56cec8e21e4a3990a1945b2d88932ded6f6264e0053e8343f.svg

Lastly, another way we can visualise this is by layer.

pos_by_layer = onionnet.visualisation.layout_by_layer(search_res, layer_prop_name='layer', spacing=20, epsilon=10)
graph_draw(search_res,
            pos=pos_by_layer,
            output_size=(1000, 1000),
            edge_pen_width=3,
            vertex_size=8,
            vertex_text=search_res.vp['node_id_decoded'],
            vertex_fill_color=search_res.vp['v_color_level'],
            vertex_shape=search_res.vp['v_shape_layer'],
            edge_color=search_res.ep['e_color_inter'],
            vertex_halo=search_res.vp['v_halo'],
            vertex_halo_color=search_res.vp['v_halo_color'], 
            nodesfirst=False)
../_images/2abe47e68ff8c67aa7454e54730d2486dc8a11703ab71ec1e8404157e6bcb90b.png
<VertexPropertyMap object with value type 'vector<double>', for Graph 0x3e04c78f0, at 0x867893800>

Conclusion#

In this notebook you:

  • Loaded the combined LipiNet tables and built a multilayer graph in OnionNet.

  • Summarized structure with a MetaGraph, then explored layers via filtering.

  • Verified SwissLipids ↔ Rhea linkage through ChEBI and inspected neighborhoods.

  • Computed shortest paths to trace plausible routes across layers.

  • Exported clean, publication-ready figures (SVG) to document findings.

Key takeaways

  • LipiNet gives you a wiring diagram (i.e. ‘MetaGraph’) across ontologies: SL classes/species ⇄ ChEBI ⇄ Rhea reactions ⇄ EC.

  • Many hubs (generic CHEBI or high-level SL terms) are expected; use layer filters and path constraints to focus on specific chemistry.

  • Caching (use_cache=True) makes iteration fast; use force_download=True only when you want fresh sources.

Caveats & tips

  • Directionality: Rhea provides directional & undirected variants, so pick what matches your analysis.

  • Hub bias: co-participants like “fatty acid / CoA / H₂O” can dominate shortest paths, so consider masking or weighting.

  • ID granularity: SL class nodes may map to broad CHEBI; specificity improves when you work at species/subspecies.

Where to go next

  • QC & robustness: articulation points, bridges, k-core/onion layers.

  • Projection: CHEBI-CHEBI co-reaction network + communities.

  • Enrichment: EC over-representation for lipid sets.

  • Paths: edge-disjoint or constrained shortest paths (e.g., avoid hubs).

  • How-to: see parse_* and build_lipinet notebooks for reproducible rebuilds and caching details.