setreplace 0.1.0__tar.gz → 0.2.0__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 (78) hide show
  1. {setreplace-0.1.0 → setreplace-0.2.0}/.gitignore +1 -0
  2. {setreplace-0.1.0 → setreplace-0.2.0}/Cargo.lock +1 -1
  3. {setreplace-0.1.0 → setreplace-0.2.0}/PKG-INFO +1 -1
  4. {setreplace-0.1.0 → setreplace-0.2.0}/README.md +1 -0
  5. {setreplace-0.1.0 → setreplace-0.2.0}/docs/python-api.md +20 -0
  6. {setreplace-0.1.0 → setreplace-0.2.0}/pyproject.toml +1 -1
  7. {setreplace-0.1.0 → setreplace-0.2.0}/python/Cargo.toml +1 -1
  8. {setreplace-0.1.0 → setreplace-0.2.0}/python/setreplace.pyi +11 -0
  9. {setreplace-0.1.0 → setreplace-0.2.0}/python/src/lib.rs +52 -0
  10. {setreplace-0.1.0 → setreplace-0.2.0}/python/tests/test_api.py +17 -0
  11. setreplace-0.2.0/scripts/scan_growth_rates.py +143 -0
  12. setreplace-0.2.0/src/enumerate.rs +419 -0
  13. {setreplace-0.1.0 → setreplace-0.2.0}/src/lib.rs +4 -0
  14. setreplace-0.2.0/tests/enumeration_vectors.rs +150 -0
  15. setreplace-0.2.0/tests/fixtures/enumerate_rules_ground_truth.txt +10 -0
  16. setreplace-0.2.0/tests/fixtures/wl_2_2_to_2_2.txt +562 -0
  17. {setreplace-0.1.0 → setreplace-0.2.0}/.github/workflows/ci.yml +0 -0
  18. {setreplace-0.1.0 → setreplace-0.2.0}/.github/workflows/release.yml +0 -0
  19. {setreplace-0.1.0 → setreplace-0.2.0}/Cargo.toml +0 -0
  20. {setreplace-0.1.0 → setreplace-0.2.0}/LICENSE +0 -0
  21. {setreplace-0.1.0 → setreplace-0.2.0}/docs/engine.md +0 -0
  22. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/basic_hypergraph_plot.png +0 -0
  23. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/causal10_wolfram_vs_rust.png +0 -0
  24. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/evolution_result_100_steps.png +0 -0
  25. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/evolution_result_10_steps.png +0 -0
  26. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/evolution_result_1_step.png +0 -0
  27. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/layered_causal_graph.png +0 -0
  28. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/showcase/announcement_web.png +0 -0
  29. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/showcase/crumpled_ball.png +0 -0
  30. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/showcase/lens_mesh.png +0 -0
  31. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/showcase/sierpinski_fractal.png +0 -0
  32. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/showcase/triangular_net.png +0 -0
  33. {setreplace-0.1.0 → setreplace-0.2.0}/docs/images/step10_wolfram_vs_rust.png +0 -0
  34. {setreplace-0.1.0 → setreplace-0.2.0}/docs/release.md +0 -0
  35. {setreplace-0.1.0 → setreplace-0.2.0}/docs/rust.md +0 -0
  36. {setreplace-0.1.0 → setreplace-0.2.0}/examples/bench.rs +0 -0
  37. {setreplace-0.1.0 → setreplace-0.2.0}/examples/growth.rs +0 -0
  38. {setreplace-0.1.0 → setreplace-0.2.0}/examples/orderings.rs +0 -0
  39. {setreplace-0.1.0 → setreplace-0.2.0}/examples/print_states.rs +0 -0
  40. {setreplace-0.1.0 → setreplace-0.2.0}/out/basic_hypergraph_plot.png +0 -0
  41. {setreplace-0.1.0 → setreplace-0.2.0}/out/basic_hypergraph_plot.svg +0 -0
  42. {setreplace-0.1.0 → setreplace-0.2.0}/out/comparison/basic_wolfram_vs_rust.png +0 -0
  43. {setreplace-0.1.0 → setreplace-0.2.0}/out/comparison/causal10_wolfram_vs_rust.png +0 -0
  44. {setreplace-0.1.0 → setreplace-0.2.0}/out/comparison/step100_wolfram_vs_rust.png +0 -0
  45. {setreplace-0.1.0 → setreplace-0.2.0}/out/comparison/step10_wolfram_vs_rust.png +0 -0
  46. {setreplace-0.1.0 → setreplace-0.2.0}/out/comparison/step1_wolfram_vs_rust.png +0 -0
  47. {setreplace-0.1.0 → setreplace-0.2.0}/out/evolution_result_100_steps.png +0 -0
  48. {setreplace-0.1.0 → setreplace-0.2.0}/out/evolution_result_100_steps.svg +0 -0
  49. {setreplace-0.1.0 → setreplace-0.2.0}/out/evolution_result_10_steps.png +0 -0
  50. {setreplace-0.1.0 → setreplace-0.2.0}/out/evolution_result_10_steps.svg +0 -0
  51. {setreplace-0.1.0 → setreplace-0.2.0}/out/evolution_result_1_step.png +0 -0
  52. {setreplace-0.1.0 → setreplace-0.2.0}/out/evolution_result_1_step.svg +0 -0
  53. {setreplace-0.1.0 → setreplace-0.2.0}/out/layered_causal_graph.png +0 -0
  54. {setreplace-0.1.0 → setreplace-0.2.0}/out/layered_causal_graph.svg +0 -0
  55. {setreplace-0.1.0 → setreplace-0.2.0}/python/README.md +0 -0
  56. {setreplace-0.1.0 → setreplace-0.2.0}/src/atoms_index.rs +0 -0
  57. {setreplace-0.1.0 → setreplace-0.2.0}/src/error.rs +0 -0
  58. {setreplace-0.1.0 → setreplace-0.2.0}/src/matcher.rs +0 -0
  59. {setreplace-0.1.0 → setreplace-0.2.0}/src/pcg.rs +0 -0
  60. {setreplace-0.1.0 → setreplace-0.2.0}/src/rule.rs +0 -0
  61. {setreplace-0.1.0 → setreplace-0.2.0}/src/system.rs +0 -0
  62. {setreplace-0.1.0 → setreplace-0.2.0}/tests/wolfram_vectors.rs +0 -0
  63. {setreplace-0.1.0 → setreplace-0.2.0}/viz/Cargo.toml +0 -0
  64. {setreplace-0.1.0 → setreplace-0.2.0}/viz/README.md +0 -0
  65. {setreplace-0.1.0 → setreplace-0.2.0}/viz/examples/compose_comparison.rs +0 -0
  66. {setreplace-0.1.0 → setreplace-0.2.0}/viz/examples/inline_editions.rs +0 -0
  67. {setreplace-0.1.0 → setreplace-0.2.0}/viz/examples/readme_figures.rs +0 -0
  68. {setreplace-0.1.0 → setreplace-0.2.0}/viz/examples/showcase.rs +0 -0
  69. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/causal.rs +0 -0
  70. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/geometry.rs +0 -0
  71. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/layout.rs +0 -0
  72. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/lib.rs +0 -0
  73. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/pcg.rs +0 -0
  74. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/plot.rs +0 -0
  75. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/png.rs +0 -0
  76. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/style.rs +0 -0
  77. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/svg.rs +0 -0
  78. {setreplace-0.1.0 → setreplace-0.2.0}/viz/src/vec2.rs +0 -0
@@ -4,3 +4,4 @@
4
4
  __pycache__/
5
5
  *.pyc
6
6
  .DS_Store
7
+ /scans/
@@ -436,7 +436,7 @@ version = "0.1.0"
436
436
 
437
437
  [[package]]
438
438
  name = "setreplace-python"
439
- version = "0.1.0"
439
+ version = "0.2.0"
440
440
  dependencies = [
441
441
  "pyo3",
442
442
  "setreplace",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setreplace
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -123,6 +123,7 @@ what lets meshes and fractals unfold instead of freezing into hairballs.
123
123
  | `"AllEventsList"` / `"AllExpressions"` | `system.events()` / `system.tokens()` |
124
124
  | `"CausalGraph"` / `"LayeredCausalGraph"` | `system.causal_graph_edges()` / `.causal_graph_plot()` |
125
125
  | `HypergraphPlot[state, VertexLabels -> Automatic]` | `sr.plot(state, labels=True)` |
126
+ | `EnumerateWolframModelRules[{{2, 2}} -> {{3, 2}}]` | `sr.enumerate_rules([(2, 2)], [(3, 2)])` |
126
127
 
127
128
  Rules can be strings (as above), structured
128
129
  (`sr.Rule([["x", "y"]], [["x", "y"], ["y", "z"]])` — strings are variables,
@@ -237,3 +237,23 @@ pos = sr.layout(system.final_state, seed=1)
237
237
  abi3 keeps it to one wheel per platform.
238
238
  3. Later, when multiway lands in the engine, `evolve()` gains kwargs rather
239
239
  than new entry points — worth keeping in mind when naming things now.
240
+
241
+ ## Added after v0.1.0: rule enumeration
242
+
243
+ ```python
244
+ def enumerate_rules(inputs, outputs, *, connectivity="Automatic",
245
+ max_elements=None) -> list[Rule]: ...
246
+ ```
247
+
248
+ Replicates the Wolfram Physics Project's `EnumerateWolframModelRules`
249
+ exactly — same canonical forms, same order — so indices interoperate with
250
+ the registry and the literature. Signature sides are `(count, arity)` pairs;
251
+ `connectivity` is `"Automatic"` (left side connected and rule connected),
252
+ `"All"`, or `"None"`. Verified against captured ground truth: the complete
253
+ 11-rule (1_2 -> 1_2) and 562-rule (2_2 -> 2_2) lists element-by-element, and
254
+ the published 40,405 count for 2_2 -> 4_2.
255
+
256
+ ```python
257
+ rules = sr.enumerate_rules([(2, 2)], [(2, 2)]) # 562 rules, registry order
258
+ sr.evolve(rules[101], [[1, 1], [1, 1]], events=500).plot()
259
+ ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "setreplace"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Wolfram models in Python: SetReplace-exact hypergraph substitution and HypergraphPlot-style rendering, powered by Rust"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "setreplace-python"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  description = "Python bindings for the setreplace engine and visualization"
6
6
  license = "MIT"
@@ -113,6 +113,17 @@ def evolve(
113
113
  event_ordering: list[str] | None = None,
114
114
  random_seed: int = 0,
115
115
  ) -> HypergraphSystem: ...
116
+ def enumerate_rules(
117
+ inputs: list[tuple[int, int]] | list[list[int]],
118
+ outputs: list[tuple[int, int]] | list[list[int]],
119
+ *,
120
+ connectivity: str = "Automatic",
121
+ max_elements: int | None = None,
122
+ ) -> list[Rule]:
123
+ """All inequivalent rules of a signature ((count, arity) pairs per side),
124
+ replicating EnumerateWolframModelRules: same canonical forms, same order.
125
+ connectivity: "Automatic" (LHS connected, rule connected), "All", "None"."""
126
+
116
127
  def set_replace(state: list[list[int]], rules: Rules, events: int = 1) -> list[list[int]]: ...
117
128
  def set_replace_list(
118
129
  state: list[list[int]], rules: Rules, events: int
@@ -538,6 +538,57 @@ fn evolve(
538
538
  Ok(system)
539
539
  }
540
540
 
541
+ /// All inequivalent rules of a signature, replicating the Wolfram Physics
542
+ /// Project's EnumerateWolframModelRules (canonical forms, same order).
543
+ /// Signature sides are (count, arity) pairs: 2 binary edges -> 3 binary
544
+ /// edges is `enumerate_rules([(2, 2)], [(3, 2)])`.
545
+ #[pyfunction]
546
+ #[pyo3(signature = (inputs, outputs, *, connectivity="Automatic", max_elements=None))]
547
+ fn enumerate_rules(
548
+ py: Python<'_>,
549
+ inputs: Vec<Vec<usize>>,
550
+ outputs: Vec<Vec<usize>>,
551
+ connectivity: &str,
552
+ max_elements: Option<usize>,
553
+ ) -> PyResult<Vec<Rule>> {
554
+ fn groups(side: Vec<Vec<usize>>) -> PyResult<Vec<(usize, usize)>> {
555
+ side.into_iter()
556
+ .map(|g| {
557
+ if g.len() == 2 {
558
+ Ok((g[0], g[1]))
559
+ } else {
560
+ Err(PyValueError::new_err(
561
+ "signature sides must be lists of (count, arity) pairs, \
562
+ e.g. enumerate_rules([(2, 2)], [(3, 2)])",
563
+ ))
564
+ }
565
+ })
566
+ .collect()
567
+ }
568
+ let connectivity = match connectivity {
569
+ "Automatic" => engine::Connectivity::LeftConnected,
570
+ "None" => engine::Connectivity::None,
571
+ "All" => engine::Connectivity::All,
572
+ other => {
573
+ return Err(PyValueError::new_err(format!(
574
+ "unknown connectivity `{other}`; valid: Automatic, None, All"
575
+ )))
576
+ }
577
+ };
578
+ let signature = engine::RuleSignature {
579
+ inputs: groups(inputs)?,
580
+ outputs: groups(outputs)?,
581
+ };
582
+ let options = engine::EnumerationOptions {
583
+ connectivity,
584
+ max_elements,
585
+ };
586
+ let rules = py
587
+ .allow_threads(|| engine::enumerate_rules(&signature, &options))
588
+ .map_err(engine_err)?;
589
+ Ok(rules.into_iter().map(|inner| Rule { inner }).collect())
590
+ }
591
+
541
592
  /// SetReplace[state, rules, events]: applies up to `events` substitution
542
593
  /// events and returns the resulting state.
543
594
  #[pyfunction]
@@ -604,6 +655,7 @@ fn setreplace(m: &Bound<'_, PyModule>) -> PyResult<()> {
604
655
  m.add_function(wrap_pyfunction!(plot, m)?)?;
605
656
  m.add_function(wrap_pyfunction!(layout, m)?)?;
606
657
  m.add_function(wrap_pyfunction!(evolve, m)?)?;
658
+ m.add_function(wrap_pyfunction!(enumerate_rules, m)?)?;
607
659
  m.add_function(wrap_pyfunction!(set_replace, m)?)?;
608
660
  m.add_function(wrap_pyfunction!(set_replace_list, m)?)?;
609
661
  m.add_function(wrap_pyfunction!(set_replace_all, m)?)?;
@@ -173,6 +173,23 @@ def test_determinism_and_seeds():
173
173
  assert [e.inputs for e in a.events()] == [e.inputs for e in b.events()]
174
174
 
175
175
 
176
+ def test_enumerate_rules():
177
+ rules = sr.enumerate_rules([(1, 2)], [(1, 2)])
178
+ assert len(rules) == 11
179
+ assert rules[0].inputs == [[-1, -1]] and rules[0].outputs == [[-1, -1]]
180
+ assert len(sr.enumerate_rules([(1, 2)], [(1, 2)], max_elements=2)) == 7
181
+ assert len(sr.enumerate_rules([(2, 2)], [(2, 2)])) == 562
182
+ assert len(sr.enumerate_rules([(1, 2)], [(2, 2)], connectivity="None")) > 73
183
+ # Enumerated rules plug straight into evolution.
184
+ system = sr.evolve(rules[10], [[1, 2]], events=5)
185
+ assert system.events_count == 5
186
+ try:
187
+ sr.enumerate_rules([(1, 2)], [(1, 2)], connectivity="Bogus")
188
+ raise AssertionError("expected ValueError")
189
+ except ValueError as e:
190
+ assert "Bogus" in str(e)
191
+
192
+
176
193
  def main():
177
194
  tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
178
195
  for t in tests:
@@ -0,0 +1,143 @@
1
+ """Growth-rate scan over an enumerated rule signature.
2
+
3
+ Replicates the Wolfram Physics Project's triage methodology: every
4
+ inequivalent rule of a signature is evolved from the standard self-loop
5
+ initial condition under the standard event ordering; per-generation state
6
+ sizes are recorded and classified. Results go to scans/ (gitignored).
7
+
8
+ Usage: python scripts/scan_growth_rates.py [--max-gens 9] [--max-events 10000]
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import math
14
+ import time
15
+ from pathlib import Path
16
+
17
+ import setreplace as sr
18
+
19
+
20
+ def classify(sizes, reason):
21
+ """Coarse growth classification from per-generation edge counts."""
22
+ if reason == "FixedPoint":
23
+ return "terminated", 0.0
24
+ # The last generation is incomplete if we stopped on the event cap.
25
+ if reason == "MaxEvents" and len(sizes) > 2:
26
+ sizes = sizes[:-1]
27
+ if len(sizes) < 4:
28
+ return "indeterminate", 0.0
29
+ tail = sizes[-5:]
30
+ ratios = [b / a for a, b in zip(tail, tail[1:]) if a > 0]
31
+ diffs = [b - a for a, b in zip(tail, tail[1:])]
32
+ if all(d == 0 for d in diffs):
33
+ return "static", 0.0
34
+ mean_r = sum(ratios) / len(ratios)
35
+ var_r = sum((r - mean_r) ** 2 for r in ratios) / len(ratios)
36
+ mean_d = sum(diffs) / len(diffs)
37
+ var_d = sum((d - mean_d) ** 2 for d in diffs) / len(diffs)
38
+ rel_r = math.sqrt(var_r) / mean_r if mean_r else 1.0
39
+ rel_d = math.sqrt(var_d) / abs(mean_d) if mean_d else 1.0
40
+ if mean_r > 1.25 and rel_r < 0.08:
41
+ return "exponential", round(mean_r, 3)
42
+ if rel_d < 0.25:
43
+ return "linear", round(mean_d, 3)
44
+ # Power-law fit s ~ t^p over the recorded generations.
45
+ points = [(g, s) for g, s in enumerate(sizes) if g >= 1 and s > 0]
46
+ if len(points) >= 3:
47
+ xs = [math.log(g) for g, _ in points]
48
+ ys = [math.log(s) for _, s in points]
49
+ n = len(xs)
50
+ mx, my = sum(xs) / n, sum(ys) / n
51
+ denom = sum((x - mx) ** 2 for x in xs)
52
+ if denom > 0:
53
+ p = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / denom
54
+ return "polynomial", round(p, 2)
55
+ return "irregular", 0.0
56
+
57
+
58
+ def connected(state):
59
+ """Whether the hypergraph's edges form one component."""
60
+ if len(state) <= 1:
61
+ return True
62
+ parent = {}
63
+
64
+ def find(x):
65
+ while parent.setdefault(x, x) != x:
66
+ parent[x] = parent[parent[x]]
67
+ x = parent[x]
68
+ return x
69
+
70
+ for i, edge in enumerate(state):
71
+ for v in edge:
72
+ parent[find(("v", v))] = find(("e", i))
73
+ roots = {find(("e", i)) for i in range(len(state))}
74
+ return len(roots) == 1
75
+
76
+
77
+ def hub_fraction(state):
78
+ """Largest share of edges any single vertex appears in."""
79
+ if not state:
80
+ return 0.0
81
+ counts = {}
82
+ for edge in state:
83
+ for v in set(edge):
84
+ counts[v] = counts.get(v, 0) + 1
85
+ return round(max(counts.values()) / len(state), 3)
86
+
87
+
88
+ def main():
89
+ ap = argparse.ArgumentParser()
90
+ ap.add_argument("--max-gens", type=int, default=9)
91
+ ap.add_argument("--max-events", type=int, default=10_000)
92
+ args = ap.parse_args()
93
+
94
+ signature_inputs, signature_outputs = [(2, 2)], [(3, 2)]
95
+ init = [[1, 1], [1, 1]] # standard self-loop initial condition
96
+
97
+ t0 = time.perf_counter()
98
+ rules = sr.enumerate_rules(signature_inputs, signature_outputs)
99
+ t_enum = time.perf_counter() - t0
100
+ print(f"enumerated {len(rules)} rules in {t_enum:.2f}s")
101
+
102
+ out_path = Path("scans/2_2-3_2.jsonl")
103
+ out_path.parent.mkdir(exist_ok=True)
104
+ counts = {}
105
+ total_events = 0
106
+ t1 = time.perf_counter()
107
+ with out_path.open("w") as out:
108
+ for i, rule in enumerate(rules):
109
+ system = sr.evolve(
110
+ rule, init, generations=args.max_gens, events=args.max_events
111
+ )
112
+ gens = system.generations_count
113
+ sizes = [len(system.state_at_generation(g)) for g in range(gens + 1)]
114
+ reason = system.termination_reason
115
+ cls, rate = classify(sizes, reason)
116
+ final = system.final_state
117
+ record = {
118
+ "i": i,
119
+ "rule": str(rule),
120
+ "reason": reason,
121
+ "events": system.events_count,
122
+ "gens": gens,
123
+ "sizes": sizes,
124
+ "atoms": system.final_atom_count,
125
+ "connected": connected(final),
126
+ "hub": hub_fraction(final),
127
+ "class": cls,
128
+ "rate": rate,
129
+ }
130
+ out.write(json.dumps(record) + "\n")
131
+ counts[cls] = counts.get(cls, 0) + 1
132
+ total_events += system.events_count
133
+ t_scan = time.perf_counter() - t1
134
+
135
+ print(f"scanned {len(rules)} rules in {t_scan:.2f}s "
136
+ f"({total_events} events, {total_events / t_scan:,.0f} events/s)")
137
+ for cls in sorted(counts, key=counts.get, reverse=True):
138
+ print(f" {cls:14} {counts[cls]:5} ({100 * counts[cls] / len(rules):.1f}%)")
139
+ print(f"results: {out_path}")
140
+
141
+
142
+ if __name__ == "__main__":
143
+ main()