History of pubchem_literature
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: pubchem_literature 478c92b
diff --git a/entries/pubchem_literature/__init__.py b/entries/pubchem_literature/__init__.py
new file mode 100644
index 0000000..bbc5776
--- /dev/null
+++ b/entries/pubchem_literature/__init__.py
@@ -0,0 +1,130 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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/>.
+
+import pyarrow
+import pyarrow.csv
+import pyarrow.compute as pc
+
+from .. import *
+
+title = "PubChem compound-literature links (2025)"
+description = r"""A bipartite network of the chemical compounds of PubChem, the open chemistry database of the U.S. National Center for Biotechnology Information (NCBI), and the articles of PubMed linked to them. An edge connects a compound with an article that refers to it, as given in PubChem's list of links between compounds (CIDs) and PubMed articles (PMIDs).
+
+The links come from four kinds of sources, given by the edge property `type`:
+
+1. articles provided by the depositors of the substances of PubChem (e.g. the publication that describes the substance),
+2. articles indexed in PubMed with a MeSH heading linked to the compound,
+3. articles linked by the publishers of the journals (e.g. compounds described in the article),
+4. articles associated with the compound through the BioAssays of PubChem.
+
+A compound and an article linked by several kinds of sources are connected by several edges, one per kind. Only the compounds and articles with at least one link are nodes. The names of the compounds are those chosen as titles of their summary pages in PubChem (empty for the few compounds without a title).
+
+The network has the following properties:
+
+- `is_compound`: whether the node is a compound (otherwise it is an article).
+- `cid`: the PubChem compound identifier (CID) of the compound (-1 for articles); its page is `https://pubchem.ncbi.nlm.nih.gov/compound/<cid>`.
+- `pmid`: the PubMed identifier of the article (-1 for compounds).
+- `name`: the name of the compound (the title of its PubChem page; empty for articles).
+- Edge `type`: the kind of source of the link, 1 to 4 as above."""
+tags = ['Informational', 'Chemical', 'Unweighted', 'Multigraph', 'Metadata']
+url = ['https://pubchem.ncbi.nlm.nih.gov/',
+ 'https://ftp.ncbi.nlm.nih.gov/pubchem/Compound/Extras/']
+citation = [('S. Kim, J. Chen, T. Cheng, A. Gindulyte, J. He, S. He, Q. Li, B. A. Shoemaker, P. A. Thiessen, B. Yu, L. Zaslavsky, J. Zhang, E. E. Bolton, "PubChem 2025 update." Nucleic Acids Research 53, D1516-D1525 (2025)',
+ 'https://doi.org/10.1093/nar/gkae1059')]
+icon_hash = None
+upstream_license = ("Public domain (NCBI); data provided by depositors may be subject to their own terms",
+ "https://www.ncbi.nlm.nih.gov/home/about/policies/")
+upstream_prefix = 'https://ftp.ncbi.nlm.nih.gov/pubchem/Compound/Extras'
+# Both files are replaced in place, without versions (CID-PMID.gz was last
+# regenerated on 2025-08-29 at the time of writing, CID-Title.gz is
+# regenerated with every update of PubChem); the cached copies are the ones
+# used.
+files = [(("CID-PMID.gz", "CID-Title.gz"), None, None)]
+
+def fetch_upstream(force=False):
+ return fetch_upstream_files(__name__.split(".")[-1], upstream_prefix, files,
+ force)
+
+def read_tsv(f, names, types):
+ return pyarrow.csv.read_csv(
+ f,
+ read_options=pyarrow.csv.ReadOptions(column_names=names,
+ block_size=1 << 26),
+ parse_options=pyarrow.csv.ParseOptions(delimiter="\t", quote_char=False),
+ convert_options=pyarrow.csv.ConvertOptions(column_types=types))
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ # <cid> <pmid> <type>
+ with open_upstream_file(name, fnames[0], "rb") as f:
+ t = read_tsv(f, ["cid", "pmid", "type"],
+ {"cid": pyarrow.int64(), "pmid": pyarrow.int64(),
+ "type": pyarrow.int8()})
+ cid = t.column("cid").to_numpy()
+ pmid = t.column("pmid").to_numpy()
+ etype = t.column("type").to_numpy()
+ del t
+ cids, ci = numpy.unique(cid, return_inverse=True)
+ del cid
+ pmids, pi = numpy.unique(pmid, return_inverse=True)
+ del pmid
+ Nc = len(cids)
+
+ g = Graph(directed=False)
+ g.add_vertex(Nc + len(pmids))
+ g.add_edge_list(numpy.column_stack([ci, Nc + pi]))
+ del ci, pi
+ g.ep.type = g.new_ep("int8_t", vals=etype)
+ del etype
+ g.vp.is_compound = g.new_vp("bool")
+ g.vp.is_compound.a[:Nc] = 1
+ g.vp.cid = g.new_vp("int64_t", val=-1)
+ g.vp.cid.a[:Nc] = cids
+ g.vp.pmid = g.new_vp("int64_t", val=-1)
+ g.vp.pmid.a[Nc:] = pmids
+ del pmids
+
+ # <cid> <title>, for all the compounds of PubChem: only those of the
+ # network are kept
+ names = [""] * g.num_vertices()
+ cset = pyarrow.array(cids)
+ with open_upstream_file(name, fnames[1], "rb") as f:
+ reader = pyarrow.csv.open_csv(
+ f,
+ read_options=pyarrow.csv.ReadOptions(column_names=["cid", "title"],
+ block_size=1 << 26),
+ parse_options=pyarrow.csv.ParseOptions(delimiter="\t",
+ quote_char=False),
+ convert_options=pyarrow.csv.ConvertOptions(
+ column_types={"cid": pyarrow.int64(),
+ "title": pyarrow.string()}))
+ for batch in reader:
+ c = batch.column("cid")
+ keep = pc.is_in(c, value_set=cset)
+ c = pc.filter(c, keep).to_numpy()
+ ts = pc.filter(batch.column("title"), keep).to_pylist()
+ for v, s in zip(numpy.searchsorted(cids, c), ts):
+ names[v] = s
+ g.vp.name = g.new_vp("string", vals=names)
+ yield alt, g