logo Netzschleuder network catalog, repository, and centrifuge

History of intact

The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.

[entries] New entry: intact by Tiago de Paula Peixoto 92c9856
diff --git a/entries/intact/__init__.py b/entries/intact/__init__.py
new file mode 100644
index 0000000..a2dbf4b
--- /dev/null
+++ b/entries/intact/__init__.py
@@ -0,0 +1,268 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2020-2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .. import *
+import io
+import re
+import numpy as np
+import pandas as pd
+import pyarrow as pa
+import pyarrow.csv
+
+title = "IntAct molecular interactions (2026)"
+description = r"""The molecular interactions curated by the IntAct database of the European Bioinformatics Institute (EBI), together with those of the other databases of the IMEx consortium that IntAct hosts (e.g. MINT, UniProt, MatrixDB). Curators extract the interactions from the literature and from direct submissions, with details of the experiments in which they were observed. Nodes are interacting molecules (mostly proteins, of any organism, but also nucleic acids, small molecules, complexes, etc.), and an undirected edge is an interaction between two of them. Interactions involving more than two molecules (e.g. complexes pulled down with a bait) are expanded by IntAct into binary interactions, typically between the bait and each of the other molecules ("spoke expansion"). This is the release of 17 October 2026, in the PSI-MITAB format.
+
+There are four networks of the current release:
+
+- `evidence`: one edge per binary interaction evidence (an interaction observed in one experiment), so that two molecules can be connected by many edges.
+- `clustered`: the evidences clustered by IntAct into one edge per pair of molecules, with the MI score summarizing all the evidence for it.
+- `evidence_negative`, `clustered_negative`: the same, for the negative interactions, i.e. pairs of molecules reported not to interact.
+
+Interactions of a molecule with itself are self-loops.
+
+The entry also contains the networks of earlier releases, named by their date: `evidence_<date>` and `clustered_<date>` (e.g. `evidence_2024-11-29`), built in the same way. IntAct made about 250 releases since 2006, and the last release of each year from 2008 to 2025 is included: 2008-12-12 (only `clustered`: the file of that release has one line per pair of molecules), 2009-11-27, 2010-12-15, 2011-11-18, 2012-12-14, 2013-11-20, 2014-12-18, 2015-11-26, 2016-12-12, 2017-12-12, 2018-11-30, 2019-12-16, 2020-12-16, 2021-10-13, 2022-07-11, 2023-10-06, 2024-11-29 and 2025-08-08. The releases of 2006 and 2007 are only available in PSI-MI XML (not in PSI-MITAB), and are not included, nor are the negative interactions of the earlier releases. The files of the earlier releases differ in a few ways: until 2011, they are in the earlier PSI-MITAB 2.5 format, where the short name of a molecule is taken from its first alias (e.g. `Lck` from `uniprotkb:Lck`), the expansion method is given as a word (e.g. `Spoke`), and there are no MI scores (`miscore` is NaN); the clustered files of 2008 to 2011 merge the evidences for each pair of molecules into one line, whose attributes are the lists of those of the evidences; and the clustered files of 2012 to 2023 (in the simpler 15-column PSI-MITAB 2.5 format) give neither the types of the molecules nor the expansion methods (`type` and `expansion` are empty). A few lines of the file of 2008 are broken by line breaks within annotations; they are joined.
+
+The networks have the following properties:
+
+- `id`: the identifier of the molecule (e.g. `uniprotkb:P49418`, `chebi:"CHEBI:15422"`, `intact:EBI-...`).
+- `name`: the short name of the molecule (e.g. the gene name `AMPH`); empty if not given.
+- `taxid`: the NCBI taxonomy identifier of the molecule's organism (e.g. 9606 for human; negative values for special cases, e.g. -1 for in vitro, -2 for chemical synthesis; 0 if not given).
+- `organism`: the name of the organism, as given; empty if not given.
+- `type`: the type of molecule (e.g. `protein`, `small molecule`, `ribonucleic acid`); empty if not given.
+- `detection_method` (edges): the method(s) with which the interaction was detected (e.g. `two hybrid`, `anti bait coimmunoprecipitation`), separated by `|`.
+- `interaction_type` (edges): the type(s) of interaction (e.g. `physical association`, `direct interaction`, `phosphorylation reaction`), separated by `|`.
+- `publication` (edges): the identifiers of the publications (e.g. `pubmed:10542231`), separated by `|`.
+- `miscore` (edges): the IntAct MI score, a confidence score between 0 and 1 based on the evidence (in `evidence`, the score of the pair, rounded to two digits); NaN if not given (e.g. in the releases before 2012).
+- `interaction_id` (edges): the identifier(s) of the interaction evidence(s) in IntAct (e.g. `intact:EBI-7121552`), separated by `|`.
+- `expansion` (edges): the expansion method by which the binary interaction was derived (e.g. `spoke expansion`); empty if none."""
+tags = ['Biological', 'Protein interactions', 'Weighted', 'Multigraph', 'Metadata']
+url = 'https://www.ebi.ac.uk/intact/'
+citation = [('N. del Toro et al., "The IntAct database: efficient access to fine-grained molecular interaction data." Nucleic Acids Research 50, D648-D653 (2022)', 'https://doi.org/10.1093/nar/gkab1006')]
+icon_hash = None
+upstream_license = ("CC BY 4.0", "https://creativecommons.org/licenses/by/4.0/")
+upstream_prefix = 'https://ftp.ebi.ac.uk/pub/databases/intact/2026-10-17/psimitab'
+files = [("intact.zip:intact.txt", "evidence", None),
+         ("intact-micluster.zip:intact-micluster.txt", "clustered", None),
+         ("intact.zip:intact_negative.txt", "evidence_negative", None),
+         ("intact-micluster.zip:intact-micluster_negative.txt", "clustered_negative", None)]
+
+# Earlier releases: the last one of each year (from 2008, the first in
+# PSI-MITAB), with the members of their archives (date, evidence, clustered;
+# the file of 2008 has one line per pair of molecules, and is clustered)
+archive_prefix = 'https://ftp.ebi.ac.uk/pub/databases/intact'
+releases = [("2008-12-12", None, "intact.zip:intact.txt"),
+            ("2009-11-27", "intact.zip:psimitab/intact.txt", "intact-clustered.zip:intact.txt"),
+            ("2010-12-15", "intact.zip:psimitab/intact.txt", "intact-clustered.zip:intact.txt"),
+            ("2011-11-18", "intact.zip:ebi/sp/pro6/intact/local/data/released/2011-11-18/psimitab/intact.txt",
+             "intact-clustered.zip:intact-clustered.txt")]
+releases += [(d, "intact.zip:intact.txt", "intact-micluster.zip:intact-micluster.txt")
+             for d in ["2012-12-14", "2013-11-20", "2014-12-18", "2015-11-26",
+                       "2016-12-12", "2017-12-12", "2018-11-30", "2019-12-16",
+                       "2020-12-16", "2021-10-13", "2022-07-11", "2023-10-06",
+                       "2024-11-29", "2025-08-08"]]
+old_files = []
+for d, ev, cl in releases:
+    if ev is not None:
+        old_files.append((f"{d}/psimitab/{ev}", f"evidence_{d}", None))
+    if cl is not None:
+        old_files.append((f"{d}/psimitab/{cl}", f"clustered_{d}", None))
+current_files = files
+files = current_files + old_files
+# the networks in PSI-MITAB 2.5
+mitab25_alts = {alt for fname, alt, fmt in old_files if alt.split("_")[-1] < "2012"}
+
+def fetch_upstream(force=False):
+    name = __name__.split(".")[-1]
+    fetch_upstream_files(name, upstream_prefix, current_files, force)
+    fetch_upstream_files(name, archive_prefix, old_files, force)
+
+# the columns of the PSI-MITAB 2.5 files of IntAct (until 2011), for the
+# files without a header
+_cols25 = ["#ID(s) interactor A", "ID(s) interactor B", "Alt. ID(s) interactor A",
+           "Alt. ID(s) interactor B", "Alias(es) interactor A",
+           "Alias(es) interactor B", "Interaction detection method(s)",
+           "Publication 1st author(s)", "Publication Identifier(s)",
+           "Taxid interactor A", "Taxid interactor B", "Interaction type(s)",
+           "Source database(s)", "Interaction identifier(s)",
+           "Confidence value(s)", "Experimental role(s) interactor A",
+           "Experimental role(s) interactor B", "Biological role(s) interactor A",
+           "Biological role(s) interactor B", "Properties interactor A",
+           "Properties interactor B", "Type(s) interactor A",
+           "Type(s) interactor B", "HostOrganism(s)", "Expansion method(s)",
+           "Dataset name(s)", "Annotation(s) interactor A",
+           "Annotation(s) interactor B", "Parameter(s) interactor A",
+           "Parameter(s) interactor B", "Parameter(s) interaction"]
+
+_cols = {"#ID(s) interactor A": "a", "ID(s) interactor B": "b",
+         "Alias(es) interactor A": "alias_a", "Alias(es) interactor B": "alias_b",
+         "Taxid interactor A": "taxid_a", "Taxid interactor B": "taxid_b",
+         "Type(s) interactor A": "type_a", "Type(s) interactor B": "type_b",
+         "Interaction detection method(s)": "detection_method",
+         "Interaction type(s)": "interaction_type",
+         "Publication Identifier(s)": "publication",
+         "Interaction identifier(s)": "interaction_id",
+         "Confidence value(s)": "confidence",
+         "Expansion method(s)": "expansion"}
+
+_paren = re.compile(r'\(([^()]*)\)$')
+
+def mi_names(x):
+    "Names of PSI-MI terms, e.g. 'psi-mi:\"MI:0084\"(phage display)'."
+    if x == "-":
+        return ""
+    names = []
+    for t in x.split("|"):
+        m = _paren.search(t)
+        names.append(m.group(1) if m else t)
+    return "|".join(dict.fromkeys(names))
+
+def short_name(x, mitab25=False):
+    """The short name of a molecule, from its aliases (in PSI-MITAB 2.5, the
+    first alias, e.g. 'uniprotkb:Lck')."""
+    if x == "-":
+        return ""
+    if mitab25:
+        return x.split("|")[0].split(":", 1)[-1]
+    psi = []
+    for t in x.split("|"):
+        if t.endswith("(display_short)"):
+            return t.removeprefix("psi-mi:").removesuffix("(display_short)")
+        if t.startswith("psi-mi:") and "(" not in t:
+            psi.append(t.removeprefix("psi-mi:"))
+    # clustered files: display_long|display_short|...
+    if len(psi) >= 2:
+        return psi[1]
+    return psi[0] if psi else ""
+
+def taxid(x):
+    if x == "-":
+        return 0, ""
+    t = x.split("|")[0]
+    m = re.match(r'taxid:(-?\d+)(?:\((.*)\))?', t)
+    return int(m.group(1)), (m.group(2) or "")
+
+def miscore(x):
+    for t in x.split("|"):
+        if t.startswith("intact-miscore:"):
+            return float(t.removeprefix("intact-miscore:"))
+    return np.nan
+
+def publications(x):
+    if x == "-":
+        return ""
+    return "|".join(t for t in x.split("|") if not t.startswith(("mint:", "imex:")))
+
+def _join_broken_lines(f, ncols):
+    """The content of the PSI-MITAB 2.5 file f, as a file object, with the
+    lines broken by line breaks within fields (in some annotations, e.g. in
+    the release of 2008) joined with a space."""
+    out = io.BytesIO()
+    buf = b""
+    for line in f:
+        buf += line.rstrip(b"\r\n")
+        n = buf.count(b"\t") + 1
+        if n >= ncols:
+            assert n == ncols, buf[:200]
+            out.write(buf + b"\n")
+            buf = b""
+        else:
+            buf += b" "
+    assert not buf.strip(), buf[:200]
+    out.seek(0)
+    return out
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    name = __name__.split(".")[-1]
+    for fname, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        # tab-separated, without quoting (the fields contain quotes)
+        popts = pa.csv.ParseOptions(delimiter="\t", quote_char=False,
+                                    newlines_in_values=False)
+        ropts = pa.csv.ReadOptions(block_size=1 << 28)
+        # the files in PSI-MITAB 2.5 (until 2011) have no header in some
+        # releases
+        mitab25 = alt in mitab25_alts
+        with open_upstream_file(name, fname, "rb") as f:
+            header = f.readline().decode().rstrip("\r\n").split("\t")
+        if not header[0].startswith("#"):
+            assert mitab25
+            header = _cols25
+            ropts = pa.csv.ReadOptions(block_size=1 << 28, column_names=_cols25)
+        # the clustered files of 2012 to 2023 have only the 15 columns of
+        # PSI-MITAB 2.5 (without the types of the molecules and the
+        # expansion method)
+        cols = [c for c in _cols if c in header]
+        assert len(cols) == len(_cols) or alt.startswith("clustered_20"), (alt, header)
+        copts = pa.csv.ConvertOptions(include_columns=cols,
+                                      column_types={c: pa.string() for c in cols})
+        nodes = {}
+        ninfo = []
+        edges = []
+        eprops = {k: [] for k in ["detection_method", "interaction_type",
+                                  "publication", "miscore", "interaction_id",
+                                  "expansion"]}
+        with open_upstream_file(name, fname, "rb") as f:
+            if mitab25:
+                f = _join_broken_lines(f, len(_cols25))
+            reader = pa.csv.open_csv(f, read_options=ropts, parse_options=popts,
+                                     convert_options=copts)
+            for batch in reader:
+                d = {_cols[c]: (batch.column(c).to_pylist() if c in cols
+                                else ["-"] * batch.num_rows) for c in _cols}
+                for i in range(batch.num_rows):
+                    vs = []
+                    for s in "ab":
+                        mid = d[s][i]
+                        if s == "b" and mid == "-":
+                            # interaction of a molecule with itself
+                            vs.append(vs[0])
+                            continue
+                        v = nodes.get(mid)
+                        if v is None:
+                            v = nodes[mid] = len(nodes)
+                            tid, org = taxid(d[f"taxid_{s}"][i])
+                            ninfo.append((mid, short_name(d[f"alias_{s}"][i], mitab25),
+                                          tid, org, mi_names(d[f"type_{s}"][i])))
+                        vs.append(v)
+                    edges.append(vs)
+                    eprops["detection_method"].append(mi_names(d["detection_method"][i]))
+                    eprops["interaction_type"].append(mi_names(d["interaction_type"][i]))
+                    eprops["publication"].append(publications(d["publication"][i]))
+                    eprops["miscore"].append(miscore(d["confidence"][i]))
+                    x = d["interaction_id"][i]
+                    eprops["interaction_id"].append("" if x == "-" else x)
+                    eprops["expansion"].append(mi_names(d["expansion"][i]))
+                del d
+
+        g = Graph(directed=False)
+        g.add_vertex(len(ninfo))
+        for j, (p, t) in enumerate([("id", "string"), ("name", "string"),
+                                    ("taxid", "int"), ("organism", "string"),
+                                    ("type", "string")]):
+            g.vp[p] = g.new_vp(t, vals=[x[j] for x in ninfo])
+        del ninfo, nodes
+        g.add_edge_list(np.array(edges, dtype="int64").reshape(-1, 2))
+        del edges
+        for p, vals in eprops.items():
+            g.ep[p] = g.new_ep("double" if p == "miscore" else "string", vals=vals)
+        del eprops
+        yield alt, g