From 3c40b212e2fc3ae268d1c29f552417e53fa1d4eb Mon Sep 17 00:00:00 2001 From: MinLee0210 Date: Sat, 21 Jun 2025 14:33:25 +0700 Subject: [PATCH 1/4] feat: add allow sparse matrix --- Cargo.lock | 130 +++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- src/lib.rs | 36 ++------------ src/matrix.rs | 79 ++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 34 deletions(-) create mode 100644 src/matrix.rs diff --git a/Cargo.lock b/Cargo.lock index dc1e9eb..039b7b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,24 +2,76 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "alga" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f823d037a7ec6ea2197046bafd4ae150e6bc36f9ca347404f46a46823fa84f2" +dependencies = [ + "approx", + "num-complex 0.2.4", + "num-traits", +] + [[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "fastlap" version = "0.1.0" dependencies = [ "numpy", "pyo3", + "sprs", ] [[package]] @@ -28,6 +80,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "indoc" version = "2.0.6" @@ -40,6 +98,12 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -66,7 +130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ "matrixmultiply", - "num-complex", + "num-complex 0.4.6", "num-integer", "num-traits", "portable-atomic", @@ -74,6 +138,16 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -99,6 +173,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -109,7 +194,7 @@ checksum = "29f1dee9aa8d3f6f8e8b9af3803006101bb3653866ef056d530d53ae68587191" dependencies = [ "libc", "ndarray", - "num-complex", + "num-complex 0.4.6", "num-integer", "num-traits", "pyo3", @@ -225,12 +310,53 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "sprs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bff8419009a08f6cb7519a602c5590241fbff1446bcc823c07af15386eb801b" +dependencies = [ + "alga", + "ndarray", + "num-complex 0.4.6", + "num-traits", + "num_cpus", + "rayon", + "smallvec", +] + [[package]] name = "syn" version = "2.0.104" diff --git a/Cargo.toml b/Cargo.toml index 25d78cb..d43efcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.25.0", features = ["extension-module", "anyhow", "auto-initialize"] } -numpy = "0.25" \ No newline at end of file +numpy = "0.25" +sprs = "0.11" diff --git a/src/lib.rs b/src/lib.rs index 3b36895..d4c5a0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,29 +2,18 @@ use numpy::{PyArray2, PyArrayMethods}; use pyo3::prelude::*; pub mod lap; +pub mod matrix; + use crate::lap::*; +use crate::matrix::*; #[pyfunction] fn solve_lap<'py>( _py: Python<'py>, - cost_matrix: &Bound<'py, PyArray2>, // Changed to use Bound + cost_matrix: &Bound<'py, PyArray2>, algorithm: &str, ) -> PyResult<(f64, Vec, Vec)> { - // Convert NumPy array to dense matrix - let matrix: Vec> = cost_matrix - .readonly() - .as_array() - .rows() - .into_iter() - .map(|row| row.iter().copied().collect::>()) - .collect(); - - // Validate dense matrix - if matrix.is_empty() || matrix.iter().any(|row| row.len() != matrix[0].len()) { - return Err(PyErr::new::( - "Matrix must be non-empty and rectangular", - )); - } + let matrix = extract_matrix(cost_matrix)?; match algorithm { "lapjv" => Ok(lapjv(matrix)), @@ -40,18 +29,3 @@ fn fastlap(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(solve_lap, m)?)?; Ok(()) } - -pub fn fastlap_rust(_py: Python, matrix: &Vec>, algo: &str) -> PyResult<(f64, Vec, Vec)> { - if matrix.is_empty() || matrix.iter().any(|row| row.len() != matrix[0].len()) { - return Err(pyo3::exceptions::PyValueError::new_err("Matrix must be square and non-empty")); - } - - let result = match algo { - "lapjv" => lapjv(matrix.clone()), - "hungarian" => hungarian(matrix.clone()), - _ => return Err(pyo3::exceptions::PyValueError::new_err("Unsupported algorithm")), - }; - - Ok(result) -} - diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..529b6e5 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,79 @@ +use numpy::PyReadonlyArray1; +use numpy::{PyArray2, PyArrayMethods}; +use pyo3::prelude::*; +use sprs::CsMat; + +/// Convert a dense NumPy array to Vec> +pub fn extract_dense_matrix<'py>(cost_matrix: &Bound<'py, PyArray2>) -> PyResult>> { + let matrix: Vec> = cost_matrix + .readonly() + .as_array() + .rows() + .into_iter() + .map(|row| row.iter().copied().collect::>()) + .collect(); + Ok(matrix) +} + +/// Convert a scipy.sparse.csr_matrix to Vec> +pub fn extract_sparse_matrix<'py>(cost_matrix: &Bound<'py, PyArray2>) -> PyResult>> { + let indptr: PyReadonlyArray1 = cost_matrix.getattr("indptr")?.extract()?; + let indices: PyReadonlyArray1 = cost_matrix.getattr("indices")?.extract()?; + let data: PyReadonlyArray1 = cost_matrix.getattr("data")?.extract()?; + + + let shape: (usize, usize) = cost_matrix + .getattr("shape")? + .extract::<(usize, usize)>()?; + + let csr = CsMat::new( + shape, + indptr.as_slice()?.to_vec(), + indices.as_slice()?.to_vec(), + data.as_slice()?.to_vec(), + ); + + let dense: Vec> = (0..shape.0) + .map(|i| { + (0..shape.1) + .map(|j| csr.get(i, j).copied().unwrap_or(f64::INFINITY)) + .collect() + }) + .collect(); + + Ok(dense) +} + +/// Convert input (dense or CSR) to a validated dense matrix +pub fn extract_matrix<'py>(cost_matrix: &Bound<'py, PyArray2>) -> PyResult>> { + // Try dense first + if let Ok(array) = cost_matrix.downcast::>() { + let matrix = extract_dense_matrix(&array.readonly())?; + return validate_matrix(matrix); + } + + // Try sparse (CSR) + let is_csr = ["indptr", "indices", "data", "shape"] + .iter() + .all(|&attr| cost_matrix.hasattr(attr).unwrap_or(false)); + + if is_csr { + let matrix = extract_sparse_matrix(cost_matrix)?; + return validate_matrix(matrix); + } + + Err(PyErr::new::( + "Input must be a NumPy ndarray or scipy.sparse.csr_matrix", + )) +} + +/// Ensure matrix is rectangular and non-empty +pub fn validate_matrix(matrix: Vec>) -> PyResult>> { + if matrix.is_empty() || matrix.iter().any(|row| row.len() != matrix[0].len()) { + Err(PyErr::new::( + "Matrix must be non-empty and rectangular", + )) + } else { + Ok(matrix) + } +} \ No newline at end of file From 94f63f8eb287eef0b80234911885a3641fb8032e Mon Sep 17 00:00:00 2001 From: MinLee0210 Date: Sat, 21 Jun 2025 16:17:58 +0700 Subject: [PATCH 2/4] .. --- python/fastlaps.py | 57 +++++++++++++++++++++++++--------------------- src/lib.rs | 2 +- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/python/fastlaps.py b/python/fastlaps.py index a684179..9686075 100644 --- a/python/fastlaps.py +++ b/python/fastlaps.py @@ -7,33 +7,37 @@ if __name__ == "__main__": # Quick example for a 3x3 matrix - matrix = np.random.rand(4, 5) - for algo in ["lapjv", "hungarian"]: - print(f"\nAlgorithm: {algo}") - start = time.time() - fastlap_cost, fastlap_rows, fastlap_cols = fastlap.solve_lap(matrix, algo) - fastlap_end = time.time() - print(f"fastlap.{algo}: Time={fastlap_end - start:.8f}s") - print( - f"fastlap.{algo}: Cost={fastlap_cost}, Rows={fastlap_rows}, Cols={fastlap_cols}" - ) - if algo == "hungarian": + cols = 3000 + rows = 3000 + matrix = np.random.rand(rows, cols) + for i in range(10): + + for algo in ["lapjv", "hungarian"]: + print(f"\nAlgorithm: {algo}") start = time.time() - scipy_rows, scipy_cols = linear_sum_assignment(matrix) - scipy_cost = matrix[scipy_rows, scipy_cols].sum() - scipy_end = time.time() - print( - f"scipy.optimize.linear_sum_assignment: Time={scipy_end - start:.8f}s" - ) - print( - f"scipy.optimize.linear_sum_assignment: Cost={scipy_cost}, Rows={list(scipy_rows)}, Cols={list(scipy_cols)}" - ) - if algo == "lapjv": - start = time.time() - lap_cost, lap_rows, lap_cols = lap.lapjv(matrix, extend_cost=True) - lap_end = time.time() - print(f"lap.lapjv: Time={lap_end - start:.8f}s") - print(f"lap.lapjv: Cost={lap_cost}, Rows={lap_rows}, Cols={lap_cols}") + fastlap_cost, fastlap_rows, fastlap_cols = fastlap.solve_lap(matrix, algo) + fastlap_end = time.time() + print(f"fastlap.{algo}: Time={fastlap_end - start:.8f}s") + # print( + # f"fastlap.{algo}: Cost={fastlap_cost}, Rows={list(fastlap_rows)}, Cols={list(fastlap_cols)}" + # ) + if algo == "hungarian": + start = time.time() + scipy_rows, scipy_cols = linear_sum_assignment(matrix) + scipy_cost = matrix[scipy_rows, scipy_cols].sum() + scipy_end = time.time() + print( + f"scipy.optimize.linear_sum_assignment: Time={scipy_end - start:.8f}s" + ) + print( + f"scipy.optimize.linear_sum_assignment: Cost={scipy_cost}, Rows={list(scipy_rows)}, Cols={list(scipy_cols)}" + ) + if algo == "lapjv": + start = time.time() + lap_cost, lap_rows, lap_cols = lap.lapjv(matrix, extend_cost=True) + lap_end = time.time() + print(f"lap.lapjv: Time={lap_end - start:.8f}s") + # print(f"lap.lapjv: Cost={lap_cost}, Rows={lap_rows}, Cols={lap_cols}") """ First release: @@ -49,4 +53,5 @@ fastlap.hungarian: Cost=0.7465856501551806, Rows=[2, 0, 1, 3], Cols=[1, 2, 0, 3, 18446744073709551615] scipy.optimize.linear_sum_assignment: Time=0.00001287s scipy.optimize.linear_sum_assignment: Cost=0.6229432588732741, Rows=[0, 1, 2, 3], Cols=[2, 0, 1, 4] + """ diff --git a/src/lib.rs b/src/lib.rs index d4c5a0a..d4af81c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use numpy::{PyArray2, PyArrayMethods}; +use numpy::{PyArray2}; use pyo3::prelude::*; pub mod lap; From 03c8d89c6b33d466886ec0653f6c14654f32f324 Mon Sep 17 00:00:00 2001 From: MinLee0210 Date: Sun, 22 Jun 2025 15:32:29 +0700 Subject: [PATCH 3/4] add lapmod --- src/lap.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/matrix.rs | 15 ++++--- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/lap.rs b/src/lap.rs index cabbb0f..691379b 100644 --- a/src/lap.rs +++ b/src/lap.rs @@ -228,3 +228,116 @@ pub fn hungarian(matrix: Vec>) -> (f64, Vec, Vec) { (total_cost, row_assign, col_assign) } + +pub fn lapmod(matrix: Vec>) -> (f64, Vec, Vec) { + let n = matrix.len(); + if n == 0 { + return (0.0, vec![], vec![]); + } + let m = matrix[0].len(); + + // Handle non-square matrices by padding with INFINITY + let dim = n.max(m); + let padded_matrix = if n != m { + let mut new_matrix = vec![vec![f64::INFINITY; dim]; dim]; + for i in 0..n { + for j in 0..m { + new_matrix[i][j] = matrix[i][j]; + } + } + new_matrix + } else { + matrix.clone() + }; + + let n = padded_matrix.len(); + let mut u = vec![0.0; n]; // Dual variables for rows + let mut v = vec![0.0; n]; // Dual variables for columns + let mut row_assign = vec![usize::MAX; n]; + let mut col_assign = vec![usize::MAX; n]; + + // Greedy initialization: skip INFINITY costs + for i in 0..n { + if let Some((j_min, &min_val)) = padded_matrix[i] + .iter() + .enumerate() + .filter(|(j, &cost)| cost != f64::INFINITY && col_assign[*j] == usize::MAX) + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + { + row_assign[i] = j_min; + col_assign[j_min] = i; + u[i] = min_val; + } + } + + // Augmenting path loop + for i in 0..n { + if row_assign[i] != usize::MAX { + continue; + } + let mut min_slack = vec![f64::INFINITY; n]; + let mut prev = vec![usize::MAX; n]; + let mut visited = vec![false; n]; + let mut marked_row = i; + #[allow(unused_assignments)] + let mut marked_col = usize::MAX; + + loop { + visited[marked_row] = true; + // Only consider finite costs + for j in 0..n { + let cost = padded_matrix[marked_row][j]; + if cost != f64::INFINITY && !visited[j] && col_assign[j] != usize::MAX { + let slack = cost - u[marked_row] - v[j]; + if slack < min_slack[j] { + min_slack[j] = slack; + prev[j] = marked_row; + } + } + } + + let (j, &delta) = min_slack + .iter() + .enumerate() + .filter(|(j, _)| !visited[*j] && col_assign[*j] != usize::MAX) + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap_or((0, &f64::INFINITY)); + + if delta == f64::INFINITY { + let unassigned_j = (0..n).find(|&j| col_assign[j] == usize::MAX).unwrap(); + marked_col = unassigned_j; + break; + } + + for k in 0..n { + if visited[k] { + u[k] += delta; + v[k] -= delta; + } else { + min_slack[k] -= delta; + } + } + + marked_row = col_assign[j]; + } + + // Augment path + while marked_col != usize::MAX { + let i_prev = prev[marked_col]; + let j_prev = row_assign[i_prev]; + row_assign[i_prev] = marked_col; + col_assign[marked_col] = i_prev; + marked_col = j_prev; + } + } + + // Compute total cost using original matrix + let total_cost: f64 = row_assign + .iter() + .enumerate() + .filter(|(i, &j)| j != usize::MAX && *i < n && j < m) + .map(|(i, &j)| matrix[i][j]) + .sum(); + + (total_cost, row_assign, col_assign) +} diff --git a/src/lib.rs b/src/lib.rs index d4af81c..2658038 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use numpy::{PyArray2}; +use numpy::PyArray2; use pyo3::prelude::*; pub mod lap; diff --git a/src/matrix.rs b/src/matrix.rs index 529b6e5..63e1eb7 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -4,7 +4,9 @@ use pyo3::prelude::*; use sprs::CsMat; /// Convert a dense NumPy array to Vec> -pub fn extract_dense_matrix<'py>(cost_matrix: &Bound<'py, PyArray2>) -> PyResult>> { +pub fn extract_dense_matrix<'py>( + cost_matrix: &Bound<'py, PyArray2>, +) -> PyResult>> { let matrix: Vec> = cost_matrix .readonly() .as_array() @@ -16,15 +18,12 @@ pub fn extract_dense_matrix<'py>(cost_matrix: &Bound<'py, PyArray2>) -> PyR } /// Convert a scipy.sparse.csr_matrix to Vec> -pub fn extract_sparse_matrix<'py>(cost_matrix: &Bound<'py, PyArray2>) -> PyResult>> { +pub fn extract_sparse_matrix<'py>(cost_matrix: &Bound<'py, PyAny>) -> PyResult>> { let indptr: PyReadonlyArray1 = cost_matrix.getattr("indptr")?.extract()?; let indices: PyReadonlyArray1 = cost_matrix.getattr("indices")?.extract()?; let data: PyReadonlyArray1 = cost_matrix.getattr("data")?.extract()?; - - - let shape: (usize, usize) = cost_matrix - .getattr("shape")? - .extract::<(usize, usize)>()?; + + let shape: (usize, usize) = cost_matrix.getattr("shape")?.extract::<(usize, usize)>()?; let csr = CsMat::new( shape, @@ -76,4 +75,4 @@ pub fn validate_matrix(matrix: Vec>) -> PyResult>> { } else { Ok(matrix) } -} \ No newline at end of file +} From 076aee56e81cf8d14dcd02b97adecf32333a61c5 Mon Sep 17 00:00:00 2001 From: MinLee0210 Date: Sun, 22 Jun 2025 16:11:17 +0700 Subject: [PATCH 4/4] test perfomance --- python/fastlaps.py | 15 ++++++++++++--- src/lib.rs | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/python/fastlaps.py b/python/fastlaps.py index 9686075..8595fa3 100644 --- a/python/fastlaps.py +++ b/python/fastlaps.py @@ -7,12 +7,15 @@ if __name__ == "__main__": # Quick example for a 3x3 matrix - cols = 3000 - rows = 3000 + cols = 5 + rows = 5 + algos = ["lapjv", "hungarian", "lapmod"] matrix = np.random.rand(rows, cols) for i in range(10): - for algo in ["lapjv", "hungarian"]: + for algo in algos: + if algo != "lapjv": + continue print(f"\nAlgorithm: {algo}") start = time.time() fastlap_cost, fastlap_rows, fastlap_cols = fastlap.solve_lap(matrix, algo) @@ -38,6 +41,12 @@ lap_end = time.time() print(f"lap.lapjv: Time={lap_end - start:.8f}s") # print(f"lap.lapjv: Cost={lap_cost}, Rows={lap_rows}, Cols={lap_cols}") + if algo == "lapmod": + start = time.time() + lapmod_cost, lapmod_rows, lapmod_cols = lap.lapmod(matrix) + lapmod_end = time.time() + print(f"lap.lapmod: Time={lapmod_end - start:.8f}s") + # print(f"lap.lapmod: Cost={lapmod_cost}, Rows={lapmod_rows}, Cols={lapmod_cols}") """ First release: diff --git a/src/lib.rs b/src/lib.rs index 2658038..ff302d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,9 @@ fn solve_lap<'py>( match algorithm { "lapjv" => Ok(lapjv(matrix)), "hungarian" => Ok(hungarian(matrix)), + "lapmod" => Ok(lapmod(matrix)), _ => Err(PyErr::new::( - "Unknown algorithm. Supported algorithms: 'lapjv', 'hungarian'", + "Unknown algorithm. Supported algorithms: 'lapjv', 'hungarian', 'lapmod'", )), } }