Skip to content

Commit c9bfce5

Browse files
authored
Merge pull request #90 from StevenBtw/dev
Release 0.6.2
2 parents dc75dd9 + 407521c commit c9bfce5

24 files changed

Lines changed: 185 additions & 283 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
What broke, what got fixed, and what's new.
44

5+
## [0.6.2] 2026-04-07
6+
7+
### Fixed
8+
9+
- **Rust:** Dijkstra, BFS, DFS, and Bellman-Ford now raise `ValueError` on out-of-bounds source/target instead of crashing the process.
10+
- **Rust:** Floyd-Warshall callback errors are now propagated to Python instead of being silently swallowed.
11+
- **Rust:** `PyList` construction in bindings uses proper error propagation instead of `unwrap()`.
12+
- **Rust:** Dijkstra and Kruskal use `total_cmp` for deterministic NaN handling in edge weights.
13+
- **Rust:** PageRank validates `damping`, `max_iter`, and `tol` parameters at the binding boundary.
14+
15+
### Changed
16+
17+
- **Rust:** Removed `hashbrown` and `ahash` dependencies from `Cargo.toml`.
18+
519
## [0.6.1] - 2026-02-01
620

721
### Fixed

pyproject.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ PyPI = "https://pypi.org/project/solvOR/"
2828

2929
[project.optional-dependencies]
3030
dev = [
31-
"pytest>=9.0.2",
32-
"pytest-cov>=7.0.0",
33-
"ruff>=0.14.14",
31+
"pytest>=9.0",
32+
"pytest-cov>=7",
33+
"ruff>=0.15",
3434
"pre-commit>=4.5.1",
35-
"ty>=0.0.14",
35+
"ty>=0.0.29",
3636
"maturin>=1.7,<2.0",
37-
"prek>=0.3.1"
37+
"prek>=0.3"
3838
]
3939
docs = [
4040
"mkdocs>=1.6",
41-
"mkdocs-material>=9.5",
42-
"mkdocstrings[python]>=1.0.2",
41+
"mkdocs-material>=9",
42+
"mkdocstrings[python]>=1.0",
4343
"mkdocs-git-revision-date-localized-plugin>=1.5.1",
4444
]
4545

rust/Cargo.lock

Lines changed: 0 additions & 109 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ crate-type = ["cdylib"]
1111

1212
[dependencies]
1313
pyo3 = { version = "0.27", features = ["extension-module"] }
14-
hashbrown = "0.16.1"
15-
ahash = "0.8"
1614

1715
[profile.release]
1816
lto = "thin"

rust/src/algorithms/dijkstra.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@ impl Eq for State {}
1414

1515
impl Ord for State {
1616
fn cmp(&self, other: &Self) -> Ordering {
17-
// Flip ordering for min-heap
18-
other
19-
.cost
20-
.partial_cmp(&self.cost)
21-
.unwrap_or(Ordering::Equal)
17+
// Flip ordering for min-heap; total_cmp handles NaN deterministically
18+
other.cost.total_cmp(&self.cost)
2219
}
2320
}
2421

rust/src/algorithms/floyd_warshall.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
//! Time complexity: O(n³)
44
//! Space complexity: O(n²)
55
6+
use pyo3::PyResult;
7+
68
use crate::callback::ProgressCallback;
7-
use crate::types::{AlgorithmResult, Progress, Status};
9+
use crate::types::Progress;
810

911
/// Edge representation: (from, to, weight)
1012
pub type Edge = (usize, usize, f64);
@@ -36,7 +38,7 @@ pub fn floyd_warshall(
3638
n_nodes: usize,
3739
edges: &[Edge],
3840
callback: &mut ProgressCallback,
39-
) -> FloydWarshallResult {
41+
) -> PyResult<FloydWarshallResult> {
4042
let n = n_nodes;
4143

4244
// Initialize distance matrix with infinity
@@ -63,14 +65,14 @@ pub fn floyd_warshall(
6365
for k in 0..n {
6466
// Report progress every n iterations (once per k)
6567
let progress = Progress::new(k, 0.0).with_evaluations(iterations);
66-
if callback.report(&progress).unwrap_or(false) {
68+
if callback.report(&progress)? {
6769
// Early termination requested
68-
return FloydWarshallResult {
70+
return Ok(FloydWarshallResult {
6971
distances: dist,
7072
predecessors: pred,
7173
has_negative_cycle: false,
7274
iterations,
73-
};
75+
});
7476
}
7577

7678
for i in 0..n {
@@ -93,12 +95,12 @@ pub fn floyd_warshall(
9395
}
9496
}
9597

96-
FloydWarshallResult {
98+
Ok(FloydWarshallResult {
9799
distances: dist,
98100
predecessors: pred,
99101
has_negative_cycle,
100102
iterations,
101-
}
103+
})
102104
}
103105

104106
/// Reconstruct path from i to j using predecessor matrix.
@@ -148,7 +150,7 @@ mod tests {
148150
#[test]
149151
fn test_simple_graph() {
150152
let edges = vec![(0, 1, 1.0), (1, 2, 2.0), (0, 2, 5.0)];
151-
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none());
153+
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none()).unwrap();
152154

153155
assert!((result.distances[0][2] - 3.0).abs() < 1e-9);
154156
assert!(!result.has_negative_cycle);
@@ -157,7 +159,7 @@ mod tests {
157159
#[test]
158160
fn test_no_path() {
159161
let edges = vec![(0, 1, 1.0)];
160-
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none());
162+
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none()).unwrap();
161163

162164
assert!(result.distances[1][0].is_infinite());
163165
assert!(result.distances[2][0].is_infinite());
@@ -166,15 +168,15 @@ mod tests {
166168
#[test]
167169
fn test_negative_cycle() {
168170
let edges = vec![(0, 1, 1.0), (1, 2, -1.0), (2, 0, -1.0)];
169-
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none());
171+
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none()).unwrap();
170172

171173
assert!(result.has_negative_cycle);
172174
}
173175

174176
#[test]
175177
fn test_path_reconstruction() {
176178
let edges = vec![(0, 1, 1.0), (1, 2, 2.0), (0, 2, 5.0)];
177-
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none());
179+
let result = floyd_warshall(3, &edges, &mut ProgressCallback::none()).unwrap();
178180

179181
let path = reconstruct_path(&result.predecessors, &result.distances, 0, 2);
180182
assert_eq!(path, Some(vec![0, 1, 2]));

rust/src/algorithms/kruskal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub fn kruskal(n_nodes: usize, edges: &[(usize, usize, f64)]) -> KruskalResult {
7676

7777
// Sort edges by weight
7878
let mut sorted_edges: Vec<(usize, usize, f64)> = edges.to_vec();
79-
sorted_edges.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
79+
sorted_edges.sort_by(|a, b| a.2.total_cmp(&b.2));
8080

8181
let mut uf = UnionFind::new(n_nodes);
8282
let mut mst_edges = Vec::with_capacity(n_nodes - 1);

rust/src/algorithms/pagerank.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub fn pagerank(
5555
let mut new_scores = vec![0.0; n_nodes];
5656

5757
let base = (1.0 - damping) / n_nodes as f64;
58-
let mut converged = false;
58+
let converged = false;
5959

6060
for iteration in 0..max_iter {
6161
// Compute new scores

rust/src/bindings/centrality.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! PyO3 bindings for centrality algorithms.
22
3+
use pyo3::exceptions::PyValueError;
34
use pyo3::prelude::*;
45
use pyo3::types::{PyDict, PyList};
56

@@ -27,6 +28,18 @@ pub fn pagerank(
2728
max_iter: usize,
2829
tol: f64,
2930
) -> PyResult<Py<PyDict>> {
31+
if !(0.0..1.0).contains(&damping) {
32+
return Err(PyValueError::new_err(format!(
33+
"damping {damping} must be in [0, 1)"
34+
)));
35+
}
36+
if max_iter == 0 {
37+
return Err(PyValueError::new_err("max_iter must be positive"));
38+
}
39+
if tol <= 0.0 {
40+
return Err(PyValueError::new_err("tol must be positive"));
41+
}
42+
3043
let result = py.detach(|| pr::pagerank(n_nodes, &edges, damping, max_iter, tol));
3144

3245
let dict = PyDict::new(py);

rust/src/bindings/components.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@ pub fn strongly_connected_components(
2626
let dict = PyDict::new(py);
2727

2828
// Convert components to list of lists
29-
let py_components = PyList::new(
30-
py,
31-
result
32-
.components
33-
.iter()
34-
.map(|comp| PyList::new(py, comp.iter()).unwrap()),
35-
)?;
29+
let inner_lists = result
30+
.components
31+
.iter()
32+
.map(|comp| PyList::new(py, comp.iter()))
33+
.collect::<PyResult<Vec<_>>>()?;
34+
let py_components = PyList::new(py, inner_lists)?;
3635
dict.set_item("components", py_components)?;
3736
dict.set_item("n_components", result.n_components)?;
3837
dict.set_item("status", Status::Optimal.as_i32())?;

0 commit comments

Comments
 (0)