netgamut 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netgamut/__init__.py +486 -0
- netgamut/__main__.py +349 -0
- netgamut/_core/__init__.py +74 -0
- netgamut/_core/cache.py +373 -0
- netgamut/_core/columns.py +308 -0
- netgamut/_core/domains.py +285 -0
- netgamut/_core/download.py +282 -0
- netgamut/_core/formats.py +246 -0
- netgamut/_core/hypergraph.py +520 -0
- netgamut/_core/in_memory.py +184 -0
- netgamut/_core/models.py +52 -0
- netgamut/_core/multilayer.py +225 -0
- netgamut/_core/network.py +718 -0
- netgamut/_core/nodes.py +193 -0
- netgamut/_core/state_network.py +343 -0
- netgamut/_export.py +352 -0
- netgamut/ahorn/__init__.py +291 -0
- netgamut/ahorn/_catalog.py +364 -0
- netgamut/ahorn/_models.py +252 -0
- netgamut/ahorn/catalog.json +3662 -0
- netgamut/generate/__init__.py +908 -0
- netgamut/generate/_flowgen.py +1957 -0
- netgamut/generate/_higher_order.py +315 -0
- netgamut/generate/_lfr/__init__.py +508 -0
- netgamut/generate/_lfr/_build.py +119 -0
- netgamut/generate/_lfr/_io.py +114 -0
- netgamut/generate/_lfr/_sources.py +238 -0
- netgamut/generate/_mlgm/LICENSE +25 -0
- netgamut/generate/_mlgm/__init__.py +36 -0
- netgamut/generate/_mlgm/_dcsbm.py +148 -0
- netgamut/generate/_mlgm/_partition.py +71 -0
- netgamut/generate/_mlgm/_tensors.py +156 -0
- netgamut/generate/_multilayer_gm.py +174 -0
- netgamut/generate/_naming.py +29 -0
- netgamut/generate/_overlapping_memory.py +237 -0
- netgamut/generate/_params.py +343 -0
- netgamut/generate/_partdist.py +331 -0
- netgamut/konect/__init__.py +451 -0
- netgamut/konect/_catalog.py +367 -0
- netgamut/konect/_copies.py +462 -0
- netgamut/konect/_ent.py +114 -0
- netgamut/konect/_models.py +263 -0
- netgamut/konect/_scrape.py +152 -0
- netgamut/konect/catalog.json +22866 -0
- netgamut/netzschleuder/__init__.py +332 -0
- netgamut/netzschleuder/_catalog.py +265 -0
- netgamut/netzschleuder/_models.py +240 -0
- netgamut/netzschleuder/catalog.json +63648 -0
- netgamut/ogb/__init__.py +298 -0
- netgamut/ogb/_catalog.py +189 -0
- netgamut/ogb/_models.py +196 -0
- netgamut/ogb/catalog.json +365 -0
- netgamut/paths/__init__.py +214 -0
- netgamut/paths/_catalog.py +161 -0
- netgamut/paths/_models.py +393 -0
- netgamut/paths/_parsers.py +328 -0
- netgamut/paths/catalog.json +92 -0
- netgamut/py.typed +0 -0
- netgamut/pyg/__init__.py +378 -0
- netgamut/pyg/_catalog.py +227 -0
- netgamut/pyg/_models.py +194 -0
- netgamut/pyg/_scrape.py +190 -0
- netgamut/pyg/catalog.json +1644 -0
- netgamut/snap/__init__.py +370 -0
- netgamut/snap/_catalog.py +304 -0
- netgamut/snap/_models.py +367 -0
- netgamut/snap/_scrape.py +446 -0
- netgamut/snap/catalog.json +5056 -0
- netgamut-0.4.0.dist-info/METADATA +1204 -0
- netgamut-0.4.0.dist-info/RECORD +73 -0
- netgamut-0.4.0.dist-info/WHEEL +4 -0
- netgamut-0.4.0.dist-info/entry_points.txt +2 -0
- netgamut-0.4.0.dist-info/licenses/LICENSE +21 -0
netgamut/__init__.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Browse, download and cache network datasets.
|
|
2
|
+
|
|
3
|
+
Each repository lives in its own subpackage:
|
|
4
|
+
|
|
5
|
+
* :mod:`netgamut.snap` -- the Stanford Large Network Dataset Collection, 109
|
|
6
|
+
datasets. Large, well known, and skewed to social, web and technological
|
|
7
|
+
systems.
|
|
8
|
+
* :mod:`netgamut.netzschleuder` -- networks.skewed.de, 286 networks tagged by
|
|
9
|
+
domain, including the biological, transport and multilayer networks SNAP has
|
|
10
|
+
none of.
|
|
11
|
+
* :mod:`netgamut.konect` -- KONECT, the Koblenz Network Collection, 300
|
|
12
|
+
networks described in one fixed vocabulary of format, edge type and loops.
|
|
13
|
+
A network Netzschleuder or SNAP also holds is listed there but used from the
|
|
14
|
+
other source here, so nothing is counted twice.
|
|
15
|
+
* :mod:`netgamut.ogb` -- Open Graph Benchmark, 12 datasets whose node
|
|
16
|
+
categories are *named* rather than left as anonymous codes.
|
|
17
|
+
* :mod:`netgamut.pyg` -- the PyTorch Geometric corpus, 99 datasets: citation,
|
|
18
|
+
web, social and heterophilous graphs.
|
|
19
|
+
* :mod:`netgamut.paths` -- curated path data (flight itineraries, navigation
|
|
20
|
+
trails) plus the conversion into second-order state networks for Infomap's
|
|
21
|
+
memory model, which no edge list can express.
|
|
22
|
+
* :mod:`netgamut.ahorn` -- the Aachen Higher-Order Repository of Networks, 83
|
|
23
|
+
hypergraphs and simplicial complexes. Its unit is a *set* of nodes rather than
|
|
24
|
+
a pair, so it returns a
|
|
25
|
+
:class:`~netgamut.Hypergraph` rather than a ``Network``.
|
|
26
|
+
|
|
27
|
+
Metadata for every source ships inside the package, so browsing is offline and
|
|
28
|
+
instant; only ``load()`` touches the network, and it caches into a shared
|
|
29
|
+
user-level directory so a dataset is fetched once per machine.
|
|
30
|
+
|
|
31
|
+
Beyond the datasets, :mod:`netgamut.generate` samples synthetic benchmark
|
|
32
|
+
networks with planted communities from any blueprint in the pool (requires
|
|
33
|
+
the ``generate`` extra).
|
|
34
|
+
|
|
35
|
+
Every network -- downloaded or generated, first-order, state or multilayer --
|
|
36
|
+
converts and writes the same way: ``to_networkx()``, ``to_infomap()``, and
|
|
37
|
+
``to_pajek(directory)`` / ``to_link_list(directory)``, which return the path
|
|
38
|
+
of the file they wrote.
|
|
39
|
+
|
|
40
|
+
All sources are mapped onto one domain vocabulary, so a comparison can span
|
|
41
|
+
kinds of system rather than kinds of provenance::
|
|
42
|
+
|
|
43
|
+
import netgamut
|
|
44
|
+
|
|
45
|
+
netgamut.domains()
|
|
46
|
+
# {'Social': 201, 'Informational': 68, 'Biological': 36, ...}
|
|
47
|
+
|
|
48
|
+
for dataset in netgamut.search(domain="Biological", max_nodes=1000):
|
|
49
|
+
print(dataset.source, dataset.name)
|
|
50
|
+
|
|
51
|
+
Please cite the datasets you use -- each source's records carry the reference
|
|
52
|
+
its site asks for.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
from dataclasses import dataclass
|
|
58
|
+
from typing import Any
|
|
59
|
+
|
|
60
|
+
from . import ahorn, generate, konect, netzschleuder, ogb, paths, pyg, snap
|
|
61
|
+
from ._core.cache import (
|
|
62
|
+
CACHE_ENV_VAR,
|
|
63
|
+
CachedDataset,
|
|
64
|
+
CacheInfo,
|
|
65
|
+
cache_dir,
|
|
66
|
+
cache_info,
|
|
67
|
+
clear_cache,
|
|
68
|
+
set_cache_dir,
|
|
69
|
+
)
|
|
70
|
+
from ._core.columns import Columns
|
|
71
|
+
from ._core.domains import DOMAINS
|
|
72
|
+
from ._core.hypergraph import Hypergraph
|
|
73
|
+
from ._core.in_memory import InMemoryNetwork
|
|
74
|
+
from ._core.multilayer import MultilayerNetwork
|
|
75
|
+
from ._core.network import Network
|
|
76
|
+
from ._core.state_network import StateNetwork
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"CACHE_ENV_VAR",
|
|
80
|
+
"DOMAINS",
|
|
81
|
+
"CacheInfo",
|
|
82
|
+
"CachedDataset",
|
|
83
|
+
"Columns",
|
|
84
|
+
"Hypergraph",
|
|
85
|
+
"Match",
|
|
86
|
+
"MultilayerNetwork",
|
|
87
|
+
"Network",
|
|
88
|
+
"StateNetwork",
|
|
89
|
+
"InMemoryNetwork",
|
|
90
|
+
"__version__",
|
|
91
|
+
"ahorn",
|
|
92
|
+
"cache_dir",
|
|
93
|
+
"cache_info",
|
|
94
|
+
"clear_cache",
|
|
95
|
+
"domains",
|
|
96
|
+
"generate",
|
|
97
|
+
"konect",
|
|
98
|
+
"load",
|
|
99
|
+
"netzschleuder",
|
|
100
|
+
"ogb",
|
|
101
|
+
"paths",
|
|
102
|
+
"pyg",
|
|
103
|
+
"search",
|
|
104
|
+
"set_cache_dir",
|
|
105
|
+
"snap",
|
|
106
|
+
"sources",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
__version__ = "0.4.0"
|
|
110
|
+
|
|
111
|
+
#: Source name -> subpackage, in the order results are reported.
|
|
112
|
+
_SOURCES: dict[str, Any] = {
|
|
113
|
+
"snap": snap,
|
|
114
|
+
"netzschleuder": netzschleuder,
|
|
115
|
+
"konect": konect,
|
|
116
|
+
"ogb": ogb,
|
|
117
|
+
"pyg": pyg,
|
|
118
|
+
"paths": paths,
|
|
119
|
+
"ahorn": ahorn,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#: The kinds of network :class:`Match` can say yes, no or unknown to, which
|
|
123
|
+
#: :func:`search`, the explorer's index and the web mirror's manifest share.
|
|
124
|
+
_KINDS = ("weighted", "bipartite", "temporal", "multilayer", "signed", "multigraph")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def sources() -> tuple[str, ...]:
|
|
128
|
+
"""The available data sources."""
|
|
129
|
+
return tuple(_SOURCES)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _shadowed(source: str, dataset: Any) -> bool:
|
|
133
|
+
"""Whether cross-source queries skip *dataset* for another source's copy.
|
|
134
|
+
|
|
135
|
+
Only KONECT defers: a third of it is Netzschleuder's or SNAP's data. See
|
|
136
|
+
:func:`netgamut.konect.duplicates`.
|
|
137
|
+
"""
|
|
138
|
+
return source == "konect" and dataset.duplicate_of is not None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _owners(name: str) -> list[str]:
|
|
142
|
+
"""The sources holding *name*, leaving out a KONECT copy of another's."""
|
|
143
|
+
found = [key for key, module in _SOURCES.items() if name in module.catalog()]
|
|
144
|
+
if len(found) > 1 and "konect" in found and _shadowed("konect", konect.info(name)):
|
|
145
|
+
found.remove("konect")
|
|
146
|
+
return found
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class Match:
|
|
151
|
+
"""One dataset found by a cross-source query.
|
|
152
|
+
|
|
153
|
+
Keeps the source alongside the record, because the two catalogs use
|
|
154
|
+
different names for the same idea and a few names appear in both.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
source: str
|
|
158
|
+
dataset: Any
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def name(self) -> str:
|
|
162
|
+
name: str = self.dataset.name
|
|
163
|
+
return name
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def domains(self) -> tuple[str, ...]:
|
|
167
|
+
result: tuple[str, ...] = self.dataset.domains
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def nodes(self) -> int | None:
|
|
172
|
+
return self.dataset.nodes # type: ignore[no-any-return]
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def edges(self) -> int | None:
|
|
176
|
+
return self.dataset.edges # type: ignore[no-any-return]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def directed(self) -> bool | None:
|
|
180
|
+
return self.dataset.directed # type: ignore[no-any-return]
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def node_metadata(self) -> bool | None:
|
|
184
|
+
"""Whether :meth:`Network.nodes` returns per-node attributes; ``None`` if unknown.
|
|
185
|
+
|
|
186
|
+
Each source answers from its own catalog, before any download: KONECT and
|
|
187
|
+
Netzschleuder from their file listings and analyses, SNAP from its label
|
|
188
|
+
files, OGB and PyG from their class labels. AHORN keeps node lines inside
|
|
189
|
+
the dataset file, so it cannot say.
|
|
190
|
+
"""
|
|
191
|
+
value = getattr(self.dataset, "node_metadata", None)
|
|
192
|
+
return None if value is None else bool(value)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def node_attributes(self) -> tuple[str, ...] | None:
|
|
196
|
+
"""The per-node columns the source's catalog can name, ``None`` if unknown.
|
|
197
|
+
|
|
198
|
+
Exact for Netzschleuder, KONECT and SNAP. OGB names only its extra files
|
|
199
|
+
(``year``, ``species``) and PyG none, since their other column names come
|
|
200
|
+
with the download; ``nodes()`` returns those too.
|
|
201
|
+
"""
|
|
202
|
+
value = getattr(self.dataset, "node_attributes", None)
|
|
203
|
+
return None if value is None else tuple(value)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def loops(self) -> bool | None:
|
|
207
|
+
"""Whether any node links to itself, where some source has checked.
|
|
208
|
+
|
|
209
|
+
KONECT checks every network; for another source's network, KONECT's
|
|
210
|
+
check is used when it holds the same edge list. ``None`` means unknown.
|
|
211
|
+
"""
|
|
212
|
+
value = getattr(self.dataset, "loops", None)
|
|
213
|
+
if value is not None:
|
|
214
|
+
return bool(value)
|
|
215
|
+
return konect.loops_for(self.key)
|
|
216
|
+
|
|
217
|
+
def _flag(self, name: str) -> bool | None:
|
|
218
|
+
value = getattr(self.dataset, name, None)
|
|
219
|
+
return None if value is None else bool(value)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def weighted(self) -> bool | None:
|
|
223
|
+
"""Whether links carry weights; ``None`` where the source cannot say.
|
|
224
|
+
|
|
225
|
+
From Netzschleuder's tags, KONECT's edge type, SNAP's graph type and
|
|
226
|
+
AHORN's catalog. OGB, PyG and the path data cannot say.
|
|
227
|
+
"""
|
|
228
|
+
return self._flag("weighted")
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def bipartite(self) -> bool | None:
|
|
232
|
+
"""Whether links only join two kinds of node; ``None`` where the source cannot say.
|
|
233
|
+
|
|
234
|
+
KONECT and SNAP say so in their catalogs. For Netzschleuder it is the
|
|
235
|
+
site's structural check, no odd cycle, which every two-mode network
|
|
236
|
+
passes and so do a few trees and grids.
|
|
237
|
+
"""
|
|
238
|
+
return self._flag("bipartite")
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def temporal(self) -> bool | None:
|
|
242
|
+
"""Whether links carry timestamps; ``None`` where the source cannot say.
|
|
243
|
+
|
|
244
|
+
From Netzschleuder's ``Temporal`` and ``Timestamps`` tags, KONECT's
|
|
245
|
+
timestamp column, SNAP's graph type and AHORN's catalog.
|
|
246
|
+
"""
|
|
247
|
+
return self._flag("temporal")
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def multilayer(self) -> bool | None:
|
|
251
|
+
"""Whether Netzschleuder publishes it as a multilayer or multiplex network.
|
|
252
|
+
|
|
253
|
+
``None`` for every other source, whose catalogs do not say.
|
|
254
|
+
"""
|
|
255
|
+
return self._flag("multilayer")
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def signed(self) -> bool | None:
|
|
259
|
+
"""Whether links are positive or negative; ``None`` where the source cannot say.
|
|
260
|
+
|
|
261
|
+
From Netzschleuder's ``Signed`` tag, KONECT's edge type and SNAP's graph type.
|
|
262
|
+
"""
|
|
263
|
+
return self._flag("signed")
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def multigraph(self) -> bool | None:
|
|
267
|
+
"""Whether some pair of nodes links more than once; ``None`` if the source cannot say.
|
|
268
|
+
|
|
269
|
+
From Netzschleuder's ``Multigraph`` tag and KONECT's edge type.
|
|
270
|
+
"""
|
|
271
|
+
return self._flag("multigraph")
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def key(self) -> str:
|
|
275
|
+
"""``"<source>/<name>"`` -- unique across sources."""
|
|
276
|
+
return f"{self.source}/{self.name}"
|
|
277
|
+
|
|
278
|
+
def load(self, **kwargs: Any) -> Network:
|
|
279
|
+
"""Download this dataset via its own source."""
|
|
280
|
+
result: Network = _SOURCES[self.source].load(self.name, **kwargs)
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
def to_dict(self) -> dict[str, Any]:
|
|
284
|
+
"""A JSON-serialisable summary, the same shape for every source.
|
|
285
|
+
|
|
286
|
+
Holds the fields :func:`search` filters on plus what a reader needs to
|
|
287
|
+
choose and cite a dataset: its description, citations, its page at the
|
|
288
|
+
source (``url``) and, where the catalog records it, the original
|
|
289
|
+
publisher (``upstream_url``). ``weighted``, ``bipartite``, ``temporal``,
|
|
290
|
+
``multilayer``, ``signed`` and ``multigraph`` are the properties of the
|
|
291
|
+
same names, ``None`` where the source cannot say. ``duplicate_of`` is
|
|
292
|
+
the key of the network a KONECT copy defers to (see
|
|
293
|
+
:func:`netgamut.konect.duplicates`), else ``None``.
|
|
294
|
+
``subgraph_count`` is how many networks the name holds: a Netzschleuder
|
|
295
|
+
entry can hold several, the first few named in ``subgraphs``, and the
|
|
296
|
+
statistics describe the first, which :meth:`load` fetches by default.
|
|
297
|
+
Other sources hold one and name none.
|
|
298
|
+
"""
|
|
299
|
+
dataset = self.dataset
|
|
300
|
+
attributes = self.node_attributes
|
|
301
|
+
upstream = getattr(dataset, "upstream_url", "") or getattr(dataset, "source_url", "")
|
|
302
|
+
return {
|
|
303
|
+
"key": self.key,
|
|
304
|
+
"source": self.source,
|
|
305
|
+
"name": self.name,
|
|
306
|
+
"title": getattr(dataset, "title", ""),
|
|
307
|
+
"domains": list(self.domains),
|
|
308
|
+
"nodes": self.nodes,
|
|
309
|
+
"edges": self.edges,
|
|
310
|
+
"directed": self.directed,
|
|
311
|
+
"loops": self.loops,
|
|
312
|
+
"node_metadata": self.node_metadata,
|
|
313
|
+
"node_attributes": None if attributes is None else list(attributes),
|
|
314
|
+
**{name: getattr(self, name) for name in _KINDS},
|
|
315
|
+
"subgraph_count": getattr(dataset, "subgraph_count", 1),
|
|
316
|
+
"subgraphs": list(getattr(dataset, "subgraphs", ())),
|
|
317
|
+
"description": getattr(dataset, "description", ""),
|
|
318
|
+
"citations": [_citation(entry) for entry in getattr(dataset, "citations", ())],
|
|
319
|
+
"url": dataset.url,
|
|
320
|
+
"upstream_url": upstream,
|
|
321
|
+
"duplicate_of": getattr(dataset, "duplicate_of", None),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
def __str__(self) -> str:
|
|
325
|
+
shape = []
|
|
326
|
+
if self.nodes is not None:
|
|
327
|
+
shape.append(f"{self.nodes:,} nodes")
|
|
328
|
+
if self.edges is not None:
|
|
329
|
+
shape.append(f"{self.edges:,} edges")
|
|
330
|
+
domains = "/".join(self.domains) or "uncategorised"
|
|
331
|
+
tail = f" ({', '.join(shape)})" if shape else ""
|
|
332
|
+
return f"{self.key} [{domains}]{tail}"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _citation(entry: Any) -> dict[str, str]:
|
|
336
|
+
"""One citation as ``{"text", "url"}``: Netzschleuder pairs each text with a link."""
|
|
337
|
+
if isinstance(entry, str):
|
|
338
|
+
return {"text": entry, "url": ""}
|
|
339
|
+
text, *link = entry
|
|
340
|
+
return {"text": str(text), "url": str(link[0]) if link else ""}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def domains() -> dict[str, int]:
|
|
344
|
+
"""Dataset counts per domain, summed over every source.
|
|
345
|
+
|
|
346
|
+
A dataset listed under two domains counts once in each, and a network held
|
|
347
|
+
by two sources counts once.
|
|
348
|
+
"""
|
|
349
|
+
counts = dict.fromkeys(DOMAINS, 0)
|
|
350
|
+
for source, module in _SOURCES.items():
|
|
351
|
+
for dataset in module.datasets():
|
|
352
|
+
if _shadowed(source, dataset):
|
|
353
|
+
continue
|
|
354
|
+
for domain in dataset.domains:
|
|
355
|
+
counts[domain] += 1
|
|
356
|
+
return counts
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def search(
|
|
360
|
+
query: str = "",
|
|
361
|
+
*,
|
|
362
|
+
domain: str | None = None,
|
|
363
|
+
source: str | None = None,
|
|
364
|
+
directed: bool | None = None,
|
|
365
|
+
max_nodes: int | None = None,
|
|
366
|
+
min_nodes: int | None = None,
|
|
367
|
+
max_edges: int | None = None,
|
|
368
|
+
min_edges: int | None = None,
|
|
369
|
+
loops: bool | None = None,
|
|
370
|
+
node_metadata: bool | None = None,
|
|
371
|
+
node_attribute: str | None = None,
|
|
372
|
+
weighted: bool | None = None,
|
|
373
|
+
bipartite: bool | None = None,
|
|
374
|
+
temporal: bool | None = None,
|
|
375
|
+
multilayer: bool | None = None,
|
|
376
|
+
signed: bool | None = None,
|
|
377
|
+
multigraph: bool | None = None,
|
|
378
|
+
unknown: bool = False,
|
|
379
|
+
) -> tuple[Match, ...]:
|
|
380
|
+
"""Search every source at once, filtering on the shared domain vocabulary.
|
|
381
|
+
|
|
382
|
+
Only the fields every source agrees on are accepted here. For anything
|
|
383
|
+
source-specific -- SNAP's ground-truth partitions, Netzschleuder's subtypes,
|
|
384
|
+
OGB's loadability, PyG's class -- use that source's own ``search()``.
|
|
385
|
+
|
|
386
|
+
Size filters exclude datasets whose size the source does not publish rather than
|
|
387
|
+
assuming anything: most PyG entries and the largest OGB ones have no counts.
|
|
388
|
+
``loops`` does the same with datasets no source has checked for self-links
|
|
389
|
+
(see :attr:`Match.loops`), and ``node_metadata`` with datasets whose catalog
|
|
390
|
+
cannot say (see :attr:`Match.node_metadata`), and so do ``weighted``,
|
|
391
|
+
``bipartite``, ``temporal``, ``multilayer``, ``signed`` and ``multigraph``
|
|
392
|
+
(see :attr:`Match.weighted` and the rest). With ``unknown=True`` a dataset
|
|
393
|
+
passes every filter its source cannot answer instead, so
|
|
394
|
+
``search(weighted=False, unknown=True)`` keeps the networks no catalog calls
|
|
395
|
+
weighted. ``node_attribute`` finds
|
|
396
|
+
datasets with a per-node column of that name, case-insensitively, among the
|
|
397
|
+
names each catalog knows -- ``node_attribute="gender"`` for something to score
|
|
398
|
+
a partition against.
|
|
399
|
+
|
|
400
|
+
A network held by both KONECT and Netzschleuder or SNAP appears once, from
|
|
401
|
+
the other source; ``source="konect"`` lists KONECT's copy too.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
KeyError: Unknown *domain* or *source*.
|
|
405
|
+
"""
|
|
406
|
+
if domain is not None and domain not in DOMAINS:
|
|
407
|
+
raise KeyError(f"Unknown domain {domain!r}. Available: {', '.join(DOMAINS)}")
|
|
408
|
+
if source is not None and source not in _SOURCES:
|
|
409
|
+
raise KeyError(f"Unknown source {source!r}. Available: {', '.join(_SOURCES)}")
|
|
410
|
+
|
|
411
|
+
needle = query.casefold().strip()
|
|
412
|
+
results: list[Match] = []
|
|
413
|
+
wanted = (weighted, bipartite, temporal, multilayer, signed, multigraph)
|
|
414
|
+
kinds = dict(zip(_KINDS, wanted, strict=True))
|
|
415
|
+
|
|
416
|
+
def within(value: int | None, low: int | None, high: int | None) -> bool:
|
|
417
|
+
if low is None and high is None:
|
|
418
|
+
return True
|
|
419
|
+
if value is None:
|
|
420
|
+
return unknown
|
|
421
|
+
return (low is None or value >= low) and (high is None or value <= high)
|
|
422
|
+
|
|
423
|
+
def says(value: bool | None, want: bool | None) -> bool:
|
|
424
|
+
if want is None:
|
|
425
|
+
return True
|
|
426
|
+
return unknown if value is None else value is want
|
|
427
|
+
|
|
428
|
+
for name, module in _SOURCES.items():
|
|
429
|
+
if source is not None and name != source:
|
|
430
|
+
continue
|
|
431
|
+
for dataset in module.datasets():
|
|
432
|
+
if source is None and _shadowed(name, dataset):
|
|
433
|
+
continue
|
|
434
|
+
if domain is not None and domain not in dataset.domains:
|
|
435
|
+
continue
|
|
436
|
+
text = f"{dataset.name} {getattr(dataset, 'description', '')}".casefold()
|
|
437
|
+
if needle and needle not in text:
|
|
438
|
+
continue
|
|
439
|
+
if not says(dataset.directed, directed):
|
|
440
|
+
continue
|
|
441
|
+
if not within(dataset.nodes, min_nodes, max_nodes):
|
|
442
|
+
continue
|
|
443
|
+
if not within(dataset.edges, min_edges, max_edges):
|
|
444
|
+
continue
|
|
445
|
+
match = Match(source=name, dataset=dataset)
|
|
446
|
+
if not says(match.loops, loops) or not says(match.node_metadata, node_metadata):
|
|
447
|
+
continue
|
|
448
|
+
if not all(says(getattr(match, name), want) for name, want in kinds.items()):
|
|
449
|
+
continue
|
|
450
|
+
columns = match.node_attributes
|
|
451
|
+
if node_attribute is not None and not (
|
|
452
|
+
unknown
|
|
453
|
+
if columns is None
|
|
454
|
+
else any(column.casefold() == node_attribute.casefold() for column in columns)
|
|
455
|
+
):
|
|
456
|
+
continue
|
|
457
|
+
results.append(match)
|
|
458
|
+
|
|
459
|
+
return tuple(results)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def load(name: str, *, source: str | None = None, **kwargs: Any) -> Network:
|
|
463
|
+
"""Download a dataset by name, searching every source.
|
|
464
|
+
|
|
465
|
+
Pass *source* to disambiguate. Without it, a name present in more than one
|
|
466
|
+
source is an error rather than an arbitrary choice -- except that KONECT's
|
|
467
|
+
copy of another source's network defers to that source.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
KeyError: No source has *name*, or several do and *source* was omitted.
|
|
471
|
+
"""
|
|
472
|
+
if source is not None:
|
|
473
|
+
if source not in _SOURCES:
|
|
474
|
+
raise KeyError(f"Unknown source {source!r}. Available: {', '.join(_SOURCES)}")
|
|
475
|
+
result: Network = _SOURCES[source].load(name, **kwargs)
|
|
476
|
+
return result
|
|
477
|
+
|
|
478
|
+
found = _owners(name)
|
|
479
|
+
if not found:
|
|
480
|
+
raise KeyError(f"No dataset named {name!r} in any source: {', '.join(_SOURCES)}.")
|
|
481
|
+
if len(found) > 1:
|
|
482
|
+
raise KeyError(
|
|
483
|
+
f"{name!r} exists in several sources ({', '.join(found)}). Pass source= to choose."
|
|
484
|
+
)
|
|
485
|
+
loaded: Network = _SOURCES[found[0]].load(name, **kwargs)
|
|
486
|
+
return loaded
|