gatagat-lap-bb6ad3a/ 0000775 0001750 0001750 00000000000 15272350177 014015 5 ustar pieter pieter gatagat-lap-bb6ad3a/README.md 0000664 0001750 0001750 00000011240 15272350177 015272 0 ustar pieter pieter [](https://github.com/gatagat/lap/actions/workflows/test_simple.yaml)
[](https://github.com/gatagat/lap/actions/workflows/test_full.yaml)
[](https://github.com/gatagat/lap/actions/workflows/benchmark.yaml)
[](https://github.com/gatagat/lap/actions/workflows/prepublish.yaml)
[](https://github.com/gatagat/lap/actions/workflows/publish.yaml)
# lap: Linear Assignment Problem Solver
[`lap`](https://github.com/gatagat/lap) is a [linear assignment
problem](https://en.wikipedia.org/wiki/Assignment_problem) solver using
Jonker-Volgenant algorithm for dense LAPJV¹ or sparse LAPMOD² matrices. Both
algorithms are implemented from scratch based solely on the papers¹˒² and the
public domain Pascal implementation provided by A. Volgenant³. The LAPMOD
implementation seems to be faster than the LAPJV implementation for matrices
with a side of more than ~5000 and with less than 50% finite coefficients.
¹ R. Jonker and A. Volgenant, "A Shortest Augmenting Path Algorithm for
Dense and Sparse Linear Assignment Problems", Computing 38, 325-340 (1987)
² A. Volgenant, "Linear and Semi-Assignment Problems: A Core
Oriented Approach", Computer Ops Res. 23, 917-932 (1996)
³
http://www.assignmentproblems.com/LAPJV.htm |
[[archive.org](https://web.archive.org/web/20220221010749/http://www.assignmentproblems.com/LAPJV.htm)]
## 💽 Installation
### Install from [PyPI](https://pypi.org/project/lap/):
[](https://badge.fury.io/py/lap)
[](https://pepy.tech/project/lap)
[](https://pepy.tech/project/lap)
```
pip install lap
```
| [**PyPI Wheels**](https://pypi.org/project/lap/#files) 🛞 | **Windows** ✅ | **Linux** ✅ | **macOS** ✅ |
|:---:|:---:|:---:|:---:|
| Python 3.7 | AMD64 | x86_64/aarch64 | x86_64 |
| Python 3.8 | AMD64 | x86_64/aarch64 | x86_64/arm64 |
| Python 3.9-3.14 ¹ | AMD64/ARM64 ² | x86_64/aarch64 | x86_64/arm64 |
¹ v0.5.13 supports both numpy 1.x and 2.x for Python 3.8-3.14. 🆕
² Windows ARM64 is experimental.
Other options
### Install from GitHub repo (requires C++ compiler):
```
pip install git+https://github.com/gatagat/lap.git
```
### Build and install (requires C++ compiler):
```
git clone https://github.com/gatagat/lap.git
cd lap
pip install "setuptools>=67.8.0"
pip install wheel build
python -m build --wheel
cd dist
```
## 🧪 Usage
```
import lap
import numpy as np
print(lap.lapjv(np.random.rand(4, 5), extend_cost=True))
```
More details
### `cost, x, y = lap.lapjv(C)`
The function `lapjv(C)` returns the assignment cost `cost` and two arrays `x`
and `y`. If cost matrix `C` has shape NxM, then `x` is a size-N array
specifying to which column each row is assigned, and `y` is a size-M array
specifying to which row each column is assigned. For example, an output of `x =
[1, 0]` indicates that row 0 is assigned to column 1 and row 1 is assigned to
column 0. Similarly, an output of `x = [2, 1, 0]` indicates that row 0 is
assigned to column 2, row 1 is assigned to column 1, and row 2 is assigned to
column 0.
Note that this function *does not* return the assignment matrix (as done by
scipy's
[`linear_sum_assignment`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html)
and lapsolver's [`solve dense`](https://github.com/cheind/py-lapsolver)). The
assignment matrix can be constructed from `x` as follows: ``` A = np.zeros((N,
M)) for i in range(N): A[i, x[i]] = 1 ```
Equivalently, we could construct the assignment matrix from `y`:
```
A = np.zeros((N, M))
for j in range(M):
A[y[j], j] = 1
```
Finally, note that the outputs are redundant: we can construct `x` from `y`,
and vise versa:
```
x = [np.where(y == i)[0][0] for i in range(N)]
y = [np.where(x == j)[0][0] for j in range(M)]
```
# License
Released under the 2-clause BSD license, see [LICENSE](./LICENSE).
Copyright (C) 2012-2025, Tomas Kazmar
Contributors (in alphabetic order):
- Benjamin Eysenbach
- Léo Duret
- Pieter Lenaerts
- Raphael Reme
- Ratha Siv
- Robert Wen
- Steven
- Tom White
- Tomas Kazmar
- Wok
gatagat-lap-bb6ad3a/MANIFEST.in 0000664 0001750 0001750 00000000305 15272350177 015551 0 ustar pieter pieter include *.txt *.md *.in LICENSE
recursive-include _lapjv_cpp *
recursive-include lap *
recursive-include lap/tests *
global-exclude */__pycache__
global-exclude *.pyc
prune .github
prune benchmark
gatagat-lap-bb6ad3a/.gitignore 0000664 0001750 0001750 00000000041 15272350177 016000 0 ustar pieter pieter dist
build
lap.egg-info
MANIFEST
gatagat-lap-bb6ad3a/_lapjv_cpp/ 0000775 0001750 0001750 00000000000 15272350177 016132 5 ustar pieter pieter gatagat-lap-bb6ad3a/_lapjv_cpp/lapjv.h 0000664 0001750 0001750 00000003074 15272350177 017423 0 ustar pieter pieter #ifndef LAPJV_H
#define LAPJV_H
#define LARGE 1000000
#if !defined TRUE
#define TRUE 1
#endif
#if !defined FALSE
#define FALSE 0
#endif
#define NEW(x, t, n) if ((x = (t *)malloc(sizeof(t) * (n))) == 0) { return -1; }
#define FREE(x) if (x != 0) { free(x); x = 0; }
#define SWAP_INDICES(a, b) { int_t _temp_index = a; a = b; b = _temp_index; }
#if 0
#include
#define ASSERT(cond) assert(cond)
#define PRINTF(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define PRINT_COST_ARRAY(a, n) \
while (1) { \
printf(#a" = ["); \
if ((n) > 0) { \
printf("%f", (a)[0]); \
for (uint_t j = 1; j < n; j++) { \
printf(", %f", (a)[j]); \
} \
} \
printf("]\n"); \
break; \
}
#define PRINT_INDEX_ARRAY(a, n) \
while (1) { \
printf(#a" = ["); \
if ((n) > 0) { \
printf("%d", (a)[0]); \
for (uint_t j = 1; j < n; j++) { \
printf(", %d", (a)[j]); \
} \
} \
printf("]\n"); \
break; \
}
#else
#define ASSERT(cond)
#define PRINTF(fmt, ...)
#define PRINT_COST_ARRAY(a, n)
#define PRINT_INDEX_ARRAY(a, n)
#endif
typedef signed int int_t;
typedef unsigned int uint_t;
typedef double cost_t;
typedef char boolean;
typedef enum fp_t { FP_1 = 1, FP_2 = 2, FP_DYNAMIC = 3 } fp_t;
extern int_t lapjv_internal(
const uint_t n, cost_t *cost[],
int_t *x, int_t *y);
extern int_t lapmod_internal(
const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
int_t *x, int_t *y, fp_t fp_version);
#endif // LAPJV_H
gatagat-lap-bb6ad3a/_lapjv_cpp/lapjv.cpp 0000664 0001750 0001750 00000021105 15272350177 017751 0 ustar pieter pieter #include
#include
#include
#include "lapjv.h"
/** Column-reduction and reduction transfer for a dense cost matrix. */
int_t _ccrrt_dense(const uint_t n, cost_t *cost[],
int_t *free_rows, int_t *x, int_t *y, cost_t *v)
{
int_t n_free_rows;
boolean *unique;
for (uint_t i = 0; i < n; i++) {
x[i] = -1;
v[i] = LARGE;
y[i] = 0;
}
for (uint_t i = 0; i < n; i++) {
for (uint_t j = 0; j < n; j++) {
const cost_t c = cost[i][j];
if (c < v[j]) {
v[j] = c;
y[j] = i;
}
PRINTF("i=%d, j=%d, c[i,j]=%f, v[j]=%f y[j]=%d\n", i, j, c, v[j], y[j]);
}
}
PRINT_COST_ARRAY(v, n);
PRINT_INDEX_ARRAY(y, n);
NEW(unique, boolean, n);
memset(unique, TRUE, n);
{
int_t j = n;
do {
j--;
const int_t i = y[j];
if (x[i] < 0) {
x[i] = j;
} else {
unique[i] = FALSE;
y[j] = -1;
}
} while (j > 0);
}
n_free_rows = 0;
for (uint_t i = 0; i < n; i++) {
if (x[i] < 0) {
free_rows[n_free_rows++] = i;
} else if (unique[i]) {
const int_t j = x[i];
cost_t min = LARGE;
for (uint_t j2 = 0; j2 < n; j2++) {
if (j2 == (uint_t)j) {
continue;
}
const cost_t c = cost[i][j2] - v[j2];
if (c < min) {
min = c;
}
}
PRINTF("v[%d] = %f - %f\n", j, v[j], min);
v[j] -= min;
}
}
FREE(unique);
return n_free_rows;
}
/** Augmenting row reduction for a dense cost matrix. */
int_t _carr_dense(const uint_t n, cost_t *cost[], const uint_t n_free_rows,
int_t *free_rows, int_t *x, int_t *y, cost_t *v)
{
uint_t current = 0;
int_t new_free_rows = 0;
uint_t rr_cnt = 0;
PRINT_INDEX_ARRAY(x, n);
PRINT_INDEX_ARRAY(y, n);
PRINT_COST_ARRAY(v, n);
PRINT_INDEX_ARRAY(free_rows, n_free_rows);
while (current < n_free_rows) {
int_t i0;
int_t j1, j2;
cost_t v1, v2, v1_new;
boolean v1_lowers;
rr_cnt++;
PRINTF("current = %d rr_cnt = %d\n", current, rr_cnt);
const int_t free_i = free_rows[current++];
j1 = 0;
v1 = cost[free_i][0] - v[0];
j2 = -1;
v2 = LARGE;
for (uint_t j = 1; j < n; j++) {
PRINTF("%d = %f %d = %f\n", j1, v1, j2, v2);
const cost_t c = cost[free_i][j] - v[j];
if (c < v2) {
if (c >= v1) {
v2 = c;
j2 = j;
} else {
v2 = v1;
v1 = c;
j2 = j1;
j1 = j;
}
}
}
i0 = y[j1];
v1_new = v[j1] - (v2 - v1);
v1_lowers = v1_new < v[j1];
PRINTF("%d %d 1=%d,%f 2=%d,%f v1'=%f(%d,%g) \n",
free_i, i0, j1, v1, j2, v2, v1_new, v1_lowers, v[j1] - v1_new);
if (rr_cnt < current * n) {
if (v1_lowers) {
v[j1] = v1_new;
} else if (i0 >= 0 && j2 >= 0) {
j1 = j2;
i0 = y[j2];
}
if (i0 >= 0) {
if (v1_lowers) {
free_rows[--current] = i0;
} else {
free_rows[new_free_rows++] = i0;
}
}
} else {
PRINTF("rr_cnt=%d >= %d (current=%d * n=%d)\n", rr_cnt, current * n, current, n);
if (i0 >= 0) {
free_rows[new_free_rows++] = i0;
}
}
x[free_i] = j1;
y[j1] = free_i;
}
return new_free_rows;
}
/** Find columns with minimum d[j] and put them on the SCAN list. */
uint_t _find_dense(const uint_t n, uint_t lo, cost_t *d, int_t *cols, int_t *y)
{
uint_t hi = lo + 1;
cost_t mind = d[cols[lo]];
for (uint_t k = hi; k < n; k++) {
int_t j = cols[k];
if (d[j] <= mind) {
if (d[j] < mind) {
hi = lo;
mind = d[j];
}
cols[k] = cols[hi];
cols[hi++] = j;
}
}
return hi;
}
/**
* Scan all columns in TODO starting from arbitrary column in SCAN
* and try to decrease d of the TODO columns using the SCAN column.
*/
int_t _scan_dense(const uint_t n, cost_t *cost[], uint_t *plo, uint_t*phi,
cost_t *d, int_t *cols, int_t *pred, int_t *y, cost_t *v)
{
uint_t lo = *plo;
uint_t hi = *phi;
cost_t h, cred_ij;
while (lo != hi) {
int_t j = cols[lo++];
const int_t i = y[j];
const cost_t mind = d[j];
h = cost[i][j] - v[j] - mind;
PRINTF("i=%d j=%d h=%f\n", i, j, h);
// For all columns in TODO
for (uint_t k = hi; k < n; k++) {
j = cols[k];
cred_ij = cost[i][j] - v[j] - h;
if (cred_ij < d[j]) {
d[j] = cred_ij;
pred[j] = i;
if (cred_ij == mind) {
if (y[j] < 0) {
return j;
}
cols[k] = cols[hi];
cols[hi++] = j;
}
}
}
}
*plo = lo;
*phi = hi;
return -1;
}
/**
* Single iteration of modified Dijkstra shortest path algorithm as explained in the JV paper.
* This is a dense matrix version.
* @return The closest free column index.
*/
int_t find_path_dense(const uint_t n, cost_t *cost[], const int_t start_i,
int_t *y, cost_t *v, int_t *pred)
{
uint_t lo = 0, hi = 0;
int_t final_j = -1;
uint_t n_ready = 0;
int_t *cols;
cost_t *d;
NEW(cols, int_t, n);
NEW(d, cost_t, n);
for (uint_t i = 0; i < n; i++) {
cols[i] = i;
pred[i] = start_i;
d[i] = cost[start_i][i] - v[i];
}
PRINT_COST_ARRAY(d, n);
while (final_j == -1) {
// No columns left on the SCAN list.
if (lo == hi) {
PRINTF("%d..%d -> find\n", lo, hi);
n_ready = lo;
hi = _find_dense(n, lo, d, cols, y);
PRINTF("check %d..%d\n", lo, hi);
PRINT_INDEX_ARRAY(cols, n);
for (uint_t k = lo; k < hi; k++) {
const int_t j = cols[k];
if (y[j] < 0) {
final_j = j;
}
}
}
if (final_j == -1) {
PRINTF("%d..%d -> scan\n", lo, hi);
final_j = _scan_dense(
n, cost, &lo, &hi, d, cols, pred, y, v);
PRINT_COST_ARRAY(d, n);
PRINT_INDEX_ARRAY(cols, n);
PRINT_INDEX_ARRAY(pred, n);
}
}
PRINTF("found final_j=%d\n", final_j);
PRINT_INDEX_ARRAY(cols, n);
{
const cost_t mind = d[cols[lo]];
for (uint_t k = 0; k < n_ready; k++) {
const int_t j = cols[k];
v[j] += d[j] - mind;
}
}
FREE(cols);
FREE(d);
return final_j;
}
/** Augment for a dense cost matrix. */
int_t _ca_dense(const uint_t n, cost_t *cost[], const uint_t n_free_rows,
int_t *free_rows, int_t *x, int_t *y, cost_t *v)
{
int_t *pred;
NEW(pred, int_t, n);
for (int_t *pfree_i = free_rows; pfree_i < free_rows + n_free_rows; pfree_i++) {
int_t i = -1, j;
uint_t k = 0;
PRINTF("looking at free_i=%d\n", *pfree_i);
j = find_path_dense(n, cost, *pfree_i, y, v, pred);
ASSERT(j >= 0);
ASSERT(j < n);
while (i != *pfree_i) {
PRINTF("augment %d\n", j);
PRINT_INDEX_ARRAY(pred, n);
i = pred[j];
PRINTF("y[%d]=%d -> %d\n", j, y[j], i);
y[j] = i;
PRINT_INDEX_ARRAY(x, n);
SWAP_INDICES(j, x[i]);
k++;
if (k >= n) {
ASSERT(FALSE);
}
}
}
FREE(pred);
return 0;
}
/** Solve dense sparse LAP. */
int lapjv_internal(const uint_t n, cost_t *cost[], int_t *x, int_t *y)
{
int ret;
int_t *free_rows;
cost_t *v;
NEW(free_rows, int_t, n);
NEW(v, cost_t, n);
ret = _ccrrt_dense(n, cost, free_rows, x, y, v);
int i = 0;
while (ret > 0 && i < 2) {
ret = _carr_dense(n, cost, ret, free_rows, x, y, v);
i++;
}
if (ret > 0) {
ret = _ca_dense(n, cost, ret, free_rows, x, y, v);
}
FREE(v);
FREE(free_rows);
return ret;
}
gatagat-lap-bb6ad3a/_lapjv_cpp/.gitignore 0000664 0001750 0001750 00000000013 15272350177 020114 0 ustar pieter pieter _lapjv.cpp
gatagat-lap-bb6ad3a/_lapjv_cpp/_lapjv.pyx 0000664 0001750 0001750 00000012353 15272350177 020153 0 ustar pieter pieter # Force compiling with Python 3
# cython: language_level=3
import numpy as np
cimport numpy as cnp
cimport cython
from libc.stdlib cimport malloc, free
cdef extern from "lapjv.h" nogil:
ctypedef signed int int_t
ctypedef unsigned int uint_t
cdef int LARGE
cdef enum fp_t:
FP_1
FP_2
FP_DYNAMIC
int lapjv_internal(const uint_t n,
double *cost[],
int_t *x,
int_t *y)
int lapmod_internal(const uint_t n,
double *cc,
uint_t *ii,
uint_t *kk,
int_t *x,
int_t *y,
fp_t fp_version)
LARGE_ = LARGE
FP_1_ = FP_1
FP_2_ = FP_2
FP_DYNAMIC_ = FP_DYNAMIC
@cython.boundscheck(False)
@cython.wraparound(False)
def lapjv(cnp.ndarray cost not None, char extend_cost=False,
double cost_limit=np.inf, char return_cost=True):
"""Solve linear assignment problem using Jonker-Volgenant algorithm.
Parameters
----------
cost: (N,N) ndarray
Cost matrix. Entry `cost[i, j]` is the cost of assigning row `i` to
column `j`.
extend_cost: bool, optional
Whether or not extend a non-square matrix. Default: False.
cost_limit: double, optional
An upper limit for a cost of a single assignment. Default: `np.inf`.
return_cost: bool, optional
Whether or not to return the assignment cost.
Returns
-------
opt: double
Assignment cost. Not returned if `return_cost is False`.
x: (N,) ndarray
Assignment. `x[i]` specifies the column to which row `i` is assigned.
y: (N,) ndarray
Assignment. `y[j]` specifies the row to which column `j` is assigned.
Notes
-----
For non-square matrices (with `extend_cost is True`) or `cost_limit` set
low enough, there will be unmatched rows, columns in the solution `x`, `y`.
All such entries are set to -1.
"""
if cost.ndim != 2:
raise ValueError('2-dimensional array expected')
cdef cnp.ndarray[cnp.double_t, ndim=2, mode='c'] cost_c = \
np.ascontiguousarray(cost, dtype=np.double)
cdef cnp.ndarray[cnp.double_t, ndim=2, mode='c'] cost_c_extended
cdef uint_t n_rows = cost_c.shape[0]
cdef uint_t n_cols = cost_c.shape[1]
cdef uint_t n = 0
if n_rows == n_cols:
n = n_rows
else:
if not extend_cost:
raise ValueError(
'Square cost array expected. If cost is intentionally '
'non-square, pass extend_cost=True.')
if cost_limit < np.inf:
n = n_rows + n_cols
cost_c_extended = np.empty((n, n), dtype=np.double)
cost_c_extended[:] = cost_limit / 2.
cost_c_extended[n_rows:, n_cols:] = 0
cost_c_extended[:n_rows, :n_cols] = cost_c
cost_c = cost_c_extended
elif extend_cost:
n = max(n_rows, n_cols)
cost_c_extended = np.zeros((n, n), dtype=np.double)
cost_c_extended[:n_rows, :n_cols] = cost_c
cost_c = cost_c_extended
cdef double **cost_ptr
cost_ptr = malloc(n * sizeof(double *))
cdef int i
for i in range(n):
cost_ptr[i] = &cost_c[i, 0]
cdef cnp.ndarray[int_t, ndim=1, mode='c'] x_c = \
np.empty((n,), dtype=np.int32)
cdef cnp.ndarray[int_t, ndim=1, mode='c'] y_c = \
np.empty((n,), dtype=np.int32)
cdef int ret
with nogil:
ret = lapjv_internal(n, cost_ptr, &x_c[0], &y_c[0])
free(cost_ptr)
if ret != 0:
if ret == -1:
raise MemoryError('Out of memory.')
raise RuntimeError('Unknown error (lapjv_internal returned %d).' % ret)
cdef double opt = np.nan
if cost_limit < np.inf or extend_cost:
x_c[x_c >= n_cols] = -1
y_c[y_c >= n_rows] = -1
x_c = x_c[:n_rows]
y_c = y_c[:n_cols]
if return_cost:
opt = cost_c[np.nonzero(x_c != -1)[0], x_c[x_c != -1]].sum()
elif return_cost:
opt = cost_c[np.arange(n_rows), x_c].sum()
if return_cost:
return opt, x_c, y_c
else:
return x_c, y_c
@cython.boundscheck(False)
@cython.wraparound(False)
def _lapmod(const uint_t n,
cnp.ndarray cc not None,
cnp.ndarray ii not None,
cnp.ndarray kk not None,
fp_t fp_version=FP_DYNAMIC):
"""Internal function called from lapmod(..., fast=True)."""
cdef cnp.ndarray[cnp.double_t, ndim=1, mode='c'] cc_c = \
np.ascontiguousarray(cc, dtype=np.double)
cdef cnp.ndarray[uint_t, ndim=1, mode='c'] ii_c = \
np.ascontiguousarray(ii, dtype=np.uint32)
cdef cnp.ndarray[uint_t, ndim=1, mode='c'] kk_c = \
np.ascontiguousarray(kk, dtype=np.uint32)
cdef cnp.ndarray[int_t, ndim=1, mode='c'] x_c = \
np.empty((n,), dtype=np.int32)
cdef cnp.ndarray[int_t, ndim=1, mode='c'] y_c = \
np.empty((n,), dtype=np.int32)
cdef int_t ret
with nogil:
ret = lapmod_internal(n, &cc_c[0], &ii_c[0], &kk_c[0],
&x_c[0], &y_c[0], fp_version)
if ret != 0:
if ret == -1:
raise MemoryError('Out of memory.')
raise RuntimeError('Unknown error (lapmod_internal returned %d).' % ret)
return x_c, y_c
gatagat-lap-bb6ad3a/_lapjv_cpp/lapmod.cpp 0000664 0001750 0001750 00000043102 15272350177 020112 0 ustar pieter pieter #include
#include
#include
#include "lapjv.h"
/** Column-reduction and reduction transfer for a sparse cost matrix. */
int_t _ccrrt_sparse(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
int_t *free_rows, int_t *x, int_t *y, cost_t *v)
{
int_t n_free_rows;
boolean *unique;
for (uint_t i = 0; i < n; i++) {
x[i] = -1;
v[i] = LARGE;
y[i] = 0;
}
for (uint_t i = 0; i < n; i++) {
for (uint_t k = ii[i]; k < ii[i+1]; k++) {
const int_t j = kk[k];
const cost_t c = cc[k];
if (c < v[j]) {
v[j] = c;
y[j] = i;
}
PRINTF("i=%d, k=%d, j=%d, c[i,j]=%f, v[j]=%f y[j]=%d\n", i, k, j, c, v[j], y[j]);
}
}
PRINT_COST_ARRAY(v, n);
PRINT_INDEX_ARRAY(y, n);
NEW(unique, boolean, n);
memset(unique, TRUE, n);
{
int_t j = n;
do {
j--;
const int_t i = y[j];
if (x[i] < 0) {
x[i] = j;
} else {
unique[i] = FALSE;
y[j] = -1;
}
} while (j > 0);
}
n_free_rows = 0;
for (uint_t i = 0; i < n; i++) {
if (x[i] < 0) {
free_rows[n_free_rows++] = i;
} else if (unique[i] && (ii[i+1] - ii[i] > 1)) {
const int_t j = x[i];
cost_t min = LARGE;
for (uint_t k = ii[i]; k < ii[i+1]; k++) {
const int_t j2 = kk[k];
if (j2 == j) {
continue;
}
const cost_t c = cc[k] - v[j2];
if (c < min) {
min = c;
}
}
PRINTF("v[%d] = %f - %f\n", j, v[j], min);
v[j] -= min;
}
}
FREE(unique);
return n_free_rows;
}
/** Augmenting row reduction for a sparse cost matrix. */
int_t _carr_sparse(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
const uint_t n_free_rows, int_t *free_rows,
int_t *x, int_t *y, cost_t *v)
{
uint_t current = 0;
int_t new_free_rows = 0;
uint_t rr_cnt = 0;
PRINT_INDEX_ARRAY(x, n);
PRINT_INDEX_ARRAY(y, n);
PRINT_COST_ARRAY(v, n);
PRINT_INDEX_ARRAY(free_rows, n_free_rows);
while (current < n_free_rows) {
int_t i0;
int_t j1, j2;
cost_t v1, v2, v1_new;
boolean v1_lowers;
rr_cnt++;
PRINTF("current = %d rr_cnt = %d\n", current, rr_cnt);
const int_t free_i = free_rows[current++];
if (ii[free_i+1] - ii[free_i] > 0) {
const uint_t k = ii[free_i];
j1 = kk[k];
v1 = cc[k] - v[j1];
} else {
j1 = 0;
v1 = LARGE;
}
j2 = -1;
v2 = LARGE;
for (uint_t k = ii[free_i]+1; k < ii[free_i+1]; k++) {
PRINTF("%d = %f %d = %f\n", j1, v1, j2, v2);
const int_t j = kk[k];
const cost_t c = cc[k] - v[j];
if (c < v2) {
if (c >= v1) {
v2 = c;
j2 = j;
} else {
v2 = v1;
v1 = c;
j2 = j1;
j1 = j;
}
}
}
i0 = y[j1];
v1_new = v[j1] - (v2 - v1);
v1_lowers = v1_new < v[j1];
PRINTF("%d %d 1=%d,%f 2=%d,%f v1'=%f(%d,%g) \n",
free_i, i0, j1, v1, j2, v2, v1_new, v1_lowers, v[j1] - v1_new);
if (rr_cnt < current * n) {
if (v1_lowers) {
v[j1] = v1_new;
} else if (i0 >= 0 && j2 >= 0) {
j1 = j2;
i0 = y[j2];
}
if (i0 >= 0) {
if (v1_lowers) {
free_rows[--current] = i0;
} else {
free_rows[new_free_rows++] = i0;
}
}
} else {
PRINTF("rr_cnt=%d >= %d (current=%d * n=%d)\n", rr_cnt, current * n, current, n);
if (i0 >= 0) {
free_rows[new_free_rows++] = i0;
}
}
x[free_i] = j1;
y[j1] = free_i;
}
return new_free_rows;
}
/** Find columns with minimum d[j] and put them on the SCAN list. */
uint_t _find_sparse_1(const uint_t n, uint_t lo, cost_t *d, int_t *cols, int_t *y)
{
uint_t hi = lo + 1;
cost_t mind = d[cols[lo]];
for (uint_t k = hi; k < n; k++) {
int_t j = cols[k];
if (d[j] <= mind) {
if (d[j] < mind) {
hi = lo;
mind = d[j];
}
cols[k] = cols[hi];
cols[hi++] = j;
}
}
return hi;
}
/** Find columns with minimum d[j] and put them on the SCAN list. */
int_t _find_sparse_2(cost_t *d, int_t *scan, const uint_t n_todo, int_t *todo, boolean *done)
{
int_t hi = 0;
cost_t mind = LARGE;
for (uint_t k = 0; k < n_todo; k++) {
int_t j = todo[k];
if (done[j]) {
continue;
}
if (d[j] <= mind) {
if (d[j] < mind) {
hi = 0;
mind = d[j];
}
scan[hi++] = j;
}
}
return hi;
}
/**
* Scan all columns in TODO starting from arbitrary column in SCAN and try to
* decrease d of the TODO columns using the SCAN column.
*/
int_t _scan_sparse_1(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
uint_t *plo, uint_t *phi, cost_t *d, int_t *cols,
int_t *pred, int_t *y, cost_t *v)
{
uint_t lo = *plo;
uint_t hi = *phi;
cost_t h, cred_ij;
int_t *rev_kk;
NEW(rev_kk, int_t, n);
while (lo != hi) {
int_t kj;
int_t j = cols[lo++];
const int_t i = y[j];
const cost_t mind = d[j];
for (uint_t k = 0; k < n; k++) {
rev_kk[k] = -1;
}
for (uint_t k = ii[i]; k < ii[i+1]; k++) {
const int_t j = kk[k];
rev_kk[j] = k;
}
PRINTF("?%d kk[%d:%d]=", j, ii[i], ii[i+1]);
PRINT_INDEX_ARRAY(kk + ii[i], ii[i+1] - ii[i]);
kj = rev_kk[j];
if (kj == -1) {
continue;
}
ASSERT(kk[kj] == j);
h = cc[kj] - v[j] - mind;
PRINTF("i=%d j=%d kj=%d h=%f\n", i, j, kj, h);
// For all columns in TODO
for (uint_t k = hi; k < n; k++) {
j = cols[k];
PRINTF("?%d kk[%d:%d]=", j, ii[i], ii[i+1]);
PRINT_INDEX_ARRAY(kk + ii[i], ii[i+1] - ii[i]);
if ((kj = rev_kk[j]) == -1) {
continue;
}
ASSERT(kk[kj] == j);
cred_ij = cc[kj] - v[j] - h;
if (cred_ij < d[j]) {
d[j] = cred_ij;
pred[j] = i;
if (cred_ij == mind) {
if (y[j] < 0) {
FREE(rev_kk);
return j;
}
cols[k] = cols[hi];
cols[hi++] = j;
}
}
}
}
*plo = lo;
*phi = hi;
FREE(rev_kk);
return -1;
}
/**
* Scan all columns in TODO starting from arbitrary column in SCAN and try to
* decrease d of the TODO columns using the SCAN column.
*/
int_t _scan_sparse_2(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
uint_t *plo, uint_t *phi, cost_t *d, int_t *pred,
boolean *done, uint_t *pn_ready, int_t *ready,
int_t *scan, uint_t *pn_todo, int_t *todo,
boolean *added, int_t *y, cost_t *v)
{
uint_t lo = *plo;
uint_t hi = *phi;
uint_t n_todo = *pn_todo;
uint_t n_ready = *pn_ready;
cost_t h, cred_ij;
int_t *rev_kk;
NEW(rev_kk, int_t, n);
for (uint_t k = 0; k < n; k++) {
rev_kk[k] = -1;
}
while (lo != hi) {
int_t kj;
int_t j = scan[lo++];
const int_t i = y[j];
ready[n_ready++] = j;
const cost_t mind = d[j];
for (uint_t k = ii[i]; k < ii[i+1]; k++) {
const int_t j = kk[k];
rev_kk[j] = k;
}
PRINTF("?%d kk[%d:%d]=", j, ii[i], ii[i+1]);
PRINT_INDEX_ARRAY(kk + ii[i], ii[i+1] - ii[i]);
kj = rev_kk[j];
ASSERT(kj != -1);
ASSERT(kk[kj] == j);
h = cc[kj] - v[j] - mind;
PRINTF("i=%d j=%d kj=%d h=%f\n", i, j, kj, h);
// For all columns in TODO
for (uint_t k = 0; k < ii[i+1] - ii[i]; k++) {
j = kk[ii[i] + k];
if (done[j]) {
continue;
}
PRINTF("?%d kk[%d:%d]=", j, ii[i], ii[i+1]);
PRINT_INDEX_ARRAY(kk + ii[i], ii[i+1] - ii[i]);
cred_ij = cc[ii[i] + k] - v[j] - h;
if (cred_ij < d[j]) {
d[j] = cred_ij;
pred[j] = i;
if (cred_ij <= mind) {
if (y[j] < 0) {
FREE(rev_kk);
return j;
}
scan[hi++] = j;
done[j] = TRUE;
} else if (!added[j]) {
todo[n_todo++] = j;
added[j] = TRUE;
}
}
}
for (uint_t k = ii[i]; k < ii[i+1]; k++) {
const int_t j = kk[k];
rev_kk[j] = -1;
}
}
*pn_todo = n_todo;
*pn_ready = n_ready;
*plo = lo;
*phi = hi;
FREE(rev_kk);
return -1;
}
/**
* Single iteration of modified Dijkstra shortest path algorithm as explained in the JV paper.
* This version loops over all column indices (some of which might be inf).
* @return The closest free column index.
*/
int_t find_path_sparse_1(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
const int_t start_i, int_t *y, cost_t *v,int_t *pred)
{
uint_t lo = 0, hi = 0;
int_t final_j = -1;
uint_t n_ready = 0;
int_t *cols;
cost_t *d;
NEW(cols, int_t, n);
NEW(d, cost_t, n);
for (uint_t i = 0; i < n; i++) {
cols[i] = i;
d[i] = LARGE;
pred[i] = start_i;
}
for (uint_t i = ii[start_i]; i < ii[start_i + 1]; i++) {
const int_t j = kk[i];
d[j] = cc[i] - v[j];
}
PRINT_COST_ARRAY(d, n);
while (final_j == -1) {
// No columns left on the SCAN list.
if (lo == hi) {
PRINTF("%d..%d -> find\n", lo, hi);
n_ready = lo;
hi = _find_sparse_1(n, lo, d, cols, y);
PRINTF("check %d..%d\n", lo, hi);
PRINT_INDEX_ARRAY(cols, n);
for (uint_t k = lo; k < hi; k++) {
const int_t j = cols[k];
if (y[j] < 0) {
final_j = j;
}
}
}
if (final_j == -1) {
PRINTF("%d..%d -> scan\n", lo, hi);
final_j = _scan_sparse_1(n, cc, ii, kk, &lo, &hi, d, cols, pred, y, v);
PRINT_COST_ARRAY(d, n);
PRINT_INDEX_ARRAY(cols, n);
PRINT_INDEX_ARRAY(pred, n);
}
}
PRINTF("found final_j=%d\n", final_j);
PRINT_INDEX_ARRAY(cols, n);
{
const cost_t mind = d[cols[lo]];
for (uint_t k = 0; k < n_ready; k++) {
const int_t j = cols[k];
v[j] += d[j] - mind;
}
}
FREE(cols);
FREE(d);
return final_j;
}
/**
* Single iteration of modified Dijkstra shortest path algorithm as explained in the JV paper.
* This version loops over non-inf column indices (which requires some additional bookkeeping).
* @return The closest free column index.
*/
int_t find_path_sparse_2(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
const int_t start_i, int_t *y, cost_t *v, int_t *pred)
{
uint_t lo = 0, hi = 0;
int_t final_j = -1;
uint_t n_ready = 0;
uint_t n_todo = (ii[start_i + 1] - ii[start_i]);
boolean *done, *added;
int_t *ready, *scan, *todo;
cost_t *d;
NEW(done, boolean, n);
NEW(added, boolean, n);
NEW(ready, int_t, n);
NEW(scan, int_t, n);
NEW(todo, int_t, n);
NEW(d, cost_t, n);
memset(done, FALSE, n);
memset(added, FALSE, n);
for (uint_t i = 0; i < n; i++) {
d[i] = LARGE;
pred[i] = start_i;
}
for (uint_t i = ii[start_i]; i < ii[start_i + 1]; i++) {
const int_t j = kk[i];
d[j] = cc[i] - v[j];
todo[i - ii[start_i]] = j;
added[j] = TRUE;
}
PRINT_COST_ARRAY(d, n);
PRINT_INDEX_ARRAY(pred, n);
PRINT_INDEX_ARRAY(done, n);
PRINT_INDEX_ARRAY(ready, n_ready);
PRINT_INDEX_ARRAY(scan + lo, hi - lo);
PRINT_INDEX_ARRAY(todo, n_todo);
PRINT_INDEX_ARRAY(added, n);
while (final_j == -1) {
// No columns left on the SCAN list.
if (lo == hi) {
PRINTF("%d..%d -> find\n", lo, hi);
lo = 0;
hi = _find_sparse_2(d, scan, n_todo, todo, done);
PRINTF("check %d..%d\n", lo, hi);
if (!hi) {
// XXX: the assignment is unsolvable, lets try to return
// something reasonable nevertheless.
for (uint_t j = 0; j < n; j++) {
if (!done[j] && y[j] < 0) {
final_j = j;
}
}
ASSERT(final_j != -1);
break;
}
ASSERT(hi > lo);
for (uint_t k = lo; k < hi; k++) {
const int_t j = scan[k];
if (y[j] < 0) {
final_j = j;
} else {
done[j] = TRUE;
}
}
}
if (final_j == -1) {
PRINTF("%d..%d -> scan\n", lo, hi);
PRINT_INDEX_ARRAY(done, n);
PRINT_INDEX_ARRAY(ready, n_ready);
PRINT_INDEX_ARRAY(scan + lo, hi - lo);
PRINT_INDEX_ARRAY(todo, n_todo);
final_j = _scan_sparse_2(n, cc, ii, kk, &lo, &hi, d, pred,
done, &n_ready, ready, scan,
&n_todo, todo, added, y, v);
PRINT_COST_ARRAY(d, n);
PRINT_INDEX_ARRAY(pred, n);
PRINT_INDEX_ARRAY(done, n);
PRINT_INDEX_ARRAY(ready, n_ready);
PRINT_INDEX_ARRAY(scan + lo, hi - lo);
PRINT_INDEX_ARRAY(todo, n_todo);
PRINT_INDEX_ARRAY(added, n);
}
}
PRINTF("found final_j=%d\n", final_j);
{
const cost_t mind = d[scan[lo]];
for (uint_t k = 0; k < n_ready; k++) {
const int_t j = ready[k];
v[j] += d[j] - mind;
}
}
FREE(done);
FREE(added);
FREE(ready);
FREE(scan);
FREE(todo);
FREE(d);
return final_j;
}
/** Find path using one of the two find_path variants selected based on sparsity. */
int_t find_path_sparse_dynamic(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
const int_t start_i, int_t *y, cost_t *v, int_t *pred)
{
const uint_t n_i = ii[start_i+1] - ii[start_i];
// XXX: wouldnt it be better to decide for the whole matrix?
if (n_i > 0.25 * n) {
return find_path_sparse_1(n, cc, ii, kk, start_i, y, v, pred);
} else {
return find_path_sparse_2(n, cc, ii, kk, start_i, y, v, pred);
}
}
typedef int_t (*fp_function_t)(const uint_t, cost_t *, uint_t *, uint_t *,
const int_t, int_t *, cost_t *, int_t *);
fp_function_t get_better_find_path(const uint_t n, uint_t *ii)
{
const double sparsity = ii[n] / (double)(n * n);
if (sparsity > 0.25) {
PRINTF("Using find_path_sparse_1 for sparsity=%f\n", sparsity);
return find_path_sparse_1;
} else {
PRINTF("Using find_path_sparse_2 for sparsity=%f\n", sparsity);
return find_path_sparse_2;
}
}
/** Augment for a sparse cost matrix. */
int_t _ca_sparse(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk, const uint_t n_free_rows,
int_t *free_rows, int_t *x, int_t *y, cost_t *v, int fp_version)
{
int_t *pred;
NEW(pred, int_t, n);
fp_function_t fp;
switch (fp_version) {
case FP_1: fp = find_path_sparse_1; break;
case FP_2: fp = find_path_sparse_2; break;
case FP_DYNAMIC: fp = get_better_find_path(n, ii); break;
default: return -2;
}
for (int_t *pfree_i = free_rows; pfree_i < free_rows + n_free_rows; pfree_i++) {
int_t i = -1, j;
uint_t k = 0;
PRINTF("looking at free_i=%d\n", *pfree_i);
j = fp(n, cc, ii, kk, *pfree_i, y, v, pred);
ASSERT(j >= 0);
ASSERT(j < n);
while (i != *pfree_i) {
PRINTF("augment %d\n", j);
PRINT_INDEX_ARRAY(pred, n);
i = pred[j];
PRINTF("y[%d]=%d -> %d\n", j, y[j], i);
y[j] = i;
PRINT_INDEX_ARRAY(x, n);
SWAP_INDICES(j, x[i]);
k++;
if (k >= n) {
ASSERT(FALSE);
}
}
}
FREE(pred);
return 0;
}
/** Solve square sparse LAP. */
int lapmod_internal(const uint_t n, cost_t *cc, uint_t *ii, uint_t *kk,
int_t *x, int_t *y, fp_t fp_version)
{
int ret;
int_t *free_rows;
cost_t *v;
NEW(free_rows, int_t, n);
NEW(v, cost_t, n);
ret = _ccrrt_sparse(n, cc, ii, kk, free_rows, x, y, v);
int i = 0;
while (ret > 0 && i < 2) {
ret = _carr_sparse(n, cc, ii, kk, ret, free_rows, x, y, v);
i++;
}
if (ret > 0) {
ret = _ca_sparse(n, cc, ii, kk, ret, free_rows, x, y, v, fp_version);
}
FREE(v);
FREE(free_rows);
return ret;
}
gatagat-lap-bb6ad3a/.github/ 0000775 0001750 0001750 00000000000 15272350177 015355 5 ustar pieter pieter gatagat-lap-bb6ad3a/.github/workflows/ 0000775 0001750 0001750 00000000000 15272350177 017412 5 ustar pieter pieter gatagat-lap-bb6ad3a/.github/workflows/benchmark.yaml 0000664 0001750 0001750 00000001712 15272350177 022231 0 ustar pieter pieter name: Benchmark
on: [workflow_dispatch]
jobs:
build_and_test:
name: "${{ matrix.os }} + python ${{ matrix.python-version }}"
runs-on: "${{ matrix.os }}"
strategy:
fail-fast: false
matrix:
os: ["windows-latest", "ubuntu-22.04", "macos-15-intel"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install lap from GitHub (dynamic branch)
shell: bash
run: |
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
pip install "git+https://github.com/gatagat/lap.git@$BRANCH"
- name: Install scipy
run: pip install scipy
- name: 📊 Run benchmark_simple.py
run: |
cd benchmark
python benchmark_simple.py
gatagat-lap-bb6ad3a/.github/workflows/test_full.yaml 0000664 0001750 0001750 00000007302 15272350177 022301 0 ustar pieter pieter name: Test Full
on: [workflow_dispatch]
jobs:
build_and_test_p37:
name: "${{ matrix.os }} + python ${{ matrix.python-version }}"
runs-on: "${{ matrix.os }}"
strategy:
fail-fast: false
matrix:
os: ["windows-latest", "ubuntu-22.04", "macos-15-intel"]
python-version: ["3.7"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Prepare for build
run: |
python -m pip install --upgrade pip
pip install "setuptools>=67.8.0"
pip install wheel build
- name: 👷 Test build from source ⚒�
run: python -m build --wheel
- name: 👩�💻 Test install from GitHub �
run: pip install git+https://github.com/gatagat/lap.git
- name: 🧪 Run full tests with pytest 👨�⚖�
run: |
pip install -U pytest pytest-timeout numpy scipy
pytest -v --pyargs lap.tests
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: wheel-${{ matrix.os }}-${{ matrix.python-version }}
path: dist/*.whl
build_and_test_p38_p315:
name: "${{ matrix.os }} + python ${{ matrix.python-version }}"
runs-on: "${{ matrix.os }}"
strategy:
fail-fast: false
matrix:
os: ["windows-latest", "ubuntu-24.04", "ubuntu-24.04-arm", "macos-15-intel", "macos-latest"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Prepare for build
run: |
python -m pip install --upgrade pip
pip install "setuptools>=67.8.0"
pip install wheel build
- name: 👷 Test build from source ⚒�
run: python -m build --wheel
- name: 👩�💻 Test install from GitHub �
run: pip install git+https://github.com/gatagat/lap.git
- name: 🧪 Run full tests with pytest 👨�⚖�
run: |
pip install -U pytest pytest-timeout numpy scipy
pytest -v --pyargs lap.tests
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: wheel-${{ matrix.os }}-${{ matrix.python-version }}
path: dist/*.whl
build_and_test_win_arm_p311_p315:
name: "${{ matrix.os }} + python ${{ matrix.python-version }}"
runs-on: "${{ matrix.os }}"
strategy:
fail-fast: false
matrix:
os: ["windows-11-arm"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Prepare for build
run: |
python -m pip install --upgrade pip
pip install "setuptools>=67.8.0"
pip install wheel build
- name: 👷 Test build from source ⚒�
run: python -m build --wheel
- name: 👩�💻 Test install from GitHub �
run: pip install git+https://github.com/gatagat/lap.git
- name: 🧪 Run full tests with pytest 👨�⚖�
run: |
pip install -U pytest pytest-timeout numpy scipy
pytest -v --pyargs lap.tests
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: wheel-${{ matrix.os }}-${{ matrix.python-version }}
path: dist/*.whl
gatagat-lap-bb6ad3a/.github/workflows/test_simple.yaml 0000664 0001750 0001750 00000002532 15272350177 022630 0 ustar pieter pieter name: Test Simple
on: [workflow_dispatch]
jobs:
build_and_test:
name: "${{ matrix.os }} + python ${{ matrix.python-version }}"
runs-on: "${{ matrix.os }}"
strategy:
fail-fast: false
matrix:
os: ["windows-latest", "ubuntu-22.04", "macos-15-intel"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install "setuptools>=67.8.0"
pip install wheel build
- name: 👷 Test build from source ⚒�
run: python -m build --wheel
- name: Test direct install from GitHub (dynamic branch)
shell: bash
run: |
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
pip install "git+https://github.com/gatagat/lap.git@$BRANCH"
- name: 🧪 Run test_simple.py 👩�💻
run: |
cd lap/tests/
python test_simple.py
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: wheel-${{ matrix.os }}-${{ matrix.python-version }}
path: dist/*.whl
gatagat-lap-bb6ad3a/.github/workflows/prepublish.yaml 0000664 0001750 0001750 00000022031 15272350177 022451 0 ustar pieter pieter name: PyPI Build
on: [workflow_dispatch]
jobs:
# # # # # # # # # # # # # # # # # # # # #
# Windows jobs
# # # # # # # # # # # # # # # # # # # # #
build_windows:
name: Build windows (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: windows-latest
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "*-win32 pp*"
CIBW_ARCHS_WINDOWS: "AMD64 ARM64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: windows-all-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_windows_p37:
name: Build windows (Python 3.7)
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "*-win32 pp*"
CIBW_ARCHS_WINDOWS: "AMD64 ARM64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: windows-all-p37-wheels
path: ./wheelhouse/*.whl
# # # # # # # # # # # # # # # # # # # # #
# macOS jobs
# # # # # # # # # # # # # # # # # # # # #
build_macos_intel:
name: Build macos-intel (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: macos-15-intel
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "pp*"
CIBW_ARCHS_MACOS: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: macos-intel-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_macos_intel_p37:
name: Build macos-intel (Python 3.7)
runs-on: macos-15-intel
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "pp*"
CIBW_ARCHS_MACOS: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: macos-intel-p37-wheels
path: ./wheelhouse/*.whl
build_macos_arm:
name: Build macos-arm (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: macos-latest
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "pp*"
CIBW_ARCHS_MACOS: "arm64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: macos-arm-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
# # # # # # # # # # # # # # # # # # # # #
# Linux jobs
# # # # # # # # # # # # # # # # # # # # #
build_linux_intel:
name: Build linux-intel (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: ubuntu-24.04
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "*-manylinux_i686 pp*"
CIBW_ARCHS_LINUX: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: linux-intel-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_linux_intel_p37:
name: Build linux-intel (Python 3.7)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "*-manylinux_i686 pp*"
CIBW_ARCHS_LINUX: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: linux-intel-p37-wheels
path: ./wheelhouse/*.whl
build_linux_arm:
name: Build linux-arm (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: ubuntu-24.04-arm
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "pp*"
CIBW_ARCHS_LINUX: "aarch64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: linux-arm-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_linux_arm_p37:
name: Build linux-arm (Python 3.7)
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "pp*"
CIBW_ARCHS_LINUX: "aarch64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: linux-arm-p37-wheels
path: ./wheelhouse/*.whl
# # # # # # # # # # # # # # # # # # # # #
# Source dist job
# # # # # # # # # # # # # # # # # # # # #
build_source:
name: Build source dist
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install "setuptools>=67.8.0"
pip install wheel build
- name: Build .tar.gz
run: python -m build --sdist
- name: Archive .tar.gz
uses: actions/upload-artifact@v7
with:
name: source-dist
path: dist/*.tar.gz
gatagat-lap-bb6ad3a/.github/workflows/publish.yaml 0000664 0001750 0001750 00000024032 15272350177 021745 0 ustar pieter pieter name: Publish to PyPI
on:
push:
tags:
- "v*"
jobs:
# # # # # # # # # # # # # # # # # # # # #
# Windows jobs
# # # # # # # # # # # # # # # # # # # # #
build_windows:
name: Build windows (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: windows-latest
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "*-win32 pp*"
CIBW_ARCHS_WINDOWS: "AMD64 ARM64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-windows-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_windows_p37:
name: Build windows (Python 3.7)
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "*-win32 pp*"
CIBW_ARCHS_WINDOWS: "AMD64 ARM64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-windows-all-p37-wheels
path: ./wheelhouse/*.whl
# # # # # # # # # # # # # # # # # # # # #
# macOS jobs
# # # # # # # # # # # # # # # # # # # # #
build_macos_intel:
name: Build macos-intel (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: macos-15-intel
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "pp*"
CIBW_ARCHS_MACOS: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-macos-intel-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_macos_intel_p37:
name: Build macos-intel (Python 3.7)
runs-on: macos-15-intel
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "pp*"
CIBW_ARCHS_MACOS: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-macos-intel-p37-wheels
path: ./wheelhouse/*.whl
build_macos_arm:
name: Build macos-arm (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: macos-latest
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "pp*"
CIBW_ARCHS_MACOS: "arm64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-macos-arm-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
# # # # # # # # # # # # # # # # # # # # #
# Linux jobs
# # # # # # # # # # # # # # # # # # # # #
build_linux_intel:
name: Build linux-intel (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: ubuntu-24.04
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "*-manylinux_i686 pp*"
CIBW_ARCHS_LINUX: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-linux-intel-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_linux_intel_p37:
name: Build linux-intel (Python 3.7)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "*-manylinux_i686 pp*"
CIBW_ARCHS_LINUX: "x86_64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-linux-intel-p37-wheels
path: ./wheelhouse/*.whl
build_linux_arm:
name: Build linux-arm (cibuildwheel ${{ matrix.cibuildwheel }})
runs-on: ubuntu-24.04-arm
strategy:
matrix:
cibuildwheel: ["3.3.1", "4.3.5"]
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==${{ matrix.cibuildwheel }}
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: ${{ matrix.cibuildwheel == '3.3.1' && 'cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*' || 'cp315-*' }}
CIBW_SKIP: "pp*"
CIBW_ARCHS_LINUX: "aarch64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-linux-arm-wheels-${{ matrix.cibuildwheel }}
path: ./wheelhouse/*.whl
build_linux_arm_p37:
name: Build linux-arm (Python 3.7)
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel==2.23.3
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BUILD_VERBOSITY: 1
CIBW_BUILD: "cp37-*"
CIBW_SKIP: "pp*"
CIBW_ARCHS_LINUX: "aarch64"
- name: Archive wheels
uses: actions/upload-artifact@v7
with:
name: pypi-linux-arm-p37-wheels
path: ./wheelhouse/*.whl
# # # # # # # # # # # # # # # # # # # # #
# Source dist job
# # # # # # # # # # # # # # # # # # # # #
build_source:
name: Build source dist
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install "setuptools>=67.8.0"
pip install wheel build
- name: Build .tar.gz
run: python -m build --sdist
- name: Archive .tar.gz
uses: actions/upload-artifact@v7
with:
name: pypi-source-dist
path: dist/*.tar.gz
# # # # # # # # # # # # # # # # # # # # #
# Publish job
# # # # # # # # # # # # # # # # # # # # #
publish_pypi:
name: Publish to PyPI
needs:
- build_windows
- build_windows_p37
- build_macos_intel
- build_macos_intel_p37
- build_macos_arm
- build_linux_intel
- build_linux_intel_p37
- build_linux_arm
- build_linux_arm_p37
- build_source
runs-on: ubuntu-24.04
environment: pypi
permissions:
id-token: write
steps:
- name: Collect and extract artifact
uses: actions/download-artifact@v8
with:
path: dist
pattern: pypi-*
merge-multiple: true
- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
gatagat-lap-bb6ad3a/lap/ 0000775 0001750 0001750 00000000000 15272350177 014571 5 ustar pieter pieter gatagat-lap-bb6ad3a/lap/lapmod.py 0000664 0001750 0001750 00000026074 15272350177 016430 0 ustar pieter pieter import numpy as np
from bisect import bisect_left
# import logging
from ._lapjv import _lapmod, FP_DYNAMIC_ as FP_DYNAMIC, LARGE_ as LARGE
def _pycrrt(n, cc, ii, kk, free_rows, x, y, v):
# log = logging.getLogger('do_column_reduction_and_reduction_transfer')
x[:] = -1
y[:] = -1
v[:] = LARGE
for i in range(n):
ks = slice(ii[i], ii[i+1])
js = kk[ks]
ccs = cc[ks]
mask = ccs < v[js]
js = js[mask]
v[js] = ccs[mask]
y[js] = i
# log.debug('v = %s', v)
# for j in range(cost.shape[1]):
unique = np.empty((n,), dtype=bool)
unique[:] = True
for j in range(n-1, -1, -1):
i = y[j]
# If row is not taken yet, initialize it with the minimum stored in y.
if x[i] < 0:
x[i] = j
else:
unique[i] = False
y[j] = -1
# log.debug('bw %s %s %s %s', i, j, x, y)
# log.debug('unique %s', unique)
n_free_rows = 0
for i in range(n):
# Store unassigned row i.
if x[i] < 0:
free_rows[n_free_rows] = i
n_free_rows += 1
elif unique[i] and ii[i+1] - ii[i] > 1:
# >1 check prevents choking on rows with a single entry
# Transfer from an assigned row.
j = x[i]
# Find the current 2nd minimum of the reduced column costs:
# (cost[i,j] - v[j]) for some j.
ks = slice(ii[i], ii[i+1])
js = kk[ks]
minv = np.min(cc[ks][js != j] - v[js][js != j])
# log.debug("v[%d] = %f - %f", j, v[j], minv)
v[j] -= minv
# log.debug('free: %s', free_rows[:n_free_rows])
# log.debug('%s %s', x, v)
return n_free_rows
def find_minima(indices, values):
if len(indices) > 0:
j1 = indices[0]
v1 = values[0]
else:
j1 = 0
v1 = LARGE
j2 = -1
v2 = LARGE
# log = logging.getLogger('find_minima')
# log.debug(sorted(zip(values, indices))[:2])
for j, h in zip(indices[1:], values[1:]):
# log.debug('%d = %f %d = %f', j1, v1, j2, v2)
if h < v2:
if h >= v1:
v2 = h
j2 = j
else:
v2 = v1
v1 = h
j2 = j1
j1 = j
# log.debug('%d = %f %d = %f', j1, v1, j2, v2)
return j1, v1, j2, v2
def _pyarr(n, cc, ii, kk, n_free_rows, free_rows, x, y, v):
# log = logging.getLogger('do_augmenting_row_reduction')
# log.debug('%s %s %s', x, y, v)
current = 0
# log.debug('free: %s', free_rows[:n_free_rows])
new_free_rows = 0
while current < n_free_rows:
free_i = free_rows[current]
# log.debug('current = %d', current)
current += 1
ks = slice(ii[free_i], ii[free_i+1])
js = kk[ks]
j1, v1, j2, v2 = find_minima(js, cc[ks] - v[js])
i0 = y[j1]
v1_new = v[j1] - (v2 - v1)
v1_lowers = v1_new < v[j1]
# log.debug(
# '%d %d 1=%s,%f 2=%s,%f %f %s',
# free_i, i0, j1, v1, j2, v2, v1_new, v1_lowers)
if v1_lowers:
v[j1] = v1_new
elif i0 >= 0 and j2 != -1: # i0 is assigned, try j2
j1 = j2
i0 = y[j2]
x[free_i] = j1
y[j1] = free_i
if i0 >= 0:
if v1_lowers:
current -= 1
# log.debug('continue augmenting path from current %s %s %s')
free_rows[current] = i0
else:
# log.debug('stop the augmenting path and keep for later')
free_rows[new_free_rows] = i0
new_free_rows += 1
# log.debug('free: %s', free_rows[:new_free_rows])
return new_free_rows
def binary_search(data, key):
# log = logging.getLogger('binary_search')
i = bisect_left(data, key)
# log.debug('Found data[%d]=%d for %d', i, data[i], key)
if i < len(data) and data[i] == key:
return i
else:
return None
def _find(hi, d, cols, y):
lo, hi = hi, hi + 1
minv = d[cols[lo]]
# XXX: anytime this happens to be NaN, i'm screwed...
# assert not np.isnan(minv)
for k in range(hi, len(cols)):
j = cols[k]
if d[j] <= minv:
# New minimum found, trash the new SCAN columns found so far.
if d[j] < minv:
hi = lo
minv = d[j]
cols[k], cols[hi] = cols[hi], j
hi += 1
return minv, hi, cols
def _scan(n, cc, ii, kk, minv, lo, hi, d, cols, pred, y, v):
# log = logging.getLogger('_scan')
# Scan all TODO columns.
while lo != hi:
j = cols[lo]
lo += 1
i = y[j]
# log.debug('?%d kk[%d:%d]=%s', j, ii[i], ii[i+1], kk[ii[i]:ii[i+1]])
kj = binary_search(kk[ii[i]:ii[i+1]], j)
if kj is None:
continue
kj = ii[i] + kj
h = cc[kj] - v[j] - minv
# log.debug('i=%d j=%d kj=%s h=%f', i, j, kj, h)
for k in range(hi, n):
j = cols[k]
kj = binary_search(kk[ii[i]:ii[i+1]], j)
if kj is None:
continue
kj = ii[i] + kj
cred_ij = cc[kj] - v[j] - h
if cred_ij < d[j]:
d[j] = cred_ij
pred[j] = i
if cred_ij == minv:
if y[j] < 0:
return j, None, None, d, cols, pred
cols[k] = cols[hi]
cols[hi] = j
hi += 1
return -1, lo, hi, d, cols, pred
def find_path(n, cc, ii, kk, start_i, y, v):
# log = logging.getLogger('find_path')
cols = np.arange(n, dtype=int)
pred = np.empty((n,), dtype=int)
pred[:] = start_i
d = np.empty((n,), dtype=float)
d[:] = LARGE
ks = slice(ii[start_i], ii[start_i+1])
js = kk[ks]
d[js] = cc[ks] - v[js]
# log.debug('d = %s', d)
minv = LARGE
lo, hi = 0, 0
n_ready = 0
final_j = -1
while final_j == -1:
# No SCAN columns, find new ones.
if lo == hi:
# log.debug('%d..%d -> find', lo, hi)
# log.debug('cols = %s', cols)
n_ready = lo
minv, hi, cols = _find(hi, d, cols, y)
# log.debug('%d..%d -> check', lo, hi)
# log.debug('cols = %s', cols)
# log.debug('y = %s', y)
for h in range(lo, hi):
# If any of the new SCAN columns is unassigned, use it.
if y[cols[h]] < 0:
final_j = cols[h]
if final_j == -1:
# log.debug('%d..%d -> scan', lo, hi)
final_j, lo, hi, d, cols, pred = _scan(
n, cc, ii, kk, minv, lo, hi, d, cols, pred, y, v)
# log.debug('d = %s', d)
# log.debug('cols = %s', cols)
# log.debug('pred = %s', pred)
# Update prices for READY columns.
for k in range(n_ready):
j0 = cols[k]
v[j0] += d[j0] - minv
assert final_j >= 0
assert final_j < n
return final_j, pred
def _pya(n, cc, ii, kk, n_free_rows, free_rows, x, y, v):
# log = logging.getLogger('augment')
for free_i in free_rows[:n_free_rows]:
# log.debug('looking at free_i=%s', free_i)
j, pred = find_path(n, cc, ii, kk, free_i, y, v)
# Augment the path starting from column j and backtracking to free_i.
i = -1
while i != free_i:
# log.debug('augment %s', j)
# log.debug('pred = %s', pred)
i = pred[j]
assert i >= 0
assert i < n
# log.debug('y[%d]=%d -> %d', j, y[j], i)
y[j] = i
j, x[i] = x[i], j
def check_cost(n, cc, ii, kk):
if n == 0:
raise ValueError('Cost matrix has zero rows.')
if len(kk) == 0:
raise ValueError('Cost matrix has zero columns.')
lo = cc.min()
hi = cc.max()
if lo < 0:
raise ValueError('Cost matrix values must be non-negative.')
if hi >= LARGE:
raise ValueError(
'Cost matrix values must be less than %s' % LARGE)
def get_cost(n, cc, ii, kk, x0):
ret = 0
for i, j in enumerate(x0):
kj = binary_search(kk[ii[i]:ii[i+1]], j)
if kj is None:
return np.inf
kj = ii[i] + kj
ret += cc[kj]
return ret
def lapmod(n, cc, ii, kk, fast=True, return_cost=True, fp_version=FP_DYNAMIC):
"""Solve sparse linear assignment problem using Jonker-Volgenant algorithm.
n: number of rows of the assignment cost matrix
cc: 1D array of all finite elements of the assignment cost matrix
ii: 1D array of indices of the row starts in cc. The following must hold:
ii[0] = 0 and ii[n+1] = len(cc).
kk: 1D array of the column indices so that:
cost[i, kk[ii[i] + k]] == cc[ii[i] + k].
Indices within one row must be sorted.
extend_cost: whether or not extend a non-square matrix [default: False]
cost_limit: an upper limit for a cost of a single assignment
[default: np.inf]
return_cost: whether or not to return the assignment cost
Returns (opt, x, y) where:
opt: cost of the assignment
x: vector of columns assigned to rows
y: vector of rows assigned to columns
or (x, y) if return_cost is not True.
When extend_cost and/or cost_limit is set, all unmatched entries will be
marked by -1 in x/y.
"""
# log = logging.getLogger('lapmod')
check_cost(n, cc, ii, kk)
if fast is True:
# log.debug('[----CR & RT & ARR & augmentation ----]')
x, y = _lapmod(n, cc, ii, kk, fp_version=fp_version)
else:
cc = np.ascontiguousarray(cc, dtype=np.float64)
ii = np.ascontiguousarray(ii, dtype=np.int32)
kk = np.ascontiguousarray(kk, dtype=np.int32)
x = np.empty((n,), dtype=np.int32)
y = np.empty((n,), dtype=np.int32)
v = np.empty((n,), dtype=np.float64)
free_rows = np.empty((n,), dtype=np.int32)
# log.debug('[----Column reduction & reduction transfer----]')
n_free_rows = _pycrrt(n, cc, ii, kk, free_rows, x, y, v)
# log.debug(
# 'free, x, y, v: %s %s %s %s', free_rows[:n_free_rows], x, y, v)
if n_free_rows == 0:
# log.info('Reduction solved it.')
if return_cost is True:
return get_cost(n, cc, ii, kk, x), x, y
else:
return x, y
for it in range(2):
# log.debug('[---Augmenting row reduction (iteration: %d)---]', it)
n_free_rows = _pyarr(
n, cc, ii, kk, n_free_rows, free_rows, x, y, v)
# log.debug(
# 'free, x, y, v: %s %s %s %s', free_rows[:n_free_rows], x, y, v)
if n_free_rows == 0:
# log.info('Augmenting row reduction solved it.')
if return_cost is True:
return get_cost(n, cc, ii, kk, x), x, y
else:
return x, y
# log.info('[----Augmentation----]')
_pya(n, cc, ii, kk, n_free_rows, free_rows, x, y, v)
# log.debug('x, y, v: %s %s %s', x, y, v)
if return_cost is True:
return get_cost(n, cc, ii, kk, x), x, y
else:
return x, y
gatagat-lap-bb6ad3a/lap/__init__.py 0000664 0001750 0001750 00000001054 15272350177 016702 0 ustar pieter pieter """LAP
``lap`` is a linear assignment problem solver using Jonker-Volgenant
algorithm for dense (LAPJV) or sparse (LAPMOD) matrices.
Functions
---------
lapjv
Find optimal (minimum-cost) assignment for a dense cost matrix.
lapmod
Find optimal (minimum-cost) assignment for a sparse cost matrix.
"""
__version__ = '0.5.14'
from ._lapjv import (
lapjv,
LARGE_ as LARGE,
FP_1_ as FP_1,
FP_2_ as FP_2,
FP_DYNAMIC_ as FP_DYNAMIC
)
from .lapmod import lapmod
__all__ = ['lapjv', 'lapmod', 'FP_1', 'FP_2', 'FP_DYNAMIC', 'LARGE']
gatagat-lap-bb6ad3a/lap/tests/ 0000775 0001750 0001750 00000000000 15272350177 015733 5 ustar pieter pieter gatagat-lap-bb6ad3a/lap/tests/test_simple.py 0000664 0001750 0001750 00000000130 15272350177 020627 0 ustar pieter pieter import lap
import numpy as np
print(lap.lapjv(np.random.rand(4, 5), extend_cost=True))
gatagat-lap-bb6ad3a/lap/tests/test_lapjv.py 0000664 0001750 0001750 00000022136 15272350177 020464 0 ustar pieter pieter from pytest import mark, fixture, raises
import numpy as np
from lap import lapjv
from .test_utils import (
get_dense_8x8_int,
get_dense_100x100_int, get_dense_100x100_int_hard, get_sparse_100x100_int,
get_dense_1kx1k_int, get_dense_1kx1k_int_hard, get_sparse_1kx1k_int,
get_sparse_4kx4k_int,
get_dense_eps,
get_platform_maxint
)
def test_lapjv_empty():
with raises(ValueError):
lapjv(np.ndarray([]))
def test_lapjv_non_square_fail():
with raises(ValueError):
lapjv(np.zeros((3, 2)))
def test_lapjv_non_contigous():
cost = get_dense_8x8_int()[0]
ret = lapjv(cost[:3, :3])
assert ret[0] == 8.0
assert np.all(ret[1] == [1, 2, 0])
assert np.all(ret[2] == [2, 0, 1])
def test_lapjv_extension():
cost = get_dense_8x8_int()[0]
ret = lapjv(cost[:2, :4], extend_cost=True)
assert ret[0] == 3.0
assert np.all(ret[1] == [1, 2])
assert np.all(ret[2] == [-1, 0, 1, -1])
def test_lapjv_noextension():
cost = get_dense_8x8_int()[0]
c = np.r_[cost[:2, :4],
[[1001, 1001, 1001, 2001], [2001, 1001, 1001, 1001]]]
ret = lapjv(c, extend_cost=False)
assert ret[0] - 2002 == 3.0
assert np.all(ret[1] == [1, 2, 0, 3])
assert np.all(ret[2] == [2, 0, 1, 3])
def test_lapjv_cost_limit():
cost = get_dense_8x8_int()[0]
ret = lapjv(cost[:3, :3], cost_limit=4.99)
assert ret[0] == 3.0
assert np.all(ret[1] == [1, 2, -1])
assert np.all(ret[2] == [-1, 0, 1])
@mark.parametrize('cost,expected', [
(np.array([[1000, 2, 11, 10, 8, 7, 6, 5],
[6, 1000, 1, 8, 8, 4, 6, 7],
[5, 12, 1000, 11, 8, 12, 3, 11],
[11, 9, 10, 1000, 1, 9, 8, 10],
[11, 11, 9, 4, 1000, 2, 10, 9],
[12, 8, 5, 2, 11, 1000, 11, 9],
[10, 11, 12, 10, 9, 12, 1000, 3],
[10, 10, 10, 10, 6, 3, 1, 1000]]),
(17.0, [1, 2, 0, 4, 5, 3, 7, 6], [2, 0, 1, 5, 3, 4, 7, 6])),
# Solved in column reduction.
(np.array([[1000, 4, 1],
[1, 1000, 3],
[5, 1, 1000]]),
(3., [2, 0, 1], [1, 2, 0])),
# Solved in augmenting row reduction.
(np.array([[5, 1000, 3],
[1000, 2, 2],
[1, 5, 1000]]),
(6., [2, 1, 0], [2, 1, 0])),
# Needs augmentating row reduction - only a single row previously assigned.
(np.array([[1000, 1000+1, 1000],
[1000, 1000, 1000+1],
[1, 2, 3]]),
(1000+1000+1., [2, 1, 0], [2, 1, 0])),
# Triggers the trackmate bug
# Solution is ambiguous, [1, 0, 2] gives the same cost, depends on whether
# in column reduction columns are iterated over from largest to smallest or
# the other way around.
(np.array([[10, 10, 13],
[4, 8, 8],
[8, 5, 8]]),
(13+4+5, [2, 0, 1], [1, 2, 0])),
(np.array([[11, 10, 6],
[10, 11, 11],
[11, 12, 15]]),
(6+10+12, [2, 0, 1], [1, 2, 0])),
(np.array([[12, 4, 9],
[16, 15, 14],
[19, 13, 17]]),
(4+16+17, [1, 0, 2], [1, 0, 2])),
(np.array([[2, 5, 7],
[7, 10, 12],
[1, 5, 9]]),
(7+10+1, [2, 1, 0], [2, 1, 0])),
# This triggered error in augmentation.
(np.array([[10, 6, 14, 1],
[17, 18, 17, 15],
[14, 17, 15, 8],
[11, 13, 11, 4]]),
(6+17+14+4, [1, 2, 0, 3], [2, 0, 1, 3])),
# Test matrix from centrosome
(np.array([[10, 10, 13],
[4, 8, 8],
[8, 5, 8]]),
(22., [2, 0, 1], [1, 2, 0])),
# Test matrix from centrosome
(np.array([[2, 5, 7],
[7, 10, 12],
[1, 5, 9]]),
(18., [2, 1, 0], [2, 1, 0])),
])
def test_square(cost, expected):
ret = lapjv(cost)
assert len(ret) == len(expected)
assert cost[range(cost.shape[0]), ret[1]].sum() == ret[0]
assert cost[ret[2], range(cost.shape[1])].sum() == ret[0]
assert ret[0] == expected[0]
assert np.all(ret[1] == expected[1])
assert np.all(ret[2] == expected[2])
@mark.parametrize('cost,expected', [
(np.array([[11., 20., np.inf, np.inf, np.inf],
[12., np.inf, 12., np.inf, np.inf],
[np.inf, 11., 10., 15., 9.],
[15., np.inf, np.inf, 22., np.inf],
[13., np.inf, np.inf, np.inf, 15.]], dtype=float),
(11+12+11+22+15, [0, 2, 1, 3, 4], [0, 2, 1, 3, 4])),
])
def test_sparse_square(cost, expected):
ret = lapjv(cost)
assert len(ret) == len(expected)
assert cost[range(cost.shape[0]), ret[1]].sum() == ret[0]
assert cost[ret[2], range(cost.shape[1])].sum() == ret[0]
assert ret[0] == expected[0]
assert np.all(ret[1] == expected[1])
assert np.all(ret[2] == expected[2])
# This test triggers a possibly infinite loop in ARR.
@mark.timeout(60)
def test_infs_unsolvable():
cost = np.array([[0., 0., 0., np.inf, np.inf],
[np.inf, np.inf, np.inf, 0., 0.],
[np.inf, np.inf, np.inf, 0., 0.],
[np.inf, np.inf, np.inf, 0., 0.],
[0., 0., 0., np.inf, np.inf]], dtype=float)
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == np.inf
cost = np.array([[19., 22., 16., np.inf, np.inf],
[np.inf, np.inf, np.inf, 4., 13.],
[np.inf, np.inf, np.inf, 3., 14.],
[np.inf, np.inf, np.inf, 10., 12.],
[11., 14., 13., np.inf, np.inf]], dtype=float)
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == np.inf
def test_inf_unique():
cost = np.array([[1000, 4, 1],
[1, 1000, 3],
[5, 1, 1000]])
cost_ext = np.empty((4, 4))
cost_ext[:] = np.inf
cost_ext[:3, :3] = cost
cost_ext[3, 3] = 0
ret = lapjv(cost_ext)
assert len(ret) == 3
assert ret[0] == 3.
assert np.all(ret[1] == [2, 0, 1, 3])
@mark.timeout(2)
def test_inf_col():
cost = np.array([[0., np.inf, 0., 0., np.inf],
[np.inf, np.inf, 0., 0., 0.],
[np.inf, np.inf, np.inf, 0., np.inf],
[np.inf, np.inf, np.inf, 0., 0.],
[0., np.inf, 0., np.inf, np.inf]], dtype=float)
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == np.inf
@mark.timeout(2)
def test_inf_row():
cost = np.array([[0., 0., 0., 0., np.inf],
[np.inf, np.inf, 0., 0., 0.],
[np.inf, np.inf, np.inf, np.inf, np.inf],
[np.inf, np.inf, np.inf, 0., 0.],
[0., 0., 0., np.inf, np.inf]], dtype=float)
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == np.inf
def test_all_inf():
cost = np.empty((5, 5), dtype=float)
cost[:] = np.inf
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == np.inf
@fixture
def dense_8x8_int():
return get_dense_8x8_int()
@fixture
def dense_100x100_int():
return get_dense_100x100_int()
@fixture
def dense_100x100_int_hard():
return get_dense_100x100_int_hard()
@fixture
def sparse_100x100_int():
return get_sparse_100x100_int()
@fixture
def dense_1kx1k_int():
return get_dense_1kx1k_int()
@fixture
def dense_1kx1k_int_hard():
return get_dense_1kx1k_int_hard()
@fixture
def sparse_1kx1k_int():
return get_sparse_1kx1k_int()
@fixture
def sparse_4kx4k_int():
return get_sparse_4kx4k_int()
@fixture
def dense_eps():
return get_dense_eps()
@mark.timeout(60)
def test_eps(dense_eps):
cost, opt = dense_eps
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
def test_dense_100x100_int(dense_100x100_int):
cost, opt = dense_100x100_int
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
def test_dense_100x100_int_hard(dense_100x100_int_hard):
cost, opt = dense_100x100_int_hard
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
# TODO: too sparse unsolvable matrices cause sometimne IndexError, easily
# generated - just set the mask threshold low enough
def test_sparse_100x100_int(sparse_100x100_int):
cost, mask, opt = sparse_100x100_int
cost[~mask] = get_platform_maxint()
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
@mark.timeout(60)
def test_dense_1kx1k_int(dense_1kx1k_int):
cost, opt = dense_1kx1k_int
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
@mark.timeout(60)
def test_dense_1kx1k_int_hard(dense_1kx1k_int_hard):
cost, opt = dense_1kx1k_int_hard
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
@mark.timeout(60)
def test_sparse_1kx1k_int(sparse_1kx1k_int):
cost, mask, opt = sparse_1kx1k_int
cost[~mask] = get_platform_maxint()
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
@mark.timeout(60)
def test_sparse_4kx4k_int(sparse_4kx4k_int):
cost, mask, opt = sparse_4kx4k_int
cost[~mask] = get_platform_maxint()
ret = lapjv(cost)
assert len(ret) == 3
assert ret[0] == opt
gatagat-lap-bb6ad3a/lap/tests/test_arr_loop.py 0000664 0001750 0001750 00000005366 15272350177 021173 0 ustar pieter pieter from pytest import approx
import numpy as np
from lap import lapmod, lapjv
def prepare_sparse_cost(shape, cc, ii, jj, cost_limit):
'''
Transform the given sparse matrix extending it to a square sparse matrix.
Parameters
==========
shape: tuple
- cost matrix shape
(cc, ii, jj): tuple of floats, ints, ints)
- cost matrix in COO format, see [1]
cost_limit: float
Returns
=======
cc, ii, kk
- extended square cost matrix in CSR format
1. https://en.wikipedia.org/wiki/Sparse_matrix
'''
assert cost_limit < np.inf
n, m = shape
cc_ = np.r_[cc, [cost_limit] * n,
[cost_limit] * m, [0] * len(cc)]
ii_ = np.r_[ii, np.arange(0, n, dtype=np.uint32),
np.arange(n, n + m, dtype=np.uint32), n + jj]
jj_ = np.r_[jj, np.arange(m, n + m, dtype=np.uint32),
np.arange(0, m, dtype=np.uint32), m + ii]
order = np.lexsort((jj_, ii_))
cc_ = cc_[order]
kk_ = jj_[order]
ii_ = ii_.astype(np.intp)
ii_ = np.bincount(ii_, minlength=shape[0]-1)
ii_ = np.r_[[0], np.cumsum(ii_)]
ii_ = ii_.astype(np.uint32)
assert ii_[-1] == 2 * len(cc) + n + m
return cc_, ii_, kk_
def test_lapjv_arr_loop():
shape = (7, 3)
cc = np.array([
2.593883482138951146e-01, 3.080381437461217620e-01,
1.976243020727339317e-01, 2.462740976049606068e-01,
4.203993396282833528e-01, 4.286184525458427985e-01,
1.706431415909629434e-01, 2.192929371231896185e-01,
2.117769622802734286e-01, 2.604267578125001315e-01])
ii = np.array([0, 0, 1, 1, 2, 2, 5, 5, 6, 6])
jj = np.array([0, 1, 0, 1, 1, 2, 0, 1, 0, 1])
cost = np.empty(shape)
cost[:] = 1000.
cost[ii, jj] = cc
opt, ind1, ind0 = lapjv(cost, extend_cost=True, return_cost=True)
assert opt == approx(0.8455356917416, 1e-10)
assert np.all(ind0 == [5, 1, 2]) or np.all(ind0 == [1, 5, 2])
def test_lapmod_arr_loop():
shape = (7, 3)
cc = np.array([
2.593883482138951146e-01, 3.080381437461217620e-01,
1.976243020727339317e-01, 2.462740976049606068e-01,
4.203993396282833528e-01, 4.286184525458427985e-01,
1.706431415909629434e-01, 2.192929371231896185e-01,
2.117769622802734286e-01, 2.604267578125001315e-01])
ii = np.array([0, 0, 1, 1, 2, 2, 5, 5, 6, 6])
jj = np.array([0, 1, 0, 1, 1, 2, 0, 1, 0, 1])
cost_limit = 1e3
cc, ii, kk = prepare_sparse_cost(shape, cc, ii, jj, cost_limit)
opt, ind1, ind0 = lapmod(len(ii)-1, cc, ii, kk, return_cost=True)
ind1[ind1 >= shape[1]] = -1
ind0[ind0 >= shape[0]] = -1
ind1 = ind1[:shape[0]]
ind0 = ind0[:shape[1]]
assert opt == approx(4000.8455356917416, 1e-10)
assert np.all(ind0 == [5, 1, 2]) or np.all(ind0 == [1, 5, 2])
gatagat-lap-bb6ad3a/lap/tests/__init__.py 0000664 0001750 0001750 00000000000 15272350177 020032 0 ustar pieter pieter gatagat-lap-bb6ad3a/lap/tests/cost_eps.csv.gz 0000664 0001750 0001750 00005314216 15272350177 020722 0 ustar pieter pieter ‹{úUtest_cost_eps.csv T�[–4;n«ßÿ©¸]K
ÝÔóŸ‚ñ!Rd¸ÏYv{׮ʅD‘ Ö¿Ýö¥�ÙÏ<íyöóßÿ)å?õoµZûhM?¬{Ô5êï§þSZyNïO[ïoÌ¿^ÆjOÛO-{õVVûïÿ–úŸÃgÔ§Ö5[ïuîÓŽPÿÚXëÑo´³úóÌ2çï3JëcìÆ*»<ã‰oµöyJií”U÷î}ý~0ô…¶~0Û˜sîyÞߨïQú³õ/׺õ1÷Ãשóé§—¶*¿¡oÅoô?}�§Ï³êxŽ~Žõû�g—3ø:Z‘Ywm÷[m}HÓ‡¬®_êU_å÷ƒ¹ÊSž¢µ\k®}Ÿã´6K?ÏÑ�Gÿý÷ƒ^ôÛgèïͦÇ+ý]öñWÖ³õ�úsôWžUµ$ï·šúm=GãcõÜc÷\’uúìãÑŸ9û”y—]oàh¡Îisè¿Öóû�¶´ìzíçÑzi󷈳kÑùûõ´½ÇS÷ïOõÑ[_O=µÍ¾´+îû¨Ú8zŸ»ë¥?k<ín탃/»>¾ùì©ï{›òÙã.¢ÞÑÚü%LZ‚û=ÁÑŸŸcóù[¿±ù^k/~{Þo¥¨¾ÎdUøÒwÙÇ*³vmr½amêùûS�~£hã,=ug>úá]öÁ©ØC»žÝ Ãð;ú’½ë…?¥�Rõ(í÷G«pØtK«8k«÷Eé´ÌÂöáWôrË]«;õ+»,í¹¡½»¤k©Ö£�ízËO9÷à¬G›}itxô2w¿oµÖÒ'ÔªÇñËíãnj=Åáˆ4~§h×ÝÏЙ¨ÚXú#z'ZÞûuçÔ!®}rÔ¼š+±®ºVÓ™ÝSoäþ)½!íÁ©S{´6;?ï´5dé¼Ãy?£Ž®g²Ÿª8ô{kè´î‡ S´Ç´*¿çP\mí¹yõK´N‹ó©³V´$eÞ×lk�£sµõZÊ<¿ÝÞçP(Ó³—:9êåµýh¿Ï±´»–VPûw8õvo
™Ú¸S!fÜ]¢3¯OO¢Ã¬G-£Å‡-£^Ü`OkÕˆ»úó“UÕ.ÒWï·ZÚê¼}†6ºþܾßjèã†6ƒð-×]ö×wÕÛx}ö/Š>ƒÿS;Æ£/õèøO)„÷¢Ý¤O~´I§‚þÝ»›«ÕÒcêÍÖþÛ¢Ï/¡è›*4LÙ1Qw�¾ís+j%¨ôßslB·®”¸¹[nxÕ™Xsèèô¹IµM~{W[J/|ëvÒ69zïó÷uuäu?h�üBߺ_W—YS Ó›ÓfœºMîÁÑùÖFQ\M1^ñ}ŽóWY(EÉÖ[{<î©mCÁ�÷ÕõÚÇ}À6?öà28ºÚºNw¦¢FÕ9Ó«U˜?-Â+· ¾OÑ>jz³ýwã?¬—–!"œ¶¸6¡žŒ}Õ[\EZÑ]ˆ¾ºµÎëÆ]s½9턇‹ˆõ¿—°þ¸nµVN†a‰u=\³Sí—ˆÔÚùeö¦ 4uæÚ3c3tm¹3xý¼æ©0¬ðlEEý§Çn_�“ªÝ¬3ª]='J{WoV_T{KG±ˆb´NE—Žr¥
q÷(ôU-/DïuÖûEÿŽb®^•–i=Zä¸�ðAôIÚ;ß¹6[c-ôJZSÈ®ñ
�Ú†�.JmÔ½#Èh-ªÂÉÒKWK2�ß¾Z|ŽV¹N‡Œ¿ÍEÀ"èë
()Ò•¶û¸ööM¼ºNåÖÂŽ¢+¡ëbÿ%^Ša2Ρ–„g÷öŸ¾Žî>½G†Á"Þݾ:‡~*Ô±ÕoE„Ó•k|×R wÙ¯Z©—ö•r�CwTaçpo¶ó}z�º…š~I2â9ôÚÎÚ›zD½òH&*\ŠZõ¯Ý½«,¨èfÒ:\FûÜÏÐ'ËØŠ
~òûáúcZÀ}È>Zf–UR—¿^#Y†¶{„¥AÏ~³H;íÈ›““¬÷‚ÒÞQd‰ÃIbªèª-ñpXîŸÒ#Vrëôh©ï‡å%Z`%~Ú-œžxÀªOÑÞT–¬5$½Ž,\¡EçFßZAE_+Žiý}§£½C¶ö{ç:ôd&•$¯ò˜õ÷µ¡Éãµ=õ%ŽDdúzÓ|%✨Ö=²ðÆ…3:´Iu#̈Š_º“7á—ä~]m’鯵ÔEØg°ëëêù´ŽZp%¨G©ÐÍK”…“:²ïtÞGNmýuÝ,:k{žã�+ž+²}o“û¿u1+¹T€#}8ñ€Š,Jq”¦ê{�ûÊ2”y