pudo-sequences 0.1.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.
- pudo_sequences-0.1.0/.gitignore +11 -0
- pudo_sequences-0.1.0/LICENSE +21 -0
- pudo_sequences-0.1.0/PKG-INFO +336 -0
- pudo_sequences-0.1.0/README.md +299 -0
- pudo_sequences-0.1.0/pudo_sequences/__init__.py +22 -0
- pudo_sequences-0.1.0/pudo_sequences/core.py +407 -0
- pudo_sequences-0.1.0/pudo_sequences/index.py +127 -0
- pudo_sequences-0.1.0/pudo_sequences/py.typed +1 -0
- pudo_sequences-0.1.0/pyproject.toml +73 -0
- pudo_sequences-0.1.0/tests/__init__.py +0 -0
- pudo_sequences-0.1.0/tests/test_pudo.py +132 -0
- pudo_sequences-0.1.0/tests/test_trie.py +15 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Breno Beirigo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pudo-sequences
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate, count, and index valid pickup/drop-off event sequences.
|
|
5
|
+
Project-URL: Homepage, https://github.com/brenobeirigo/pudo-sequences
|
|
6
|
+
Project-URL: Repository, https://github.com/brenobeirigo/pudo-sequences
|
|
7
|
+
Project-URL: Issues, https://github.com/brenobeirigo/pudo-sequences/issues
|
|
8
|
+
Author: Breno Beirigo
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: combinatorics,paired-events,pickup-and-delivery,pickup-dropoff,precedence-constraints,topological-sorts
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: networkx>=3.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: networkx
|
|
32
|
+
Requires-Dist: networkx>=3.0; extra == 'networkx'
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: networkx>=3.0; extra == 'test'
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# pudo-sequences
|
|
39
|
+
|
|
40
|
+
`pudo-sequences` is a small Python package for PU/DO constrained sequence
|
|
41
|
+
combinatorics: counting, generating, and indexing event orders where every
|
|
42
|
+
pickup must occur before its paired drop-off.
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
pickup_i before dropoff_i
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use it when a routing, ridesharing, dispatching, simulation, optimization, or
|
|
49
|
+
learning system needs to reason about feasible local service orders before
|
|
50
|
+
evaluating distance, time windows, capacity, cost, or reward.
|
|
51
|
+
|
|
52
|
+
## Why It Matters
|
|
53
|
+
|
|
54
|
+
A PU/DO route is not just a permutation of events. For two new requests, the
|
|
55
|
+
four events are:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
0 = pickup request 0
|
|
59
|
+
1 = pickup request 1
|
|
60
|
+
2 = drop-off request 0
|
|
61
|
+
3 = drop-off request 1
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`itertools.permutations` returns all `24` event orders, including impossible
|
|
65
|
+
orders such as `(2, 0, 1, 3)`, where request `0` is dropped off before it is
|
|
66
|
+
picked up.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from itertools import permutations
|
|
70
|
+
|
|
71
|
+
from pudo_sequences import pudo_sequences
|
|
72
|
+
|
|
73
|
+
all_orders = list(permutations([0, 1, 2, 3]))
|
|
74
|
+
valid_orders = pudo_sequences([0, 1])
|
|
75
|
+
|
|
76
|
+
print(len(all_orders))
|
|
77
|
+
print(len(valid_orders))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
24
|
|
82
|
+
6
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The valid orders are:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
[
|
|
89
|
+
(0, 1, 2, 3),
|
|
90
|
+
(0, 1, 3, 2),
|
|
91
|
+
(0, 2, 1, 3),
|
|
92
|
+
(1, 0, 2, 3),
|
|
93
|
+
(1, 0, 3, 2),
|
|
94
|
+
(1, 3, 0, 2),
|
|
95
|
+
]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
You can filter normal permutations yourself:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
def is_valid(route):
|
|
102
|
+
return route.index(0) < route.index(2) and route.index(1) < route.index(3)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
filtered = [route for route in all_orders if is_valid(route)]
|
|
106
|
+
|
|
107
|
+
assert filtered == valid_orders
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For small teaching examples that is fine. In real local-search or learning
|
|
111
|
+
loops, generating impossible actions first adds noise and waste. The gap grows
|
|
112
|
+
quickly:
|
|
113
|
+
|
|
114
|
+
| Requests | Unconstrained permutations | Valid PU/DO sequences | Valid share |
|
|
115
|
+
| ---: | ---: | ---: | ---: |
|
|
116
|
+
| 1 | 2 | 1 | 50.0% |
|
|
117
|
+
| 2 | 24 | 6 | 25.0% |
|
|
118
|
+
| 3 | 720 | 90 | 12.5% |
|
|
119
|
+
| 4 | 40,320 | 2,520 | 6.25% |
|
|
120
|
+
| 5 | 3,628,800 | 113,400 | 3.125% |
|
|
121
|
+
|
|
122
|
+
The package focuses on this first layer:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
PU/DO combinatorics: which event orders are logically possible?
|
|
126
|
+
Domain evaluation: which of those orders satisfy time, capacity, or cost rules?
|
|
127
|
+
Selection: which feasible order should the system choose?
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
It is not a routing solver. It gives you the constrained local sequence space
|
|
131
|
+
that a solver, simulator, heuristic, or policy can evaluate.
|
|
132
|
+
|
|
133
|
+
## Install
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install pudo-sequences
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For local development:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install -e ".[dev]"
|
|
143
|
+
pytest
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The core package has no runtime dependency beyond Python. The optional
|
|
147
|
+
`topological` strategy requires NetworkX:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install "pudo-sequences[networkx]"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Quick Start
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from pudo_sequences import count_pudo_sequences, pudo_sequences
|
|
157
|
+
|
|
158
|
+
print(count_pudo_sequences(2))
|
|
159
|
+
print(pudo_sequences([0, 1]))
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
With the default integer convention, pickup `i` maps to drop-off `i + n`, where
|
|
163
|
+
`n` is the number of requests. For `requests=[0, 1]`, drop-offs are `2` and
|
|
164
|
+
`3`.
|
|
165
|
+
|
|
166
|
+
To stream instead of materializing every route:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from pudo_sequences import iter_pudo_sequences
|
|
170
|
+
|
|
171
|
+
best_route = None
|
|
172
|
+
best_score = float("inf")
|
|
173
|
+
|
|
174
|
+
for route in iter_pudo_sequences([0, 1, 2]):
|
|
175
|
+
score = sum(route) # Replace with distance, time, reward, or feasibility logic.
|
|
176
|
+
if score < best_score:
|
|
177
|
+
best_route = route
|
|
178
|
+
best_score = score
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
To check whether a candidate from a heuristic is PU/DO-valid, compare it with
|
|
182
|
+
the generated local action set for small neighborhoods:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from pudo_sequences import pudo_sequences
|
|
186
|
+
|
|
187
|
+
valid_routes = set(pudo_sequences([0, 1, 2]))
|
|
188
|
+
|
|
189
|
+
assert (0, 3, 1, 4, 2, 5) in valid_routes
|
|
190
|
+
assert (3, 0, 1, 4, 2, 5) not in valid_routes
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Custom Labels
|
|
194
|
+
|
|
195
|
+
Use `dropoff_for` when events are not integers:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from pudo_sequences import pudo_sequences
|
|
199
|
+
|
|
200
|
+
routes = pudo_sequences(
|
|
201
|
+
["alice", "bob"],
|
|
202
|
+
dropoff_for=lambda pickup: ("dropoff", pickup),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
assert (
|
|
206
|
+
"alice",
|
|
207
|
+
("dropoff", "alice"),
|
|
208
|
+
"bob",
|
|
209
|
+
("dropoff", "bob"),
|
|
210
|
+
) in routes
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
By requiring a mapping from pickup labels to drop-off labels, the package can
|
|
214
|
+
use the same combinatorics for rider IDs, job IDs, task names, tuples, or other
|
|
215
|
+
hashable labels.
|
|
216
|
+
|
|
217
|
+
## Already-Open Drop-Offs
|
|
218
|
+
|
|
219
|
+
Sometimes a planning horizon starts with requests already onboard. Their
|
|
220
|
+
drop-offs have no pickup inside the local sequence, so they may appear anywhere.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from pudo_sequences import count_pudo_sequences, pudo_sequences
|
|
224
|
+
|
|
225
|
+
routes = pudo_sequences([0, 1], open_dropoffs=[4])
|
|
226
|
+
|
|
227
|
+
assert len(routes) == count_pudo_sequences(2, n_open_dropoffs=1)
|
|
228
|
+
assert len(routes) == 30
|
|
229
|
+
assert (4, 0, 1, 2, 3) in routes
|
|
230
|
+
assert (0, 1, 2, 3, 4) in routes
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
This represents two new requests plus one already-open request whose drop-off
|
|
234
|
+
is `4`.
|
|
235
|
+
|
|
236
|
+
## Counts
|
|
237
|
+
|
|
238
|
+
For `n` labeled pickup/drop-off pairs, the number of valid sequences is:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
(2n)! / 2^n
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Equivalently:
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
n! * (1 * 3 * 5 * ... * (2n - 1))
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If there are also `m` already-open drop-offs, the count becomes:
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
(2n + m)! / 2^n
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
API:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from pudo_sequences import (
|
|
260
|
+
count_fixed_pickup_order_dropoff_insertions,
|
|
261
|
+
count_open_dropoff_insertions,
|
|
262
|
+
count_pudo_sequences,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
assert count_pudo_sequences(3) == 90
|
|
266
|
+
assert count_pudo_sequences(3, n_open_dropoffs=2) == 5040
|
|
267
|
+
assert count_fixed_pickup_order_dropoff_insertions(3) == 15
|
|
268
|
+
assert count_open_dropoff_insertions(3, 2) == 56
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The constructive count is useful for explaining the structure:
|
|
272
|
+
|
|
273
|
+
```text
|
|
274
|
+
1. choose a pickup order;
|
|
275
|
+
2. insert each paired drop-off only after its pickup;
|
|
276
|
+
3. optionally insert already-open drop-offs anywhere.
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Generation Strategies
|
|
280
|
+
|
|
281
|
+
The default strategy is dependency-free DFS over the PU/DO state space:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from pudo_sequences import iter_pudo_sequences
|
|
285
|
+
|
|
286
|
+
for route in iter_pudo_sequences([0, 1, 2], strategy="dfs"):
|
|
287
|
+
pass
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Other strategies are available for comparison:
|
|
291
|
+
|
|
292
|
+
- `strategy="insertion"`: generate pickup orders, then insert each paired
|
|
293
|
+
drop-off only after its pickup.
|
|
294
|
+
- `strategy="topological"`: use NetworkX to enumerate all topological orders of
|
|
295
|
+
a precedence graph with edges `pickup_i -> dropoff_i`.
|
|
296
|
+
|
|
297
|
+
For `n=3`, all strategies represent the same constrained sequence set:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from pudo_sequences import pudo_sequences
|
|
301
|
+
|
|
302
|
+
dfs_routes = set(pudo_sequences([0, 1, 2], strategy="dfs"))
|
|
303
|
+
insertion_routes = set(pudo_sequences([0, 1, 2], strategy="insertion"))
|
|
304
|
+
|
|
305
|
+
assert dfs_routes == insertion_routes
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The generator is intended for small local neighborhoods. PU/DO sequence counts
|
|
309
|
+
grow factorially, so large request sets should usually be handled by heuristics,
|
|
310
|
+
optimization models, or sampling rather than full enumeration.
|
|
311
|
+
|
|
312
|
+
## Prefix Indexing
|
|
313
|
+
|
|
314
|
+
When a caller repeatedly asks for valid continuations after a partial route,
|
|
315
|
+
build a prefix index:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from pudo_sequences import build_pudo_index
|
|
319
|
+
|
|
320
|
+
index = build_pudo_index([0, 1])
|
|
321
|
+
|
|
322
|
+
assert index.continuations([0, 1]) == {(2, 3), (3, 2)}
|
|
323
|
+
assert index.continuations([0, 2]) == {(1, 3)}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The index is a secondary feature. Its purpose is fast continuation lookup, not
|
|
327
|
+
the definition of the package.
|
|
328
|
+
|
|
329
|
+
## Development
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
pip install -e ".[dev]"
|
|
333
|
+
pytest -q
|
|
334
|
+
python -m build --sdist --wheel
|
|
335
|
+
python -m twine check dist/*
|
|
336
|
+
```
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# pudo-sequences
|
|
2
|
+
|
|
3
|
+
`pudo-sequences` is a small Python package for PU/DO constrained sequence
|
|
4
|
+
combinatorics: counting, generating, and indexing event orders where every
|
|
5
|
+
pickup must occur before its paired drop-off.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
pickup_i before dropoff_i
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use it when a routing, ridesharing, dispatching, simulation, optimization, or
|
|
12
|
+
learning system needs to reason about feasible local service orders before
|
|
13
|
+
evaluating distance, time windows, capacity, cost, or reward.
|
|
14
|
+
|
|
15
|
+
## Why It Matters
|
|
16
|
+
|
|
17
|
+
A PU/DO route is not just a permutation of events. For two new requests, the
|
|
18
|
+
four events are:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
0 = pickup request 0
|
|
22
|
+
1 = pickup request 1
|
|
23
|
+
2 = drop-off request 0
|
|
24
|
+
3 = drop-off request 1
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`itertools.permutations` returns all `24` event orders, including impossible
|
|
28
|
+
orders such as `(2, 0, 1, 3)`, where request `0` is dropped off before it is
|
|
29
|
+
picked up.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from itertools import permutations
|
|
33
|
+
|
|
34
|
+
from pudo_sequences import pudo_sequences
|
|
35
|
+
|
|
36
|
+
all_orders = list(permutations([0, 1, 2, 3]))
|
|
37
|
+
valid_orders = pudo_sequences([0, 1])
|
|
38
|
+
|
|
39
|
+
print(len(all_orders))
|
|
40
|
+
print(len(valid_orders))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
24
|
|
45
|
+
6
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The valid orders are:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
[
|
|
52
|
+
(0, 1, 2, 3),
|
|
53
|
+
(0, 1, 3, 2),
|
|
54
|
+
(0, 2, 1, 3),
|
|
55
|
+
(1, 0, 2, 3),
|
|
56
|
+
(1, 0, 3, 2),
|
|
57
|
+
(1, 3, 0, 2),
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You can filter normal permutations yourself:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
def is_valid(route):
|
|
65
|
+
return route.index(0) < route.index(2) and route.index(1) < route.index(3)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
filtered = [route for route in all_orders if is_valid(route)]
|
|
69
|
+
|
|
70
|
+
assert filtered == valid_orders
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For small teaching examples that is fine. In real local-search or learning
|
|
74
|
+
loops, generating impossible actions first adds noise and waste. The gap grows
|
|
75
|
+
quickly:
|
|
76
|
+
|
|
77
|
+
| Requests | Unconstrained permutations | Valid PU/DO sequences | Valid share |
|
|
78
|
+
| ---: | ---: | ---: | ---: |
|
|
79
|
+
| 1 | 2 | 1 | 50.0% |
|
|
80
|
+
| 2 | 24 | 6 | 25.0% |
|
|
81
|
+
| 3 | 720 | 90 | 12.5% |
|
|
82
|
+
| 4 | 40,320 | 2,520 | 6.25% |
|
|
83
|
+
| 5 | 3,628,800 | 113,400 | 3.125% |
|
|
84
|
+
|
|
85
|
+
The package focuses on this first layer:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
PU/DO combinatorics: which event orders are logically possible?
|
|
89
|
+
Domain evaluation: which of those orders satisfy time, capacity, or cost rules?
|
|
90
|
+
Selection: which feasible order should the system choose?
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
It is not a routing solver. It gives you the constrained local sequence space
|
|
94
|
+
that a solver, simulator, heuristic, or policy can evaluate.
|
|
95
|
+
|
|
96
|
+
## Install
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install pudo-sequences
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For local development:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pip install -e ".[dev]"
|
|
106
|
+
pytest
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The core package has no runtime dependency beyond Python. The optional
|
|
110
|
+
`topological` strategy requires NetworkX:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install "pudo-sequences[networkx]"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Quick Start
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from pudo_sequences import count_pudo_sequences, pudo_sequences
|
|
120
|
+
|
|
121
|
+
print(count_pudo_sequences(2))
|
|
122
|
+
print(pudo_sequences([0, 1]))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
With the default integer convention, pickup `i` maps to drop-off `i + n`, where
|
|
126
|
+
`n` is the number of requests. For `requests=[0, 1]`, drop-offs are `2` and
|
|
127
|
+
`3`.
|
|
128
|
+
|
|
129
|
+
To stream instead of materializing every route:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from pudo_sequences import iter_pudo_sequences
|
|
133
|
+
|
|
134
|
+
best_route = None
|
|
135
|
+
best_score = float("inf")
|
|
136
|
+
|
|
137
|
+
for route in iter_pudo_sequences([0, 1, 2]):
|
|
138
|
+
score = sum(route) # Replace with distance, time, reward, or feasibility logic.
|
|
139
|
+
if score < best_score:
|
|
140
|
+
best_route = route
|
|
141
|
+
best_score = score
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
To check whether a candidate from a heuristic is PU/DO-valid, compare it with
|
|
145
|
+
the generated local action set for small neighborhoods:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from pudo_sequences import pudo_sequences
|
|
149
|
+
|
|
150
|
+
valid_routes = set(pudo_sequences([0, 1, 2]))
|
|
151
|
+
|
|
152
|
+
assert (0, 3, 1, 4, 2, 5) in valid_routes
|
|
153
|
+
assert (3, 0, 1, 4, 2, 5) not in valid_routes
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Custom Labels
|
|
157
|
+
|
|
158
|
+
Use `dropoff_for` when events are not integers:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from pudo_sequences import pudo_sequences
|
|
162
|
+
|
|
163
|
+
routes = pudo_sequences(
|
|
164
|
+
["alice", "bob"],
|
|
165
|
+
dropoff_for=lambda pickup: ("dropoff", pickup),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
assert (
|
|
169
|
+
"alice",
|
|
170
|
+
("dropoff", "alice"),
|
|
171
|
+
"bob",
|
|
172
|
+
("dropoff", "bob"),
|
|
173
|
+
) in routes
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
By requiring a mapping from pickup labels to drop-off labels, the package can
|
|
177
|
+
use the same combinatorics for rider IDs, job IDs, task names, tuples, or other
|
|
178
|
+
hashable labels.
|
|
179
|
+
|
|
180
|
+
## Already-Open Drop-Offs
|
|
181
|
+
|
|
182
|
+
Sometimes a planning horizon starts with requests already onboard. Their
|
|
183
|
+
drop-offs have no pickup inside the local sequence, so they may appear anywhere.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pudo_sequences import count_pudo_sequences, pudo_sequences
|
|
187
|
+
|
|
188
|
+
routes = pudo_sequences([0, 1], open_dropoffs=[4])
|
|
189
|
+
|
|
190
|
+
assert len(routes) == count_pudo_sequences(2, n_open_dropoffs=1)
|
|
191
|
+
assert len(routes) == 30
|
|
192
|
+
assert (4, 0, 1, 2, 3) in routes
|
|
193
|
+
assert (0, 1, 2, 3, 4) in routes
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This represents two new requests plus one already-open request whose drop-off
|
|
197
|
+
is `4`.
|
|
198
|
+
|
|
199
|
+
## Counts
|
|
200
|
+
|
|
201
|
+
For `n` labeled pickup/drop-off pairs, the number of valid sequences is:
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
(2n)! / 2^n
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Equivalently:
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
n! * (1 * 3 * 5 * ... * (2n - 1))
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If there are also `m` already-open drop-offs, the count becomes:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
(2n + m)! / 2^n
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
API:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from pudo_sequences import (
|
|
223
|
+
count_fixed_pickup_order_dropoff_insertions,
|
|
224
|
+
count_open_dropoff_insertions,
|
|
225
|
+
count_pudo_sequences,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
assert count_pudo_sequences(3) == 90
|
|
229
|
+
assert count_pudo_sequences(3, n_open_dropoffs=2) == 5040
|
|
230
|
+
assert count_fixed_pickup_order_dropoff_insertions(3) == 15
|
|
231
|
+
assert count_open_dropoff_insertions(3, 2) == 56
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The constructive count is useful for explaining the structure:
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
1. choose a pickup order;
|
|
238
|
+
2. insert each paired drop-off only after its pickup;
|
|
239
|
+
3. optionally insert already-open drop-offs anywhere.
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Generation Strategies
|
|
243
|
+
|
|
244
|
+
The default strategy is dependency-free DFS over the PU/DO state space:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from pudo_sequences import iter_pudo_sequences
|
|
248
|
+
|
|
249
|
+
for route in iter_pudo_sequences([0, 1, 2], strategy="dfs"):
|
|
250
|
+
pass
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Other strategies are available for comparison:
|
|
254
|
+
|
|
255
|
+
- `strategy="insertion"`: generate pickup orders, then insert each paired
|
|
256
|
+
drop-off only after its pickup.
|
|
257
|
+
- `strategy="topological"`: use NetworkX to enumerate all topological orders of
|
|
258
|
+
a precedence graph with edges `pickup_i -> dropoff_i`.
|
|
259
|
+
|
|
260
|
+
For `n=3`, all strategies represent the same constrained sequence set:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from pudo_sequences import pudo_sequences
|
|
264
|
+
|
|
265
|
+
dfs_routes = set(pudo_sequences([0, 1, 2], strategy="dfs"))
|
|
266
|
+
insertion_routes = set(pudo_sequences([0, 1, 2], strategy="insertion"))
|
|
267
|
+
|
|
268
|
+
assert dfs_routes == insertion_routes
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The generator is intended for small local neighborhoods. PU/DO sequence counts
|
|
272
|
+
grow factorially, so large request sets should usually be handled by heuristics,
|
|
273
|
+
optimization models, or sampling rather than full enumeration.
|
|
274
|
+
|
|
275
|
+
## Prefix Indexing
|
|
276
|
+
|
|
277
|
+
When a caller repeatedly asks for valid continuations after a partial route,
|
|
278
|
+
build a prefix index:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from pudo_sequences import build_pudo_index
|
|
282
|
+
|
|
283
|
+
index = build_pudo_index([0, 1])
|
|
284
|
+
|
|
285
|
+
assert index.continuations([0, 1]) == {(2, 3), (3, 2)}
|
|
286
|
+
assert index.continuations([0, 2]) == {(1, 3)}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The index is a secondary feature. Its purpose is fast continuation lookup, not
|
|
290
|
+
the definition of the package.
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
pip install -e ".[dev]"
|
|
296
|
+
pytest -q
|
|
297
|
+
python -m build --sdist --wheel
|
|
298
|
+
python -m twine check dist/*
|
|
299
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""PU/DO constrained sequence combinatorics."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
count_fixed_pickup_order_dropoff_insertions,
|
|
5
|
+
count_open_dropoff_insertions,
|
|
6
|
+
count_pudo_sequences,
|
|
7
|
+
iter_pudo_sequences,
|
|
8
|
+
pudo_sequences,
|
|
9
|
+
)
|
|
10
|
+
from .index import PudoSequenceIndex, build_pudo_index
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"PudoSequenceIndex",
|
|
14
|
+
"build_pudo_index",
|
|
15
|
+
"count_fixed_pickup_order_dropoff_insertions",
|
|
16
|
+
"count_open_dropoff_insertions",
|
|
17
|
+
"count_pudo_sequences",
|
|
18
|
+
"iter_pudo_sequences",
|
|
19
|
+
"pudo_sequences",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|