rete-graph 0.3.2__tar.gz → 0.3.4__tar.gz

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.
Files changed (100) hide show
  1. {rete_graph-0.3.2 → rete_graph-0.3.4}/Cargo.toml +20 -2
  2. {rete_graph-0.3.2 → rete_graph-0.3.4}/PKG-INFO +29 -2
  3. {rete_graph-0.3.2/clients/python → rete_graph-0.3.4}/README.md +28 -1
  4. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/Cargo.lock +37 -7
  5. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/Cargo.toml +1 -1
  6. {rete_graph-0.3.2 → rete_graph-0.3.4/clients/python}/README.md +28 -1
  7. rete_graph-0.3.4/clients/python/examples/graph-data-science.ipynb +270 -0
  8. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/jupyterlite-build-tutorial.ipynb +68 -10
  9. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/src/lib.rs +203 -162
  10. rete_graph-0.3.4/clients/python/tests/test_rdf12.py +204 -0
  11. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/tests/test_rete_graph.py +180 -0
  12. rete_graph-0.3.4/clients/python/tests/test_warnings.py +75 -0
  13. rete_graph-0.3.4/crates/rete-core/Cargo.toml +124 -0
  14. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/bgp.rs +4 -1
  15. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/block_cache.rs +10 -1
  16. rete_graph-0.3.4/crates/rete-core/src/card_derive.rs +2673 -0
  17. rete_graph-0.3.4/crates/rete-core/src/card_input.rs +617 -0
  18. rete_graph-0.3.4/crates/rete-core/src/card_queries.rs +3363 -0
  19. rete_graph-0.3.4/crates/rete-core/src/chunk_cache.rs +419 -0
  20. rete_graph-0.3.4/crates/rete-core/src/dict.rs +1331 -0
  21. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/dictionary.rs +117 -2
  22. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/extbuild.rs +1220 -67
  23. rete_graph-0.3.4/crates/rete-core/src/file.rs +8615 -0
  24. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/header.rs +173 -6
  25. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/index.rs +669 -72
  26. rete_graph-0.3.4/crates/rete-core/src/ingest.rs +1618 -0
  27. rete_graph-0.3.4/crates/rete-core/src/iri.rs +1105 -0
  28. rete_graph-0.3.4/crates/rete-core/src/lib.rs +269 -0
  29. rete_graph-0.3.4/crates/rete-core/src/manifest.rs +306 -0
  30. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/pyramid.rs +239 -20
  31. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/reader.rs +62 -1
  32. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/row.rs +50 -1
  33. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql/aggregate.rs +179 -70
  34. rete_graph-0.3.4/crates/rete-core/src/sparql/diag.rs +750 -0
  35. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql/eval.rs +140 -27
  36. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql/expr.rs +1038 -179
  37. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql/lower.rs +2 -2
  38. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql/path.rs +1 -1
  39. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql.rs +260 -17
  40. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/terms.rs +191 -5
  41. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/triples.rs +48 -0
  42. rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/jev-commander-145.nt +149 -0
  43. rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/jev-random30-30.nt +34 -0
  44. rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/jev-random63-63.nt +67 -0
  45. rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/min-15-level0.nt +20 -0
  46. rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/min-18-level1.nt +21 -0
  47. rete_graph-0.3.4/crates/rete-core/tests/fixtures/v1/glue-stub.js +8 -0
  48. rete_graph-0.3.4/crates/rete-core/tests/iri_differential.rs +254 -0
  49. rete_graph-0.3.4/crates/rete-core/tests/louvain_termination.rs +155 -0
  50. rete_graph-0.3.4/crates/rete-core/tests/polyglot_roundtrip.rs +235 -0
  51. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/properties.rs +62 -0
  52. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/ranged.rs +409 -0
  53. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/robustness.rs +2 -1
  54. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/sparql_integration.rs +51 -0
  55. rete_graph-0.3.4/crates/rete-core/tests/sparql_warnings.rs +586 -0
  56. {rete_graph-0.3.2 → rete_graph-0.3.4}/pyproject.toml +1 -1
  57. {rete_graph-0.3.2 → rete_graph-0.3.4}/python/rete_graph/__init__.py +277 -22
  58. rete_graph-0.3.2/crates/rete-core/Cargo.toml +0 -77
  59. rete_graph-0.3.2/crates/rete-core/src/dict.rs +0 -833
  60. rete_graph-0.3.2/crates/rete-core/src/file.rs +0 -4647
  61. rete_graph-0.3.2/crates/rete-core/src/ingest.rs +0 -869
  62. rete_graph-0.3.2/crates/rete-core/src/lib.rs +0 -200
  63. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/.gitignore +0 -0
  64. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/agents/README.md +0 -0
  65. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/agents/langchain_agent.py +0 -0
  66. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/agents/pydantic_ai_agent.py +0 -0
  67. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/agents/rete_tools.py +0 -0
  68. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/jupyterlite-demo.ipynb +0 -0
  69. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/examples/tutorial.ipynb +0 -0
  70. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/src/readers.rs +0 -0
  71. {rete_graph-0.3.2 → rete_graph-0.3.4}/clients/python/tests/conftest.py +0 -0
  72. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/LICENSE +0 -0
  73. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/README.md +0 -0
  74. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/examples/coherence_bench.rs +0 -0
  75. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/examples/parallel_bench.rs +0 -0
  76. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/examples/qbench.rs +0 -0
  77. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/geo.rs +0 -0
  78. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/geo3.rs +0 -0
  79. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/meta.rs +0 -0
  80. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/parallel.rs +0 -0
  81. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/reach.rs +0 -0
  82. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/reason.rs +0 -0
  83. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/results.rs +0 -0
  84. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/schema_pyramid.rs +0 -0
  85. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/service.rs +0 -0
  86. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/shacl.rs +0 -0
  87. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/sparql/ql.rs +0 -0
  88. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/text_index.rs +0 -0
  89. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/tiling.rs +0 -0
  90. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/src/varint.rs +0 -0
  91. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/community_eval.rs +0 -0
  92. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/compatibility_v1.rs +0 -0
  93. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
  94. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
  95. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/properties.proptest-regressions +0 -0
  96. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/public_api.rs +0 -0
  97. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/roundtrip.rs +0 -0
  98. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/service_federation.rs +0 -0
  99. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/shacl_core.rs +0 -0
  100. {rete_graph-0.3.2 → rete_graph-0.3.4}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
@@ -2,6 +2,7 @@
2
2
  resolver = "2"
3
3
  members = [
4
4
  "crates/rete-core",
5
+ "crates/rete-graph",
5
6
  "crates/rete-cli",
6
7
  "crates/rete-wasm",
7
8
  "crates/docgen",
@@ -14,10 +15,10 @@ members = [
14
15
  # release gates (--locked checks, feature matrix, coverage floors) stay
15
16
  # untouched by binding-only changes. Future clients (R, Go) follow the same
16
17
  # pattern — clients/ consumes crates/, never the other way around.
17
- exclude = ["fuzz", "clients/python", "clients/r/src/rust", "clients/java/ffi"]
18
+ exclude = ["fuzz", "clients/python", "clients/r/src/rust", "clients/java/ffi", "clients/tauri/src-tauri"]
18
19
 
19
20
  [workspace.package]
20
- version = "0.3.2"
21
+ version = "0.3.4"
21
22
  edition = "2021"
22
23
  license = "Apache-2.0"
23
24
  repository = "https://github.com/caviri/rete"
@@ -28,8 +29,25 @@ rust-version = "1.87"
28
29
  [workspace.dependencies]
29
30
  # RDF model + SPARQL, reused rather than reinvented (see docs/SPEC.md §2).
30
31
  # `rdf-star` enables quoted triples (`<< s p o >>`) in the parsers and SPARQL.
32
+ # Oxigraph's own RFC 3987 IRI parser -- the crate its N-Triples reader
33
+ # validates with. Already in this tree via `oxrdf`; named here so `rete-core`
34
+ # can call it directly and decide validity the way the reference loader does.
35
+ oxiri = "0.2"
31
36
  oxrdf = { version = "0.2", features = ["rdf-star"] }
32
37
  oxttl = { version = "0.1", features = ["rdf-star"] }
38
+ # The RDF 1.2 Turtle/TriG reader, ALONGSIDE the 0.1 line above rather than
39
+ # instead of it. The two are semver-incompatible, which is the whole reason they
40
+ # coexist: 0.1 reads `<< s p o >>` as an RDF-star QUOTED TRIPLE, 0.2 reads the
41
+ # same bytes as an RDF 1.2 REIFIER, and no single parser can mean both at once.
42
+ # `--quoted-triple-syntax` says which one an input file means. Renamed because
43
+ # one `[dependencies]` table cannot hold two entries called `oxttl`; it is the
44
+ # same crate, two major lines, the way `oxrdf` 0.2 and 0.3 already sit in
45
+ # Cargo.lock together.
46
+ oxttl12 = { package = "oxttl", version = "0.2", features = ["rdf-12"] }
47
+ # The RDF 1.2 term model the 0.2 parsers hand back. `oxttl` does not re-export
48
+ # it, so naming it here is the only way to match on a `GraphName`; it adds no
49
+ # code that `oxttl12` was not already pulling in.
50
+ oxrdf12 = { package = "oxrdf", version = "0.3", features = ["rdf-12"] }
33
51
  oxrdfxml = "0.1"
34
52
  spargebra = { version = "0.3", features = ["rdf-star"] }
35
53
  serde_json = "1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rete-graph
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -82,7 +82,7 @@ More:
82
82
 
83
83
  ```python
84
84
  g = rete.open("data/example.rete") # local file, opened lazily too
85
- g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl")
85
+ g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl"/"trig"/"rdfxml")
86
86
  g = rete.open(rete.build(rdflib_graph)) # or straight from an rdflib Graph/Dataset
87
87
  g = rete.open(url, headers={"Authorization": "Bearer ..."}) # authed hosts
88
88
 
@@ -128,6 +128,33 @@ reads at `batch_size=1`, versus 44 MB at the default 10 000), a large one
128
128
  speeds up a full walk. Scope with `graph=`: `rete.DEFAULT_GRAPH` for the
129
129
  unnamed default graph alone, or an IRI for one named graph.
130
130
 
131
+ ### RDF 1.2 and quoted triples
132
+
133
+ The same `--quoted-triple-syntax` vocabulary and defaults as the CLI, in both
134
+ directions:
135
+
136
+ ```python
137
+ # Reading: what Turtle/TriG `<< s p o >>` means. "rdf-star" (default) = a quoted
138
+ # triple, as before; "rdf12" = RDF 1.2 (`<<( s p o )>>` triple terms, `<< … >>`
139
+ # reifiers, `{| … |}` annotations). N-Triples/N-Quads/RDF-XML ignore it.
140
+ g = rete.open(rete.build(ttl_text, "ttl", quoted_triple_syntax="rdf12"))
141
+ rete.Builder().add_file("data.trig", quoted_triple_syntax="rdf12")
142
+
143
+ # Writing: to_nquads spells a quoted triple as the RDF 1.2 triple term
144
+ # `<<( s p o )>>` by default, which pyoxigraph / Oxigraph 0.5 and Jena 5 read;
145
+ # "rdf-star" writes rete's stored `<<s p o>>`. RDF 1.2 has no subject-position
146
+ # triple term, so such a statement raises ValueError under "rdf12".
147
+ g.to_nquads("out.nq") # rdf12
148
+ g.to_nquads("out.nq", quoted_triple_syntax="rdf-star")
149
+
150
+ # The card says whether a file holds any, from its header (so old files answer):
151
+ g.card()["signals"]["quoted_triples"]
152
+ # {"present": True, "export_surfaces": ["rdf12", "rdf-star"], "export_default": "rdf12"}
153
+ ```
154
+
155
+ The Pyodide wheel has no RDF 1.2 Turtle/TriG reader and refuses
156
+ `quoted_triple_syntax="rdf12"` for those formats by name.
157
+
131
158
  ### Prepare a `.rete` step by step
132
159
 
133
160
  The lazy `Builder` configures a build — sources, embedded Dataset Card,
@@ -54,7 +54,7 @@ More:
54
54
 
55
55
  ```python
56
56
  g = rete.open("data/example.rete") # local file, opened lazily too
57
- g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl")
57
+ g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl"/"trig"/"rdfxml")
58
58
  g = rete.open(rete.build(rdflib_graph)) # or straight from an rdflib Graph/Dataset
59
59
  g = rete.open(url, headers={"Authorization": "Bearer ..."}) # authed hosts
60
60
 
@@ -100,6 +100,33 @@ reads at `batch_size=1`, versus 44 MB at the default 10 000), a large one
100
100
  speeds up a full walk. Scope with `graph=`: `rete.DEFAULT_GRAPH` for the
101
101
  unnamed default graph alone, or an IRI for one named graph.
102
102
 
103
+ ### RDF 1.2 and quoted triples
104
+
105
+ The same `--quoted-triple-syntax` vocabulary and defaults as the CLI, in both
106
+ directions:
107
+
108
+ ```python
109
+ # Reading: what Turtle/TriG `<< s p o >>` means. "rdf-star" (default) = a quoted
110
+ # triple, as before; "rdf12" = RDF 1.2 (`<<( s p o )>>` triple terms, `<< … >>`
111
+ # reifiers, `{| … |}` annotations). N-Triples/N-Quads/RDF-XML ignore it.
112
+ g = rete.open(rete.build(ttl_text, "ttl", quoted_triple_syntax="rdf12"))
113
+ rete.Builder().add_file("data.trig", quoted_triple_syntax="rdf12")
114
+
115
+ # Writing: to_nquads spells a quoted triple as the RDF 1.2 triple term
116
+ # `<<( s p o )>>` by default, which pyoxigraph / Oxigraph 0.5 and Jena 5 read;
117
+ # "rdf-star" writes rete's stored `<<s p o>>`. RDF 1.2 has no subject-position
118
+ # triple term, so such a statement raises ValueError under "rdf12".
119
+ g.to_nquads("out.nq") # rdf12
120
+ g.to_nquads("out.nq", quoted_triple_syntax="rdf-star")
121
+
122
+ # The card says whether a file holds any, from its header (so old files answer):
123
+ g.card()["signals"]["quoted_triples"]
124
+ # {"present": True, "export_surfaces": ["rdf12", "rdf-star"], "export_default": "rdf12"}
125
+ ```
126
+
127
+ The Pyodide wheel has no RDF 1.2 Turtle/TriG reader and refuses
128
+ `quoted_triple_syntax="rdf12"` for those formats by name.
129
+
103
130
  ### Prepare a `.rete` step by step
104
131
 
105
132
  The lazy `Builder` configures a build — sources, embedded Dataset Card,
@@ -379,6 +379,18 @@ dependencies = [
379
379
  "thiserror",
380
380
  ]
381
381
 
382
+ [[package]]
383
+ name = "oxrdf"
384
+ version = "0.3.4"
385
+ source = "registry+https://github.com/rust-lang/crates.io-index"
386
+ checksum = "4be15186205eb60ddbdc2fb47cb25df472c7f399bbe119b68c0972b6b6dd46ea"
387
+ dependencies = [
388
+ "oxilangtag",
389
+ "oxiri",
390
+ "rand",
391
+ "thiserror",
392
+ ]
393
+
382
394
  [[package]]
383
395
  name = "oxrdfxml"
384
396
  version = "0.1.7"
@@ -387,7 +399,7 @@ checksum = "d8d4bf9c5331127f01efbd1245d90fd75b7c546a97cb3e95461121ce1ad5b1c8"
387
399
  dependencies = [
388
400
  "oxilangtag",
389
401
  "oxiri",
390
- "oxrdf",
402
+ "oxrdf 0.2.4",
391
403
  "quick-xml",
392
404
  "thiserror",
393
405
  ]
@@ -401,7 +413,20 @@ dependencies = [
401
413
  "memchr",
402
414
  "oxilangtag",
403
415
  "oxiri",
404
- "oxrdf",
416
+ "oxrdf 0.2.4",
417
+ "thiserror",
418
+ ]
419
+
420
+ [[package]]
421
+ name = "oxttl"
422
+ version = "0.2.4"
423
+ source = "registry+https://github.com/rust-lang/crates.io-index"
424
+ checksum = "b1d0fe19fa62a6a102f85b052bbd86e3cdc3360cd6a338d01bab1a6743bf1d8e"
425
+ dependencies = [
426
+ "memchr",
427
+ "oxilangtag",
428
+ "oxiri",
429
+ "oxrdf 0.3.4",
405
430
  "thiserror",
406
431
  ]
407
432
 
@@ -602,16 +627,20 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
602
627
 
603
628
  [[package]]
604
629
  name = "rete-core"
605
- version = "0.3.2"
630
+ version = "0.3.4"
606
631
  dependencies = [
607
632
  "blake3",
608
633
  "getrandom 0.2.17",
609
634
  "md-5",
610
- "oxrdf",
635
+ "oxiri",
636
+ "oxrdf 0.2.4",
637
+ "oxrdf 0.3.4",
611
638
  "oxrdfxml",
612
- "oxttl",
639
+ "oxttl 0.1.8",
640
+ "oxttl 0.2.4",
613
641
  "regex-lite",
614
642
  "ruzstd",
643
+ "serde",
615
644
  "serde_json",
616
645
  "sha1",
617
646
  "sha2",
@@ -622,7 +651,7 @@ dependencies = [
622
651
 
623
652
  [[package]]
624
653
  name = "rete-python"
625
- version = "0.3.2"
654
+ version = "0.3.4"
626
655
  dependencies = [
627
656
  "pyo3",
628
657
  "rete-core",
@@ -701,6 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
701
730
  checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
702
731
  dependencies = [
703
732
  "serde_core",
733
+ "serde_derive",
704
734
  ]
705
735
 
706
736
  [[package]]
@@ -778,7 +808,7 @@ checksum = "8907e262be4b4b363218f4688f5654d423a958aa4b8d7c7a7f898be591fa474e"
778
808
  dependencies = [
779
809
  "oxilangtag",
780
810
  "oxiri",
781
- "oxrdf",
811
+ "oxrdf 0.2.4",
782
812
  "peg",
783
813
  "rand",
784
814
  "thiserror",
@@ -6,7 +6,7 @@ name = "rete-python"
6
6
  # Kept in lockstep with pyproject.toml's [project] version, and with the engine's
7
7
  # major.minor, by scripts/sync_versions.py (--check runs in CI). __version__ comes
8
8
  # from here (CARGO_PKG_VERSION), the wheel metadata from pyproject.
9
- version = "0.3.2"
9
+ version = "0.3.4"
10
10
  edition = "2021"
11
11
  license = "Apache-2.0"
12
12
  repository = "https://github.com/caviri/rete"
@@ -54,7 +54,7 @@ More:
54
54
 
55
55
  ```python
56
56
  g = rete.open("data/example.rete") # local file, opened lazily too
57
- g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl")
57
+ g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl"/"trig"/"rdfxml")
58
58
  g = rete.open(rete.build(rdflib_graph)) # or straight from an rdflib Graph/Dataset
59
59
  g = rete.open(url, headers={"Authorization": "Bearer ..."}) # authed hosts
60
60
 
@@ -100,6 +100,33 @@ reads at `batch_size=1`, versus 44 MB at the default 10 000), a large one
100
100
  speeds up a full walk. Scope with `graph=`: `rete.DEFAULT_GRAPH` for the
101
101
  unnamed default graph alone, or an IRI for one named graph.
102
102
 
103
+ ### RDF 1.2 and quoted triples
104
+
105
+ The same `--quoted-triple-syntax` vocabulary and defaults as the CLI, in both
106
+ directions:
107
+
108
+ ```python
109
+ # Reading: what Turtle/TriG `<< s p o >>` means. "rdf-star" (default) = a quoted
110
+ # triple, as before; "rdf12" = RDF 1.2 (`<<( s p o )>>` triple terms, `<< … >>`
111
+ # reifiers, `{| … |}` annotations). N-Triples/N-Quads/RDF-XML ignore it.
112
+ g = rete.open(rete.build(ttl_text, "ttl", quoted_triple_syntax="rdf12"))
113
+ rete.Builder().add_file("data.trig", quoted_triple_syntax="rdf12")
114
+
115
+ # Writing: to_nquads spells a quoted triple as the RDF 1.2 triple term
116
+ # `<<( s p o )>>` by default, which pyoxigraph / Oxigraph 0.5 and Jena 5 read;
117
+ # "rdf-star" writes rete's stored `<<s p o>>`. RDF 1.2 has no subject-position
118
+ # triple term, so such a statement raises ValueError under "rdf12".
119
+ g.to_nquads("out.nq") # rdf12
120
+ g.to_nquads("out.nq", quoted_triple_syntax="rdf-star")
121
+
122
+ # The card says whether a file holds any, from its header (so old files answer):
123
+ g.card()["signals"]["quoted_triples"]
124
+ # {"present": True, "export_surfaces": ["rdf12", "rdf-star"], "export_default": "rdf12"}
125
+ ```
126
+
127
+ The Pyodide wheel has no RDF 1.2 Turtle/TriG reader and refuses
128
+ `quoted_triple_syntax="rdf12"` for those formats by name.
129
+
103
130
  ### Prepare a `.rete` step by step
104
131
 
105
132
  The lazy `Builder` configures a build — sources, embedded Dataset Card,
@@ -0,0 +1,270 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "intro",
6
+ "metadata": {},
7
+ "source": "# Graph Data Science on RDF — the full algorithm catalog on a single `.rete` file\n\nGraph data science — centrality, community detection, embeddings, link\nprediction, GNNs — usually assumes a graph database with an analytics add-on\nbolted to it. This notebook runs the whole workflow over an **RDF graph in one\nfile**, with no server anywhere:\n\n1. **Project** — a SPARQL query pulls an edge list out of a single 1 MB\n **`.rete`** file on object storage, read lazily over HTTP range requests\n (only the byte ranges the query touches are downloaded);\n2. **Compute** — NetworkX, NumPy and scikit-learn run the standard\n graph-algorithm catalog on the projection;\n3. **Write back** — the results become new triples in a *derived* `.rete` file.\n\nIf you come from a property-graph stack, this project → compute → write-back\nloop is the same architecture as Neo4j's Graph Data Science library — the next\ncell maps its catalog item for item. If you come from NetworkX or igraph,\nsteps 2–3 are home turf and step 1 replaces your CSV loader.\n\n**Where to run it** — the same cells work in both:\n\n- **in the browser** — [JupyterLite](https://caviri.github.io/rete/jupyterlite/lab/index.html?path=graph-data-science.ipynb):\n the kernel is [Pyodide](https://pyodide.org) (Python compiled to WebAssembly),\n nothing to install, no account;\n- **on any Jupyter server** — [Colab](https://colab.research.google.com/github/caviri/rete/blob/main/clients/python/examples/graph-data-science.ipynb),\n [Binder](https://mybinder.org/v2/gh/caviri/rete/main?labpath=clients%2Fpython%2Fexamples%2Fgraph-data-science.ipynb),\n or your own `jupyter lab` (`rete-graph` ships plain abi3 wheels).\n\nTwo optional cells (§10–11) want `scikit-network` / `torch`: where those are\nmissing they **skip with a printed note** rather than fail — Pyodide has\nneither, Binder preinstalls `scikit-network`, Colab ships `torch`. Everything\nelse runs everywhere.\n\n**The dataset**: [Mark Lombardi Networks](https://lombardinetworks.net) — 51\n*Narrative Structures*, the network drawings in which Lombardi (1951–2000) mapped\nglobal-finance scandals (BCCI, Iran–Contra, Harken Energy, the Vatican bank…) by\nhand: **2,934 actors** and **4,205 arcs whose line style carries the relationship\ntype**. He computed centrality by eye; we get to check his work.\n\nRun the cells top to bottom (`Shift+Enter`). Dataset license: CC BY-NC-SA 4.0."
8
+ },
9
+ {
10
+ "cell_type": "markdown",
11
+ "id": "catalog-map",
12
+ "metadata": {},
13
+ "source": "## The algorithm catalog, translated\n\nThe first column is the standard graph-data-science catalog — the algorithm\nnames you will meet in any analytics library (readers arriving from Neo4j GDS\nwill recognise their catalog item for item); the second is what runs here.\n\n| Algorithm | Here | Runs |\n|---|---|---|\n| Graph projection | SPARQL SELECT → DataFrame → NetworkX / CSR | anywhere |\n| Filtered / subgraph projection | `FILTER` / `VALUES` in the projection query | anywhere |\n| **Projection by ontology** *(no property-graph equivalent)* | `reason=True`: OWL `subPropertyOf` hierarchy widens the edge set | anywhere |\n| Degree Centrality | pure SPARQL `GROUP BY` — no export needed | anywhere |\n| PageRank, Article Rank | `nx.pagerank` | anywhere |\n| Betweenness (sampled) | `nx.betweenness_centrality(k=…)` | anywhere |\n| Closeness / Harmonic / Eigenvector | `nx.closeness_centrality`, … | anywhere |\n| HITS | `nx.hits` | anywhere |\n| Articulation Points, Bridges | `nx.articulation_points`, `nx.bridges` | anywhere |\n| Louvain, Modularity | `nx.community.louvain_communities` | anywhere |\n| Leiden | `sknetwork`/`leidenalg` | + `sknetwork` |\n| Label Propagation | `nx.community.label_propagation_communities` | anywhere |\n| WCC / SCC | `nx.connected_components`, `nx.strongly_connected_components` | anywhere |\n| Triangle Count / Clustering Coefficient | `nx.triangles`, `nx.average_clustering` | anywhere |\n| K-Core Decomposition | `nx.core_number` | anywhere |\n| K-1 Coloring | `nx.greedy_color` | anywhere |\n| K-Means / HDBSCAN (on embeddings) | `sklearn.cluster` | anywhere |\n| Conductance | `nx.conductance` | anywhere |\n| Node Similarity (Jaccard/Overlap/Cosine) | set ops / `scipy.spatial.distance` | anywhere |\n| K-Nearest Neighbors | `sklearn.neighbors.NearestNeighbors` | anywhere |\n| Dijkstra / A* / Yen / Bellman-Ford | `nx.shortest_path`, `nx.astar_path`, `nx.shortest_simple_paths` | anywhere |\n| BFS / DFS / All-pairs shortest path | `nx.bfs_tree`, `nx.all_pairs_shortest_path_length` | anywhere |\n| Spanning trees / Steiner / Max-flow | `nx.minimum_spanning_tree`, `nx.approximation.steiner_tree`, `nx.maximum_flow` | anywhere |\n| Topological sort / DAG longest path | `nx.topological_sort`, `nx.dag_longest_path` | anywhere |\n| Random Walk | a few lines of NumPy | anywhere |\n| **FastRP** node embeddings | ~15 lines of NumPy (below) | anywhere |\n| Node2Vec | random walks + `gensim` | + `gensim` |\n| GraphSAGE / GNN embeddings | PyTorch Geometric | + `torch` |\n| Adamic-Adar / Common Neighbors / Pref. Attachment / Resource Allocation | `nx.adamic_adar_index`, … | anywhere |\n| Node classification / regression pipelines | `sklearn` pipelines (below) | anywhere |\n| Link prediction pipelines | `sklearn` on pair features | anywhere |\n| Training methods (LogReg / RF / MLP) | `sklearn.linear_model`, `.ensemble`, `.neural_network` | anywhere |\n| Write-back of computed properties | bake results into a **derived `.rete`** (below) | anywhere |\n| Bulk-synchronous iteration (Pregel-style) | Python loop over the CSR / native frameworks | anywhere |\n\n**Runs**: *anywhere* = pure NetworkX / NumPy / scikit-learn / SPARQL — every\nruntime this notebook targets, the in-browser Pyodide kernel included.\n*+ `package`* = needs a compiled wheel Pyodide doesn't ship; any regular\nJupyter kernel runs it after a `pip install` (Binder preinstalls\n`scikit-network`, Colab ships `torch`).\n\n**Scale honesty:** NetworkX — in any runtime — is comfortable to ~10⁵–10⁶\nedges. Past that, hand the *same CSR matrix* to\n[scikit-network](https://scikit-network.readthedocs.io) and the same edge list to\n[PyTorch Geometric](https://pytorch-geometric.readthedocs.io) (both demoed at the\nend). Because `.rete` reads are lazy, you only ever download what you project."
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "install",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": "%pip install rete-graph pandas networkx scipy scikit-learn matplotlib\n\nimport sys, numpy as np, pandas as pd, networkx as nx\nimport rete_graph as rete\nprint(f\"rete-graph {rete.__version__} · networkx {nx.__version__} · platform {sys.platform!r}\")"
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "execution_count": null,
26
+ "id": "open",
27
+ "metadata": {},
28
+ "outputs": [],
29
+ "source": "g = rete.open(\"https://data.graphplaza.com/lombardi/lombardi.rete\")\ns = g.stats()\nprint(f\"{g.quads:,} triples reachable; opened after fetching only \"\n f\"{s['bytes']:,} of {s['fileLength']:,} bytes in {s['requests']} range requests\")\nprint(g.card()[\"title\"], \"—\", g.card()[\"license\"])\n\nPREFIXES = \"\"\"\nPREFIX lomb: <https://w3id.org/rete/lombardi/>\nPREFIX lo: <http://www.lombardinetworks.net/lombardi.owl#>\nPREFIX skos: <http://www.w3.org/2004/02/skos/core#>\nPREFIX schema: <https://schema.org/>\n\"\"\""
30
+ },
31
+ {
32
+ "cell_type": "markdown",
33
+ "id": "project-md",
34
+ "metadata": {},
35
+ "source": "## 1 · Graph projection — one SPARQL query\n\nThe projection step pulls node and relationship tables out of storage into an\nin-memory graph. In this graph every drawn arc is *reified*: an `Arc` resource\nwith `lomb:source`, `lomb:target`, `lomb:arcType` (the line style — influence,\nmoney, blocked deal…) and `lomb:inDrawing`. One SPARQL query is the whole\nprojection — **including the filters**, which in a graph-analytics library\nwould be relationship-type and node-label projection config: we drop\nchronology arrows (`YearArrow`, …) and Lombardi's year-mark pseudo-actors\n(`lo:Year`), keeping only substantive relationships:"
36
+ },
37
+ {
38
+ "cell_type": "code",
39
+ "execution_count": null,
40
+ "id": "project",
41
+ "metadata": {},
42
+ "outputs": [],
43
+ "source": "edges = g.query_df(PREFIXES + \"\"\"\nSELECT ?src ?tgt ?type ?drawing WHERE {\n ?arc lomb:source ?src ; lomb:target ?tgt ;\n lomb:arcType ?type ; lomb:inDrawing ?drawing .\n FILTER(?type NOT IN (lo:YearArrow, lo:YearLine, lo:Final, lo:SingleNearby))\n FILTER(?src != ?tgt)\n FILTER NOT EXISTS { ?src a lo:Year } FILTER NOT EXISTS { ?tgt a lo:Year }\n FILTER NOT EXISTS { ?src a lo:YearFinal } FILTER NOT EXISTS { ?tgt a lo:YearFinal }\n}\"\"\")\n\nnames = g.query_df(PREFIXES + \"\"\"\nSELECT ?actor ?label WHERE { ?actor skos:prefLabel ?label ; lomb:actorId ?id }\"\"\")\nlabel = dict(zip(names[\"actor\"], names[\"label\"]))\n\nprint(f\"projected {len(edges):,} typed edges over {len(names):,} labelled actors\")\nedges[\"type\"].map(lambda t: t.split(\"#\")[-1]).value_counts().to_frame(\"arcs\")"
44
+ },
45
+ {
46
+ "cell_type": "markdown",
47
+ "id": "reason-md",
48
+ "metadata": {},
49
+ "source": "### Projection by ontology — something property graphs cannot do\n\nEvery arc predicate (`lomb:rel/financialTransaction`, `lomb:rel/influenceControl`, …)\nis declared `rdfs:subPropertyOf lomb:connectedTo` in the file's own OWL ontology.\nAsk for the *super*-property with OWL 2 QL reasoning on, and the ontology **is**\nthe projection config — no re-projection, no duplication:"
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": null,
54
+ "id": "reason",
55
+ "metadata": {},
56
+ "outputs": [],
57
+ "source": "plain = len(g.query(PREFIXES + \"SELECT ?s ?o WHERE { ?s lomb:connectedTo ?o }\"))\nentailed = len(g.query(PREFIXES + \"SELECT ?s ?o WHERE { ?s lomb:connectedTo ?o }\", reason=True))\nprint(f\"asserted connectedTo edges: {plain:,}\")\nprint(f\"with subPropertyOf entailment: {entailed:,} ← every typed arc, one query\")"
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "build-graph",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": "G = nx.Graph() # undirected, weight = number of parallel arcs\nD = nx.DiGraph() # directed (arcs have direction: who controls whom)\nfor s_, t_ in zip(edges[\"src\"], edges[\"tgt\"]):\n G.add_edge(s_, t_, weight=G[s_][t_][\"weight\"] + 1 if G.has_edge(s_, t_) else 1)\n D.add_edge(s_, t_)\nprint(f\"projected graph: {G.number_of_nodes():,} nodes · {G.number_of_edges():,} edges\")\n\ndef top(scores, n=10):\n \"\"\"Top-n of a score dict, with human names.\"\"\"\n return pd.DataFrame(\n [(label.get(k, k), round(v, 5)) for k, v in\n sorted(scores.items(), key=lambda kv: -kv[1])[:n]],\n columns=[\"actor\", \"score\"])"
66
+ },
67
+ {
68
+ "cell_type": "markdown",
69
+ "id": "degree-md",
70
+ "metadata": {},
71
+ "source": "## 2 · Centrality\n\n**Degree centrality never needs an export** — it is a SPARQL `GROUP BY`,\ncomputed inside the engine over the reified arcs:"
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": null,
76
+ "id": "degree",
77
+ "metadata": {},
78
+ "outputs": [],
79
+ "source": "deg = g.query_df(PREFIXES + \"\"\"\nSELECT ?actor (COUNT(?arc) AS ?degree) WHERE {\n { ?arc lomb:source ?actor } UNION { ?arc lomb:target ?actor }\n}\nGROUP BY ?actor ORDER BY DESC(?degree) LIMIT 10\"\"\")\ndeg[\"actor\"] = deg[\"actor\"].map(label)\ndeg"
80
+ },
81
+ {
82
+ "cell_type": "markdown",
83
+ "id": "centrality-md",
84
+ "metadata": {},
85
+ "source": "For the rest of the centrality family we use the projection. Betweenness is\nsampled (`k=400` pivots), the standard trick on graphs this size — it is still\nthe slowest cell, give it ~a minute. Lombardi's drawings reward it: betweenness\nfinds the *brokers* his drawings are literally about — BCCI, the Zurich shell\nKapital Beratung, the Harvard endowment:"
86
+ },
87
+ {
88
+ "cell_type": "code",
89
+ "execution_count": null,
90
+ "id": "centrality",
91
+ "metadata": {},
92
+ "outputs": [],
93
+ "source": "pagerank = nx.pagerank(D, alpha=0.85)\nbetweenness = nx.betweenness_centrality(G, k=400, seed=1) # sampled, k pivots\ncloseness = nx.closeness_centrality(G)\neigenvector = nx.eigenvector_centrality(G, max_iter=500)\nhubs, auths = nx.hits(D)\n\nsummary = pd.DataFrame({\n \"pagerank\": top(pagerank)[\"actor\"].values,\n \"betweenness\": top(betweenness)[\"actor\"].values,\n \"closeness\": top(closeness)[\"actor\"].values,\n \"eigenvector\": top(eigenvector)[\"actor\"].values,\n \"hits-hubs\": top(hubs)[\"actor\"].values,\n}, index=range(1, 11))\nprint(f\"articulation points (single points of failure): {len(list(nx.articulation_points(G))):,}\")\nsummary"
94
+ },
95
+ {
96
+ "cell_type": "markdown",
97
+ "id": "community-md",
98
+ "metadata": {},
99
+ "source": "## 3 · Community detection\n\nLombardi drew each scandal as one drawing. If community detection works, it\nshould **rediscover the scandals** from the merged 51-drawing graph — and it\ndoes: Louvain lands at modularity **0.91** (an extremely well-clustered graph)\nwith ~70 communities, and the big ones read like his catalogue raisonné:"
100
+ },
101
+ {
102
+ "cell_type": "code",
103
+ "execution_count": null,
104
+ "id": "community",
105
+ "metadata": {},
106
+ "outputs": [],
107
+ "source": "wcc = list(nx.connected_components(G))\nscc = list(nx.strongly_connected_components(D))\nlouvain = nx.community.louvain_communities(G, seed=42)\nlabelprop = list(nx.community.label_propagation_communities(G))\nprint(f\"WCC: {len(wcc)} components (largest {max(map(len, wcc)):,}) · \"\n f\"SCC: {len(scc):,} · label-prop: {len(labelprop)} communities\")\nprint(f\"Louvain: {len(louvain)} communities · \"\n f\"modularity {nx.community.modularity(G, louvain):.3f}\")\n\npd.DataFrame([\n {\"size\": len(c),\n \"members\": \", \".join(sorted(label.get(a, \"?\") for a in c)[:4]) + \" …\"}\n for c in sorted(louvain, key=len, reverse=True)[:6]])"
108
+ },
109
+ {
110
+ "cell_type": "code",
111
+ "execution_count": null,
112
+ "id": "triangles",
113
+ "metadata": {},
114
+ "outputs": [],
115
+ "source": "tri = sum(nx.triangles(G).values()) // 3\nprint(f\"triangles: {tri:,} · average clustering coefficient: {nx.average_clustering(G):.3f}\")\ncore = nx.core_number(G) # k-core decomposition\nkmax = max(core.values())\nmembers = sorted(label.get(n, n) for n, k in core.items() if k == kmax)\nprint(f\"max k-core: k={kmax} · {len(members)} actors in the graph's densest shell, e.g.:\")\nprint(\" \" + \", \".join(members[:10]) + \" …\")"
116
+ },
117
+ {
118
+ "cell_type": "markdown",
119
+ "id": "similarity-md",
120
+ "metadata": {},
121
+ "source": "## 4 · Node Similarity — with ground truth in the file\n\nNode similarity by neighbourhood overlap (Jaccard) is a staple of every\nanalytics catalog. Here we compare *drawings* by shared cast — and this\ndataset ships its own precomputed answer (`lomb:sharesActorsWith` overlap\nnodes carrying `lomb:jaccard`), so for once an algorithm demo can be\n**checked against the file itself**:"
122
+ },
123
+ {
124
+ "cell_type": "code",
125
+ "execution_count": null,
126
+ "id": "similarity",
127
+ "metadata": {},
128
+ "outputs": [],
129
+ "source": "dep = g.query_df(PREFIXES + \"SELECT ?d ?actor WHERE { ?d lomb:depicts ?actor }\")\ntdf = g.query_df(PREFIXES + \"SELECT ?d ?name WHERE { ?d a lomb:Drawing ; schema:name ?name }\")\ntitle = dict(zip(tdf[\"d\"], tdf[\"name\"]))\ncast = {}\nfor d_, a_ in zip(dep[\"d\"], dep[\"actor\"]):\n cast.setdefault(d_, set()).add(a_)\n\nshipped = g.query_df(PREFIXES + \"\"\"\nSELECT ?a ?b ?j WHERE {\n ?ov lomb:betweenDrawing ?a, ?b ; lomb:jaccard ?j . FILTER(STR(?a) < STR(?b))\n}\"\"\")\nrows, agree = [], 0\nfor a_, b_, j_ in zip(shipped[\"a\"], shipped[\"b\"], shipped[\"j\"]):\n ours = len(cast[a_] & cast[b_]) / len(cast[a_] | cast[b_])\n agree += abs(ours - float(j_)) < 0.02\n rows.append({\"drawing A\": title.get(a_, a_)[:34], \"drawing B\": title.get(b_, b_)[:34],\n \"ours\": round(ours, 3), \"shipped\": round(float(j_), 3)})\nprint(f\"our Jaccard agrees with the file's own on {agree}/{len(shipped)} drawing pairs\")\npd.DataFrame(sorted(rows, key=lambda r: -r[\"ours\"])[:8])"
130
+ },
131
+ {
132
+ "cell_type": "markdown",
133
+ "id": "linkpred-md",
134
+ "metadata": {},
135
+ "source": "## 5 · Topological link prediction — which doubles as entity resolution\n\nAdamic-Adar, Common Neighbors, Preferential Attachment, Resource Allocation —\nNetworkX has all four classic link predictors as one-liners. The top\npredictions here split into two delightful categories:\n\n- **near-duplicates under different spellings** — \"FNN\" ↔ \"Financial News\n Network\", \"Indian Springs St Bk\" ↔ \"Indian Springs State Bank\". The dataset's\n own `lomb:sameNameAs` links duplicates with *identical* names (we checked:\n 250 of its 306 pairs are byte-equal labels), so link prediction **extends the\n curators' entity resolution** to the spellings exact matching cannot catch;\n- **plausible ties Lombardi never drew as one arc** — \"Tory party\" ↔ \"Pergau\n dam\" *is* the 1994 aid-for-arms affair."
136
+ },
137
+ {
138
+ "cell_type": "code",
139
+ "execution_count": null,
140
+ "id": "linkpred",
141
+ "metadata": {},
142
+ "outputs": [],
143
+ "source": "from itertools import combinations\n# candidate pairs = non-adjacent nodes sharing >= 1 neighbour (the usual restriction)\ncand = set()\nfor n in G.nodes:\n for u, v in combinations(G[n], 2):\n if not G.has_edge(u, v):\n cand.add((u, v) if u < v else (v, u))\nprint(f\"{len(cand):,} candidate pairs at distance 2\")\n\naa = sorted(nx.adamic_adar_index(G, cand), key=lambda x: -x[2])[:8]\nra = {(u, v): s_ for u, v, s_ in nx.resource_allocation_index(G, [p[:2] for p in aa])}\npa = {(u, v): s_ for u, v, s_ in nx.preferential_attachment(G, [p[:2] for p in aa])}\n\npd.DataFrame([{\"actor A\": label.get(u, u)[:30], \"actor B\": label.get(v, v)[:30],\n \"adamic-adar\": round(s_, 2),\n \"common nbrs\": len(list(nx.common_neighbors(G, u, v))),\n \"resource-alloc\": round(ra[(u, v)], 2),\n \"pref-attach\": pa[(u, v)]}\n for u, v, s_ in aa])"
144
+ },
145
+ {
146
+ "cell_type": "markdown",
147
+ "id": "paths-md",
148
+ "metadata": {},
149
+ "source": "## 6 · Path finding\n\nLombardi's most famous drawing connects George W. Bush to the bin Laden family\nthrough Texas oil money. Dijkstra agrees with him — and note the SPARQL\ncross-check: a *property path* answers reachability inside the engine, no\nexport needed (it runs over all `connectedTo` arcs, chronology included, hence\nthe larger count):"
150
+ },
151
+ {
152
+ "cell_type": "code",
153
+ "execution_count": null,
154
+ "id": "paths",
155
+ "metadata": {},
156
+ "outputs": [],
157
+ "source": "bush = next(n for n in G.nodes if \"George W. Bush\" in label.get(n, \"\"))\nosama = next(n for n in G.nodes if label.get(n, \"\") == \"Osama bin Laden\")\n\npath = nx.shortest_path(G, bush, osama) # Dijkstra\nprint(\" → \".join(label.get(n, n) for n in path))\n\nk_paths = []\nimport itertools\nfor p_ in itertools.islice(nx.shortest_simple_paths(G, bush, osama), 3): # Yen's k-shortest\n k_paths.append(\" → \".join(label.get(n, n) for n in p_))\nprint(\"\\nYen's 3 shortest:\", *k_paths, sep=\"\\n \")\n\nreach = g.query(PREFIXES +\n \"SELECT (COUNT(DISTINCT ?x) AS ?n) WHERE { <%s> (lomb:connectedTo|^lomb:connectedTo)+ ?x }\" % bush)\nprint(f\"\\nSPARQL property-path: {reach[0]['n'].value} actors reachable from Bush \"\n f\"(NetworkX component: {len(nx.node_connected_component(G, bush)):,})\")"
158
+ },
159
+ {
160
+ "cell_type": "markdown",
161
+ "id": "fastrp-md",
162
+ "metadata": {},
163
+ "source": "## 7 · Node embeddings — FastRP is ~15 lines of NumPy\n\nFastRP (Chen et al. 2019), the workhorse node-embedding algorithm of the\nanalytics catalogs, is sparse random projection plus a weighted sum of powers\nof the normalized adjacency matrix. That is three matrix products — NumPy\nterritory. Same CSR matrix, and the customary iteration weights:"
164
+ },
165
+ {
166
+ "cell_type": "code",
167
+ "execution_count": null,
168
+ "id": "fastrp",
169
+ "metadata": {},
170
+ "outputs": [],
171
+ "source": "import scipy.sparse as sp\nnodes = list(G.nodes); nix = {n: i for i, n in enumerate(nodes)}\nr = [nix[u] for u, v in G.edges] + [nix[v] for u, v in G.edges]\nc = [nix[v] for u, v in G.edges] + [nix[u] for u, v in G.edges]\nA = sp.csr_matrix((np.ones(len(r)), (r, c)), shape=(len(nodes), len(nodes)))\n\ndeg_ = np.asarray(A.sum(1)).ravel(); deg_[deg_ == 0] = 1\nAn = sp.diags(1 / deg_) @ A # row-normalized adjacency\nrng = np.random.default_rng(42)\nR = rng.choice([-1.0, 0.0, 1.0], size=(len(nodes), 64), p=[1/6, 2/3, 1/6]) * np.sqrt(3)\nX1 = An @ R; X2 = An @ X1; X3 = An @ X2 # 1-, 2-, 3-hop views\nemb = X1 + 0.8 * X2 + 0.6 * X3 # FastRP iteration weights\nemb /= np.linalg.norm(emb, axis=1, keepdims=True) + 1e-12\nprint(\"FastRP embeddings:\", emb.shape)\n\n# K-Nearest Neighbors on the embeddings:\nfrom sklearn.neighbors import NearestNeighbors\nnn = NearestNeighbors(n_neighbors=5, metric=\"cosine\").fit(emb)\n_, ind = nn.kneighbors(emb[[nix[bush]]])\nprint(\"nearest to George W. Bush in embedding space:\",\n \", \".join(label.get(nodes[j], \"?\") for j in ind[0][1:]))"
172
+ },
173
+ {
174
+ "cell_type": "code",
175
+ "execution_count": null,
176
+ "id": "plot",
177
+ "metadata": {},
178
+ "outputs": [],
179
+ "source": "import matplotlib.pyplot as plt\nfrom sklearn.decomposition import PCA\n\nxy = PCA(n_components=2).fit_transform(emb)\nbig6 = sorted(louvain, key=len, reverse=True)[:6]\nfig, ax = plt.subplots(figsize=(7, 5.5))\nax.scatter(xy[:, 0], xy[:, 1], s=3, c=\"#cccccc\", label=\"_\")\nfor i, comm in enumerate(big6):\n ix = [nix[n] for n in comm if n in nix]\n ax.scatter(xy[ix, 0], xy[ix, 1], s=6, label=f\"community {i} · {len(comm)} actors\")\nax.legend(fontsize=8, loc=\"best\"); ax.set_axis_off()\nax.set_title(\"FastRP embedding (PCA) coloured by Louvain community —\\neach cluster ≈ one Lombardi scandal\")\nplt.show()"
180
+ },
181
+ {
182
+ "cell_type": "markdown",
183
+ "id": "nodeclass-md",
184
+ "metadata": {},
185
+ "source": "## 8 · The ML pipeline — node classification, honestly\n\nA node-classification pipeline is features + train/test split + a classifier.\nEvery actor here is typed `lo:Person` or `lo:Institution` — can structure alone\nrecover that? **Almost not**: FastRP-only scores ≈ the majority baseline.\n\nThe fix is the RDF advantage. In a property graph the arc *types* live in the\nprojection config; in RDF they are first-class data. Count each node's incident\narcs **by type** and the classifier works — and is interpretable: institutions\n*receive* `InfluenceControl` arcs, people *exert* them. Lombardi would nod."
186
+ },
187
+ {
188
+ "cell_type": "code",
189
+ "execution_count": null,
190
+ "id": "nodeclass",
191
+ "metadata": {},
192
+ "outputs": [],
193
+ "source": "types_df = g.query_df(PREFIXES + \"\"\"\nSELECT ?actor ?cls WHERE {\n ?actor a ?cls ; lomb:actorId ?id . FILTER(?cls IN (lo:Person, lo:Institution))\n}\"\"\")\ncls_map = dict(zip(types_df[\"actor\"], types_df[\"cls\"]))\n\narc_types = sorted(edges[\"type\"].unique()); tix = {t: i for i, t in enumerate(arc_types)}\nF = np.zeros((len(nodes), 2 * len(arc_types)))\nfor s_, t_, ty in zip(edges[\"src\"], edges[\"tgt\"], edges[\"type\"]):\n F[nix[s_], tix[ty]] += 1 # arcs OUT, by type\n F[nix[t_], len(arc_types) + tix[ty]] += 1 # arcs IN, by type\n\nkeep = [i for i, n in enumerate(nodes) if cls_map.get(n, \"\").endswith((\"#Person\", \"#Institution\"))]\ny = np.array([1 if cls_map[nodes[i]].endswith(\"#Person\") else 0 for i in keep])\n\nfrom sklearn.model_selection import cross_val_score\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.preprocessing import StandardScaler\nbase = max(y.mean(), 1 - y.mean())\nacc_emb = cross_val_score(LogisticRegression(max_iter=3000), emb[keep], y, cv=5).mean()\nXs = StandardScaler().fit_transform(F[keep])\nacc_feat = cross_val_score(LogisticRegression(max_iter=3000), Xs, y, cv=5).mean()\nprint(f\"majority baseline {base:.3f}\")\nprint(f\"FastRP embeddings only {acc_emb:.3f} ← structure alone barely helps\")\nprint(f\"typed-edge features {acc_feat:.3f} ← RDF predicates are the signal\")\n\nlr = LogisticRegression(max_iter=3000).fit(Xs, y)\nfeat_names = [f\"out:{t.split('#')[-1]}\" for t in arc_types] + [f\"in:{t.split('#')[-1]}\" for t in arc_types]\npd.DataFrame(sorted(zip(feat_names, lr.coef_[0]), key=lambda kv: -abs(kv[1]))[:6],\n columns=[\"feature\", \"coef (+person / −institution)\"]).round(2)"
194
+ },
195
+ {
196
+ "cell_type": "markdown",
197
+ "id": "writeback-md",
198
+ "metadata": {},
199
+ "source": "## 9 · Write-back — immutably\n\nAn analytics server writes results back by mutating the database in place. A\n`.rete` file is immutable — so write-back means **deriving a new file**: bake\nthe computed scores into triples, build, and the analytics become\nSPARQL-queryable data (card and provenance included). This is the same builder\nthat made the original file:"
200
+ },
201
+ {
202
+ "cell_type": "code",
203
+ "execution_count": null,
204
+ "id": "writeback",
205
+ "metadata": {},
206
+ "outputs": [],
207
+ "source": "XSD = \"http://www.w3.org/2001/XMLSchema#\"\ncomm_of = {n: i for i, c in enumerate(sorted(louvain, key=len, reverse=True)) for n in c}\nnt = []\nfor n in nodes:\n nt.append(f'<{n}> <urn:gds:pagerank> \"{pagerank.get(n, 0):.6f}\"^^<{XSD}double> .')\n nt.append(f'<{n}> <urn:gds:community> \"{comm_of.get(n, -1)}\"^^<{XSD}integer> .')\n\nderived = (rete.Builder()\n .add(\"\\n\".join(nt))\n .card(title=\"Lombardi centrality & communities — derived .rete\",\n license=\"CC BY-NC-SA 4.0 (derived from lombardinetworks.net)\")\n .example(\"SELECT ?actor ?pr WHERE { ?actor <urn:gds:pagerank> ?pr } ORDER BY DESC(?pr) LIMIT 5\",\n title=\"Top actors by PageRank\")\n .graph())\nprint(f\"derived graph: {derived.quads:,} triples — analytics as queryable RDF\")\n\ndf = derived.query_df(\"\"\"\nSELECT ?community (COUNT(?actor) AS ?actors) WHERE {\n ?actor <urn:gds:community> ?community\n} GROUP BY ?community ORDER BY DESC(?actors) LIMIT 5\"\"\")\ndf"
208
+ },
209
+ {
210
+ "cell_type": "markdown",
211
+ "id": "sknetwork-md",
212
+ "metadata": {},
213
+ "source": "## 10 · Same matrix, bigger engines — scikit-network\n\nThe CSR matrix `A` we built for FastRP is *exactly* what\n[scikit-network](https://scikit-network.readthedocs.io) consumes — its Louvain\nand PageRank are compiled and orders of magnitude faster than NetworkX, which is\nwhat you want past ~10⁶ edges. Its compiled wheels aren't available to the\nin-browser Pyodide kernel, where this cell skips with a note; Binder\npreinstalls it, and anywhere else `pip install scikit-network` lights it up:"
214
+ },
215
+ {
216
+ "cell_type": "code",
217
+ "execution_count": null,
218
+ "id": "sknetwork",
219
+ "metadata": {},
220
+ "outputs": [],
221
+ "source": "try:\n from sknetwork.ranking import PageRank\n from sknetwork.clustering import Louvain\n pr = PageRank().fit_predict(A)\n communities = Louvain().fit_predict(A)\n print(\"scikit-network PageRank top-5:\",\n \", \".join(label.get(nodes[i], \"?\") for i in np.argsort(-pr)[:5]))\n print(f\"scikit-network Louvain: {len(set(communities))} communities\")\nexcept ImportError:\n print(\"scikit-network is not installed in this environment — skipping.\")\n print(\"Run `pip install scikit-network` on any Jupyter server (Binder has it\")\n print(\"preinstalled; the in-browser Pyodide kernel can't install it) and this\")\n print(\"cell lights up; A (csr_matrix) and the edge list are already in shape.\")"
222
+ },
223
+ {
224
+ "cell_type": "markdown",
225
+ "id": "pyg-md",
226
+ "metadata": {},
227
+ "source": "## 11 · The GNN bridge — PyTorch Geometric\n\nFor GNNs — GraphSAGE and friends — and for learned pipelines at scale, the\nopen ecosystem's tool is\n[PyTorch Geometric](https://pytorch-geometric.readthedocs.io). The projection\nDataFrame maps 1:1 onto PyG's two graph containers:\n\n- **`Data`** — `edge_index` is just our `src/tgt` columns factorized;\n- **`HeteroData`** — one relation per `arcType`, which is *exactly the RDF shape*:\n `(\"actor\", \"FinancialTransaction\", \"actor\")`.\n\nVerified on this projection: a 2-layer GCN reaches **0.695** test accuracy on\nPerson-vs-Institution (baseline 0.576) — and, a lesson worth keeping, the\nhumble typed-edge logistic regression above (0.81) beats it at this scale.\nTorch isn't available to the in-browser kernel and is deliberately left out of\nthe Binder image (~800 MB of wheels); Colab already ships it, and anywhere\nelse `pip install torch torch_geometric` activates this cell:"
228
+ },
229
+ {
230
+ "cell_type": "code",
231
+ "execution_count": null,
232
+ "id": "pyg",
233
+ "metadata": {},
234
+ "outputs": [],
235
+ "source": "try:\n import torch\n from torch_geometric.data import Data, HeteroData\n from torch_geometric.nn import GCNConv\n import torch.nn.functional as Fn\n\n ei = torch.tensor([[nix[s_] for s_ in edges[\"src\"]],\n [nix[t_] for t_ in edges[\"tgt\"]]], dtype=torch.long)\n ei = torch.cat([ei, ei.flip(0)], dim=1)\n X = torch.tensor(F, dtype=torch.float) # typed-edge features\n y_all = torch.full((len(nodes),), -1, dtype=torch.long)\n for i in keep:\n y_all[i] = 1 if cls_map[nodes[i]].endswith(\"#Person\") else 0\n data = Data(x=X, edge_index=ei, y=y_all)\n print(data)\n\n hetero = HeteroData(); hetero[\"actor\"].num_nodes = len(nodes)\n for t_ in arc_types:\n m = edges[\"type\"] == t_\n hetero[\"actor\", t_.split(\"#\")[-1], \"actor\"].edge_index = torch.tensor(\n [[nix[s_] for s_ in edges[\"src\"][m]], [nix[x] for x in edges[\"tgt\"][m]]])\n print(\"hetero relations:\", [r for _, r, _ in hetero.edge_types])\n\n class GCN(torch.nn.Module):\n def __init__(self):\n super().__init__()\n self.c1, self.c2 = GCNConv(X.shape[1], 32), GCNConv(32, 2)\n def forward(self, d):\n return self.c2(Fn.relu(self.c1(d.x, d.edge_index)), d.edge_index)\n\n torch.manual_seed(0)\n lab = y_all >= 0\n perm = torch.randperm(int(lab.sum()))\n li = lab.nonzero(as_tuple=True)[0][perm]\n tr, te = li[:int(0.7 * len(li))], li[int(0.7 * len(li)):]\n model, opt = GCN(), None\n opt = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)\n for _ in range(120):\n model.train(); opt.zero_grad()\n Fn.cross_entropy(model(data)[tr], y_all[tr]).backward(); opt.step()\n model.eval()\n acc = (model(data).argmax(1)[te] == y_all[te]).float().mean().item()\n print(f\"GCN test accuracy: {acc:.3f}\")\nexcept ImportError:\n print(\"PyTorch (Geometric) is not installed in this environment — skipping.\")\n print(\"On Colab torch is already there (`pip install torch_geometric` completes\")\n print(\"it); elsewhere: pip install torch torch_geometric. Then this cell builds\")\n print(\"Data, HeteroData and trains the GCN as-is.\")"
236
+ },
237
+ {
238
+ "cell_type": "markdown",
239
+ "id": "a08f18b7",
240
+ "source": "## 12 · Close the loop — show the drawings\n\nEvery score above was computed from arcs Lombardi drew in pencil, and the file\nknows what the sheets look like. Each drawing is a `schema:VisualArtwork` that\n`lomb:depicts` its actors, and 17 of the 51 carry image links (all IRIs):\n`schema:image` — MoMA's photograph of the sheet at ≤1024 px — plus a 2000 px\n`lomb:momaImageFull`, while `lomb:imagePage` points at write-ups around the\nweb (those are pages, not images). Only the MoMA-photographed drawings have\nimage URLs, so we walk down the PageRank ranking and collect drawings until we\nhave six.\n\nOne last SPARQL query joins the analysis back to the art: *for the actors the\nalgorithms ranked highest, show me the drawings* — the score in the caption,\nLombardi's actual pencil lines above it. Where the notebook can reach the\nnetwork directly it embeds the image bytes; otherwise your browser loads the\nURL, and if an image ever fails to load the card degrades to its title and\nlinks instead of a broken grid.",
241
+ "metadata": {}
242
+ },
243
+ {
244
+ "cell_type": "code",
245
+ "id": "9acaff04",
246
+ "source": "import base64, html as html_mod\nfrom IPython.display import HTML, display\n\nart = g.query_df(PREFIXES + \"\"\"\nSELECT ?actor ?drawing ?title ?img ?full WHERE {\n ?drawing a schema:VisualArtwork ;\n lomb:depicts ?actor ;\n schema:name ?title ;\n schema:image ?img .\n OPTIONAL { ?drawing lomb:momaImageFull ?full }\n}\"\"\")\nby_actor = {}\nfor row in art.itertuples(index=False):\n by_actor.setdefault(row.actor, []).append(row)\nprint(f\"{art['drawing'].nunique()} drawings with images, \"\n f\"depicting {art['actor'].nunique():,} of our ranked actors\")\n\ndef as_data_uri(url, timeout=20):\n \"\"\"Embed the image bytes if this runtime can fetch them (CPython can;\n the Pyodide kernel has no socket layer, so there we return None and let\n the browser's <img> load the URL itself).\"\"\"\n try:\n from urllib.request import urlopen, Request\n with urlopen(Request(url, headers={\"User-Agent\": \"rete-gds-notebook\"}),\n timeout=timeout) as rsp:\n ctype = rsp.headers.get(\"Content-Type\", \"\")\n if not ctype.startswith(\"image/\"):\n return None\n return f\"data:{ctype};base64,{base64.b64encode(rsp.read()).decode()}\"\n except Exception:\n return None\n\n# walk down the PageRank ranking; first drawing we haven't shown yet wins\ncards, picked, seen = [], [], set()\nfor rank, actor in enumerate(sorted(pagerank, key=pagerank.get, reverse=True), 1):\n for row in by_actor.get(actor, []):\n if row.img in seen:\n continue\n seen.add(row.img)\n full = row.full if isinstance(row.full, str) else row.img\n esc = html_mod.escape\n caption = (\n f\"<b>#{rank} {esc(label.get(actor, actor))}</b> — \"\n f\"PageRank {pagerank[actor]:.4f} · community {comm_of.get(actor, '?')}<br>\"\n f\"<i>{esc(row.title)}</i><br>\"\n f'<a href=\"{esc(row.drawing)}\">drawing page</a> · '\n f'<a href=\"{esc(full)}\">full resolution</a>')\n src = as_data_uri(row.img) or row.img\n cards.append(\n f'<figure style=\"margin:0;width:230px;font-size:11px;line-height:1.5\">'\n f'<a href=\"{esc(full)}\"><img src=\"{esc(src)}\" alt=\"{esc(row.title)}\" '\n f'style=\"width:100%;border:1px solid #999;border-radius:3px\" '\n f'onerror=\"this.style.display=\\'none\\'\"></a>'\n f'<figcaption>{caption}</figcaption></figure>')\n picked.append({\"rank\": rank, \"actor\": label.get(actor, actor),\n \"pagerank\": round(pagerank[actor], 4),\n \"community\": comm_of.get(actor, -1),\n \"drawing\": row.title})\n break # one drawing per actor\n if len(cards) >= 6:\n break\n\ndisplay(pd.DataFrame(picked).set_index(\"rank\"))\ndisplay(HTML('<div style=\"display:flex;flex-wrap:wrap;gap:14px\">' + \"\".join(cards) + \"</div>\"))\nprint(\"Drawings © the estate of Mark Lombardi; photographs via MoMA. CC BY-NC-SA 4.0 metadata.\")",
247
+ "metadata": {},
248
+ "execution_count": null,
249
+ "outputs": []
250
+ },
251
+ {
252
+ "cell_type": "markdown",
253
+ "id": "outro",
254
+ "metadata": {},
255
+ "source": "---\n\n## What needs extra packages, and where this scales\n\n- **Leiden, Node2Vec, GraphSAGE, CELF** — same projection code, plus one\n `pip install` (`sknetwork`/`leidenalg`, `gensim`, PyG) on any Jupyter server.\n- **Billions of edges** — don't stream them into a notebook kernel. The same\n `.rete` files carry Parquet companions for bulk analytics, and any file is a\n standard SPARQL 1.1 endpoint, so server-side tooling (property-graph stores\n included — Neo4j reads RDF via neosemantics) can consume it.\n- **Write-back** here means *derive a new immutable file* — analytics results\n stay versioned, citable data instead of mutated database state.\n\n**Where next:** [the playground](https://caviri.github.io/rete/playground.html)\n(this dataset is `lombardi`) · [Python API](https://caviri.github.io/rete/python.html) ·\n[agent frameworks over .rete](https://caviri.github.io/rete/agent-frameworks.html) ·\n[docs index](https://caviri.github.io/rete/)\n\nSource & issues: **<https://github.com/caviri/rete>**\n\n© 2026 Carlos Vivar Ríos — Apache License 2.0. Dataset: *Mark Lombardi\nNetworks* ([lombardinetworks.net](https://lombardinetworks.net)),\nCC BY-NC-SA 4.0; drawings © the estate of Mark Lombardi."
256
+ }
257
+ ],
258
+ "metadata": {
259
+ "kernelspec": {
260
+ "display_name": "Python (Pyodide)",
261
+ "language": "python",
262
+ "name": "python"
263
+ },
264
+ "language_info": {
265
+ "name": "python"
266
+ }
267
+ },
268
+ "nbformat": 4,
269
+ "nbformat_minor": 5
270
+ }