rxgraph 0.6.0__tar.gz → 0.7.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.
- {rxgraph-0.6.0 → rxgraph-0.7.0}/Cargo.lock +5 -16
- {rxgraph-0.6.0 → rxgraph-0.7.0}/Cargo.toml +1 -1
- {rxgraph-0.6.0 → rxgraph-0.7.0}/PKG-INFO +12 -2
- {rxgraph-0.6.0 → rxgraph-0.7.0}/README.md +11 -1
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/Cargo.toml +12 -1
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/README.md +5 -5
- rxgraph-0.7.0/crates/rxgraph/examples/native_row_lazy.rs +378 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/lib.rs +6 -1
- rxgraph-0.6.0/crates/rxgraph-py/src/lib.rs → rxgraph-0.7.0/crates/rxgraph/src/python.rs +9 -9
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/mod.rs +2 -0
- rxgraph-0.7.0/crates/rxgraph/src/traversal/native.rs +1126 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph-python/Cargo.toml +2 -2
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph-python/src/lib.rs +3 -3
- rxgraph-0.6.0/crates/rxgraph-py/Cargo.toml +0 -23
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/benches/flight_routes.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/benches/memory.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/benches/payment_risk.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/examples/flight_routes.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/arrow.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/arrow_value.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/bind.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/eval.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/expr.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/mod.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/list.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/mod.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/scalar.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/string.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/ops/struct_.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/polars_json.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/dsl/value.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/examples/kernels.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/examples/mod.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/csr.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/graph.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/mod.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/graph/repo.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/algo.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/config.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/kernel.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/progress.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/crates/rxgraph/src/traversal/registry.rs +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/pyproject.toml +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/__init__.py +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/__init__.pyi +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/_graph_tables.py +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/plugin.py +0 -0
- {rxgraph-0.6.0 → rxgraph-0.7.0}/python/rxgraph/py.typed +0 -0
|
@@ -1302,7 +1302,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
|
1302
1302
|
|
|
1303
1303
|
[[package]]
|
|
1304
1304
|
name = "rxgraph"
|
|
1305
|
-
version = "0.
|
|
1305
|
+
version = "0.7.0"
|
|
1306
1306
|
dependencies = [
|
|
1307
1307
|
"anyhow",
|
|
1308
1308
|
"arrow",
|
|
@@ -1310,6 +1310,8 @@ dependencies = [
|
|
|
1310
1310
|
"criterion",
|
|
1311
1311
|
"inventory",
|
|
1312
1312
|
"pretty_assertions",
|
|
1313
|
+
"pyo3",
|
|
1314
|
+
"pyo3-arrow",
|
|
1313
1315
|
"rand",
|
|
1314
1316
|
"rayon",
|
|
1315
1317
|
"serde",
|
|
@@ -1319,25 +1321,12 @@ dependencies = [
|
|
|
1319
1321
|
"stats_alloc",
|
|
1320
1322
|
]
|
|
1321
1323
|
|
|
1322
|
-
[[package]]
|
|
1323
|
-
name = "rxgraph-py"
|
|
1324
|
-
version = "0.5.0"
|
|
1325
|
-
dependencies = [
|
|
1326
|
-
"anyhow",
|
|
1327
|
-
"arrow",
|
|
1328
|
-
"pyo3",
|
|
1329
|
-
"pyo3-arrow",
|
|
1330
|
-
"rayon",
|
|
1331
|
-
"rxgraph",
|
|
1332
|
-
"serde_json",
|
|
1333
|
-
]
|
|
1334
|
-
|
|
1335
1324
|
[[package]]
|
|
1336
1325
|
name = "rxgraph-python"
|
|
1337
|
-
version = "0.
|
|
1326
|
+
version = "0.7.0"
|
|
1338
1327
|
dependencies = [
|
|
1339
1328
|
"pyo3",
|
|
1340
|
-
"rxgraph
|
|
1329
|
+
"rxgraph",
|
|
1341
1330
|
]
|
|
1342
1331
|
|
|
1343
1332
|
[[package]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rxgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.11
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -185,10 +185,20 @@ result = graph.search(
|
|
|
185
185
|
)
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
The consumer's crate depends on `rxgraph` with the `python` feature (their own
|
|
189
|
+
crate is the `cdylib` that needs pyo3's `extension-module`; rxgraph's `python`
|
|
190
|
+
feature deliberately does not enable it):
|
|
191
|
+
|
|
192
|
+
```toml
|
|
193
|
+
[dependencies]
|
|
194
|
+
rxgraph = { version = "0.6", features = ["python"] }
|
|
195
|
+
pyo3 = { version = "0.28", features = ["extension-module"] }
|
|
196
|
+
```
|
|
197
|
+
|
|
188
198
|
The Rust side implements `rxgraph::Kernel` and ends with a small macro call:
|
|
189
199
|
|
|
190
200
|
```rust
|
|
191
|
-
|
|
201
|
+
rxgraph::plugin! {
|
|
192
202
|
module = _native;
|
|
193
203
|
"hop_budget" => HopBudget::from_params,
|
|
194
204
|
}
|
|
@@ -171,10 +171,20 @@ result = graph.search(
|
|
|
171
171
|
)
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
The consumer's crate depends on `rxgraph` with the `python` feature (their own
|
|
175
|
+
crate is the `cdylib` that needs pyo3's `extension-module`; rxgraph's `python`
|
|
176
|
+
feature deliberately does not enable it):
|
|
177
|
+
|
|
178
|
+
```toml
|
|
179
|
+
[dependencies]
|
|
180
|
+
rxgraph = { version = "0.6", features = ["python"] }
|
|
181
|
+
pyo3 = { version = "0.28", features = ["extension-module"] }
|
|
182
|
+
```
|
|
183
|
+
|
|
174
184
|
The Rust side implements `rxgraph::Kernel` and ends with a small macro call:
|
|
175
185
|
|
|
176
186
|
```rust
|
|
177
|
-
|
|
187
|
+
rxgraph::plugin! {
|
|
178
188
|
module = _native;
|
|
179
189
|
"hop_budget" => HopBudget::from_params,
|
|
180
190
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rxgraph"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
edition.workspace = true
|
|
5
5
|
rust-version.workspace = true
|
|
6
6
|
description = "High-performance graph traversal engine"
|
|
@@ -22,6 +22,11 @@ anyhow.workspace = true
|
|
|
22
22
|
arrow.workspace = true
|
|
23
23
|
arrow-schema.workspace = true
|
|
24
24
|
inventory.workspace = true
|
|
25
|
+
# Optional: only pulled in by the `python` feature for the PyO3 binding layer.
|
|
26
|
+
# `extension-module` is intentionally NOT enabled here; the final extension
|
|
27
|
+
# crate (cdylib) enables it, so this stays linkable/testable as an rlib.
|
|
28
|
+
pyo3 = { workspace = true, optional = true }
|
|
29
|
+
pyo3-arrow = { workspace = true, optional = true }
|
|
25
30
|
rand.workspace = true
|
|
26
31
|
rayon.workspace = true
|
|
27
32
|
serde.workspace = true
|
|
@@ -30,6 +35,12 @@ smallvec.workspace = true
|
|
|
30
35
|
sprs.workspace = true
|
|
31
36
|
stats_alloc.workspace = true
|
|
32
37
|
|
|
38
|
+
[features]
|
|
39
|
+
# Exposes the PyO3 binding layer (`register`, the `plugin!` macro, and pyclasses)
|
|
40
|
+
# so downstream consumers can build native kernel plugins against the published
|
|
41
|
+
# crate without an unpublished path-dependency.
|
|
42
|
+
python = ["dep:pyo3", "dep:pyo3-arrow"]
|
|
43
|
+
|
|
33
44
|
[dev-dependencies]
|
|
34
45
|
criterion.workspace = true
|
|
35
46
|
pretty_assertions.workspace = true
|
|
@@ -21,7 +21,7 @@ evaluates stateful traversal kernels without copying user columns out of Arrow.
|
|
|
21
21
|
|
|
22
22
|
```toml
|
|
23
23
|
[dependencies]
|
|
24
|
-
rxgraph = "0.
|
|
24
|
+
rxgraph = "0.6"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Data Model
|
|
@@ -121,8 +121,8 @@ parallelism and optional per-node intermediate state materialization.
|
|
|
121
121
|
|
|
122
122
|
## Python Bindings
|
|
123
123
|
|
|
124
|
-
Python bindings are published
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
124
|
+
Python bindings are published as the `rxgraph` package on PyPI. The same PyO3
|
|
125
|
+
binding layer is available from this crate via the `python` feature; downstream
|
|
126
|
+
native-kernel plugins enable `features = ["python"]` and use the
|
|
127
|
+
`rxgraph::plugin!` macro to register kernels and build a custom extension; see
|
|
128
128
|
[`examples/rust-kernel-plugin/`](../../examples/rust-kernel-plugin/README.md).
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
cell::RefCell,
|
|
3
|
+
collections::{BTreeMap, BTreeSet},
|
|
4
|
+
sync::OnceLock,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use anyhow::{Context, Result};
|
|
8
|
+
use rxgraph::{
|
|
9
|
+
GraphId, NodeId, RunOptions, TraversalStrategy, search_native,
|
|
10
|
+
traversal::native::{self, GraphStore, OutgoingEdge},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
fn main() -> Result<()> {
|
|
14
|
+
let store = FraudStore::sample();
|
|
15
|
+
let result = run_search(&store, false)?;
|
|
16
|
+
|
|
17
|
+
println!(
|
|
18
|
+
"paths={} evaluated_edges={} loaded_nodes={:?} loaded_edges={:?} loaded_outgoing={:?}",
|
|
19
|
+
result.paths.len(),
|
|
20
|
+
result.stats.evaluated_edges,
|
|
21
|
+
store.loaded_nodes(),
|
|
22
|
+
store.loaded_edges(),
|
|
23
|
+
store.loaded_outgoing(),
|
|
24
|
+
);
|
|
25
|
+
for path in result.paths {
|
|
26
|
+
let labels = path
|
|
27
|
+
.nodes
|
|
28
|
+
.iter()
|
|
29
|
+
.map(|node| node.payload.label)
|
|
30
|
+
.collect::<Vec<_>>();
|
|
31
|
+
println!(
|
|
32
|
+
"path labels={labels:?} risk={} checkpoints={:?}",
|
|
33
|
+
path.state.total_risk, path.state.checkpoints
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Ok(())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn run_search(
|
|
41
|
+
store: &FraudStore,
|
|
42
|
+
intermediate_states: bool,
|
|
43
|
+
) -> Result<native::SearchResult<'_, Account, Transfer, RiskState>> {
|
|
44
|
+
search_native(
|
|
45
|
+
store,
|
|
46
|
+
RiskKernel {
|
|
47
|
+
max_risk: 7,
|
|
48
|
+
require_checkpoint: true,
|
|
49
|
+
},
|
|
50
|
+
RunOptions {
|
|
51
|
+
start_nodes: vec![0_u64.into()],
|
|
52
|
+
strategy: TraversalStrategy::BreadthFirst,
|
|
53
|
+
intermediate_states,
|
|
54
|
+
..RunOptions::default()
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
60
|
+
struct Account {
|
|
61
|
+
label: &'static str,
|
|
62
|
+
target: bool,
|
|
63
|
+
blocked: bool,
|
|
64
|
+
checkpoint: bool,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
68
|
+
struct Transfer {
|
|
69
|
+
risk: u64,
|
|
70
|
+
allowed: bool,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[derive(Clone, Debug)]
|
|
74
|
+
struct TransferRow {
|
|
75
|
+
src: NodeId,
|
|
76
|
+
dest: NodeId,
|
|
77
|
+
transfer: Transfer,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
81
|
+
struct RiskState {
|
|
82
|
+
total_risk: u64,
|
|
83
|
+
visited: BTreeSet<NodeId>,
|
|
84
|
+
checkpoints: BTreeMap<NodeId, &'static str>,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[derive(Clone, Debug)]
|
|
88
|
+
struct RiskKernel {
|
|
89
|
+
max_risk: u64,
|
|
90
|
+
require_checkpoint: bool,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl native::Kernel for RiskKernel {
|
|
94
|
+
type Node = Account;
|
|
95
|
+
type Edge = Transfer;
|
|
96
|
+
type State = RiskState;
|
|
97
|
+
|
|
98
|
+
fn initial_state(
|
|
99
|
+
&self,
|
|
100
|
+
cx: &native::StartCtx<'_, Self::Node, Self::Edge>,
|
|
101
|
+
) -> Result<Self::State> {
|
|
102
|
+
let account = cx.node()?;
|
|
103
|
+
let mut visited = BTreeSet::new();
|
|
104
|
+
visited.insert(cx.id());
|
|
105
|
+
let mut checkpoints = BTreeMap::new();
|
|
106
|
+
if account.checkpoint {
|
|
107
|
+
checkpoints.insert(cx.id(), account.label);
|
|
108
|
+
}
|
|
109
|
+
Ok(RiskState {
|
|
110
|
+
total_risk: 0,
|
|
111
|
+
visited,
|
|
112
|
+
checkpoints,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn visit(&self, cx: &native::EdgeCtx<'_, Self::Node, Self::Edge, Self::State>) -> Result<bool> {
|
|
117
|
+
let transfer = cx.edge()?;
|
|
118
|
+
let dest = cx.dest()?;
|
|
119
|
+
Ok(transfer.allowed
|
|
120
|
+
&& !dest.blocked
|
|
121
|
+
&& cx.state().total_risk.saturating_add(transfer.risk) <= self.max_risk)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn next_state(
|
|
125
|
+
&self,
|
|
126
|
+
cx: &native::EdgeCtx<'_, Self::Node, Self::Edge, Self::State>,
|
|
127
|
+
) -> Result<Self::State> {
|
|
128
|
+
let transfer = cx.edge()?;
|
|
129
|
+
let dest = cx.dest()?;
|
|
130
|
+
let mut next = cx.state().clone();
|
|
131
|
+
next.total_risk += transfer.risk;
|
|
132
|
+
next.visited.insert(cx.dest_id());
|
|
133
|
+
if dest.checkpoint {
|
|
134
|
+
next.checkpoints.insert(cx.dest_id(), dest.label);
|
|
135
|
+
}
|
|
136
|
+
Ok(next)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn stop(&self, cx: &native::EdgeCtx<'_, Self::Node, Self::Edge, Self::State>) -> Result<bool> {
|
|
140
|
+
Ok(cx.dest()?.target && (!self.require_checkpoint || !cx.state().checkpoints.is_empty()))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
struct FraudStore {
|
|
145
|
+
accounts: Vec<Account>,
|
|
146
|
+
transfers: Vec<TransferRow>,
|
|
147
|
+
outgoing_index: Vec<Vec<usize>>,
|
|
148
|
+
account_cache: Vec<OnceLock<Account>>,
|
|
149
|
+
transfer_cache: Vec<OnceLock<Transfer>>,
|
|
150
|
+
outgoing_cache: Vec<OnceLock<Vec<OutgoingEdge>>>,
|
|
151
|
+
account_loads: RefCell<BTreeSet<NodeId>>,
|
|
152
|
+
transfer_loads: RefCell<BTreeSet<u32>>,
|
|
153
|
+
outgoing_loads: RefCell<BTreeSet<NodeId>>,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
impl FraudStore {
|
|
157
|
+
fn sample() -> Self {
|
|
158
|
+
let accounts = vec![
|
|
159
|
+
Account {
|
|
160
|
+
label: "origin",
|
|
161
|
+
target: false,
|
|
162
|
+
blocked: false,
|
|
163
|
+
checkpoint: false,
|
|
164
|
+
},
|
|
165
|
+
Account {
|
|
166
|
+
label: "merchant",
|
|
167
|
+
target: false,
|
|
168
|
+
blocked: false,
|
|
169
|
+
checkpoint: true,
|
|
170
|
+
},
|
|
171
|
+
Account {
|
|
172
|
+
label: "sink",
|
|
173
|
+
target: true,
|
|
174
|
+
blocked: false,
|
|
175
|
+
checkpoint: false,
|
|
176
|
+
},
|
|
177
|
+
Account {
|
|
178
|
+
label: "cold-wallet",
|
|
179
|
+
target: false,
|
|
180
|
+
blocked: false,
|
|
181
|
+
checkpoint: false,
|
|
182
|
+
},
|
|
183
|
+
Account {
|
|
184
|
+
label: "blocked",
|
|
185
|
+
target: true,
|
|
186
|
+
blocked: true,
|
|
187
|
+
checkpoint: false,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
let transfers = vec![
|
|
191
|
+
TransferRow {
|
|
192
|
+
src: 0,
|
|
193
|
+
dest: 1,
|
|
194
|
+
transfer: Transfer {
|
|
195
|
+
risk: 2,
|
|
196
|
+
allowed: true,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
TransferRow {
|
|
200
|
+
src: 1,
|
|
201
|
+
dest: 2,
|
|
202
|
+
transfer: Transfer {
|
|
203
|
+
risk: 4,
|
|
204
|
+
allowed: true,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
TransferRow {
|
|
208
|
+
src: 1,
|
|
209
|
+
dest: 4,
|
|
210
|
+
transfer: Transfer {
|
|
211
|
+
risk: 1,
|
|
212
|
+
allowed: true,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
TransferRow {
|
|
216
|
+
src: 3,
|
|
217
|
+
dest: 2,
|
|
218
|
+
transfer: Transfer {
|
|
219
|
+
risk: 1,
|
|
220
|
+
allowed: true,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
Self::new(accounts, transfers)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn new(accounts: Vec<Account>, transfers: Vec<TransferRow>) -> Self {
|
|
228
|
+
let mut outgoing_index = vec![Vec::new(); accounts.len()];
|
|
229
|
+
for (edge, transfer) in transfers.iter().enumerate() {
|
|
230
|
+
outgoing_index[transfer.src as usize].push(edge);
|
|
231
|
+
}
|
|
232
|
+
let account_cache = (0..accounts.len()).map(|_| OnceLock::new()).collect();
|
|
233
|
+
let transfer_cache = (0..transfers.len()).map(|_| OnceLock::new()).collect();
|
|
234
|
+
let outgoing_cache = (0..accounts.len()).map(|_| OnceLock::new()).collect();
|
|
235
|
+
Self {
|
|
236
|
+
accounts,
|
|
237
|
+
transfers,
|
|
238
|
+
outgoing_index,
|
|
239
|
+
account_cache,
|
|
240
|
+
transfer_cache,
|
|
241
|
+
outgoing_cache,
|
|
242
|
+
account_loads: RefCell::new(BTreeSet::new()),
|
|
243
|
+
transfer_loads: RefCell::new(BTreeSet::new()),
|
|
244
|
+
outgoing_loads: RefCell::new(BTreeSet::new()),
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn loaded_nodes(&self) -> BTreeSet<NodeId> {
|
|
249
|
+
self.account_loads.borrow().clone()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn loaded_edges(&self) -> BTreeSet<u32> {
|
|
253
|
+
self.transfer_loads.borrow().clone()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fn loaded_outgoing(&self) -> BTreeSet<NodeId> {
|
|
257
|
+
self.outgoing_loads.borrow().clone()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
impl GraphStore for FraudStore {
|
|
262
|
+
type Node = Account;
|
|
263
|
+
type Edge = Transfer;
|
|
264
|
+
|
|
265
|
+
fn resolve_node(&self, external: GraphId<'_>) -> Result<Option<NodeId>> {
|
|
266
|
+
Ok(match external {
|
|
267
|
+
GraphId::U64(value) if (value as usize) < self.accounts.len() => Some(value as NodeId),
|
|
268
|
+
_ => None,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn external_node(&self, internal: NodeId) -> Result<Option<GraphId<'_>>> {
|
|
273
|
+
Ok(((internal as usize) < self.accounts.len()).then_some(GraphId::U64(internal as u64)))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fn external_edge(&self, internal: rxgraph::EdgeId) -> Result<Option<GraphId<'_>>> {
|
|
277
|
+
Ok(((internal as usize) < self.transfers.len()).then_some(GraphId::U64(internal as u64)))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn outgoing(&self, src: NodeId) -> Result<&[OutgoingEdge]> {
|
|
281
|
+
let src_index = src as usize;
|
|
282
|
+
let edge_ids = self
|
|
283
|
+
.outgoing_index
|
|
284
|
+
.get(src_index)
|
|
285
|
+
.with_context(|| format!("node row {src} is out of range"))?;
|
|
286
|
+
let outgoing = self
|
|
287
|
+
.outgoing_cache
|
|
288
|
+
.get(src_index)
|
|
289
|
+
.context("outgoing cache row is missing")?
|
|
290
|
+
.get_or_init(|| {
|
|
291
|
+
self.outgoing_loads.borrow_mut().insert(src);
|
|
292
|
+
edge_ids
|
|
293
|
+
.iter()
|
|
294
|
+
.map(|&edge| OutgoingEdge {
|
|
295
|
+
edge: edge as rxgraph::EdgeId,
|
|
296
|
+
dest: self.transfers[edge].dest,
|
|
297
|
+
})
|
|
298
|
+
.collect()
|
|
299
|
+
});
|
|
300
|
+
Ok(outgoing)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn node(&self, id: NodeId) -> Result<&Self::Node> {
|
|
304
|
+
let index = id as usize;
|
|
305
|
+
let account = self
|
|
306
|
+
.accounts
|
|
307
|
+
.get(index)
|
|
308
|
+
.with_context(|| format!("node row {id} is out of range"))?;
|
|
309
|
+
Ok(self
|
|
310
|
+
.account_cache
|
|
311
|
+
.get(index)
|
|
312
|
+
.context("account cache row is missing")?
|
|
313
|
+
.get_or_init(|| {
|
|
314
|
+
self.account_loads.borrow_mut().insert(id);
|
|
315
|
+
account.clone()
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn edge(&self, id: rxgraph::EdgeId) -> Result<&Self::Edge> {
|
|
320
|
+
let index = id as usize;
|
|
321
|
+
let transfer = self
|
|
322
|
+
.transfers
|
|
323
|
+
.get(index)
|
|
324
|
+
.with_context(|| format!("edge row {id} is out of range"))?;
|
|
325
|
+
Ok(self
|
|
326
|
+
.transfer_cache
|
|
327
|
+
.get(index)
|
|
328
|
+
.context("transfer cache row is missing")?
|
|
329
|
+
.get_or_init(|| {
|
|
330
|
+
self.transfer_loads.borrow_mut().insert(id);
|
|
331
|
+
transfer.transfer.clone()
|
|
332
|
+
}))
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[cfg(test)]
|
|
337
|
+
mod tests {
|
|
338
|
+
use pretty_assertions::assert_eq;
|
|
339
|
+
|
|
340
|
+
use super::*;
|
|
341
|
+
|
|
342
|
+
#[test]
|
|
343
|
+
fn native_example_returns_native_state_and_payloads() {
|
|
344
|
+
let store = FraudStore::sample();
|
|
345
|
+
let result = run_search(&store, true).unwrap();
|
|
346
|
+
|
|
347
|
+
assert_eq!(result.paths.len(), 1);
|
|
348
|
+
let path = &result.paths[0];
|
|
349
|
+
assert_eq!(
|
|
350
|
+
path.nodes
|
|
351
|
+
.iter()
|
|
352
|
+
.map(|node| node.payload.label)
|
|
353
|
+
.collect::<Vec<_>>(),
|
|
354
|
+
vec!["origin", "merchant", "sink"]
|
|
355
|
+
);
|
|
356
|
+
assert_eq!(path.state.total_risk, 6);
|
|
357
|
+
assert_eq!(path.state.visited, BTreeSet::from([0, 1, 2]));
|
|
358
|
+
assert_eq!(path.state.checkpoints, BTreeMap::from([(1, "merchant")]));
|
|
359
|
+
assert_eq!(
|
|
360
|
+
path.nodes
|
|
361
|
+
.iter()
|
|
362
|
+
.map(|node| node.state.as_ref().unwrap().total_risk)
|
|
363
|
+
.collect::<Vec<_>>(),
|
|
364
|
+
vec![0, 2, 6]
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#[test]
|
|
369
|
+
fn native_example_loads_only_reached_rows() {
|
|
370
|
+
let store = FraudStore::sample();
|
|
371
|
+
let result = run_search(&store, false).unwrap();
|
|
372
|
+
|
|
373
|
+
assert_eq!(result.stats.evaluated_edges, 3);
|
|
374
|
+
assert_eq!(store.loaded_outgoing(), BTreeSet::from([0, 1]));
|
|
375
|
+
assert_eq!(store.loaded_nodes(), BTreeSet::from([0, 1, 2, 4]));
|
|
376
|
+
assert_eq!(store.loaded_edges(), BTreeSet::from([0, 1, 2]));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -28,14 +28,19 @@ mod arrow;
|
|
|
28
28
|
pub mod dsl;
|
|
29
29
|
pub mod examples;
|
|
30
30
|
pub mod graph;
|
|
31
|
+
#[cfg(feature = "python")]
|
|
32
|
+
mod python;
|
|
31
33
|
pub mod traversal;
|
|
32
34
|
|
|
35
|
+
#[cfg(feature = "python")]
|
|
36
|
+
pub use python::register;
|
|
37
|
+
|
|
33
38
|
pub use dsl::{DslExpr, DslKernel, Scalar, StateRow, Value};
|
|
34
39
|
pub use graph::{EdgeId, Graph, GraphId, GraphRepo, NodeId, OwnedGraphId};
|
|
35
40
|
pub use traversal::{
|
|
36
41
|
BoxedRun, EdgeCtx, GraphPath, Kernel, KernelEntry, RunKernel, RunOptions, SearchResult,
|
|
37
42
|
SearchStats, TraversalConfig, TraversalConfigBuilder, TraversalStrategy, boxed_run,
|
|
38
|
-
build_kernel, register_kernel,
|
|
43
|
+
build_kernel, register_kernel, search_native,
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
// Re-exported so plugin crates can register kernels without their own dependency.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
DslKernel, Graph, GraphId, OwnedGraphId, RunOptions, SearchResult, SearchStats, StateRow,
|
|
3
|
+
TraversalConfigBuilder, TraversalStrategy, Value, build_kernel,
|
|
4
|
+
};
|
|
1
5
|
use pyo3::{
|
|
2
6
|
Borrowed,
|
|
3
7
|
conversion::IntoPyObjectExt,
|
|
@@ -7,10 +11,6 @@ use pyo3::{
|
|
|
7
11
|
};
|
|
8
12
|
use pyo3_arrow::PyTable;
|
|
9
13
|
use rayon::ThreadPoolBuilder;
|
|
10
|
-
use rxgraph::{
|
|
11
|
-
DslKernel, Graph, GraphId, OwnedGraphId, RunOptions, SearchResult, SearchStats, StateRow,
|
|
12
|
-
TraversalConfigBuilder, TraversalStrategy, Value, build_kernel,
|
|
13
|
-
};
|
|
14
14
|
use std::thread;
|
|
15
15
|
|
|
16
16
|
/// Registers all rxgraph native classes and functions into a Python module.
|
|
@@ -37,7 +37,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
37
37
|
/// `rxgraph::Kernel`.
|
|
38
38
|
///
|
|
39
39
|
/// ```ignore
|
|
40
|
-
///
|
|
40
|
+
/// rxgraph::plugin! {
|
|
41
41
|
/// module = _native;
|
|
42
42
|
/// "hop_budget" => HopBudget::from_params,
|
|
43
43
|
/// }
|
|
@@ -49,10 +49,10 @@ macro_rules! plugin {
|
|
|
49
49
|
$($name:literal => $factory:expr),+ $(,)?
|
|
50
50
|
) => {
|
|
51
51
|
$(
|
|
52
|
-
::
|
|
53
|
-
::
|
|
52
|
+
$crate::inventory::submit! {
|
|
53
|
+
$crate::KernelEntry {
|
|
54
54
|
name: $name,
|
|
55
|
-
make: |params| Ok(::
|
|
55
|
+
make: |params| Ok($crate::boxed_run(($factory)(params)?)),
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
)+
|
|
@@ -466,7 +466,7 @@ struct PySearchPath {
|
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
impl PySearchPath {
|
|
469
|
-
fn from_path(_py: Python<'_>, path:
|
|
469
|
+
fn from_path(_py: Python<'_>, path: crate::GraphPath<'_>) -> PyResult<Self> {
|
|
470
470
|
Ok(Self {
|
|
471
471
|
nodes: path.nodes.into_iter().map(GraphId::into_owned).collect(),
|
|
472
472
|
edges: path.edges.into_iter().map(GraphId::into_owned).collect(),
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
mod algo;
|
|
22
22
|
mod config;
|
|
23
23
|
mod kernel;
|
|
24
|
+
pub mod native;
|
|
24
25
|
mod progress;
|
|
25
26
|
mod registry;
|
|
26
27
|
|
|
@@ -29,6 +30,7 @@ use crate::{dsl::StateRow, graph::GraphId};
|
|
|
29
30
|
pub use algo::RunOptions;
|
|
30
31
|
pub use config::{TraversalConfig, TraversalConfigBuilder, TraversalStrategy};
|
|
31
32
|
pub use kernel::{EdgeCtx, Kernel};
|
|
33
|
+
pub use native::search_native;
|
|
32
34
|
pub use registry::{
|
|
33
35
|
BoxedRun, KernelEntry, RunKernel, boxed_run, build_kernel, inventory, register_kernel,
|
|
34
36
|
};
|