simulation-alg 0.1.9__tar.gz → 0.1.10__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.
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/.gitignore +1 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/Cargo.lock +1 -1
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/Cargo.toml +2 -2
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/PKG-INFO +1 -1
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/pyproject.toml +1 -1
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/simulation.pyi +10 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/src/graph/networkx_graph.rs +125 -21
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/src/lib.rs +1 -0
- simulation_alg-0.1.10/test_bounded_simulation.py +84 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/.github/workflows/ci.yml +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/.gitmodules +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/LICENSE +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/README.md +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/hyper-simulation.log +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/python/__init__.py +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/requirements.txt +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/src/graph/hypergraph.rs +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/src/graph/mod.rs +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/src/utils.rs +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/test.py +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/tests/test_hyper_simulation.py +0 -0
- {simulation_alg-0.1.9 → simulation_alg-0.1.10}/tests/test_simulation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "simulation"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.10"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
description = "A Python package for simulation algorithms"
|
|
@@ -15,7 +15,7 @@ crate-type = ["cdylib"]
|
|
|
15
15
|
[dependencies]
|
|
16
16
|
pyo3 = { version = "0.27.1", features = ["extension-module"] }
|
|
17
17
|
graph-simulation = "0.1.24"
|
|
18
|
-
graph-base = "0.1.
|
|
18
|
+
graph-base = "0.1.14"
|
|
19
19
|
rayon = "1.10.0"
|
|
20
20
|
# [tool.maturin]
|
|
21
21
|
# include-pyi = true # 自动打包生成的 .pyi 文件
|
|
@@ -33,6 +33,16 @@ def is_simulation_isomorphic_of_edge_fn(nx_graph1: networkx.DiGraph, nx_graph2:
|
|
|
33
33
|
Check if two graphs are isomorphic by graph simulation.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
+
def get_bounded_simulation(nx_graph1: networkx.DiGraph, nx_graph2: networkx.DiGraph, compare: Callable, bound: Callable, is_label_cached=False) -> Dict:
|
|
37
|
+
"""
|
|
38
|
+
Get the bounded simulation between two graphs.
|
|
39
|
+
|
|
40
|
+
The compare function should take two node attribute dictionaries and return True if they have the same label.
|
|
41
|
+
The bound function should take a node attribute dictionary and return an integer representing the bound value for that node.
|
|
42
|
+
|
|
43
|
+
Returns a dictionary mapping nodes from nx_graph1 to sets of nodes from nx_graph2 that satisfy the bounded simulation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
36
46
|
class Node:
|
|
37
47
|
"""
|
|
38
48
|
Node for hypergraph.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
use graph_base::interfaces::vertex::Vertex;
|
|
2
2
|
use graph_simulation::algorithm::simulation::Simulation;
|
|
3
|
+
use graph_simulation::algorithm::bounded::{BoundedSimulation, Bounded};
|
|
3
4
|
use pyo3::types::PySet;
|
|
4
5
|
use pyo3::{prelude::*, types::PyDict};
|
|
5
6
|
use graph_base::interfaces::labeled::{Label, Labeled, LabeledAdjacency};
|
|
6
|
-
use graph_base::interfaces::graph::{Graph, Directed, Adjacency, AdjacencyInv, SingleId, IdPair};
|
|
7
|
+
use graph_base::interfaces::graph::{Graph, Directed, Adjacency, AdjacencyInv, SingleId, IdPair, Degree, DegreeList};
|
|
7
8
|
|
|
8
9
|
use pyo3::prelude::*;
|
|
9
10
|
use std::collections::{HashMap, HashSet};
|
|
@@ -139,7 +140,8 @@ pub struct NetworkXGraph {
|
|
|
139
140
|
same_label_fn: Option<Py<PyAny>>,
|
|
140
141
|
same_edge_fn: Option<Py<PyAny>>,
|
|
141
142
|
same_node_edge_fn: Option<Py<PyAny>>,
|
|
142
|
-
same_label_cache: Option<HashSet<(usize, usize)
|
|
143
|
+
same_label_cache: Option<HashSet<(usize, usize)>>,
|
|
144
|
+
bound_values: HashMap<usize, usize>, // 节点 ID 到 bound 值的映射
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
impl Clone for NetworkXGraph {
|
|
@@ -153,6 +155,7 @@ impl Clone for NetworkXGraph {
|
|
|
153
155
|
same_edge_fn: self.same_edge_fn.as_ref().map(|f| f.clone_ref(py)),
|
|
154
156
|
same_node_edge_fn: self.same_node_edge_fn.as_ref().map(|f| f.clone_ref(py)),
|
|
155
157
|
same_label_cache: self.same_label_cache.clone(),
|
|
158
|
+
bound_values: self.bound_values.clone(),
|
|
156
159
|
}
|
|
157
160
|
})
|
|
158
161
|
}
|
|
@@ -181,6 +184,7 @@ impl NetworkXGraph {
|
|
|
181
184
|
same_edge_fn: None,
|
|
182
185
|
same_node_edge_fn: None,
|
|
183
186
|
same_label_cache: None,
|
|
187
|
+
bound_values: HashMap::new(),
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
|
|
@@ -321,17 +325,6 @@ impl NetworkXGraph {
|
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
fn build_compare_cache(&mut self, other: &NetworkXGraph) {
|
|
324
|
-
// use rayon::prelude::*;
|
|
325
|
-
// let cache: HashSet<_> = self.nodes.par_iter().flat_map(|node1| {
|
|
326
|
-
// let local: HashSet<_> = other.nodes.par_iter().filter_map(|node2| {
|
|
327
|
-
// if self.label_same(node1, node2) {
|
|
328
|
-
// Some((node1.id, node2.id))
|
|
329
|
-
// } else {
|
|
330
|
-
// None
|
|
331
|
-
// }
|
|
332
|
-
// }).collect();
|
|
333
|
-
// local
|
|
334
|
-
// }).collect();
|
|
335
328
|
|
|
336
329
|
let cache: HashSet<_> = self.nodes.iter().flat_map(|node1| {
|
|
337
330
|
other.nodes.iter().filter_map(|node2| {
|
|
@@ -345,6 +338,22 @@ impl NetworkXGraph {
|
|
|
345
338
|
|
|
346
339
|
self.same_label_cache = Some(cache);
|
|
347
340
|
}
|
|
341
|
+
|
|
342
|
+
fn set_bound_values(&mut self, bound_fn: Py<PyAny>) {
|
|
343
|
+
// 使用 bound 函数为每个节点设置 bound 值
|
|
344
|
+
for node in &self.nodes {
|
|
345
|
+
let bound_value = Python::attach(|py| {
|
|
346
|
+
let node_attrs = node.attributes.0.iter()
|
|
347
|
+
.map(|(k, v)| (k.clone(), v.clone_ref(py)))
|
|
348
|
+
.collect::<HashMap<_, _>>();
|
|
349
|
+
bound_fn.call1(py, (node_attrs,))
|
|
350
|
+
.ok()
|
|
351
|
+
.and_then(|result| result.extract::<usize>(py).ok())
|
|
352
|
+
.unwrap_or(0)
|
|
353
|
+
});
|
|
354
|
+
self.bound_values.insert(node.id, bound_value);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
348
357
|
}
|
|
349
358
|
|
|
350
359
|
impl Vertex for Node {}
|
|
@@ -363,6 +372,7 @@ impl<'a> Graph<'a> for NetworkXGraph {
|
|
|
363
372
|
same_edge_fn: None,
|
|
364
373
|
same_node_edge_fn: None,
|
|
365
374
|
same_label_cache: None,
|
|
375
|
+
bound_values: HashMap::new(),
|
|
366
376
|
}
|
|
367
377
|
}
|
|
368
378
|
|
|
@@ -489,14 +499,14 @@ impl std::fmt::Display for NetworkXGraph {
|
|
|
489
499
|
}
|
|
490
500
|
}
|
|
491
501
|
|
|
492
|
-
fn to_nx_node(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
502
|
+
fn to_nx_node(_py: Python, node: &Node) -> PyResult<Py<PyAny>> {
|
|
503
|
+
// Return the node id as a Python object
|
|
504
|
+
Python::attach(|py| {
|
|
505
|
+
let builtins = py.import("builtins")?;
|
|
506
|
+
let int_fn = builtins.getattr("int")?;
|
|
507
|
+
let val = int_fn.call1((node.id,))?;
|
|
508
|
+
Ok(val.into())
|
|
509
|
+
})
|
|
500
510
|
}
|
|
501
511
|
|
|
502
512
|
#[pyfunction]
|
|
@@ -616,6 +626,100 @@ impl Adjacency<'_> for NetworkXGraph {}
|
|
|
616
626
|
|
|
617
627
|
impl AdjacencyInv<'_> for NetworkXGraph {}
|
|
618
628
|
|
|
629
|
+
impl<'a> Degree<'a> for NetworkXGraph {
|
|
630
|
+
fn get_out_degree(&'a self) -> DegreeList<'a, Self> {
|
|
631
|
+
let mut degree_map = HashMap::new();
|
|
632
|
+
|
|
633
|
+
for node in &self.nodes {
|
|
634
|
+
let out_degree = self.edges.iter()
|
|
635
|
+
.filter(|edge| edge.source == node.id)
|
|
636
|
+
.count();
|
|
637
|
+
degree_map.insert(node, out_degree);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 使用 unsafe 代码来创建 DegreeList,因为其字段是私有的
|
|
641
|
+
unsafe { std::mem::transmute::<HashMap<&'a Self::Node, usize>, DegreeList<'a, Self>>(degree_map) }
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
fn get_in_degree(&'a self) -> DegreeList<'a, Self> {
|
|
645
|
+
let mut degree_map = HashMap::new();
|
|
646
|
+
|
|
647
|
+
for node in &self.nodes {
|
|
648
|
+
let in_degree = self.edges.iter()
|
|
649
|
+
.filter(|edge| edge.target == node.id)
|
|
650
|
+
.count();
|
|
651
|
+
degree_map.insert(node, in_degree);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// 使用 unsafe 代码来创建 DegreeList,因为其字段是私有的
|
|
655
|
+
unsafe { std::mem::transmute::<HashMap<&'a Self::Node, usize>, DegreeList<'a, Self>>(degree_map) }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
fn out_degree(&'a self, degree_list: &DegreeList<'a, Self>, node: &Self::Node) -> usize {
|
|
659
|
+
// 使用 unsafe 代码访问私有字段
|
|
660
|
+
unsafe {
|
|
661
|
+
let degree_map = &*(degree_list as *const _ as *const HashMap<&'a Self::Node, usize>);
|
|
662
|
+
*degree_map.get(node).unwrap_or(&0)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
fn in_degree(&'a self, degree_list: &DegreeList<'a, Self>, node: &Self::Node) -> usize {
|
|
667
|
+
// 使用 unsafe 代码访问私有字段
|
|
668
|
+
unsafe {
|
|
669
|
+
let degree_map = &*(degree_list as *const _ as *const HashMap<&'a Self::Node, usize>);
|
|
670
|
+
*degree_map.get(node).unwrap_or(&0)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
impl<'a> Bounded<'a> for NetworkXGraph {
|
|
676
|
+
fn get_bound(&'a self, u: &'a Self::Node, v: &'a Self::Node) -> usize {
|
|
677
|
+
// 从 u 到 v 的 bound 值。我们定义为 u 节点的 bound_values 中的值
|
|
678
|
+
// 或者可以定义为基于 (u, v) 对的某个函数
|
|
679
|
+
*self.bound_values.get(&u.id).unwrap_or(&0)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#[pyfunction]
|
|
684
|
+
#[pyo3(signature = (nx_graph1, nx_graph2, compare, bound, is_label_cached = false))]
|
|
685
|
+
pub fn get_bounded_simulation(
|
|
686
|
+
nx_graph1: &Bound<'_, PyAny>,
|
|
687
|
+
nx_graph2: &Bound<'_, PyAny>,
|
|
688
|
+
compare: Py<PyAny>,
|
|
689
|
+
bound: Py<PyAny>,
|
|
690
|
+
is_label_cached: bool
|
|
691
|
+
) -> PyResult<Py<PyAny>> {
|
|
692
|
+
// 1. 从 NetworkX 图转换
|
|
693
|
+
let mut graph1 = NetworkXGraph::from_networkx(nx_graph1)?;
|
|
694
|
+
let graph2 = NetworkXGraph::from_networkx(nx_graph2)?;
|
|
695
|
+
|
|
696
|
+
// 2. 注册 compare 函数
|
|
697
|
+
graph1.register_compare_fn(compare);
|
|
698
|
+
|
|
699
|
+
// 3. 为 graph1 设置 bound 值
|
|
700
|
+
graph1.set_bound_values(bound);
|
|
701
|
+
|
|
702
|
+
// 4. 构建缓存
|
|
703
|
+
if is_label_cached {
|
|
704
|
+
graph1.build_compare_cache(&graph2);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 5. 执行 bounded simulation
|
|
708
|
+
let sim = graph1.get_bounded_simulation(&graph2);
|
|
709
|
+
|
|
710
|
+
// 6. 转换结果为 Python 对象
|
|
711
|
+
Python::attach(|py| {
|
|
712
|
+
let map = PyDict::new(py);
|
|
713
|
+
|
|
714
|
+
for (node, set) in sim.iter() {
|
|
715
|
+
let py_set = PySet::new(py, set.iter().map(|node| to_nx_node(py, node)).collect::<PyResult<Vec<_>>>()?)?;
|
|
716
|
+
map.set_item(to_nx_node(py, node)?, py_set)?;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
Ok(map.into())
|
|
720
|
+
})
|
|
721
|
+
}
|
|
722
|
+
|
|
619
723
|
// 模块定义
|
|
620
724
|
// #[pymodule]
|
|
621
725
|
// pub fn networkx_graph(_py: Python, m: &PyModule) -> PyResult<()> {
|
|
@@ -21,5 +21,6 @@ fn simulation(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
21
21
|
m.add_function(wrap_pyfunction!(graph::networkx_graph::is_simulation_isomorphic_fn, m)?)?;
|
|
22
22
|
m.add_function(wrap_pyfunction!(graph::networkx_graph::is_simulation_isomorphic_of_edge_fn, m)?)?;
|
|
23
23
|
m.add_function(wrap_pyfunction!(graph::networkx_graph::is_simulation_isomorphic_of_node_edge_fn, m)?)?;
|
|
24
|
+
m.add_function(wrap_pyfunction!(graph::networkx_graph::get_bounded_simulation, m)?)?;
|
|
24
25
|
Ok(())
|
|
25
26
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test script for bounded simulation functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import networkx as nx
|
|
7
|
+
import sys
|
|
8
|
+
sys.path.insert(0, '/home/vincent/simulation/target/release')
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import simulation
|
|
12
|
+
print("✓ Successfully imported simulation module")
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
print(f"✗ Failed to import simulation module: {e}")
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
def test_bounded_simulation():
|
|
18
|
+
"""Test the get_bounded_simulation function."""
|
|
19
|
+
|
|
20
|
+
# Create two simple graphs
|
|
21
|
+
graph1 = nx.DiGraph()
|
|
22
|
+
graph1.add_node(0, label='A')
|
|
23
|
+
graph1.add_node(1, label='B')
|
|
24
|
+
graph1.add_edge(0, 1)
|
|
25
|
+
|
|
26
|
+
graph2 = nx.DiGraph()
|
|
27
|
+
graph2.add_node(0, label='A')
|
|
28
|
+
graph2.add_node(1, label='B')
|
|
29
|
+
graph2.add_node(2, label='B')
|
|
30
|
+
graph2.add_edge(0, 1)
|
|
31
|
+
graph2.add_edge(0, 2)
|
|
32
|
+
|
|
33
|
+
# Define the compare function (checks if labels are the same)
|
|
34
|
+
def compare_labels(attr1, attr2):
|
|
35
|
+
return attr1.get('label') == attr2.get('label')
|
|
36
|
+
|
|
37
|
+
# Define the bound function (returns a fixed bound value for each node)
|
|
38
|
+
def get_bound_value(attr):
|
|
39
|
+
return 2 # bound value = 2 for all nodes
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
result = simulation.get_bounded_simulation(
|
|
43
|
+
graph1,
|
|
44
|
+
graph2,
|
|
45
|
+
compare_labels,
|
|
46
|
+
get_bound_value,
|
|
47
|
+
is_label_cached=False
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
print("✓ get_bounded_simulation executed successfully")
|
|
51
|
+
print(f" Result type: {type(result)}")
|
|
52
|
+
|
|
53
|
+
# Check if result is a dictionary
|
|
54
|
+
if isinstance(result, dict):
|
|
55
|
+
print("✓ Result is a dictionary")
|
|
56
|
+
print(f" Result size: {len(result)} entries")
|
|
57
|
+
for i, (node, sim_set) in enumerate(result.items()):
|
|
58
|
+
if i < 5: # Print only first 5 entries
|
|
59
|
+
print(f" Node: {node}, Simulated nodes: {list(sim_set) if hasattr(sim_set, '__iter__') else sim_set}")
|
|
60
|
+
else:
|
|
61
|
+
print(f"✗ Expected dict, got {type(result)}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"✗ Error calling get_bounded_simulation: {e}")
|
|
68
|
+
import traceback
|
|
69
|
+
traceback.print_exc()
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
print("Testing bounded simulation functionality...")
|
|
75
|
+
print("=" * 50)
|
|
76
|
+
|
|
77
|
+
success = test_bounded_simulation()
|
|
78
|
+
|
|
79
|
+
print("=" * 50)
|
|
80
|
+
if success:
|
|
81
|
+
print("✓ All tests passed!")
|
|
82
|
+
else:
|
|
83
|
+
print("✗ Some tests failed")
|
|
84
|
+
sys.exit(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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|