directedstructure 0.2.3__tar.gz → 0.3.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 (112) hide show
  1. {directedstructure-0.2.3 → directedstructure-0.3.0}/PKG-INFO +48 -14
  2. {directedstructure-0.2.3 → directedstructure-0.3.0}/README.md +47 -13
  3. {directedstructure-0.2.3 → directedstructure-0.3.0}/bindings/python/bindings.cpp +196 -30
  4. directedstructure-0.3.0/examples/python/adjacency.png +0 -0
  5. directedstructure-0.3.0/examples/python/example.py +69 -0
  6. directedstructure-0.3.0/examples/python/node_properties.png +0 -0
  7. directedstructure-0.3.0/examples/python/q_trajectory.png +0 -0
  8. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/__init__.py +2 -1
  9. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/_version.py +3 -3
  10. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/core.py +274 -34
  11. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/input_output.py +302 -44
  12. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/MCMC_bookkeeping.cpp +83 -17
  13. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/MCMC_core.cpp +47 -3
  14. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/MCMC_core.h +18 -5
  15. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/MCMC_proposals.cpp +200 -19
  16. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/fit.cpp +19 -3
  17. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/fit.h +7 -3
  18. directedstructure-0.3.0/src/fit_result.cpp +304 -0
  19. directedstructure-0.3.0/src/fit_result.h +80 -0
  20. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/globals.h +1 -0
  21. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/helpers.cpp +4 -0
  22. directedstructure-0.3.0/tests/python/test_hw_routing.py +101 -0
  23. directedstructure-0.3.0/tests/python/test_hw_sensitivity.py +117 -0
  24. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/python/test_package.py +325 -41
  25. directedstructure-0.3.0/tests/python/test_weighted_sbm_model_selection.py +234 -0
  26. directedstructure-0.2.3/examples/data/generate_synthetic_networks.py +0 -139
  27. directedstructure-0.2.3/examples/data/networks/example_3_nodes.gml +0 -103
  28. directedstructure-0.2.3/examples/data/networks/friends_raw.gml +0 -1079
  29. directedstructure-0.2.3/examples/data/networks/highschool_30_raw.gml +0 -12183
  30. directedstructure-0.2.3/examples/data/networks/highschool_51_raw.gml +0 -6307
  31. directedstructure-0.2.3/examples/data/networks/highschool_84_raw.gml +0 -15219
  32. directedstructure-0.2.3/examples/data/networks/snr025_sigC2_rep07.gml +0 -2951
  33. directedstructure-0.2.3/examples/data/networks/snr150_sigC5_rep09.gml +0 -3003
  34. directedstructure-0.2.3/examples/data/process_friendship_nominations.py +0 -42
  35. directedstructure-0.2.3/examples/data/take_samples.py +0 -19
  36. directedstructure-0.2.3/examples/data/test_2.gml +0 -21
  37. directedstructure-0.2.3/examples/directed_undirected_communities.py +0 -76
  38. directedstructure-0.2.3/examples/highschool_30_group_assignments_comparison.png +0 -0
  39. directedstructure-0.2.3/examples/highschool_30_neutral_inference.gml +0 -7461
  40. directedstructure-0.2.3/examples/highschool_30_original_inference.gml +0 -13306
  41. directedstructure-0.2.3/examples/highschool_51_group_assignments_comparison.png +0 -0
  42. directedstructure-0.2.3/examples/highschool_51_neutral_inference.gml +0 -4660
  43. directedstructure-0.2.3/examples/highschool_51_original_inference.gml +0 -7490
  44. directedstructure-0.2.3/examples/network_properties.csv +0 -11
  45. directedstructure-0.2.3/examples/node_properties.csv +0 -493
  46. directedstructure-0.2.3/examples/prediction.py +0 -1
  47. directedstructure-0.2.3/examples/python/consensus.py +0 -27
  48. directedstructure-0.2.3/examples/python/directed_undirected_communities.py +0 -76
  49. directedstructure-0.2.3/examples/python/prediction.py +0 -1
  50. directedstructure-0.2.3/examples/python/sampling.py +0 -1
  51. directedstructure-0.2.3/examples/python/speed_test.py +0 -1
  52. directedstructure-0.2.3/examples/python/testing.py +0 -110
  53. directedstructure-0.2.3/examples/python/testing_fixed.py +0 -104
  54. directedstructure-0.2.3/examples/sampling.py +0 -1
  55. directedstructure-0.2.3/examples/speed_test.py +0 -1
  56. directedstructure-0.2.3/examples/testing copy.png +0 -0
  57. directedstructure-0.2.3/examples/testing.png +0 -0
  58. directedstructure-0.2.3/examples/testing.py +0 -242
  59. directedstructure-0.2.3/examples/testing_coincidence_comparison.png +0 -0
  60. directedstructure-0.2.3/examples/testing_fixed.py +0 -104
  61. directedstructure-0.2.3/examples/testing_graph.png +0 -0
  62. directedstructure-0.2.3/examples/testing_inferences.gml +0 -3621
  63. directedstructure-0.2.3/examples/testing_node_properties.png +0 -0
  64. directedstructure-0.2.3/examples/testing_node_properties_from_samples.png +0 -0
  65. directedstructure-0.2.3/examples/testing_num_groups.png +0 -0
  66. directedstructure-0.2.3/examples/testing_samples.csv +0 -1001
  67. directedstructure-0.2.3/src/fit_result.cpp +0 -113
  68. directedstructure-0.2.3/src/fit_result.h +0 -48
  69. {directedstructure-0.2.3 → directedstructure-0.3.0}/.copier-answers.yml +0 -0
  70. {directedstructure-0.2.3 → directedstructure-0.3.0}/.git_archival.txt +0 -0
  71. {directedstructure-0.2.3 → directedstructure-0.3.0}/.gitattributes +0 -0
  72. {directedstructure-0.2.3 → directedstructure-0.3.0}/.github/CONTRIBUTING.md +0 -0
  73. {directedstructure-0.2.3 → directedstructure-0.3.0}/.github/dependabot.yml +0 -0
  74. {directedstructure-0.2.3 → directedstructure-0.3.0}/.github/release.yml +0 -0
  75. {directedstructure-0.2.3 → directedstructure-0.3.0}/.github/workflows/release.yml +0 -0
  76. {directedstructure-0.2.3 → directedstructure-0.3.0}/.gitignore +0 -0
  77. {directedstructure-0.2.3 → directedstructure-0.3.0}/.pre-commit-config.yaml +0 -0
  78. {directedstructure-0.2.3 → directedstructure-0.3.0}/.readthedocs.yaml +0 -0
  79. {directedstructure-0.2.3 → directedstructure-0.3.0}/CMakeLists.txt +0 -0
  80. {directedstructure-0.2.3 → directedstructure-0.3.0}/LICENSE +0 -0
  81. {directedstructure-0.2.3 → directedstructure-0.3.0}/Makefile +0 -0
  82. {directedstructure-0.2.3 → directedstructure-0.3.0}/TODO.md +0 -0
  83. {directedstructure-0.2.3 → directedstructure-0.3.0}/docs/conf.py +0 -0
  84. {directedstructure-0.2.3 → directedstructure-0.3.0}/docs/examples/prediction.md +0 -0
  85. {directedstructure-0.2.3 → directedstructure-0.3.0}/docs/examples/speed_test.md +0 -0
  86. {directedstructure-0.2.3 → directedstructure-0.3.0}/docs/examples/visualization.md +0 -0
  87. {directedstructure-0.2.3 → directedstructure-0.3.0}/docs/index.md +0 -0
  88. {directedstructure-0.2.3 → directedstructure-0.3.0}/docs/modules.md +0 -0
  89. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/business_depts.gml +0 -0
  90. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/championship_2021.gml +0 -0
  91. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/english-football-2025.gml +0 -0
  92. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/friends.gml +0 -0
  93. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/highschool_30.gml +0 -0
  94. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/highschool_51.gml +0 -0
  95. {directedstructure-0.2.3 → directedstructure-0.3.0}/examples/data/networks/highschool_84.gml +0 -0
  96. {directedstructure-0.2.3 → directedstructure-0.3.0}/node_properties.csv +0 -0
  97. {directedstructure-0.2.3 → directedstructure-0.3.0}/noxfile.py +0 -0
  98. {directedstructure-0.2.3 → directedstructure-0.3.0}/pyproject.toml +0 -0
  99. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/MCMC_core.pyi +0 -0
  100. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/_core.pyi +0 -0
  101. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/_graph_cache.py +0 -0
  102. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/_util.py +0 -0
  103. {directedstructure-0.2.3 → directedstructure-0.3.0}/python/directedstructure/src/directedstructure/py.typed +0 -0
  104. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/globals.cpp +0 -0
  105. {directedstructure-0.2.3 → directedstructure-0.3.0}/src/helpers.h +0 -0
  106. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/README.md +0 -0
  107. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/data/inputs/wins_ties_3node_point_eval.csv +0 -0
  108. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/data/outputs/triangle_coupled_coincidence.csv +0 -0
  109. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/data/outputs/triangle_independent_coincidence.csv +0 -0
  110. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/data/outputs/wins_ties_3node_coincidence.csv +0 -0
  111. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/data/outputs/wins_ties_3node_point_eval_components.csv +0 -0
  112. {directedstructure-0.2.3 → directedstructure-0.3.0}/tests/mcmc_core_test.cpp +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: directedstructure
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Infer communities, hierarchies, and their connection in directed graphs
5
5
  Author-Email: Maximilian Jerdee <mjerdee@santafe.edu>
6
6
  License-Expression: MIT
@@ -117,25 +117,44 @@ result = ds.fit(
117
117
  ### Inspect results
118
118
 
119
119
  ```python
120
- # Per-node hierarchy scores: posterior mean and uncertainty
121
- scores = result.score_means # np.ndarray, shape [n_nodes]
122
- uncertainty = result.score_stds # np.ndarray, shape [n_nodes]
120
+ # A concise summary is printed by the result itself
121
+ print(result)
122
+ # directedstructure.Result(n=62, samples=1000, mean_groups=3.2, mdl=412.100)
123
123
 
124
- # Network-level parameter summary: mean and std of each inferred parameter
125
- net_df = result.network_properties()
124
+ # Network-level posterior summary: columns parameter, mean, std, hdi_lo, hdi_hi
125
+ # Includes ICC (intraclass correlation) and sigma_T.
126
+ # Computed in C++ without materialising the full samples DataFrame.
127
+ npdf = result.network_properties_df() # default 90% HDI
128
+ npdf90 = result.network_properties_df(prob=0.90)
126
129
 
127
- # Integer community labels for the consensus sample
128
- partition = result.consensus_partition()
130
+ icc_row = npdf[npdf["parameter"] == "ICC"].iloc[0]
131
+ print(f"ICC = {icc_row['mean']:.3f} 90% HDI [{icc_row['hdi_lo']:.3f}, {icc_row['hdi_hi']:.3f}]")
129
132
 
130
- # n×n co-assignment frequency matrix
131
- comatrix = result.coincidence_matrix()
133
+ # Per-node posterior summary: columns node, score_mean, score_std, group, group_score_mean
134
+ # For n ≤ 3000 uses the consensus partition (posterior summary);
135
+ # for larger networks falls back to the MDL partition with a warning.
136
+ node_df = result.node_properties_df() # auto
137
+ node_df = result.node_properties_df(partition="consensus") # always consensus
138
+ node_df = result.node_properties_df(partition="mdl") # always MDL (free)
139
+
140
+ print(node_df.sort_values("score_mean", ascending=False).head())
132
141
 
133
- # High-level summary dict
134
- print(result.summary)
135
- # {'num_samples': 500, 'mean_num_groups': 3.2, 'mdl_value': 412.1}
142
+ # Minimum description length value (model comparison)
143
+ print(result.mdl_value)
144
+
145
+ # n×n co-assignment frequency matrix (posterior)
146
+ comatrix = result.coincidence_matrix()
136
147
 
137
- # Full posterior samples DataFrame
148
+ # Full posterior samples DataFrame (all parameters + per-node groups/scores)
138
149
  samples_df = result.samples_df()
150
+
151
+ # Posterior-predictive probability of each directed outcome between two nodes.
152
+ # Returns {"a_to_b": ..., "tie": ..., "b_to_a": ...}, values sum to 1.
153
+ p = result.direction_probabilities("Spain", "Morocco")
154
+
155
+ # Set ties_allowed=False for standard Bradley-Terry (no tie term).
156
+ p = result.direction_probabilities("Spain", "Morocco", ties_allowed=False)
157
+ # {"a_to_b": ..., "b_to_a": ...}
139
158
  ```
140
159
 
141
160
  ### Visualization
@@ -143,9 +162,24 @@ samples_df = result.samples_df()
143
162
  ```python
144
163
  import matplotlib.pyplot as plt
145
164
 
165
+ # Node scores ranked within each inferred community (±1 std error bars,
166
+ # group mean line, communities labeled G1/G2/…)
146
167
  fig, ax = ds.plot_node_properties(result)
147
168
  plt.savefig("node_scores.png")
148
169
 
170
+ # Adjacency matrix of G reordered by inferred community, with colored
171
+ # community strips on the top and left edges.
172
+ # G can be any graph — it does not have to be the graph used in fit().
173
+ fig, ax = ds.plot_adjacency_matrix(result, G, cmap="Greens", title="Network")
174
+ plt.savefig("adjacency.png")
175
+
176
+ # Compose both panels side by side
177
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
178
+ ds.plot_node_properties(result, ax=axes[0])
179
+ ds.plot_adjacency_matrix(result, G, ax=axes[1])
180
+ fig.tight_layout()
181
+ plt.savefig("combined.png")
182
+
149
183
  fig, ax = ds.plot_MCMC_entropy(result) # convergence diagnostic
150
184
  ```
151
185
 
@@ -85,25 +85,44 @@ result = ds.fit(
85
85
  ### Inspect results
86
86
 
87
87
  ```python
88
- # Per-node hierarchy scores: posterior mean and uncertainty
89
- scores = result.score_means # np.ndarray, shape [n_nodes]
90
- uncertainty = result.score_stds # np.ndarray, shape [n_nodes]
88
+ # A concise summary is printed by the result itself
89
+ print(result)
90
+ # directedstructure.Result(n=62, samples=1000, mean_groups=3.2, mdl=412.100)
91
91
 
92
- # Network-level parameter summary: mean and std of each inferred parameter
93
- net_df = result.network_properties()
92
+ # Network-level posterior summary: columns parameter, mean, std, hdi_lo, hdi_hi
93
+ # Includes ICC (intraclass correlation) and sigma_T.
94
+ # Computed in C++ without materialising the full samples DataFrame.
95
+ npdf = result.network_properties_df() # default 90% HDI
96
+ npdf90 = result.network_properties_df(prob=0.90)
94
97
 
95
- # Integer community labels for the consensus sample
96
- partition = result.consensus_partition()
98
+ icc_row = npdf[npdf["parameter"] == "ICC"].iloc[0]
99
+ print(f"ICC = {icc_row['mean']:.3f} 90% HDI [{icc_row['hdi_lo']:.3f}, {icc_row['hdi_hi']:.3f}]")
97
100
 
98
- # n×n co-assignment frequency matrix
99
- comatrix = result.coincidence_matrix()
101
+ # Per-node posterior summary: columns node, score_mean, score_std, group, group_score_mean
102
+ # For n ≤ 3000 uses the consensus partition (posterior summary);
103
+ # for larger networks falls back to the MDL partition with a warning.
104
+ node_df = result.node_properties_df() # auto
105
+ node_df = result.node_properties_df(partition="consensus") # always consensus
106
+ node_df = result.node_properties_df(partition="mdl") # always MDL (free)
107
+
108
+ print(node_df.sort_values("score_mean", ascending=False).head())
100
109
 
101
- # High-level summary dict
102
- print(result.summary)
103
- # {'num_samples': 500, 'mean_num_groups': 3.2, 'mdl_value': 412.1}
110
+ # Minimum description length value (model comparison)
111
+ print(result.mdl_value)
112
+
113
+ # n×n co-assignment frequency matrix (posterior)
114
+ comatrix = result.coincidence_matrix()
104
115
 
105
- # Full posterior samples DataFrame
116
+ # Full posterior samples DataFrame (all parameters + per-node groups/scores)
106
117
  samples_df = result.samples_df()
118
+
119
+ # Posterior-predictive probability of each directed outcome between two nodes.
120
+ # Returns {"a_to_b": ..., "tie": ..., "b_to_a": ...}, values sum to 1.
121
+ p = result.direction_probabilities("Spain", "Morocco")
122
+
123
+ # Set ties_allowed=False for standard Bradley-Terry (no tie term).
124
+ p = result.direction_probabilities("Spain", "Morocco", ties_allowed=False)
125
+ # {"a_to_b": ..., "b_to_a": ...}
107
126
  ```
108
127
 
109
128
  ### Visualization
@@ -111,9 +130,24 @@ samples_df = result.samples_df()
111
130
  ```python
112
131
  import matplotlib.pyplot as plt
113
132
 
133
+ # Node scores ranked within each inferred community (±1 std error bars,
134
+ # group mean line, communities labeled G1/G2/…)
114
135
  fig, ax = ds.plot_node_properties(result)
115
136
  plt.savefig("node_scores.png")
116
137
 
138
+ # Adjacency matrix of G reordered by inferred community, with colored
139
+ # community strips on the top and left edges.
140
+ # G can be any graph — it does not have to be the graph used in fit().
141
+ fig, ax = ds.plot_adjacency_matrix(result, G, cmap="Greens", title="Network")
142
+ plt.savefig("adjacency.png")
143
+
144
+ # Compose both panels side by side
145
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
146
+ ds.plot_node_properties(result, ax=axes[0])
147
+ ds.plot_adjacency_matrix(result, G, ax=axes[1])
148
+ fig.tight_layout()
149
+ plt.savefig("combined.png")
150
+
117
151
  fig, ax = ds.plot_MCMC_entropy(result) # convergence diagnostic
118
152
  ```
119
153
 
@@ -78,6 +78,9 @@ public:
78
78
  fill("H_A_G_b", [](const SampleState& s){ return s.H_A_G_b; });
79
79
  fill("H_Adir_G_A_s_nu", [](const SampleState& s){ return s.H_Adir_G_A_s_nu; });
80
80
  fill("H_s_G_b_depths", [](const SampleState& s){ return s.H_s_G_b_depths; });
81
+ fill("two_layer_mode", [](const SampleState& s){ return s.two_layer_mode; });
82
+ fill("weighted_SBM", [](const SampleState& s){ return s.weighted_SBM; });
83
+ fill("H_W", [](const SampleState& s){ return s.H_W; });
81
84
  fill("chain_id", [](const SampleState& s){ return s.chain_id; });
82
85
  fill("beta", [](const SampleState& s){ return s.beta; });
83
86
 
@@ -100,12 +103,104 @@ public:
100
103
  return d;
101
104
  }
102
105
 
106
+ py::dict network_properties_dict(double prob = 0.90) const {
107
+ auto r = FitResult::network_properties(prob);
108
+ py::dict d;
109
+ d["parameter"] = py::cast(r.parameter);
110
+ d["mean"] = py::cast(r.mean);
111
+ d["std"] = py::cast(r.std);
112
+ d["hdi_lo"] = py::cast(r.hdi_lo);
113
+ d["hdi_hi"] = py::cast(r.hdi_hi);
114
+ return d;
115
+ }
116
+
117
+ py::dict node_properties_dict(const std::string& mode = "auto") const {
118
+ auto r = FitResult::node_properties(mode);
119
+ py::dict d;
120
+ d["mode_used"] = py::cast(r.mode_used);
121
+ d["score_mean"] = py::cast(r.score_mean);
122
+ d["score_std"] = py::cast(r.score_std);
123
+ d["group"] = py::cast(r.group);
124
+ d["group_score_mean"] = py::cast(r.group_score_mean);
125
+ return d;
126
+ }
127
+
103
128
  py::object coincidence_matrix_numpy() const {
104
129
  auto flat = coincidence_matrix();
105
130
  int n = num_nodes_;
106
131
  py::module_ np = py::module_::import("numpy");
107
132
  return np.attr("reshape")(py::cast(flat), py::make_tuple(n, n));
108
133
  }
134
+
135
+ py::tuple getstate() const {
136
+ py::list lst;
137
+ for (const auto& s : samples_) {
138
+ py::dict d;
139
+ d["chain_id"] = s.chain_id;
140
+ d["beta"] = s.beta;
141
+ d["partition"] = s.partition;
142
+ d["scores"] = s.scores;
143
+ d["num_groups"] = s.num_groups;
144
+ d["density_in"] = s.density_in;
145
+ d["density_out"] = s.density_out;
146
+ d["variation_in"] = s.variation_in;
147
+ d["variation_out"] = s.variation_out;
148
+ d["degree_correction"] = s.degree_correction;
149
+ d["individual_depth"] = s.individual_depth;
150
+ d["group_depth"] = s.group_depth;
151
+ d["ties_parameter"] = s.ties_parameter;
152
+ d["H_A_b"] = s.H_A_b;
153
+ d["H_b"] = s.H_b;
154
+ d["H_M"] = s.H_M;
155
+ d["H_k"] = s.H_k;
156
+ d["H_A_G_k_M_b"] = s.H_A_G_k_M_b;
157
+ d["H_A_G_b"] = s.H_A_G_b;
158
+ d["H_Adir_G_A_s_nu"] = s.H_Adir_G_A_s_nu;
159
+ d["H_s_G_b_depths"] = s.H_s_G_b_depths;
160
+ d["two_layer_mode"] = s.two_layer_mode;
161
+ d["weighted_SBM"] = s.weighted_SBM;
162
+ d["H_W"] = s.H_W;
163
+ lst.append(d);
164
+ }
165
+ return py::make_tuple(num_nodes_, lst);
166
+ }
167
+
168
+ static FitResultNative setstate(py::tuple state) {
169
+ int num_nodes = state[0].cast<int>();
170
+ auto lst = state[1].cast<py::list>();
171
+ std::vector<SampleState> samples;
172
+ samples.reserve(py::len(lst));
173
+ for (auto item : lst) {
174
+ auto d = item.cast<py::dict>();
175
+ SampleState s;
176
+ s.chain_id = d["chain_id"].cast<int>();
177
+ s.beta = d["beta"].cast<double>();
178
+ s.partition = d["partition"].cast<std::vector<int>>();
179
+ s.scores = d["scores"].cast<std::vector<double>>();
180
+ s.num_groups = d["num_groups"].cast<int>();
181
+ s.density_in = d["density_in"].cast<double>();
182
+ s.density_out = d["density_out"].cast<double>();
183
+ s.variation_in = d["variation_in"].cast<double>();
184
+ s.variation_out = d["variation_out"].cast<double>();
185
+ s.degree_correction = d["degree_correction"].cast<double>();
186
+ s.individual_depth = d["individual_depth"].cast<double>();
187
+ s.group_depth = d["group_depth"].cast<double>();
188
+ s.ties_parameter = d["ties_parameter"].cast<double>();
189
+ s.H_A_b = d["H_A_b"].cast<double>();
190
+ s.H_b = d["H_b"].cast<double>();
191
+ s.H_M = d["H_M"].cast<double>();
192
+ s.H_k = d["H_k"].cast<double>();
193
+ s.H_A_G_k_M_b = d["H_A_G_k_M_b"].cast<double>();
194
+ s.H_A_G_b = d["H_A_G_b"].cast<double>();
195
+ s.H_Adir_G_A_s_nu = d["H_Adir_G_A_s_nu"].cast<double>();
196
+ s.H_s_G_b_depths = d["H_s_G_b_depths"].cast<double>();
197
+ s.two_layer_mode = d.contains("two_layer_mode") ? d["two_layer_mode"].cast<bool>() : false;
198
+ s.weighted_SBM = d.contains("weighted_SBM") ? d["weighted_SBM"].cast<bool>() : false;
199
+ s.H_W = d.contains("H_W") ? d["H_W"].cast<double>() : 0.0;
200
+ samples.push_back(std::move(s));
201
+ }
202
+ return FitResultNative(FitResult(num_nodes, std::move(samples)));
203
+ }
109
204
  };
110
205
 
111
206
  // ---------------------------------------------------------------------------
@@ -116,6 +211,7 @@ PYBIND11_MODULE(MCMC_core, m) {
116
211
  // --- MCMC class (kept for testing / advanced use) ---
117
212
  py::class_<MCMC>(m, "MCMC")
118
213
  .def(py::init<
214
+ std::vector<std::unordered_map<int, double>>,
119
215
  std::vector<std::unordered_map<int, double>>,
120
216
  std::vector<std::unordered_map<int, double>>,
121
217
  std::vector<int>,
@@ -139,9 +235,11 @@ PYBIND11_MODULE(MCMC_core, m) {
139
235
  const bool&,
140
236
  int,
141
237
  double,
142
- double> (),
238
+ double,
239
+ bool> (),
143
240
  py::arg("wins"),
144
241
  py::arg("ties"),
242
+ py::arg("group_adj") = std::vector<std::unordered_map<int, double>>{},
145
243
  py::arg("partition"),
146
244
  py::arg("partition_fixed") = false,
147
245
  py::arg("scores"),
@@ -163,7 +261,8 @@ PYBIND11_MODULE(MCMC_core, m) {
163
261
  py::arg("ties_parameter_fixed") = false,
164
262
  py::arg("seed") = 0,
165
263
  py::arg("density_in") = -1.0,
166
- py::arg("density_out") = -1.0)
264
+ py::arg("density_out") = -1.0,
265
+ py::arg("weighted_SBM") = false)
167
266
  .def_readonly("num_groups", &MCMC::num_groups)
168
267
  .def_readonly("partition", &MCMC::partition)
169
268
  .def_readonly("scores", &MCMC::scores)
@@ -181,8 +280,10 @@ PYBIND11_MODULE(MCMC_core, m) {
181
280
  .def_readonly("H_k", &MCMC::H_k)
182
281
  .def_readonly("H_A_G_k_M_b", &MCMC::H_A_G_k_M_b)
183
282
  .def_readonly("H_A_G_b", &MCMC::H_A_G_b)
283
+ .def_readonly("H_W", &MCMC::H_W)
184
284
  .def_readonly("H_Adir_G_A_s_nu", &MCMC::H_Adir_G_A_s_nu)
185
285
  .def_readonly("H_s_G_b_depths", &MCMC::H_s_G_b_depths)
286
+ .def_readonly("weighted_SBM", &MCMC::weighted_SBM)
186
287
  .def_readonly("time_H_k", &MCMC::time_H_k)
187
288
  .def_readonly("time_H_M", &MCMC::time_H_M)
188
289
  .def_readonly("time_H_A_G_k_M_b", &MCMC::time_H_A_G_k_M_b)
@@ -191,6 +292,7 @@ PYBIND11_MODULE(MCMC_core, m) {
191
292
  .def("MCMC_sweep", &MCMC::MCMC_sweep,
192
293
  py::arg("beta") = 1.0,
193
294
  py::arg("num_sweeps") = 1)
295
+ .def("calculate_H_M", &MCMC::calculate_H_M)
194
296
  .def("change_parameter", &MCMC::change_parameter,
195
297
  py::arg("param_name"),
196
298
  py::arg("new_value"));
@@ -204,13 +306,26 @@ PYBIND11_MODULE(MCMC_core, m) {
204
306
  .def("coincidence_matrix", &FitResultNative::coincidence_matrix_numpy)
205
307
  .def("score_means", &FitResultNative::score_means)
206
308
  .def("score_stds", &FitResultNative::score_stds)
309
+ .def("parameter_means", &FitResultNative::parameter_means)
310
+ .def("parameter_stds", &FitResultNative::parameter_stds)
311
+ .def("direction_probabilities", &FitResultNative::direction_probabilities,
312
+ py::arg("node_a"), py::arg("node_b"), py::arg("ties_allowed") = true)
313
+ .def("network_properties", &FitResultNative::network_properties_dict,
314
+ py::arg("prob") = 0.90)
315
+ .def("node_properties", &FitResultNative::node_properties_dict,
316
+ py::arg("mode") = "auto")
207
317
  .def("samples_df", &FitResultNative::samples_df)
208
- .def("summary", &FitResultNative::summary);
318
+ .def("summary", &FitResultNative::summary)
319
+ .def("__getstate__", &FitResultNative::getstate)
320
+ .def("__setstate__", [](FitResultNative& self, py::tuple state) {
321
+ new (&self) FitResultNative(FitResultNative::setstate(state));
322
+ });
209
323
 
210
324
  // --- fit_backend: thin type-conversion wrapper around the C++ core fit() ---
211
325
  m.def("fit_backend", [](
212
326
  std::vector<std::unordered_map<int, double>> wins,
213
327
  std::vector<std::unordered_map<int, double>> ties,
328
+ std::vector<std::unordered_map<int, double>> group_adj,
214
329
  std::vector<int> initial_partition,
215
330
  bool partition_fixed,
216
331
  std::vector<double> initial_scores,
@@ -236,7 +351,8 @@ PYBIND11_MODULE(MCMC_core, m) {
236
351
  int num_tempering_chains,
237
352
  int seed,
238
353
  double timeout,
239
- bool show_progress
354
+ bool show_progress,
355
+ bool weighted_SBM
240
356
  ) {
241
357
  auto to_opt_d = [](py::object o) -> std::optional<double> {
242
358
  return o.is_none() ? std::nullopt : std::optional<double>(py::cast<double>(o));
@@ -248,6 +364,7 @@ PYBIND11_MODULE(MCMC_core, m) {
248
364
  FitParams params;
249
365
  params.wins = std::move(wins);
250
366
  params.ties = std::move(ties);
367
+ params.group_adj = std::move(group_adj);
251
368
  params.initial_partition = std::move(initial_partition);
252
369
  params.partition_fixed = partition_fixed;
253
370
  params.initial_scores = std::move(initial_scores);
@@ -273,41 +390,89 @@ PYBIND11_MODULE(MCMC_core, m) {
273
390
  params.num_tempering_chains = num_tempering_chains;
274
391
  params.seed = seed;
275
392
  params.timeout = timeout;
393
+ params.weighted_SBM = weighted_SBM;
276
394
 
277
- // Progress reporting: drive a stderr thread from the on_sample callback.
395
+ // Progress reporting: phase-aware stderr display (pystan style).
278
396
  std::mutex mtx;
279
397
  std::condition_variable cv;
280
- std::atomic<int> latest{0};
281
- bool done = false;
398
+ std::atomic<int> warmup_done{0}, burn_done{0}, sample_done{0};
399
+ bool fit_finished = false;
282
400
  std::thread progress_thread;
283
401
 
284
402
  if (show_progress) {
285
- params.on_sample = [&](int completed, int total) {
286
- latest.store(completed, std::memory_order_relaxed);
403
+ params.on_warmup = [&](int completed, int) {
404
+ warmup_done.store(completed, std::memory_order_relaxed);
287
405
  cv.notify_one();
288
- (void)total;
289
406
  };
290
- const int total = num_chains * samples_per_chain;
291
- progress_thread = std::thread([&]() {
292
- int last = -1;
293
- auto emit = [&](int v) {
407
+ params.on_burn_in = [&](int completed, int) {
408
+ burn_done.store(completed, std::memory_order_relaxed);
409
+ cv.notify_one();
410
+ };
411
+ params.on_sample = [&](int completed, int) {
412
+ sample_done.store(completed, std::memory_order_relaxed);
413
+ cv.notify_one();
414
+ };
415
+
416
+ const int warmup_total = num_chains * param_warmup_sweeps;
417
+ const int burn_total = num_chains * burn_sweeps;
418
+ const int sample_total = num_chains * samples_per_chain;
419
+
420
+ progress_thread = std::thread([&, warmup_total, burn_total, sample_total]() {
421
+ auto emit = [](const char* label, int cur, int total) {
422
+ int pct = total > 0 ? static_cast<int>(100.0 * cur / total) : 100;
294
423
  std::ostringstream ss;
295
- ss << '\r' << "Sampling progress: " << v << '/' << total
296
- << " (" << std::fixed << std::setprecision(1)
297
- << 100.0 * v / total << "%)";
424
+ ss << '\r' << std::left << std::setw(11) << label
425
+ << std::right << std::setw(6) << cur << " / " << total
426
+ << " [" << std::setw(3) << pct << "%]";
298
427
  std::cerr << ss.str() << std::flush;
299
428
  };
300
- emit(0);
301
- while (true) {
302
- std::unique_lock<std::mutex> lock(mtx);
303
- cv.wait_for(lock, std::chrono::milliseconds(100),
304
- [&]{ return done || latest.load() != last; });
305
- int cur = latest.load(std::memory_order_relaxed);
306
- bool stopping = done;
307
- lock.unlock();
308
- if (cur != last) { last = cur; emit(cur); }
309
- if (stopping) break;
429
+
430
+ auto run_phase = [&](std::atomic<int>& counter, int total,
431
+ const char* label) {
432
+ if (total <= 0) return;
433
+ emit(label, 0, total);
434
+ int last = -1;
435
+ while (true) {
436
+ std::unique_lock<std::mutex> lock(mtx);
437
+ cv.wait_for(lock, std::chrono::milliseconds(100),
438
+ [&]{ return fit_finished || counter.load() != last; });
439
+ int cur = counter.load(std::memory_order_relaxed);
440
+ bool stopping = fit_finished;
441
+ lock.unlock();
442
+ if (cur != last) { emit(label, cur, total); last = cur; }
443
+ if (cur >= total || stopping) break;
444
+ }
445
+ // Only print a final line if timeout ended the phase early.
446
+ int final_cur = counter.load(std::memory_order_relaxed);
447
+ if (final_cur < total)
448
+ emit(label, final_cur, total);
449
+ std::cerr << '\n' << std::flush;
450
+ };
451
+
452
+ // Show both param-warmup and burn-in under a single "Warmup:" line.
453
+ const int total_warmup_combined = warmup_total + burn_total;
454
+ if (total_warmup_combined > 0) {
455
+ emit("Warmup: ", 0, total_warmup_combined);
456
+ int last = -1;
457
+ while (true) {
458
+ std::unique_lock<std::mutex> lock(mtx);
459
+ int cur = warmup_done.load(std::memory_order_relaxed)
460
+ + burn_done.load(std::memory_order_relaxed);
461
+ cv.wait_for(lock, std::chrono::milliseconds(100),
462
+ [&]{ return fit_finished || cur != last; });
463
+ cur = warmup_done.load(std::memory_order_relaxed)
464
+ + burn_done.load(std::memory_order_relaxed);
465
+ bool stopping = fit_finished;
466
+ lock.unlock();
467
+ if (cur != last) { emit("Warmup: ", cur, total_warmup_combined); last = cur; }
468
+ if (cur >= total_warmup_combined || stopping) break;
469
+ }
470
+ int final_cur = warmup_done.load() + burn_done.load();
471
+ if (final_cur < total_warmup_combined)
472
+ emit("Warmup: ", final_cur, total_warmup_combined);
473
+ std::cerr << '\n' << std::flush;
310
474
  }
475
+ run_phase(sample_done, sample_total, "Sampling: ");
311
476
  });
312
477
  }
313
478
 
@@ -317,16 +482,16 @@ PYBIND11_MODULE(MCMC_core, m) {
317
482
  }();
318
483
 
319
484
  if (progress_thread.joinable()) {
320
- { std::lock_guard<std::mutex> lock(mtx); done = true; }
485
+ { std::lock_guard<std::mutex> lock(mtx); fit_finished = true; }
321
486
  cv.notify_all();
322
487
  progress_thread.join();
323
- if (show_progress) std::cerr << '\n' << std::flush;
324
488
  }
325
489
 
326
490
  return FitResultNative(std::move(result));
327
491
  },
328
492
  py::arg("wins"),
329
493
  py::arg("ties"),
494
+ py::arg("group_adj") = std::vector<std::unordered_map<int, double>>{},
330
495
  py::arg("initial_partition"),
331
496
  py::arg("partition_fixed") = false,
332
497
  py::arg("initial_scores"),
@@ -352,5 +517,6 @@ PYBIND11_MODULE(MCMC_core, m) {
352
517
  py::arg("num_tempering_chains") = 1,
353
518
  py::arg("seed") = 0,
354
519
  py::arg("timeout") = 60.0,
355
- py::arg("show_progress") = false);
520
+ py::arg("show_progress") = false,
521
+ py::arg("weighted_SBM") = false);
356
522
  }
@@ -0,0 +1,69 @@
1
+ from pathlib import Path
2
+
3
+ import networkx as nx
4
+ import matplotlib.pyplot as plt
5
+
6
+ import directedstructure as ds
7
+
8
+ here = Path(__file__).resolve().parents[1]
9
+ G = nx.read_gml(here / "data" / "networks" / "english-football-2025.gml", label="id")
10
+
11
+ result = ds.fit(
12
+ ds.Config(),
13
+ G,
14
+ num_chains=1,
15
+ samples_per_chain=500,
16
+ burn_sweeps=500,
17
+ seed=1,
18
+ show_progress=True,
19
+ )
20
+
21
+ print("Nodes:", G.number_of_nodes())
22
+ print("Edges:", G.number_of_edges())
23
+ print(result)
24
+
25
+ # Network-level posterior summary: mean, std, and 90% HDI for all parameters.
26
+ # ICC and sigma_T are included. Computed in C++ — no samples_df() overhead.
27
+ print("\nNetwork properties (90% HDI):")
28
+ npdf = result.network_properties_df()
29
+ print(npdf.to_string(index=False))
30
+
31
+ # Per-node posterior summary: scores, community labels, group mean score.
32
+ # Uses consensus partition for n ≤ 3000 (the Bayesian posterior summary).
33
+ print("\nTop nodes by score:")
34
+ node_df = result.node_properties_df()
35
+ print(node_df.sort_values("score_mean", ascending=False).head().to_string(index=False))
36
+
37
+ # Minimum description length (useful for model comparison)
38
+ print(f"\nMDL value: {result.mdl_value:.3f}")
39
+
40
+ # Full per-sample DataFrame — only materialise when raw samples are needed
41
+ # (e.g. convergence diagnostics, joint distributions).
42
+ samples_df = result.samples_df()
43
+
44
+ # Node properties plot — scores ranked within each inferred community
45
+ fig_a, _ = ds.plot_node_properties(result)
46
+ fig_a.tight_layout()
47
+ plot_a = Path(__file__).resolve().parent / "node_properties.png"
48
+ fig_a.savefig(plot_a, dpi=150)
49
+ plt.close(fig_a)
50
+ print("Saved node properties plot to:", plot_a)
51
+
52
+ # Adjacency matrix reordered by inferred community
53
+ fig_b, _ = ds.plot_adjacency_matrix(result, G, title="english-football-2025")
54
+ plot_b = Path(__file__).resolve().parent / "adjacency.png"
55
+ fig_b.savefig(plot_b, dpi=150, bbox_inches="tight")
56
+ plt.close(fig_b)
57
+ print("Saved adjacency matrix to:", plot_b)
58
+
59
+ # q trajectory convergence diagnostic
60
+ fig, ax = plt.subplots(figsize=(8, 3))
61
+ ax.plot(samples_df.index, samples_df["num_groups"], linewidth=1.5)
62
+ ax.set_xlabel("Sample")
63
+ ax.set_ylabel("q = num_groups")
64
+ ax.set_title("q trajectory")
65
+ fig.tight_layout()
66
+ plot_path = Path(__file__).resolve().parent / "q_trajectory.png"
67
+ fig.savefig(plot_path, dpi=150)
68
+ plt.close(fig)
69
+ print("Saved q trajectory to:", plot_path)
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from ._version import version as __version__
10
10
  from .core import Config, Result, fit
11
- from .input_output import plot_node_properties, plot_MCMC_entropy, write_gml_with_inferences
11
+ from .input_output import plot_node_properties, plot_adjacency_matrix, plot_MCMC_entropy, write_gml_with_inferences
12
12
 
13
13
  __all__ = [
14
14
  "__version__",
@@ -16,6 +16,7 @@ __all__ = [
16
16
  "Result",
17
17
  "fit",
18
18
  "plot_node_properties",
19
+ "plot_adjacency_matrix",
19
20
  "plot_MCMC_entropy",
20
21
  "write_gml_with_inferences",
21
22
  ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.3'
22
- __version_tuple__ = version_tuple = (0, 2, 3)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gbb517006b'
24
+ __commit_id__ = commit_id = 'g297119596'