fasttps 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fasttps-0.2.0/Cargo.lock +287 -0
- fasttps-0.2.0/Cargo.toml +16 -0
- fasttps-0.2.0/PKG-INFO +12 -0
- fasttps-0.2.0/README.md +4 -0
- fasttps-0.2.0/pyproject.toml +13 -0
- fasttps-0.2.0/src/lib.rs +503 -0
fasttps-0.2.0/Cargo.lock
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 3
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "autocfg"
|
|
7
|
+
version = "1.5.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "cfg-if"
|
|
13
|
+
version = "1.0.4"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "fasttps"
|
|
19
|
+
version = "0.2.0"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"fixedbitset",
|
|
22
|
+
"ndarray",
|
|
23
|
+
"numpy",
|
|
24
|
+
"ordered-float",
|
|
25
|
+
"pyo3",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "fixedbitset"
|
|
30
|
+
version = "0.4.2"
|
|
31
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
32
|
+
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
|
33
|
+
|
|
34
|
+
[[package]]
|
|
35
|
+
name = "heck"
|
|
36
|
+
version = "0.5.0"
|
|
37
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
38
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
39
|
+
|
|
40
|
+
[[package]]
|
|
41
|
+
name = "indoc"
|
|
42
|
+
version = "2.0.7"
|
|
43
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
44
|
+
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
45
|
+
dependencies = [
|
|
46
|
+
"rustversion",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[[package]]
|
|
50
|
+
name = "libc"
|
|
51
|
+
version = "0.2.182"
|
|
52
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
53
|
+
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
|
54
|
+
|
|
55
|
+
[[package]]
|
|
56
|
+
name = "matrixmultiply"
|
|
57
|
+
version = "0.3.10"
|
|
58
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
59
|
+
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
|
60
|
+
dependencies = [
|
|
61
|
+
"autocfg",
|
|
62
|
+
"rawpointer",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[[package]]
|
|
66
|
+
name = "memoffset"
|
|
67
|
+
version = "0.9.1"
|
|
68
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
69
|
+
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
|
70
|
+
dependencies = [
|
|
71
|
+
"autocfg",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[[package]]
|
|
75
|
+
name = "ndarray"
|
|
76
|
+
version = "0.16.1"
|
|
77
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
78
|
+
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
|
|
79
|
+
dependencies = [
|
|
80
|
+
"matrixmultiply",
|
|
81
|
+
"num-complex",
|
|
82
|
+
"num-integer",
|
|
83
|
+
"num-traits",
|
|
84
|
+
"portable-atomic",
|
|
85
|
+
"portable-atomic-util",
|
|
86
|
+
"rawpointer",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[[package]]
|
|
90
|
+
name = "num-complex"
|
|
91
|
+
version = "0.4.6"
|
|
92
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
93
|
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
|
94
|
+
dependencies = [
|
|
95
|
+
"num-traits",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[[package]]
|
|
99
|
+
name = "num-integer"
|
|
100
|
+
version = "0.1.46"
|
|
101
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
102
|
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
|
103
|
+
dependencies = [
|
|
104
|
+
"num-traits",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[[package]]
|
|
108
|
+
name = "num-traits"
|
|
109
|
+
version = "0.2.19"
|
|
110
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
111
|
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
112
|
+
dependencies = [
|
|
113
|
+
"autocfg",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[[package]]
|
|
117
|
+
name = "numpy"
|
|
118
|
+
version = "0.22.1"
|
|
119
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
120
|
+
checksum = "edb929bc0da91a4d85ed6c0a84deaa53d411abfb387fc271124f91bf6b89f14e"
|
|
121
|
+
dependencies = [
|
|
122
|
+
"libc",
|
|
123
|
+
"ndarray",
|
|
124
|
+
"num-complex",
|
|
125
|
+
"num-integer",
|
|
126
|
+
"num-traits",
|
|
127
|
+
"pyo3",
|
|
128
|
+
"rustc-hash",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
[[package]]
|
|
132
|
+
name = "once_cell"
|
|
133
|
+
version = "1.21.3"
|
|
134
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
135
|
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|
136
|
+
|
|
137
|
+
[[package]]
|
|
138
|
+
name = "ordered-float"
|
|
139
|
+
version = "3.9.2"
|
|
140
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
141
|
+
checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc"
|
|
142
|
+
dependencies = [
|
|
143
|
+
"num-traits",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
[[package]]
|
|
147
|
+
name = "portable-atomic"
|
|
148
|
+
version = "1.13.1"
|
|
149
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
150
|
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
151
|
+
|
|
152
|
+
[[package]]
|
|
153
|
+
name = "portable-atomic-util"
|
|
154
|
+
version = "0.2.5"
|
|
155
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
156
|
+
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
|
157
|
+
dependencies = [
|
|
158
|
+
"portable-atomic",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
[[package]]
|
|
162
|
+
name = "proc-macro2"
|
|
163
|
+
version = "1.0.106"
|
|
164
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
165
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
166
|
+
dependencies = [
|
|
167
|
+
"unicode-ident",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
[[package]]
|
|
171
|
+
name = "pyo3"
|
|
172
|
+
version = "0.22.6"
|
|
173
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
174
|
+
checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
|
|
175
|
+
dependencies = [
|
|
176
|
+
"cfg-if",
|
|
177
|
+
"indoc",
|
|
178
|
+
"libc",
|
|
179
|
+
"memoffset",
|
|
180
|
+
"once_cell",
|
|
181
|
+
"portable-atomic",
|
|
182
|
+
"pyo3-build-config",
|
|
183
|
+
"pyo3-ffi",
|
|
184
|
+
"pyo3-macros",
|
|
185
|
+
"unindent",
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
[[package]]
|
|
189
|
+
name = "pyo3-build-config"
|
|
190
|
+
version = "0.22.6"
|
|
191
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
192
|
+
checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
|
|
193
|
+
dependencies = [
|
|
194
|
+
"once_cell",
|
|
195
|
+
"target-lexicon",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
[[package]]
|
|
199
|
+
name = "pyo3-ffi"
|
|
200
|
+
version = "0.22.6"
|
|
201
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
202
|
+
checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
|
|
203
|
+
dependencies = [
|
|
204
|
+
"libc",
|
|
205
|
+
"pyo3-build-config",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
[[package]]
|
|
209
|
+
name = "pyo3-macros"
|
|
210
|
+
version = "0.22.6"
|
|
211
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
+
checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
|
|
213
|
+
dependencies = [
|
|
214
|
+
"proc-macro2",
|
|
215
|
+
"pyo3-macros-backend",
|
|
216
|
+
"quote",
|
|
217
|
+
"syn",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
[[package]]
|
|
221
|
+
name = "pyo3-macros-backend"
|
|
222
|
+
version = "0.22.6"
|
|
223
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
224
|
+
checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
|
|
225
|
+
dependencies = [
|
|
226
|
+
"heck",
|
|
227
|
+
"proc-macro2",
|
|
228
|
+
"pyo3-build-config",
|
|
229
|
+
"quote",
|
|
230
|
+
"syn",
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
[[package]]
|
|
234
|
+
name = "quote"
|
|
235
|
+
version = "1.0.44"
|
|
236
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
237
|
+
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
|
238
|
+
dependencies = [
|
|
239
|
+
"proc-macro2",
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
[[package]]
|
|
243
|
+
name = "rawpointer"
|
|
244
|
+
version = "0.2.1"
|
|
245
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
246
|
+
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
|
247
|
+
|
|
248
|
+
[[package]]
|
|
249
|
+
name = "rustc-hash"
|
|
250
|
+
version = "1.1.0"
|
|
251
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
252
|
+
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|
253
|
+
|
|
254
|
+
[[package]]
|
|
255
|
+
name = "rustversion"
|
|
256
|
+
version = "1.0.22"
|
|
257
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
258
|
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
259
|
+
|
|
260
|
+
[[package]]
|
|
261
|
+
name = "syn"
|
|
262
|
+
version = "2.0.117"
|
|
263
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
264
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
265
|
+
dependencies = [
|
|
266
|
+
"proc-macro2",
|
|
267
|
+
"quote",
|
|
268
|
+
"unicode-ident",
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
[[package]]
|
|
272
|
+
name = "target-lexicon"
|
|
273
|
+
version = "0.12.16"
|
|
274
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
275
|
+
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|
276
|
+
|
|
277
|
+
[[package]]
|
|
278
|
+
name = "unicode-ident"
|
|
279
|
+
version = "1.0.24"
|
|
280
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
281
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
282
|
+
|
|
283
|
+
[[package]]
|
|
284
|
+
name = "unindent"
|
|
285
|
+
version = "0.2.4"
|
|
286
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
287
|
+
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
fasttps-0.2.0/Cargo.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "fasttps"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
name = "fasttps"
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
pyo3 = { version = "0.22", features = ["extension-module"] }
|
|
13
|
+
numpy = "0.22"
|
|
14
|
+
ndarray = "0.16"
|
|
15
|
+
fixedbitset = "0.4"
|
|
16
|
+
ordered-float = "3"
|
fasttps-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fasttps
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Fast TPS internals for the leaguescheduler Python package
|
|
5
|
+
Requires-Python: >=3.13, <3.14
|
|
6
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
7
|
+
|
|
8
|
+
# fasttps
|
|
9
|
+
|
|
10
|
+
Internal Rust extension for [leaguescheduler](https://pypi.org/project/leaguescheduler).
|
|
11
|
+
Not intended for direct use (but be my guest), rather install `leaguescheduler`.
|
|
12
|
+
|
fasttps-0.2.0/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fasttps"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Fast TPS internals for the leaguescheduler Python package"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13,<3.14"
|
|
7
|
+
|
|
8
|
+
[build-system]
|
|
9
|
+
requires = ["maturin>=1.0,<2.0"]
|
|
10
|
+
build-backend = "maturin"
|
|
11
|
+
|
|
12
|
+
[tool.maturin]
|
|
13
|
+
features = ["pyo3/extension-module"]
|
fasttps-0.2.0/src/lib.rs
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
|
|
3
|
+
use fixedbitset::FixedBitSet;
|
|
4
|
+
use ndarray::{Array2, ArrayView2};
|
|
5
|
+
use numpy::{PyArray2, PyArrayMethods, PyReadonlyArray2};
|
|
6
|
+
use ordered_float::OrderedFloat;
|
|
7
|
+
use pyo3::prelude::*;
|
|
8
|
+
use pyo3::types::PyDict;
|
|
9
|
+
|
|
10
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
11
|
+
const DISALLOWED_NBR: f64 = 9_999_999.0; // positive if minimization, negative if maximization
|
|
12
|
+
const DISALLOWED_REPLACE: f64 = 1e15;
|
|
13
|
+
const LARGE_NBR: f64 = 9999.0; // must match Python constants.py
|
|
14
|
+
|
|
15
|
+
// ─── Hungarian algorithm (Kuhn-Munkres) ──────────────────────────────────────
|
|
16
|
+
// Courtesy to https://github.com/neka-nat/fastmunk for main implementation
|
|
17
|
+
fn kuhn_munkres(weights: ArrayView2<f64>, maximize: bool) -> Vec<(usize, usize)> {
|
|
18
|
+
let nx = weights.nrows();
|
|
19
|
+
let ny = weights.ncols();
|
|
20
|
+
|
|
21
|
+
assert!(
|
|
22
|
+
nx <= ny,
|
|
23
|
+
"number of rows must not be larger than number of columns"
|
|
24
|
+
);
|
|
25
|
+
let copied_weights = if !maximize {
|
|
26
|
+
weights.map(|x| -x).view().to_owned()
|
|
27
|
+
} else {
|
|
28
|
+
weights.to_owned()
|
|
29
|
+
};
|
|
30
|
+
let mut xy: Vec<Option<usize>> = vec![None; nx];
|
|
31
|
+
let mut yx: Vec<Option<usize>> = vec![None; ny];
|
|
32
|
+
let mut lx: Vec<OrderedFloat<f64>> = (0..nx)
|
|
33
|
+
.map(|row| {
|
|
34
|
+
(0..ny)
|
|
35
|
+
.map(|col| OrderedFloat(copied_weights[(row, col)]))
|
|
36
|
+
.max()
|
|
37
|
+
.unwrap()
|
|
38
|
+
})
|
|
39
|
+
.collect::<Vec<_>>();
|
|
40
|
+
let mut ly: Vec<f64> = vec![0.0; ny];
|
|
41
|
+
let mut s = FixedBitSet::with_capacity(nx);
|
|
42
|
+
let mut alternating = Vec::with_capacity(ny);
|
|
43
|
+
let mut slack = vec![0.0; ny];
|
|
44
|
+
let mut slackx = Vec::with_capacity(ny);
|
|
45
|
+
for root in 0..nx {
|
|
46
|
+
alternating.clear();
|
|
47
|
+
alternating.resize(ny, None);
|
|
48
|
+
let mut y = {
|
|
49
|
+
s.clear();
|
|
50
|
+
s.insert(root);
|
|
51
|
+
for y in 0..ny {
|
|
52
|
+
slack[y] = lx[root].0 + ly[y] - copied_weights[(root, y)];
|
|
53
|
+
}
|
|
54
|
+
slackx.clear();
|
|
55
|
+
slackx.resize(ny, root);
|
|
56
|
+
Some(loop {
|
|
57
|
+
let mut delta = f64::INFINITY;
|
|
58
|
+
let mut x = 0;
|
|
59
|
+
let mut y = 0;
|
|
60
|
+
for yy in 0..ny {
|
|
61
|
+
if alternating[yy].is_none() && slack[yy] < delta {
|
|
62
|
+
delta = slack[yy];
|
|
63
|
+
x = slackx[yy];
|
|
64
|
+
y = yy;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if delta > 0.0 {
|
|
68
|
+
for x in s.ones() {
|
|
69
|
+
lx[x] = lx[x] - delta;
|
|
70
|
+
}
|
|
71
|
+
for y in 0..ny {
|
|
72
|
+
if alternating[y].is_some() {
|
|
73
|
+
ly[y] = ly[y] + delta;
|
|
74
|
+
} else {
|
|
75
|
+
slack[y] = slack[y] - delta;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
alternating[y] = Some(x);
|
|
80
|
+
if yx[y].is_none() {
|
|
81
|
+
break y;
|
|
82
|
+
}
|
|
83
|
+
let x = yx[y].unwrap();
|
|
84
|
+
s.insert(x);
|
|
85
|
+
for y in 0..ny {
|
|
86
|
+
if alternating[y].is_none() {
|
|
87
|
+
let alternate_slack = lx[x] + ly[y] - copied_weights[(x, y)];
|
|
88
|
+
if slack[y] > alternate_slack.0 {
|
|
89
|
+
slack[y] = alternate_slack.0;
|
|
90
|
+
slackx[y] = x;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
};
|
|
96
|
+
while y.is_some() {
|
|
97
|
+
let x = alternating[y.unwrap()].unwrap();
|
|
98
|
+
let prec = xy[x];
|
|
99
|
+
yx[y.unwrap()] = Some(x);
|
|
100
|
+
xy[x] = y;
|
|
101
|
+
y = prec;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
xy.into_iter()
|
|
105
|
+
.enumerate()
|
|
106
|
+
.map(|(i, v)| (i, v.unwrap()))
|
|
107
|
+
.collect::<Vec<_>>()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/// Equivalent of Python legacy _get_team_array(): concatenate X[idx,:] and X[:,idx], filter out LARGE_NBR.
|
|
113
|
+
#[inline]
|
|
114
|
+
fn get_team_array(x: &ArrayView2<f64>, idx: usize) -> Vec<f64> {
|
|
115
|
+
let n = x.nrows();
|
|
116
|
+
let mut arr = Vec::with_capacity(2 * n);
|
|
117
|
+
for j in 0..x.ncols() {
|
|
118
|
+
let v = x[(idx, j)];
|
|
119
|
+
if v != LARGE_NBR {
|
|
120
|
+
arr.push(v);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for i in 0..n {
|
|
124
|
+
let v = x[(i, idx)];
|
|
125
|
+
if v != LARGE_NBR {
|
|
126
|
+
arr.push(v);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
arr
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Minimum positive value in a slice of (value - h) differences.
|
|
133
|
+
/// Returns None if no positive value exists.
|
|
134
|
+
#[inline]
|
|
135
|
+
fn min_positive_delta(games: &[f64], h: f64, forward: bool) -> Option<f64> {
|
|
136
|
+
let mut best = f64::INFINITY;
|
|
137
|
+
for &g in games {
|
|
138
|
+
let d = if forward { g - h } else { h - g };
|
|
139
|
+
if d > 0.0 && d < best {
|
|
140
|
+
best = d;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if best == f64::INFINITY {
|
|
144
|
+
None
|
|
145
|
+
} else {
|
|
146
|
+
Some(best)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Build cost matrix for a given team. This is the #1 bottleneck.
|
|
151
|
+
fn create_cost_matrix_inner(
|
|
152
|
+
x: &ArrayView2<f64>,
|
|
153
|
+
team_idx: usize,
|
|
154
|
+
set_home: &[f64],
|
|
155
|
+
opponents: &[usize],
|
|
156
|
+
sets_forbidden: &HashMap<usize, HashSet<i64>>,
|
|
157
|
+
m: f64,
|
|
158
|
+
r_max: f64,
|
|
159
|
+
penalties: &[i64],
|
|
160
|
+
max_penalty_key: usize,
|
|
161
|
+
) -> Array2<f64> {
|
|
162
|
+
let n_home = set_home.len();
|
|
163
|
+
let n_oppo = opponents.len();
|
|
164
|
+
let mut am_cost = Array2::<f64>::zeros((n_home, n_oppo));
|
|
165
|
+
|
|
166
|
+
// precompute games for the home team
|
|
167
|
+
let games_team = get_team_array(x, team_idx);
|
|
168
|
+
|
|
169
|
+
// C2 - home date availability (home_dates = set_home)
|
|
170
|
+
|
|
171
|
+
// C4 (team part) - precompute which home dates the team already plays on
|
|
172
|
+
// C5 (team part) - precompute whether any game is within r_max of each home date
|
|
173
|
+
let mut team_plays_mask = vec![false; n_home];
|
|
174
|
+
let mut games_in_r_max_team = vec![false; n_home];
|
|
175
|
+
|
|
176
|
+
for (i, &h) in set_home.iter().enumerate() {
|
|
177
|
+
for &g in &games_team {
|
|
178
|
+
// C4 - team already plays game
|
|
179
|
+
if g == h {
|
|
180
|
+
team_plays_mask[i] = true;
|
|
181
|
+
}
|
|
182
|
+
// C5 - distance < r_max means too close
|
|
183
|
+
let d = (g - h).abs() + 1.0;
|
|
184
|
+
if d < r_max {
|
|
185
|
+
games_in_r_max_team[i] = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (j, &oppo_idx) in opponents.iter().enumerate() {
|
|
191
|
+
let games_oppo = get_team_array(x, oppo_idx);
|
|
192
|
+
|
|
193
|
+
// get forbidden set for this opponent (empty if not present)
|
|
194
|
+
let forbidden_set = sets_forbidden.get(&oppo_idx);
|
|
195
|
+
|
|
196
|
+
// C6 - reciprocal game value (game j->i)
|
|
197
|
+
let reciprocal_val = x[(oppo_idx, team_idx)];
|
|
198
|
+
|
|
199
|
+
for (i, &h) in set_home.iter().enumerate() {
|
|
200
|
+
let h_i64 = h as i64;
|
|
201
|
+
|
|
202
|
+
// C3 - forbidden game set
|
|
203
|
+
let forbidden = match forbidden_set {
|
|
204
|
+
Some(fs) => fs.contains(&h_i64),
|
|
205
|
+
None => false,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// C4 - team already plays game (precomputed)
|
|
209
|
+
let team_plays = team_plays_mask[i];
|
|
210
|
+
|
|
211
|
+
// C4 - opponent already plays game
|
|
212
|
+
let mut oppo_plays = false;
|
|
213
|
+
for &g in &games_oppo {
|
|
214
|
+
if g == h {
|
|
215
|
+
oppo_plays = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// C5 - max. 2 games for 'r_max' slots
|
|
221
|
+
let mut games_in_r_max_oppo = false;
|
|
222
|
+
for &g in &games_oppo {
|
|
223
|
+
let d = (g - h).abs() + 1.0;
|
|
224
|
+
if d < r_max {
|
|
225
|
+
games_in_r_max_oppo = true;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
let r_max_violated = games_in_r_max_team[i] || games_in_r_max_oppo;
|
|
230
|
+
|
|
231
|
+
// C6 - game i-j is within m days of game j-i
|
|
232
|
+
let reciprocal_too_close = (h - reciprocal_val).abs() < m;
|
|
233
|
+
|
|
234
|
+
// set disallowed cost for all disallowed slots
|
|
235
|
+
let disallowed =
|
|
236
|
+
forbidden || team_plays || oppo_plays || reciprocal_too_close || r_max_violated;
|
|
237
|
+
|
|
238
|
+
if disallowed {
|
|
239
|
+
am_cost[(i, j)] = DISALLOWED_NBR;
|
|
240
|
+
} else {
|
|
241
|
+
// process penalty calculations for allowed slots
|
|
242
|
+
let mut total_penalty: i64 = 0;
|
|
243
|
+
|
|
244
|
+
// forward-looking for team
|
|
245
|
+
if let Some(d) = min_positive_delta(&games_team, h, true) {
|
|
246
|
+
let key = d as usize;
|
|
247
|
+
if key <= max_penalty_key {
|
|
248
|
+
total_penalty += penalties[key];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// backward-looking for team
|
|
252
|
+
if let Some(d) = min_positive_delta(&games_team, h, false) {
|
|
253
|
+
let key = d as usize;
|
|
254
|
+
if key <= max_penalty_key {
|
|
255
|
+
total_penalty += penalties[key];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// forward-looking for opponent
|
|
259
|
+
if let Some(d) = min_positive_delta(&games_oppo, h, true) {
|
|
260
|
+
let key = d as usize;
|
|
261
|
+
if key <= max_penalty_key {
|
|
262
|
+
total_penalty += penalties[key];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// backward-looking for opponent
|
|
266
|
+
if let Some(d) = min_positive_delta(&games_oppo, h, false) {
|
|
267
|
+
let key = d as usize;
|
|
268
|
+
if key <= max_penalty_key {
|
|
269
|
+
total_penalty += penalties[key];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
am_cost[(i, j)] = total_penalty as f64;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
am_cost
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Full solve: cost matrix → adjacency matrix → Munkres → process indexes → (pick, total_cost).
|
|
282
|
+
fn solve_inner(
|
|
283
|
+
x: &ArrayView2<f64>,
|
|
284
|
+
team_idx: usize,
|
|
285
|
+
set_home: &[f64],
|
|
286
|
+
opponents: &[usize],
|
|
287
|
+
sets_forbidden: &HashMap<usize, HashSet<i64>>,
|
|
288
|
+
m: f64,
|
|
289
|
+
p: f64,
|
|
290
|
+
r_max: f64,
|
|
291
|
+
penalties: &[i64],
|
|
292
|
+
max_penalty_key: usize,
|
|
293
|
+
) -> (Vec<f64>, f64) {
|
|
294
|
+
let n_home = set_home.len();
|
|
295
|
+
let n_oppo = opponents.len();
|
|
296
|
+
let dim = n_home + n_oppo;
|
|
297
|
+
|
|
298
|
+
// construct cost matrix
|
|
299
|
+
let am_cost = create_cost_matrix_inner(
|
|
300
|
+
x,
|
|
301
|
+
team_idx,
|
|
302
|
+
set_home,
|
|
303
|
+
opponents,
|
|
304
|
+
sets_forbidden,
|
|
305
|
+
m,
|
|
306
|
+
r_max,
|
|
307
|
+
penalties,
|
|
308
|
+
max_penalty_key,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// construct full adjacency matrix:
|
|
312
|
+
// [ am_cost | zeros ]
|
|
313
|
+
// [ am_bott | zeros ]
|
|
314
|
+
// where am_bott is n_oppo x n_oppo filled with p
|
|
315
|
+
// and zeros is dim x (dim - n_oppo)
|
|
316
|
+
let mut am = Array2::<f64>::zeros((dim, dim));
|
|
317
|
+
|
|
318
|
+
// fill cost block (top-left: n_home x n_oppo)
|
|
319
|
+
for i in 0..n_home {
|
|
320
|
+
for j in 0..n_oppo {
|
|
321
|
+
let v = am_cost[(i, j)];
|
|
322
|
+
// replace disallowed values with a large number
|
|
323
|
+
am[(i, j)] = if v == DISALLOWED_NBR {
|
|
324
|
+
DISALLOWED_REPLACE
|
|
325
|
+
} else {
|
|
326
|
+
v
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// fill bottom block (n_oppo x n_oppo) with p
|
|
332
|
+
for i in 0..n_oppo {
|
|
333
|
+
for j in 0..n_oppo {
|
|
334
|
+
am[(n_home + i, j)] = p;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// right block stays zeros (already initialized)
|
|
338
|
+
|
|
339
|
+
// run Hungarian algorithm (minimize)
|
|
340
|
+
let indexes = kuhn_munkres(am.view(), false);
|
|
341
|
+
|
|
342
|
+
// compute total cost
|
|
343
|
+
let total_cost: f64 = indexes.iter().map(|&(r, c)| am[(r, c)]).sum();
|
|
344
|
+
|
|
345
|
+
// process optimal indexes: sort by opponent, map to home slot or NaN
|
|
346
|
+
let mut indexes_inv: Vec<(usize, f64)> = Vec::with_capacity(n_oppo);
|
|
347
|
+
for &(k, v) in &indexes {
|
|
348
|
+
if v < n_oppo {
|
|
349
|
+
let slot = if k < n_home { set_home[k] } else { f64::NAN };
|
|
350
|
+
indexes_inv.push((opponents[v], slot));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
indexes_inv.sort_by_key(|&(opp, _)| opp);
|
|
354
|
+
|
|
355
|
+
let pick: Vec<f64> = indexes_inv.iter().map(|&(_, slot)| slot).collect();
|
|
356
|
+
|
|
357
|
+
(pick, total_cost)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── PyO3 class ──────────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
#[pyclass(module = "fasttps")]
|
|
363
|
+
struct FastTPS {
|
|
364
|
+
sets_home: HashMap<usize, Vec<f64>>,
|
|
365
|
+
sets_forbidden: HashMap<usize, HashSet<i64>>,
|
|
366
|
+
m: f64,
|
|
367
|
+
p: f64,
|
|
368
|
+
r_max: f64,
|
|
369
|
+
/// Flat penalty lookup: penalties_vec[d] = penalty for distance d.
|
|
370
|
+
penalties_vec: Vec<i64>,
|
|
371
|
+
max_penalty_key: usize,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[pymethods]
|
|
375
|
+
impl FastTPS {
|
|
376
|
+
#[new]
|
|
377
|
+
#[pyo3(signature = (sets_home, sets_forbidden, m, p, r_max, penalties))]
|
|
378
|
+
fn new(
|
|
379
|
+
sets_home: &Bound<'_, PyDict>,
|
|
380
|
+
sets_forbidden: &Bound<'_, PyDict>,
|
|
381
|
+
m: i64,
|
|
382
|
+
p: i64,
|
|
383
|
+
r_max: i64,
|
|
384
|
+
penalties: &Bound<'_, PyDict>,
|
|
385
|
+
) -> PyResult<Self> {
|
|
386
|
+
// parse sets_home: dict[int, list/set of float/int]
|
|
387
|
+
let mut sh: HashMap<usize, Vec<f64>> = HashMap::new();
|
|
388
|
+
for (k, v) in sets_home.iter() {
|
|
389
|
+
let key: usize = k.extract()?;
|
|
390
|
+
let vals: Vec<f64> = v.extract()?;
|
|
391
|
+
sh.insert(key, vals);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// parse sets_forbidden: dict[int, list/set of float/int] → HashSet<i64>
|
|
395
|
+
let mut sf: HashMap<usize, HashSet<i64>> = HashMap::new();
|
|
396
|
+
for (k, v) in sets_forbidden.iter() {
|
|
397
|
+
let key: usize = k.extract()?;
|
|
398
|
+
let vals: Vec<i64> = v.extract()?;
|
|
399
|
+
sf.insert(key, vals.into_iter().collect());
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// parse penalties dict to flat Vec
|
|
403
|
+
let mut max_key: usize = 0;
|
|
404
|
+
let mut pen_map: HashMap<usize, i64> = HashMap::new();
|
|
405
|
+
for (k, v) in penalties.iter() {
|
|
406
|
+
let key: usize = k.extract()?;
|
|
407
|
+
let val: i64 = v.extract()?;
|
|
408
|
+
pen_map.insert(key, val);
|
|
409
|
+
if key > max_key {
|
|
410
|
+
max_key = key;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
let mut penalties_vec = vec![0i64; max_key + 1];
|
|
414
|
+
for (k, v) in &pen_map {
|
|
415
|
+
penalties_vec[*k] = *v;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
Ok(FastTPS {
|
|
419
|
+
sets_home: sh,
|
|
420
|
+
sets_forbidden: sf,
|
|
421
|
+
m: m as f64,
|
|
422
|
+
p: p as f64,
|
|
423
|
+
r_max: r_max.max(2) as f64,
|
|
424
|
+
penalties_vec,
|
|
425
|
+
max_penalty_key: max_key,
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Solve transportation problem for given home team.
|
|
430
|
+
/// Modifies X in-place and returns total_cost.
|
|
431
|
+
fn solve<'py>(
|
|
432
|
+
&self,
|
|
433
|
+
py: Python<'py>,
|
|
434
|
+
x_py: &Bound<'py, PyArray2<f64>>,
|
|
435
|
+
team_idx: usize,
|
|
436
|
+
) -> PyResult<f64> {
|
|
437
|
+
let x_read = x_py.readonly();
|
|
438
|
+
let x_view = x_read.as_array();
|
|
439
|
+
let n = x_view.nrows();
|
|
440
|
+
|
|
441
|
+
let set_home = self
|
|
442
|
+
.sets_home
|
|
443
|
+
.get(&team_idx)
|
|
444
|
+
.expect("team_idx not in sets_home");
|
|
445
|
+
let opponents: Vec<usize> = (0..n).filter(|&t| t != team_idx).collect();
|
|
446
|
+
|
|
447
|
+
let (pick, total_cost) = solve_inner(
|
|
448
|
+
&x_view,
|
|
449
|
+
team_idx,
|
|
450
|
+
set_home,
|
|
451
|
+
&opponents,
|
|
452
|
+
&self.sets_forbidden,
|
|
453
|
+
self.m,
|
|
454
|
+
self.p,
|
|
455
|
+
self.r_max,
|
|
456
|
+
&self.penalties_vec,
|
|
457
|
+
self.max_penalty_key,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// release readonly borrow before writing
|
|
461
|
+
drop(x_read);
|
|
462
|
+
|
|
463
|
+
// assign selection to X in-place: X[team_idx, opponents] = pick
|
|
464
|
+
unsafe {
|
|
465
|
+
let mut x_mut = x_py.as_array_mut();
|
|
466
|
+
for (idx, &opp) in opponents.iter().enumerate() {
|
|
467
|
+
*x_mut.uget_mut([team_idx, opp]) = pick[idx];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
Ok(total_cost)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// Create cost matrix (exposed for construction phase method 2).
|
|
475
|
+
fn create_cost_matrix<'py>(
|
|
476
|
+
&self,
|
|
477
|
+
py: Python<'py>,
|
|
478
|
+
x_py: PyReadonlyArray2<'py, f64>,
|
|
479
|
+
team_idx: usize,
|
|
480
|
+
set_home: Vec<f64>,
|
|
481
|
+
opponents: Vec<usize>,
|
|
482
|
+
) -> PyResult<Py<PyArray2<f64>>> {
|
|
483
|
+
let x = x_py.as_array();
|
|
484
|
+
let am = create_cost_matrix_inner(
|
|
485
|
+
&x,
|
|
486
|
+
team_idx,
|
|
487
|
+
&set_home,
|
|
488
|
+
&opponents,
|
|
489
|
+
&self.sets_forbidden,
|
|
490
|
+
self.m,
|
|
491
|
+
self.r_max,
|
|
492
|
+
&self.penalties_vec,
|
|
493
|
+
self.max_penalty_key,
|
|
494
|
+
);
|
|
495
|
+
Ok(PyArray2::from_owned_array_bound(py, am).unbind())
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#[pymodule]
|
|
500
|
+
fn fasttps(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
501
|
+
m.add_class::<FastTPS>()?;
|
|
502
|
+
Ok(())
|
|
503
|
+
}
|