rete-graph 0.3.3__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.
- {rete_graph-0.3.3 → rete_graph-0.3.4}/Cargo.toml +1 -1
- {rete_graph-0.3.3 → rete_graph-0.3.4}/PKG-INFO +1 -1
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/Cargo.lock +2 -2
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/Cargo.toml +1 -1
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/lib.rs +2 -1
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/pyramid.rs +239 -20
- rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/jev-commander-145.nt +149 -0
- rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/jev-random30-30.nt +34 -0
- rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/jev-random63-63.nt +67 -0
- rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/min-15-level0.nt +20 -0
- rete_graph-0.3.4/crates/rete-core/tests/fixtures/louvain-hangs/min-18-level1.nt +21 -0
- rete_graph-0.3.4/crates/rete-core/tests/louvain_termination.rs +155 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/pyproject.toml +1 -1
- {rete_graph-0.3.3 → rete_graph-0.3.4}/README.md +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/.gitignore +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/README.md +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/agents/README.md +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/agents/langchain_agent.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/agents/pydantic_ai_agent.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/agents/rete_tools.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/graph-data-science.ipynb +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/jupyterlite-build-tutorial.ipynb +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/jupyterlite-demo.ipynb +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/tutorial.ipynb +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/src/lib.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/src/readers.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/tests/conftest.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/tests/test_rdf12.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/tests/test_rete_graph.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/tests/test_warnings.py +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/Cargo.toml +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/LICENSE +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/README.md +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/examples/coherence_bench.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/examples/parallel_bench.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/examples/qbench.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/bgp.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/block_cache.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/card_derive.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/card_input.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/card_queries.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/chunk_cache.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/dict.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/dictionary.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/extbuild.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/file.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/geo.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/geo3.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/header.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/index.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/ingest.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/iri.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/manifest.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/meta.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/parallel.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/reach.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/reader.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/reason.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/results.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/row.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/schema_pyramid.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/service.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/shacl.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/aggregate.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/diag.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/eval.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/expr.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/lower.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/path.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql/ql.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/sparql.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/terms.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/text_index.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/tiling.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/triples.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/src/varint.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/community_eval.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/compatibility_v1.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/fixtures/v1/glue-stub.js +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/iri_differential.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/polyglot_roundtrip.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/properties.proptest-regressions +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/properties.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/public_api.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/ranged.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/robustness.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/roundtrip.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/service_federation.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/shacl_core.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/sparql_integration.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/sparql_warnings.rs +0 -0
- {rete_graph-0.3.3 → rete_graph-0.3.4}/python/rete_graph/__init__.py +0 -0
|
@@ -18,7 +18,7 @@ members = [
|
|
|
18
18
|
exclude = ["fuzz", "clients/python", "clients/r/src/rust", "clients/java/ffi", "clients/tauri/src-tauri"]
|
|
19
19
|
|
|
20
20
|
[workspace.package]
|
|
21
|
-
version = "0.3.
|
|
21
|
+
version = "0.3.4"
|
|
22
22
|
edition = "2021"
|
|
23
23
|
license = "Apache-2.0"
|
|
24
24
|
repository = "https://github.com/caviri/rete"
|
|
@@ -627,7 +627,7 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
|
|
627
627
|
|
|
628
628
|
[[package]]
|
|
629
629
|
name = "rete-core"
|
|
630
|
-
version = "0.3.
|
|
630
|
+
version = "0.3.4"
|
|
631
631
|
dependencies = [
|
|
632
632
|
"blake3",
|
|
633
633
|
"getrandom 0.2.17",
|
|
@@ -651,7 +651,7 @@ dependencies = [
|
|
|
651
651
|
|
|
652
652
|
[[package]]
|
|
653
653
|
name = "rete-python"
|
|
654
|
-
version = "0.3.
|
|
654
|
+
version = "0.3.4"
|
|
655
655
|
dependencies = [
|
|
656
656
|
"pyo3",
|
|
657
657
|
"rete-core",
|
|
@@ -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.
|
|
9
|
+
version = "0.3.4"
|
|
10
10
|
edition = "2021"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
repository = "https://github.com/caviri/rete"
|
|
@@ -227,7 +227,8 @@ pub use meta::{
|
|
|
227
227
|
};
|
|
228
228
|
#[doc(hidden)]
|
|
229
229
|
pub use pyramid::{
|
|
230
|
-
build_dendrogram, louvain_one_level, project_graph, Dendrogram, Graph,
|
|
230
|
+
build_dendrogram, louvain_one_level, louvain_one_level_stats, project_graph, Dendrogram, Graph,
|
|
231
|
+
LevelStats, Partition, PyramidAlgo, MAX_LOUVAIN_PASSES,
|
|
231
232
|
};
|
|
232
233
|
#[doc(hidden)]
|
|
233
234
|
pub use reach::{batch_reach_serial, build_adjacency, reach_one};
|
|
@@ -154,20 +154,127 @@ impl PyramidAlgo {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/// Most local-moving passes one Louvain level may make before it stops and
|
|
158
|
+
/// returns the partition it has.
|
|
159
|
+
///
|
|
160
|
+
/// A **backstop**, not the termination argument. With the exact move test of
|
|
161
|
+
/// [`louvain_one_level`] every move raises the modularity strictly, so the
|
|
162
|
+
/// passes end on their own. Measured: at most 24 passes on any level of 500
|
|
163
|
+
/// million random graphs, and at most 186 on 21 real datasets of up to 11.7
|
|
164
|
+
/// million triples (proteinbase's base level; the next highest was 77). The
|
|
165
|
+
/// cap is about ten times that worst case (`docs/SPEC.md` §7.1). It exists so
|
|
166
|
+
/// that no future numeric defect, or a caller of the public
|
|
167
|
+
/// [`Graph::from_edges`] with weights the exact path does not cover, can turn
|
|
168
|
+
/// a build into an infinite loop again, as rete 0.3.3 and earlier could (see
|
|
169
|
+
/// [`louvain_one_level`]). Reaching it is not an error: the level ends with
|
|
170
|
+
/// the partition it has, which is valid, merely one a further pass might
|
|
171
|
+
/// still have improved.
|
|
172
|
+
pub const MAX_LOUVAIN_PASSES: usize = 2000;
|
|
173
|
+
|
|
174
|
+
/// What one Louvain level did (for tests and the termination harness).
|
|
175
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
176
|
+
pub struct LevelStats {
|
|
177
|
+
/// Local-moving passes made (each a sweep over every node).
|
|
178
|
+
pub passes: usize,
|
|
179
|
+
/// True if the level stopped at [`MAX_LOUVAIN_PASSES`] with nodes still
|
|
180
|
+
/// moving, rather than converging.
|
|
181
|
+
pub capped: bool,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// How a level compares two candidate communities' gains.
|
|
185
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
186
|
+
enum GainTest {
|
|
187
|
+
/// Integer weights and `(2m)² ≤ 2^53`: the cross-multiplied gains are
|
|
188
|
+
/// integers an `f64` holds exactly.
|
|
189
|
+
ExactF64,
|
|
190
|
+
/// Integer weights, larger graph: the same integers in `i128`.
|
|
191
|
+
ExactI128,
|
|
192
|
+
/// Non-integer weights (reachable only through the public
|
|
193
|
+
/// [`Graph::from_edges`]; nothing in rete builds such a graph): the
|
|
194
|
+
/// floating-point gain, where a move counts only when it wins by more than
|
|
195
|
+
/// a bound on its rounding error.
|
|
196
|
+
Tolerant,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// `2^53`: every integer up to it is exactly representable in an `f64`.
|
|
200
|
+
const F64_EXACT_INT: f64 = 9_007_199_254_740_992.0;
|
|
201
|
+
|
|
202
|
+
impl Graph {
|
|
203
|
+
fn gain_test(&self) -> GainTest {
|
|
204
|
+
let two_m = 2.0 * self.m;
|
|
205
|
+
let integral = |x: f64| x.is_finite() && x.fract() == 0.0;
|
|
206
|
+
// Every edge weight integral, and every sum the level forms (degrees,
|
|
207
|
+
// community totals, k_{i,c}, all ≤ 2m) exactly representable.
|
|
208
|
+
let exact_sums = integral(self.m)
|
|
209
|
+
&& two_m <= F64_EXACT_INT
|
|
210
|
+
&& self
|
|
211
|
+
.adj
|
|
212
|
+
.iter()
|
|
213
|
+
.all(|nbrs| nbrs.iter().all(|&(_, w)| integral(w)));
|
|
214
|
+
if !exact_sums {
|
|
215
|
+
GainTest::Tolerant
|
|
216
|
+
} else if two_m * two_m <= F64_EXACT_INT {
|
|
217
|
+
GainTest::ExactF64
|
|
218
|
+
} else {
|
|
219
|
+
GainTest::ExactI128
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
157
224
|
/// One level of Louvain local-moving modularity optimization.
|
|
158
225
|
///
|
|
159
226
|
/// Nodes start in their own community; each is greedily moved to the neighboring
|
|
160
227
|
/// community giving the best modularity gain until no move improves. IDs in the
|
|
161
228
|
/// result are renumbered densely.
|
|
229
|
+
///
|
|
230
|
+
/// # The move test is exact
|
|
231
|
+
///
|
|
232
|
+
/// Moving node `i` (degree `k_i`, with `k_{i,c}` of its edge weight going into
|
|
233
|
+
/// community `c`, whose other members' degrees total `Σ_c`) into `c` gains
|
|
234
|
+
/// `k_{i,c} − Σ_c·k_i / 2m`, up to a positive factor common to every `c`.
|
|
235
|
+
/// Through 0.3.3 that was computed in floating point, and the division by `2m`
|
|
236
|
+
/// is inexact whenever `2m` is not a power of two. Two candidates with *equal*
|
|
237
|
+
/// gains could then round one ulp apart, the strict `>` moved a node on a gain
|
|
238
|
+
/// of zero, a second node did the same the other way, and the passes, which
|
|
239
|
+
/// had no limit, swapped the pair forever. A graph of 15 triples is enough
|
|
240
|
+
/// (`tests/louvain_termination.rs`).
|
|
241
|
+
///
|
|
242
|
+
/// The test is now made on `k_{i,c}·2m − Σ_c·k_i`, the gain times `2m`, with no
|
|
243
|
+
/// division. Every weight rete builds is an integer count (one per triple,
|
|
244
|
+
/// summed by [`Graph::quotient`]), so these are integers. They are exact in
|
|
245
|
+
/// `f64` while `(2m)² ≤ 2^53`, because every product is at most `(2m)²`
|
|
246
|
+
/// (`k_{i,c} ≤ k_i ≤ 2m` and `Σ_c ≤ 2m`): up to 47,453,132 projected edges.
|
|
247
|
+
/// Beyond that they are computed in `i128`, exact while `2m ≤ 2^53`, the point
|
|
248
|
+
/// where the `f64` sums themselves would stop being exact. So a node moves only
|
|
249
|
+
/// on a strictly positive true gain, each move raises the modularity, and the
|
|
250
|
+
/// passes must end. For non-integer weights (only via the public
|
|
251
|
+
/// [`Graph::from_edges`]) a move must win by more than a bound on the rounding
|
|
252
|
+
/// error, which keeps every accepted move a true improvement as well.
|
|
253
|
+
/// [`MAX_LOUVAIN_PASSES`] backs all of this up.
|
|
254
|
+
///
|
|
255
|
+
/// Ties keep the old rule: candidates are scanned in sorted order and the
|
|
256
|
+
/// first best one wins (the node's own community, then the smallest id). The
|
|
257
|
+
/// partition is therefore reproducible across runs and platforms; native and
|
|
258
|
+
/// wasm compute the same integers.
|
|
162
259
|
pub fn louvain_one_level(g: &Graph) -> Partition {
|
|
260
|
+
louvain_one_level_stats(g).0
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// [`louvain_one_level`], also returning how many passes it made and whether
|
|
264
|
+
/// it stopped at [`MAX_LOUVAIN_PASSES`].
|
|
265
|
+
pub fn louvain_one_level_stats(g: &Graph) -> (Partition, LevelStats) {
|
|
163
266
|
let n = g.n;
|
|
164
267
|
if g.m == 0.0 {
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
268
|
+
return (
|
|
269
|
+
Partition {
|
|
270
|
+
comm: (0..n).collect(),
|
|
271
|
+
count: n,
|
|
272
|
+
},
|
|
273
|
+
LevelStats::default(),
|
|
274
|
+
);
|
|
169
275
|
}
|
|
170
276
|
let two_m = 2.0 * g.m;
|
|
277
|
+
let test = g.gain_test();
|
|
171
278
|
let mut comm: Vec<usize> = (0..n).collect();
|
|
172
279
|
let mut sigma_tot: Vec<f64> = g.degree.clone(); // each node alone
|
|
173
280
|
|
|
@@ -176,13 +283,19 @@ pub fn louvain_one_level(g: &Graph) -> Partition {
|
|
|
176
283
|
// recorded in `touched`. A dense array + touched-list replaces the per-node
|
|
177
284
|
// `HashMap` the textbook formulation allocates — same accumulation order (the
|
|
178
285
|
// adjacency is index-sorted, so the float sums are bit-identical) and the same
|
|
179
|
-
// sorted candidate scan
|
|
180
|
-
//
|
|
286
|
+
// sorted candidate scan. Edge weights are strictly positive, so
|
|
287
|
+
// `k_to[c] == 0.0` reliably means "untouched".
|
|
181
288
|
let mut k_to = vec![0.0f64; n];
|
|
182
289
|
let mut touched: Vec<usize> = Vec::new();
|
|
183
290
|
|
|
291
|
+
let mut stats = LevelStats::default();
|
|
184
292
|
let mut improved = true;
|
|
185
293
|
while improved {
|
|
294
|
+
if stats.passes == MAX_LOUVAIN_PASSES {
|
|
295
|
+
stats.capped = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
stats.passes += 1;
|
|
186
299
|
improved = false;
|
|
187
300
|
for (i, neighbors) in g.adj.iter().enumerate() {
|
|
188
301
|
let ci = comm[i];
|
|
@@ -203,20 +316,56 @@ pub fn louvain_one_level(g: &Graph) -> Partition {
|
|
|
203
316
|
}
|
|
204
317
|
}
|
|
205
318
|
|
|
206
|
-
// Gain of placing i into community c: k_{i,c} - sigma_tot[c]*ki/2m.
|
|
207
|
-
let gain = |c: usize| -> f64 { k_to[c] - sigma_tot[c] * ki / two_m };
|
|
208
319
|
// Scan candidate communities in a deterministic (sorted) order so an
|
|
209
320
|
// equal-gain tie always resolves to the same community across runs —
|
|
210
321
|
// the second half of making the pyramid reproducible. Strict `>`
|
|
211
|
-
// keeps the first (
|
|
212
|
-
|
|
213
|
-
let mut best_gain = gain(ci);
|
|
322
|
+
// keeps the first candidate (i's own community, then the smallest
|
|
323
|
+
// id) on ties. The gains compared are exact; see the doc comment.
|
|
214
324
|
touched.sort_unstable();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
325
|
+
let mut best_c = ci;
|
|
326
|
+
match test {
|
|
327
|
+
GainTest::ExactF64 => {
|
|
328
|
+
// Integers ≤ (2m)² ≤ 2^53: exact, and so is their difference.
|
|
329
|
+
let gain = |c: usize| k_to[c] * two_m - sigma_tot[c] * ki;
|
|
330
|
+
let mut best = gain(ci);
|
|
331
|
+
for &c in &touched {
|
|
332
|
+
let gc = gain(c);
|
|
333
|
+
if gc > best {
|
|
334
|
+
best = gc;
|
|
335
|
+
best_c = c;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
GainTest::ExactI128 => {
|
|
340
|
+
// Integers ≤ 2^53, so `as` is exact; products < 2^106.
|
|
341
|
+
let (tm, k) = (two_m as i128, ki as i128);
|
|
342
|
+
let gain = |c: usize| (k_to[c] as i128) * tm - (sigma_tot[c] as i128) * k;
|
|
343
|
+
let mut best = gain(ci);
|
|
344
|
+
for &c in &touched {
|
|
345
|
+
let gc = gain(c);
|
|
346
|
+
if gc > best {
|
|
347
|
+
best = gc;
|
|
348
|
+
best_c = c;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
GainTest::Tolerant => {
|
|
353
|
+
// Both terms of a gain are at most `ki` in magnitude
|
|
354
|
+
// (k_{i,c} ≤ k_i, Σ_c ≤ 2m) and come from at most deg+3
|
|
355
|
+
// rounded operations, so each gain is off by less than
|
|
356
|
+
// (deg+4)·ε·k_i, and a difference of two by twice that. A
|
|
357
|
+
// move must win by more than this margin, so every move
|
|
358
|
+
// accepted is a true gain.
|
|
359
|
+
let tol = 2.0 * (neighbors.len() as f64 + 4.0) * f64::EPSILON * ki;
|
|
360
|
+
let gain = |c: usize| k_to[c] - sigma_tot[c] * ki / two_m;
|
|
361
|
+
let mut best = gain(ci);
|
|
362
|
+
for &c in &touched {
|
|
363
|
+
let gc = gain(c);
|
|
364
|
+
if gc > best + tol {
|
|
365
|
+
best = gc;
|
|
366
|
+
best_c = c;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
220
369
|
}
|
|
221
370
|
}
|
|
222
371
|
|
|
@@ -240,10 +389,13 @@ pub fn louvain_one_level(g: &Graph) -> Partition {
|
|
|
240
389
|
let next = remap.len();
|
|
241
390
|
*c = *remap.entry(*c).or_insert(next);
|
|
242
391
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
392
|
+
(
|
|
393
|
+
Partition {
|
|
394
|
+
count: remap.len(),
|
|
395
|
+
comm,
|
|
396
|
+
},
|
|
397
|
+
stats,
|
|
398
|
+
)
|
|
247
399
|
}
|
|
248
400
|
|
|
249
401
|
/// Project RDF integer triples onto the undirected node graph the pyramid
|
|
@@ -407,6 +559,73 @@ mod tests {
|
|
|
407
559
|
assert_ne!(c0, c3);
|
|
408
560
|
}
|
|
409
561
|
|
|
562
|
+
/// The base graph of `tests/fixtures/louvain-hangs/min-15-level0.nt`, on
|
|
563
|
+
/// which rete 0.3.3 looped forever, as (n, edges).
|
|
564
|
+
fn min_15_edges() -> (usize, Vec<(usize, usize, f64)>) {
|
|
565
|
+
let text = include_str!("../tests/fixtures/louvain-hangs/min-15-level0.nt");
|
|
566
|
+
let quads = crate::ingest::parse_statements(text, "nt").unwrap();
|
|
567
|
+
let mut db = DictionaryBuilder::new();
|
|
568
|
+
for (s, p, o, _) in &quads {
|
|
569
|
+
db.observe(s, p, o);
|
|
570
|
+
}
|
|
571
|
+
let dict = db.build();
|
|
572
|
+
let edges = quads
|
|
573
|
+
.iter()
|
|
574
|
+
.map(|(s, p, o, _)| {
|
|
575
|
+
let (s, _, o) = dict.encode(s, p, o).unwrap();
|
|
576
|
+
(
|
|
577
|
+
dict.subject_node(s) as usize,
|
|
578
|
+
dict.object_node(o) as usize,
|
|
579
|
+
1.0,
|
|
580
|
+
)
|
|
581
|
+
})
|
|
582
|
+
.collect();
|
|
583
|
+
(dict.node_count() as usize, edges)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#[test]
|
|
587
|
+
fn the_gain_test_follows_the_weights() {
|
|
588
|
+
let (n, edges) = min_15_edges();
|
|
589
|
+
assert_eq!(Graph::from_edges(n, &edges).gain_test(), GainTest::ExactF64);
|
|
590
|
+
// (2m)² > 2^53: integers too big to cross-multiply in f64.
|
|
591
|
+
let big: Vec<_> = edges.iter().map(|&(u, v, _)| (u, v, 1e7)).collect();
|
|
592
|
+
assert_eq!(Graph::from_edges(n, &big).gain_test(), GainTest::ExactI128);
|
|
593
|
+
let half: Vec<_> = edges.iter().map(|&(u, v, _)| (u, v, 0.5)).collect();
|
|
594
|
+
assert_eq!(Graph::from_edges(n, &half).gain_test(), GainTest::Tolerant);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/// Modularity is invariant under scaling every weight, so all three paths
|
|
598
|
+
/// must find the same partition of the graph that used to hang, and none
|
|
599
|
+
/// may need the pass cap.
|
|
600
|
+
#[test]
|
|
601
|
+
fn every_gain_test_converges_on_the_old_hang_to_one_partition() {
|
|
602
|
+
let (n, edges) = min_15_edges();
|
|
603
|
+
let base = louvain_one_level_stats(&Graph::from_edges(n, &edges));
|
|
604
|
+
assert!(!base.1.capped);
|
|
605
|
+
for (scale, path) in [
|
|
606
|
+
(1e7, GainTest::ExactI128),
|
|
607
|
+
(0.5, GainTest::Tolerant),
|
|
608
|
+
(0.1, GainTest::Tolerant),
|
|
609
|
+
] {
|
|
610
|
+
let scaled: Vec<_> = edges.iter().map(|&(u, v, w)| (u, v, w * scale)).collect();
|
|
611
|
+
let g = Graph::from_edges(n, &scaled);
|
|
612
|
+
assert_eq!(g.gain_test(), path);
|
|
613
|
+
let (p, st) = louvain_one_level_stats(&g);
|
|
614
|
+
assert!(!st.capped, "scale {scale}: capped");
|
|
615
|
+
assert_eq!(p, base.0, "scale {scale}: a different partition");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#[test]
|
|
620
|
+
fn levels_report_their_passes() {
|
|
621
|
+
let (p, st) = louvain_one_level_stats(&barbell());
|
|
622
|
+
assert_eq!(p.count, 2);
|
|
623
|
+
assert!(st.passes >= 2 && st.passes < MAX_LOUVAIN_PASSES, "{st:?}");
|
|
624
|
+
assert!(!st.capped);
|
|
625
|
+
let (_, st) = louvain_one_level_stats(&Graph::from_edges(3, &[]));
|
|
626
|
+
assert_eq!(st, LevelStats::default());
|
|
627
|
+
}
|
|
628
|
+
|
|
410
629
|
/// Two fully-connected triples-clusters joined by one bridge edge, expressed
|
|
411
630
|
/// as RDF `knows` triples, then projected and clustered.
|
|
412
631
|
#[test]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Louvain non-termination regression, reported by the Jev Games consumer
|
|
2
|
+
# (rete-graph 0.3.2 never returned from build; their fixture
|
|
3
|
+
# test/fixtures/rete-louvain-hangs.json, jev-games commit 8666728, "commander").
|
|
4
|
+
# 145 distinct triples, verbatim.
|
|
5
|
+
<urn:jev-games:cmdr:g> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Game> .
|
|
6
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#status> "playing" .
|
|
7
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#phase> "main1" .
|
|
8
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#turn> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
9
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#active> <urn:jev-games:cmdr:g/side/p2> .
|
|
10
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#toMove> <urn:jev-games:cmdr:g/side/p1> .
|
|
11
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#seatCount> "3"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
12
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#firstTurn> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
13
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#stackCount> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
14
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#queueCount> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
15
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#commanderMoveCount> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
16
|
+
<urn:jev-games:cmdr:g> <https://jev-games.dev/ns/mtg-commander#startingLife> "40"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
17
|
+
<urn:jev-games:cmdr:g/stack/0> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#StackObject> .
|
|
18
|
+
<urn:jev-games:cmdr:g/stack/0> <https://jev-games.dev/ns/mtg-commander#stackIndex> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
19
|
+
<urn:jev-games:cmdr:g/stack/0> <https://jev-games.dev/ns/mtg-commander#controller> <urn:jev-games:cmdr:g/side/p2> .
|
|
20
|
+
<urn:jev-games:cmdr:g/stack/0> <https://jev-games.dev/ns/mtg-commander#stackKind> "spell" .
|
|
21
|
+
<urn:jev-games:cmdr:g/stack/0> <https://jev-games.dev/ns/mtg-commander#source> <urn:jev-games:cmdr:g/card/sol-ring~p2-38> .
|
|
22
|
+
<urn:jev-games:cmdr:g/stack/0> <https://jev-games.dev/ns/mtg-commander#isTop> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
23
|
+
<urn:jev-games:cmdr:g/stack/0> <https://jev-games.dev/ns/mtg-commander#sourceName> "Sol Ring" .
|
|
24
|
+
<urn:jev-games:cmdr:g/side/p1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Player> .
|
|
25
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#key> "p1" .
|
|
26
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#seat> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
27
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#life> "40"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
28
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#out> "false"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
29
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#drewFromEmpty> "false"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
30
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#handCount> "7"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
31
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#libraryCount> "92"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
32
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#landsPlayed> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
33
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#extraLandDrops> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
34
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#identity> "G" .
|
|
35
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#identity> "U" .
|
|
36
|
+
<urn:jev-games:cmdr:g/side/p1> <https://jev-games.dev/ns/mtg-commander#identity> "W" .
|
|
37
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Card> .
|
|
38
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#cid> "tidus-yuna-s-guardian~p1-99" .
|
|
39
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#name> "Tidus, Yuna's Guardian" .
|
|
40
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#owner> <urn:jev-games:cmdr:g/side/p1> .
|
|
41
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#zone> "command" .
|
|
42
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#zoneOf> <urn:jev-games:cmdr:g/side/p1> .
|
|
43
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#isCommander> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
44
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#commanderCasts> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
45
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#type> "Creature" .
|
|
46
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#supertype> "Legendary" .
|
|
47
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Human" .
|
|
48
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Warrior" .
|
|
49
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#colour> "G" .
|
|
50
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#colour> "U" .
|
|
51
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#colour> "W" .
|
|
52
|
+
<urn:jev-games:cmdr:g/card/tidus-yuna-s-guardian~p1-99> <https://jev-games.dev/ns/mtg-commander#manaValue> "3"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
53
|
+
<urn:jev-games:cmdr:g/side/p2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Player> .
|
|
54
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#key> "p2" .
|
|
55
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#seat> "2"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
56
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#life> "40"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
57
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#out> "false"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
58
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#drewFromEmpty> "false"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
59
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#handCount> "6"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
60
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#libraryCount> "91"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
61
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#landsPlayed> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
62
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#extraLandDrops> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
63
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#identity> "B" .
|
|
64
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#identity> "U" .
|
|
65
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#identity> "W" .
|
|
66
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#turnCount> <urn:jev-games:cmdr:g/side/p2/turn/lands> .
|
|
67
|
+
<urn:jev-games:cmdr:g/side/p2/turn/lands> <https://jev-games.dev/ns/mtg-commander#turnKey> "lands" .
|
|
68
|
+
<urn:jev-games:cmdr:g/side/p2/turn/lands> <https://jev-games.dev/ns/mtg-commander#count> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
69
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#turnCount> <urn:jev-games:cmdr:g/side/p2/turn/entered> .
|
|
70
|
+
<urn:jev-games:cmdr:g/side/p2/turn/entered> <https://jev-games.dev/ns/mtg-commander#turnKey> "entered" .
|
|
71
|
+
<urn:jev-games:cmdr:g/side/p2/turn/entered> <https://jev-games.dev/ns/mtg-commander#count> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
72
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#turnCount> <urn:jev-games:cmdr:g/side/p2/turn/spent> .
|
|
73
|
+
<urn:jev-games:cmdr:g/side/p2/turn/spent> <https://jev-games.dev/ns/mtg-commander#turnKey> "spent" .
|
|
74
|
+
<urn:jev-games:cmdr:g/side/p2/turn/spent> <https://jev-games.dev/ns/mtg-commander#count> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
75
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#turnCount> <urn:jev-games:cmdr:g/side/p2/turn/spells> .
|
|
76
|
+
<urn:jev-games:cmdr:g/side/p2/turn/spells> <https://jev-games.dev/ns/mtg-commander#turnKey> "spells" .
|
|
77
|
+
<urn:jev-games:cmdr:g/side/p2/turn/spells> <https://jev-games.dev/ns/mtg-commander#count> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
78
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#turnCount> <urn:jev-games:cmdr:g/side/p2/turn/castIds> .
|
|
79
|
+
<urn:jev-games:cmdr:g/side/p2/turn/castIds> <https://jev-games.dev/ns/mtg-commander#turnKey> "castIds" .
|
|
80
|
+
<urn:jev-games:cmdr:g/side/p2/turn/castIds> <https://jev-games.dev/ns/mtg-commander#count> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
81
|
+
<urn:jev-games:cmdr:g/side/p2> <https://jev-games.dev/ns/mtg-commander#turnCount> <urn:jev-games:cmdr:g/side/p2/turn/noncreature> .
|
|
82
|
+
<urn:jev-games:cmdr:g/side/p2/turn/noncreature> <https://jev-games.dev/ns/mtg-commander#turnKey> "noncreature" .
|
|
83
|
+
<urn:jev-games:cmdr:g/side/p2/turn/noncreature> <https://jev-games.dev/ns/mtg-commander#count> "1"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
84
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Card> .
|
|
85
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#cid> "y-shtola-night-s-blessed~p2-99" .
|
|
86
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#name> "Y'shtola, Night's Blessed" .
|
|
87
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#owner> <urn:jev-games:cmdr:g/side/p2> .
|
|
88
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#zone> "command" .
|
|
89
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#zoneOf> <urn:jev-games:cmdr:g/side/p2> .
|
|
90
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#isCommander> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
91
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#commanderCasts> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
92
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#type> "Creature" .
|
|
93
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#supertype> "Legendary" .
|
|
94
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Cat" .
|
|
95
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Warlock" .
|
|
96
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#colour> "B" .
|
|
97
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#colour> "U" .
|
|
98
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#colour> "W" .
|
|
99
|
+
<urn:jev-games:cmdr:g/card/y-shtola-night-s-blessed~p2-99> <https://jev-games.dev/ns/mtg-commander#manaValue> "4"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
100
|
+
<urn:jev-games:cmdr:g/side/p3> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Player> .
|
|
101
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#key> "p3" .
|
|
102
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#seat> "3"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
103
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#life> "40"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
104
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#out> "false"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
105
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#drewFromEmpty> "false"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
106
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#handCount> "6"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
107
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#libraryCount> "93"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
108
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#landsPlayed> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
109
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#extraLandDrops> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
110
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#identity> "B" .
|
|
111
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#identity> "R" .
|
|
112
|
+
<urn:jev-games:cmdr:g/side/p3> <https://jev-games.dev/ns/mtg-commander#identity> "W" .
|
|
113
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Card> .
|
|
114
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#cid> "terra-herald-of-hope~p3-99" .
|
|
115
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#name> "Terra, Herald of Hope" .
|
|
116
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#owner> <urn:jev-games:cmdr:g/side/p3> .
|
|
117
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#zone> "command" .
|
|
118
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#zoneOf> <urn:jev-games:cmdr:g/side/p3> .
|
|
119
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#isCommander> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
120
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#commanderCasts> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
121
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#type> "Creature" .
|
|
122
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#supertype> "Legendary" .
|
|
123
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Human" .
|
|
124
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Wizard" .
|
|
125
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#subtype> "Warrior" .
|
|
126
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#colour> "B" .
|
|
127
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#colour> "R" .
|
|
128
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#colour> "W" .
|
|
129
|
+
<urn:jev-games:cmdr:g/card/terra-herald-of-hope~p3-99> <https://jev-games.dev/ns/mtg-commander#manaValue> "3"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
130
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#Card> .
|
|
131
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#cid> "sunken-ruins~p2-29" .
|
|
132
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#name> "Sunken Ruins" .
|
|
133
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#owner> <urn:jev-games:cmdr:g/side/p2> .
|
|
134
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#zone> "battlefield" .
|
|
135
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#zoneOf> <urn:jev-games:cmdr:g/side/p2> .
|
|
136
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#type> "Land" .
|
|
137
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#manaValue> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
138
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#controller> <urn:jev-games:cmdr:g/side/p2> .
|
|
139
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#battlefieldOrder> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
140
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#boardOrder> "2000"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
141
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#tapped> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
142
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#sick> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
143
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#damage> "0"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
144
|
+
<urn:jev-games:cmdr:g/card/sunken-ruins~p2-29> <https://jev-games.dev/ns/mtg-commander#enteredThisTurn> "true"^^<http://www.w3.org/2001/XMLSchema#boolean> .
|
|
145
|
+
<https://jev-games.dev/ns/mtg-commander#CommanderRules> <https://jev-games.dev/ns/mtg-commander#commanderDamageToLose> "21"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
146
|
+
<https://jev-games.dev/ns/mtg-commander#CommanderRules> <https://jev-games.dev/ns/mtg-commander#handSize> "7"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
147
|
+
<https://jev-games.dev/ns/mtg-commander#CommanderRules> <https://jev-games.dev/ns/mtg-commander#taxPerCast> "2"^^<http://www.w3.org/2001/XMLSchema#integer> .
|
|
148
|
+
<urn:jev-games:cmdr:g/step/9> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://jev-games.dev/ns/mtg-commander#CommanderStep> .
|
|
149
|
+
<urn:jev-games:cmdr:g/step/9> <https://jev-games.dev/ns#actor> <urn:jev-games:cmdr:g/side/p1> .
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Louvain non-termination regression, reported by the Jev Games consumer
|
|
2
|
+
# (rete-graph 0.3.2 never returned from build; their fixture
|
|
3
|
+
# test/fixtures/rete-louvain-hangs.json, jev-games commit 8666728, "random"[0]).
|
|
4
|
+
# 30 distinct triples, verbatim.
|
|
5
|
+
<urn:n14> <urn:p0> "1" .
|
|
6
|
+
<urn:n4> <urn:p1> <urn:n10> .
|
|
7
|
+
<urn:n13> <urn:p0> <urn:n3> .
|
|
8
|
+
<urn:n13> <urn:p2> "4" .
|
|
9
|
+
<urn:n18> <urn:p2> "3" .
|
|
10
|
+
<urn:n17> <urn:p2> "0" .
|
|
11
|
+
<urn:n18> <urn:p1> "3" .
|
|
12
|
+
<urn:n13> <urn:p2> <urn:n11> .
|
|
13
|
+
<urn:n18> <urn:p1> <urn:n21> .
|
|
14
|
+
<urn:n8> <urn:p1> <urn:n22> .
|
|
15
|
+
<urn:n13> <urn:p2> "2" .
|
|
16
|
+
<urn:n14> <urn:p2> "2" .
|
|
17
|
+
<urn:n9> <urn:p1> <urn:n6> .
|
|
18
|
+
<urn:n2> <urn:p1> <urn:n4> .
|
|
19
|
+
<urn:n15> <urn:p1> <urn:n18> .
|
|
20
|
+
<urn:n12> <urn:p2> <urn:n20> .
|
|
21
|
+
<urn:n5> <urn:p0> "4" .
|
|
22
|
+
<urn:n18> <urn:p1> "0" .
|
|
23
|
+
<urn:n4> <urn:p0> <urn:n18> .
|
|
24
|
+
<urn:n12> <urn:p1> <urn:n17> .
|
|
25
|
+
<urn:n15> <urn:p2> <urn:n10> .
|
|
26
|
+
<urn:n21> <urn:p1> <urn:n1> .
|
|
27
|
+
<urn:n15> <urn:p2> "0" .
|
|
28
|
+
<urn:n3> <urn:p2> "3" .
|
|
29
|
+
<urn:n16> <urn:p0> <urn:n5> .
|
|
30
|
+
<urn:n8> <urn:p0> "0" .
|
|
31
|
+
<urn:n22> <urn:p2> <urn:n13> .
|
|
32
|
+
<urn:n21> <urn:p1> <urn:n15> .
|
|
33
|
+
<urn:n9> <urn:p1> <urn:n7> .
|
|
34
|
+
<urn:n16> <urn:p0> "3" .
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Louvain non-termination regression, reported by the Jev Games consumer
|
|
2
|
+
# (rete-graph 0.3.2 never returned from build; their fixture
|
|
3
|
+
# test/fixtures/rete-louvain-hangs.json, jev-games commit 8666728, "random"[1]).
|
|
4
|
+
# 63 distinct triples, verbatim.
|
|
5
|
+
<urn:n11> <urn:p2> <urn:n15> .
|
|
6
|
+
<urn:n19> <urn:p1> <urn:n1> .
|
|
7
|
+
<urn:n10> <urn:p0> "3" .
|
|
8
|
+
<urn:n6> <urn:p1> <urn:n4> .
|
|
9
|
+
<urn:n10> <urn:p0> "1" .
|
|
10
|
+
<urn:n4> <urn:p1> "2" .
|
|
11
|
+
<urn:n9> <urn:p1> "3" .
|
|
12
|
+
<urn:n7> <urn:p0> "0" .
|
|
13
|
+
<urn:n18> <urn:p2> <urn:n15> .
|
|
14
|
+
<urn:n12> <urn:p2> "3" .
|
|
15
|
+
<urn:n2> <urn:p0> <urn:n9> .
|
|
16
|
+
<urn:n12> <urn:p1> "2" .
|
|
17
|
+
<urn:n11> <urn:p2> "1" .
|
|
18
|
+
<urn:n5> <urn:p0> <urn:n14> .
|
|
19
|
+
<urn:n13> <urn:p0> <urn:n1> .
|
|
20
|
+
<urn:n15> <urn:p1> <urn:n7> .
|
|
21
|
+
<urn:n6> <urn:p0> <urn:n16> .
|
|
22
|
+
<urn:n2> <urn:p2> "2" .
|
|
23
|
+
<urn:n7> <urn:p1> <urn:n4> .
|
|
24
|
+
<urn:n8> <urn:p0> <urn:n2> .
|
|
25
|
+
<urn:n14> <urn:p0> <urn:n10> .
|
|
26
|
+
<urn:n7> <urn:p1> <urn:n12> .
|
|
27
|
+
<urn:n5> <urn:p0> <urn:n17> .
|
|
28
|
+
<urn:n4> <urn:p0> <urn:n13> .
|
|
29
|
+
<urn:n0> <urn:p1> "3" .
|
|
30
|
+
<urn:n8> <urn:p2> <urn:n7> .
|
|
31
|
+
<urn:n6> <urn:p0> "1" .
|
|
32
|
+
<urn:n15> <urn:p0> "4" .
|
|
33
|
+
<urn:n2> <urn:p1> <urn:n10> .
|
|
34
|
+
<urn:n16> <urn:p1> <urn:n19> .
|
|
35
|
+
<urn:n11> <urn:p0> <urn:n13> .
|
|
36
|
+
<urn:n0> <urn:p2> <urn:n17> .
|
|
37
|
+
<urn:n18> <urn:p2> <urn:n12> .
|
|
38
|
+
<urn:n14> <urn:p0> <urn:n3> .
|
|
39
|
+
<urn:n5> <urn:p0> <urn:n19> .
|
|
40
|
+
<urn:n13> <urn:p2> "0" .
|
|
41
|
+
<urn:n13> <urn:p2> "3" .
|
|
42
|
+
<urn:n13> <urn:p0> "0" .
|
|
43
|
+
<urn:n3> <urn:p2> <urn:n4> .
|
|
44
|
+
<urn:n12> <urn:p0> "3" .
|
|
45
|
+
<urn:n2> <urn:p2> <urn:n0> .
|
|
46
|
+
<urn:n16> <urn:p0> "3" .
|
|
47
|
+
<urn:n12> <urn:p2> <urn:n7> .
|
|
48
|
+
<urn:n4> <urn:p2> <urn:n9> .
|
|
49
|
+
<urn:n0> <urn:p1> <urn:n1> .
|
|
50
|
+
<urn:n9> <urn:p1> <urn:n19> .
|
|
51
|
+
<urn:n19> <urn:p0> <urn:n19> .
|
|
52
|
+
<urn:n14> <urn:p0> <urn:n9> .
|
|
53
|
+
<urn:n18> <urn:p2> "4" .
|
|
54
|
+
<urn:n9> <urn:p0> <urn:n3> .
|
|
55
|
+
<urn:n1> <urn:p2> <urn:n17> .
|
|
56
|
+
<urn:n11> <urn:p1> "3" .
|
|
57
|
+
<urn:n6> <urn:p1> <urn:n14> .
|
|
58
|
+
<urn:g> <urn:g> "0" .
|
|
59
|
+
<urn:g> <urn:g> "1" .
|
|
60
|
+
<urn:g> <urn:g> "2" .
|
|
61
|
+
<urn:g> <urn:g> "3" .
|
|
62
|
+
<urn:g> <urn:g> "4" .
|
|
63
|
+
<urn:g> <urn:g> "5" .
|
|
64
|
+
<urn:g> <urn:g> "6" .
|
|
65
|
+
<urn:g> <urn:g> "7" .
|
|
66
|
+
<urn:g> <urn:g> "8" .
|
|
67
|
+
<urn:g> <urn:g> "9" .
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Louvain non-termination regression (rete 0.3.3 and earlier never return from
|
|
2
|
+
# build on this graph). 15 triples, 7 nodes, so 2m = 30: two nodes swap between
|
|
3
|
+
# two communities whose gains are exactly equal (10/30 and 12/30) but round one
|
|
4
|
+
# ulp apart in floating point. The smallest hang found: none at <= 13 triples in
|
|
5
|
+
# 100M random graphs. Found by dev/louvain-hang (seed 2959679, LH_MAXT=26 LH_MAXN=20).
|
|
6
|
+
<urn:n0> <urn:p1> <urn:n0> .
|
|
7
|
+
<urn:n1> <urn:p1> "1" .
|
|
8
|
+
<urn:n1> <urn:p1> <urn:n0> .
|
|
9
|
+
<urn:n0> <urn:p1> <urn:n1> .
|
|
10
|
+
<urn:n0> <urn:p0> <urn:n1> .
|
|
11
|
+
<urn:n1> <urn:p0> <urn:n1> .
|
|
12
|
+
<urn:n2> <urn:p0> <urn:n0> .
|
|
13
|
+
<urn:n1> <urn:p1> "0" .
|
|
14
|
+
<urn:n0> <urn:p1> "0" .
|
|
15
|
+
<urn:n0> <urn:p0> "1" .
|
|
16
|
+
<urn:n0> <urn:p0> <urn:n0> .
|
|
17
|
+
<urn:n2> <urn:p1> <urn:n1> .
|
|
18
|
+
<urn:n2> <urn:p1> "2" .
|
|
19
|
+
<urn:n2> <urn:p0> "3" .
|
|
20
|
+
<urn:n1> <urn:p1> "3" .
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Louvain non-termination regression: 18 triples, a hang at level 1 (the
|
|
2
|
+
# aggregated graph, whose self-loops and summed weights are still integers).
|
|
3
|
+
# Found by dev/louvain-hang (seed 20519787, LH_MAXT=26 LH_MAXN=20).
|
|
4
|
+
<urn:n1> <urn:p0> <urn:n5> .
|
|
5
|
+
<urn:n7> <urn:p0> "0" .
|
|
6
|
+
<urn:n9> <urn:p0> "0" .
|
|
7
|
+
<urn:n11> <urn:p0> "0" .
|
|
8
|
+
<urn:n4> <urn:p0> "0" .
|
|
9
|
+
<urn:n6> <urn:p0> "0" .
|
|
10
|
+
<urn:n0> <urn:p0> "0" .
|
|
11
|
+
<urn:n0> <urn:p0> <urn:n1> .
|
|
12
|
+
<urn:n9> <urn:p0> <urn:n7> .
|
|
13
|
+
<urn:n8> <urn:p0> "0" .
|
|
14
|
+
<urn:n7> <urn:p0> <urn:n3> .
|
|
15
|
+
<urn:n8> <urn:p0> <urn:n3> .
|
|
16
|
+
<urn:n2> <urn:p0> "0" .
|
|
17
|
+
<urn:n0> <urn:p0> <urn:n10> .
|
|
18
|
+
<urn:n10> <urn:p0> "0" .
|
|
19
|
+
<urn:n6> <urn:p0> <urn:n9> .
|
|
20
|
+
<urn:n3> <urn:p0> "0" .
|
|
21
|
+
<urn:n2> <urn:p0> <urn:n7> .
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//! Louvain must terminate: graphs on which `build` never returned in rete 0.3.3
|
|
2
|
+
//! and earlier (see `pyramid::louvain_one_level`). Every level of every
|
|
3
|
+
//! fixture must converge on its own, well inside `MAX_LOUVAIN_PASSES`, and
|
|
4
|
+
//! the whole build must return. Each check runs on a worker thread under a
|
|
5
|
+
//! time limit, so a regression fails here instead of hanging the suite.
|
|
6
|
+
//!
|
|
7
|
+
//! Fixtures (`tests/fixtures/louvain-hangs/`):
|
|
8
|
+
//! - `min-15-level0.nt`, `min-18-level1.nt`: the smallest hangs found by the
|
|
9
|
+
//! random-graph harness, one in the base graph and one in the aggregated
|
|
10
|
+
//! level above it;
|
|
11
|
+
//! - `jev-*.nt`: reported by the Jev Games consumer (a 145-triple Commander
|
|
12
|
+
//! position and two random graphs of 30 and 63 triples).
|
|
13
|
+
|
|
14
|
+
use std::sync::mpsc;
|
|
15
|
+
use std::time::Duration;
|
|
16
|
+
|
|
17
|
+
use rete_core::ingest::{assemble_dataset, parse_statements};
|
|
18
|
+
use rete_core::{
|
|
19
|
+
build_dendrogram, louvain_one_level_stats, project_graph, DictionaryBuilder, MAX_LOUVAIN_PASSES,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const FIXTURES: &[(&str, &str)] = &[
|
|
23
|
+
(
|
|
24
|
+
"min-15-level0",
|
|
25
|
+
include_str!("fixtures/louvain-hangs/min-15-level0.nt"),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"min-18-level1",
|
|
29
|
+
include_str!("fixtures/louvain-hangs/min-18-level1.nt"),
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
"jev-commander-145",
|
|
33
|
+
include_str!("fixtures/louvain-hangs/jev-commander-145.nt"),
|
|
34
|
+
),
|
|
35
|
+
(
|
|
36
|
+
"jev-random30-30",
|
|
37
|
+
include_str!("fixtures/louvain-hangs/jev-random30-30.nt"),
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"jev-random63-63",
|
|
41
|
+
include_str!("fixtures/louvain-hangs/jev-random63-63.nt"),
|
|
42
|
+
),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/// Run `f` on a worker thread; fail if it has not returned within `secs`.
|
|
46
|
+
fn within<T: Send + 'static>(what: &str, secs: u64, f: impl FnOnce() -> T + Send + 'static) -> T {
|
|
47
|
+
let (tx, rx) = mpsc::channel();
|
|
48
|
+
std::thread::spawn(move || {
|
|
49
|
+
tx.send(f()).ok();
|
|
50
|
+
});
|
|
51
|
+
rx.recv_timeout(Duration::from_secs(secs))
|
|
52
|
+
.unwrap_or_else(|_| panic!("{what}: did not return within {secs} s"))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Per level: (passes, capped, communities), the base graph first.
|
|
56
|
+
fn levels(text: &str) -> Vec<(usize, bool, usize)> {
|
|
57
|
+
let quads = parse_statements(text, "nt").expect("fixture parses");
|
|
58
|
+
let mut db = DictionaryBuilder::new();
|
|
59
|
+
for (s, p, o, _) in &quads {
|
|
60
|
+
db.observe(s, p, o);
|
|
61
|
+
}
|
|
62
|
+
let dict = db.build();
|
|
63
|
+
let triples: Vec<_> = quads
|
|
64
|
+
.iter()
|
|
65
|
+
.map(|(s, p, o, _)| dict.encode(s, p, o).unwrap())
|
|
66
|
+
.collect();
|
|
67
|
+
// The loop of `build_dendrogram`, keeping each level's stats.
|
|
68
|
+
let mut g = project_graph(&dict, &triples);
|
|
69
|
+
let mut out = Vec::new();
|
|
70
|
+
loop {
|
|
71
|
+
let (p, st) = louvain_one_level_stats(&g);
|
|
72
|
+
out.push((st.passes, st.capped, p.count));
|
|
73
|
+
if p.count >= g.node_count() {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
let next = g.quotient(&p.comm, p.count);
|
|
77
|
+
if next.node_count() <= 1 {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
g = next;
|
|
81
|
+
}
|
|
82
|
+
out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn every_level_converges_without_the_cap() {
|
|
87
|
+
for &(name, text) in FIXTURES {
|
|
88
|
+
let lv = within(name, 30, move || levels(text));
|
|
89
|
+
assert!(
|
|
90
|
+
lv.len() >= 2,
|
|
91
|
+
"{name}: expected at least two levels, got {lv:?}"
|
|
92
|
+
);
|
|
93
|
+
for (i, &(passes, capped, _)) in lv.iter().enumerate() {
|
|
94
|
+
assert!(!capped, "{name}: level {i} stopped at the pass cap: {lv:?}");
|
|
95
|
+
// Converged levels need a handful of passes; the cap is a backstop.
|
|
96
|
+
assert!(
|
|
97
|
+
passes <= 50,
|
|
98
|
+
"{name}: level {i} took {passes} passes: {lv:?}"
|
|
99
|
+
);
|
|
100
|
+
assert!(passes < MAX_LOUVAIN_PASSES);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn the_whole_build_returns() {
|
|
107
|
+
for &(name, text) in FIXTURES {
|
|
108
|
+
let n = within(name, 60, move || {
|
|
109
|
+
let quads = parse_statements(text, "nt").unwrap();
|
|
110
|
+
let n = quads.len();
|
|
111
|
+
let (bytes, stats) = assemble_dataset(quads, &[]);
|
|
112
|
+
assert!(stats.pyramid_levels > 0, "a pyramid was built");
|
|
113
|
+
assert!(!bytes.is_empty());
|
|
114
|
+
n
|
|
115
|
+
});
|
|
116
|
+
assert!(n >= 15, "{name}: {n} statements");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// The partition is a pure function of the input: two builds of the same text
|
|
121
|
+
/// are byte-identical (the pyramid included), and the smallest fixture's
|
|
122
|
+
/// levels are pinned, so a change in a tie decision shows up here. The wasm
|
|
123
|
+
/// test (`crates/rete-wasm/tests/louvain_termination.rs`) pins the same file.
|
|
124
|
+
#[test]
|
|
125
|
+
fn the_fixtures_build_reproducibly() {
|
|
126
|
+
for &(name, text) in FIXTURES {
|
|
127
|
+
let build = move || assemble_dataset(parse_statements(text, "nt").unwrap(), &[]).0;
|
|
128
|
+
let a = within(name, 60, build);
|
|
129
|
+
let b = within(name, 60, build);
|
|
130
|
+
assert_eq!(a, b, "{name}: two builds differ");
|
|
131
|
+
}
|
|
132
|
+
assert_eq!(
|
|
133
|
+
dendrogram(FIXTURES[0].1),
|
|
134
|
+
PINNED_MIN_15,
|
|
135
|
+
"min-15-level0: the dendrogram (community per node, per level)"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn dendrogram(text: &str) -> Vec<Vec<usize>> {
|
|
140
|
+
let quads = parse_statements(text, "nt").unwrap();
|
|
141
|
+
let mut db = DictionaryBuilder::new();
|
|
142
|
+
for (s, p, o, _) in &quads {
|
|
143
|
+
db.observe(s, p, o);
|
|
144
|
+
}
|
|
145
|
+
let dict = db.build();
|
|
146
|
+
let triples: Vec<_> = quads
|
|
147
|
+
.iter()
|
|
148
|
+
.map(|(s, p, o, _)| dict.encode(s, p, o).unwrap())
|
|
149
|
+
.collect();
|
|
150
|
+
let d = build_dendrogram(&project_graph(&dict, &triples));
|
|
151
|
+
d.levels.into_iter().map(|p| p.comm).collect()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// `dendrogram(min-15-level0.nt)` with the exact move test.
|
|
155
|
+
const PINNED_MIN_15: &[&[usize]] = &[&[0, 0, 1, 0, 0, 1, 1]];
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rete_graph-0.3.3 → rete_graph-0.3.4}/clients/python/examples/jupyterlite-build-tutorial.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rete_graph-0.3.3 → rete_graph-0.3.4}/crates/rete-core/tests/properties.proptest-regressions
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|