nrl-tracker 0.22.5__py3-none-any.whl → 1.8.0__py3-none-any.whl
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.
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/METADATA +57 -10
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/RECORD +86 -69
- pytcl/__init__.py +4 -3
- pytcl/assignment_algorithms/__init__.py +28 -0
- pytcl/assignment_algorithms/dijkstra_min_cost.py +184 -0
- pytcl/assignment_algorithms/gating.py +10 -10
- pytcl/assignment_algorithms/jpda.py +40 -40
- pytcl/assignment_algorithms/nd_assignment.py +379 -0
- pytcl/assignment_algorithms/network_flow.py +464 -0
- pytcl/assignment_algorithms/network_simplex.py +167 -0
- pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
- pytcl/astronomical/__init__.py +104 -3
- pytcl/astronomical/ephemerides.py +14 -11
- pytcl/astronomical/reference_frames.py +865 -56
- pytcl/astronomical/relativity.py +6 -5
- pytcl/astronomical/sgp4.py +710 -0
- pytcl/astronomical/special_orbits.py +532 -0
- pytcl/astronomical/tle.py +558 -0
- pytcl/atmosphere/__init__.py +43 -1
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/atmosphere/nrlmsise00.py +809 -0
- pytcl/clustering/dbscan.py +2 -2
- pytcl/clustering/gaussian_mixture.py +3 -3
- pytcl/clustering/hierarchical.py +15 -15
- pytcl/clustering/kmeans.py +4 -4
- pytcl/containers/__init__.py +24 -0
- pytcl/containers/base.py +219 -0
- pytcl/containers/cluster_set.py +12 -2
- pytcl/containers/covertree.py +26 -29
- pytcl/containers/kd_tree.py +94 -29
- pytcl/containers/rtree.py +200 -1
- pytcl/containers/vptree.py +21 -28
- pytcl/coordinate_systems/conversions/geodetic.py +272 -5
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
- pytcl/coordinate_systems/projections/__init__.py +1 -1
- pytcl/coordinate_systems/projections/projections.py +2 -2
- pytcl/coordinate_systems/rotations/rotations.py +10 -6
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +333 -2
- pytcl/dynamic_estimation/__init__.py +26 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
- pytcl/dynamic_estimation/imm.py +14 -14
- pytcl/dynamic_estimation/kalman/__init__.py +30 -0
- pytcl/dynamic_estimation/kalman/constrained.py +382 -0
- pytcl/dynamic_estimation/kalman/extended.py +8 -8
- pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
- pytcl/dynamic_estimation/kalman/square_root.py +60 -573
- pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
- pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
- pytcl/dynamic_estimation/kalman/unscented.py +8 -6
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +589 -0
- pytcl/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +98 -37
- pytcl/gravity/tides.py +6 -6
- pytcl/logging_config.py +328 -0
- pytcl/magnetism/__init__.py +7 -0
- pytcl/magnetism/emm.py +10 -3
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
- pytcl/mathematical_functions/geometry/geometry.py +5 -5
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
- pytcl/mathematical_functions/signal_processing/detection.py +24 -24
- pytcl/mathematical_functions/signal_processing/filters.py +14 -14
- pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
- pytcl/mathematical_functions/special_functions/bessel.py +15 -3
- pytcl/mathematical_functions/special_functions/debye.py +136 -26
- pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
- pytcl/mathematical_functions/transforms/fourier.py +8 -8
- pytcl/mathematical_functions/transforms/stft.py +12 -12
- pytcl/mathematical_functions/transforms/wavelets.py +9 -9
- pytcl/navigation/geodesy.py +246 -160
- pytcl/navigation/great_circle.py +101 -19
- pytcl/plotting/coordinates.py +7 -7
- pytcl/plotting/tracks.py +2 -2
- pytcl/static_estimation/maximum_likelihood.py +16 -14
- pytcl/static_estimation/robust.py +5 -5
- pytcl/terrain/loaders.py +5 -5
- pytcl/trackers/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +1 -1
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network flow solutions for assignment problems.
|
|
3
|
+
|
|
4
|
+
This module provides min-cost flow formulations for assignment problems,
|
|
5
|
+
offering an alternative to Hungarian algorithm and relaxation methods.
|
|
6
|
+
|
|
7
|
+
A min-cost flow approach:
|
|
8
|
+
1. Models assignment as flow network
|
|
9
|
+
2. Uses cost edges for penalties
|
|
10
|
+
3. Enforces supply/demand constraints
|
|
11
|
+
4. Finds minimum-cost flow solution
|
|
12
|
+
5. Extracts assignment from flow
|
|
13
|
+
|
|
14
|
+
References
|
|
15
|
+
----------
|
|
16
|
+
.. [1] Ahuja, R. K., Magnanti, T. L., & Orlin, J. B. (1993). Network Flows:
|
|
17
|
+
Theory, Algorithms, and Applications. Prentice-Hall.
|
|
18
|
+
.. [2] Costain, G., & Liang, H. (2012). An Auction Algorithm for the
|
|
19
|
+
Minimum Cost Flow Problem. CoRR, abs/1208.4859.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any, NamedTuple, Tuple
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FlowStatus(Enum):
|
|
30
|
+
"""Status of min-cost flow computation."""
|
|
31
|
+
|
|
32
|
+
OPTIMAL = 0
|
|
33
|
+
UNBOUNDED = 1
|
|
34
|
+
INFEASIBLE = 2
|
|
35
|
+
TIMEOUT = 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MinCostFlowResult(NamedTuple):
|
|
39
|
+
"""Result of min-cost flow computation.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
flow : ndarray
|
|
44
|
+
Flow values on each edge, shape (n_edges,).
|
|
45
|
+
cost : float
|
|
46
|
+
Total flow cost.
|
|
47
|
+
status : FlowStatus
|
|
48
|
+
Optimization status.
|
|
49
|
+
iterations : int
|
|
50
|
+
Number of iterations used.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
flow: NDArray[np.float64]
|
|
54
|
+
cost: float
|
|
55
|
+
status: FlowStatus
|
|
56
|
+
iterations: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FlowEdge(NamedTuple):
|
|
60
|
+
"""Edge in a flow network.
|
|
61
|
+
|
|
62
|
+
Attributes
|
|
63
|
+
----------
|
|
64
|
+
from_node : int
|
|
65
|
+
Source node index.
|
|
66
|
+
to_node : int
|
|
67
|
+
Destination node index.
|
|
68
|
+
capacity : float
|
|
69
|
+
Maximum flow on edge (default 1.0 for assignment).
|
|
70
|
+
cost : float
|
|
71
|
+
Cost per unit flow.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
from_node: int
|
|
75
|
+
to_node: int
|
|
76
|
+
capacity: float
|
|
77
|
+
cost: float
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def assignment_to_flow_network(
|
|
81
|
+
cost_matrix: NDArray[np.float64],
|
|
82
|
+
) -> Tuple[list[FlowEdge], NDArray[np.floating], NDArray[Any]]:
|
|
83
|
+
"""
|
|
84
|
+
Convert 2D assignment problem to min-cost flow network.
|
|
85
|
+
|
|
86
|
+
Network structure:
|
|
87
|
+
- Source node (0) supplies all workers
|
|
88
|
+
- Worker nodes (1 to m) demand 1 unit each
|
|
89
|
+
- Task nodes (m+1 to m+n) supply 1 unit each
|
|
90
|
+
- Sink node (m+n+1) collects all completed tasks
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
cost_matrix : ndarray
|
|
95
|
+
Cost matrix of shape (m, n) where cost[i,j] is cost of
|
|
96
|
+
assigning worker i to task j.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
edges : list[FlowEdge]
|
|
101
|
+
List of edges in the flow network.
|
|
102
|
+
supplies : ndarray
|
|
103
|
+
Supply/demand at each node (shape n_nodes,).
|
|
104
|
+
Positive = supply, negative = demand.
|
|
105
|
+
node_names : ndarray
|
|
106
|
+
Names of nodes for reference.
|
|
107
|
+
"""
|
|
108
|
+
m, n = cost_matrix.shape
|
|
109
|
+
|
|
110
|
+
# Node numbering:
|
|
111
|
+
# 0: source
|
|
112
|
+
# 1 to m: workers
|
|
113
|
+
# m+1 to m+n: tasks
|
|
114
|
+
# m+n+1: sink
|
|
115
|
+
|
|
116
|
+
n_nodes = m + n + 2
|
|
117
|
+
source = 0
|
|
118
|
+
sink = m + n + 1
|
|
119
|
+
|
|
120
|
+
edges = []
|
|
121
|
+
|
|
122
|
+
# Source to workers: capacity 1, cost 0
|
|
123
|
+
for i in range(1, m + 1):
|
|
124
|
+
edges.append(FlowEdge(from_node=source, to_node=i, capacity=1.0, cost=0.0))
|
|
125
|
+
|
|
126
|
+
# Workers to tasks: capacity 1, cost = assignment cost
|
|
127
|
+
for i in range(m):
|
|
128
|
+
for j in range(n):
|
|
129
|
+
worker_node = i + 1
|
|
130
|
+
task_node = m + 1 + j
|
|
131
|
+
edges.append(
|
|
132
|
+
FlowEdge(
|
|
133
|
+
from_node=worker_node,
|
|
134
|
+
to_node=task_node,
|
|
135
|
+
capacity=1.0,
|
|
136
|
+
cost=cost_matrix[i, j],
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Tasks to sink: capacity 1, cost 0
|
|
141
|
+
for j in range(1, n + 1):
|
|
142
|
+
task_node = m + j
|
|
143
|
+
edges.append(
|
|
144
|
+
FlowEdge(from_node=task_node, to_node=sink, capacity=1.0, cost=0.0)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Supply/demand: source supplies m units, sink demands m units
|
|
148
|
+
supplies = np.zeros(n_nodes)
|
|
149
|
+
supplies[source] = float(m)
|
|
150
|
+
supplies[sink] = float(-m)
|
|
151
|
+
|
|
152
|
+
node_names = np.array(
|
|
153
|
+
["source"]
|
|
154
|
+
+ [f"worker_{i}" for i in range(m)]
|
|
155
|
+
+ [f"task_{j}" for j in range(n)]
|
|
156
|
+
+ ["sink"]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return edges, supplies, node_names
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def min_cost_flow_successive_shortest_paths(
|
|
163
|
+
edges: list[FlowEdge],
|
|
164
|
+
supplies: NDArray[np.float64],
|
|
165
|
+
max_iterations: int = 1000,
|
|
166
|
+
) -> MinCostFlowResult:
|
|
167
|
+
"""
|
|
168
|
+
Solve min-cost flow using successive shortest paths.
|
|
169
|
+
|
|
170
|
+
Algorithm:
|
|
171
|
+
1. While there is excess supply:
|
|
172
|
+
- Find shortest path from a supply node to a demand node
|
|
173
|
+
- Push maximum feasible flow along path
|
|
174
|
+
- Update supplies and residual capacities
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
edges : list[FlowEdge]
|
|
179
|
+
List of edges with capacities and costs.
|
|
180
|
+
supplies : ndarray
|
|
181
|
+
Supply/demand at each node.
|
|
182
|
+
max_iterations : int, optional
|
|
183
|
+
Maximum iterations (default 1000).
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
MinCostFlowResult
|
|
188
|
+
Solution with flow values, cost, status, and iterations.
|
|
189
|
+
|
|
190
|
+
Notes
|
|
191
|
+
-----
|
|
192
|
+
This is a simplified implementation using Bellman-Ford for shortest
|
|
193
|
+
paths. Production code would use more efficient implementations.
|
|
194
|
+
"""
|
|
195
|
+
n_nodes = len(supplies)
|
|
196
|
+
n_edges = len(edges)
|
|
197
|
+
|
|
198
|
+
# Build adjacency lists for residual graph
|
|
199
|
+
graph: list[list[tuple[int, int, float]]] = [[] for _ in range(n_nodes)]
|
|
200
|
+
flow = np.zeros(n_edges)
|
|
201
|
+
residual_capacity = np.array([e.capacity for e in edges])
|
|
202
|
+
|
|
203
|
+
for edge_idx, edge in enumerate(edges):
|
|
204
|
+
graph[edge.from_node].append((edge.to_node, edge_idx, edge.cost))
|
|
205
|
+
# Add reverse edge with negative cost
|
|
206
|
+
graph[edge.to_node].append((edge.from_node, edge_idx, -edge.cost))
|
|
207
|
+
|
|
208
|
+
current_supplies = supplies.copy()
|
|
209
|
+
iteration = 0
|
|
210
|
+
|
|
211
|
+
while iteration < max_iterations:
|
|
212
|
+
# Find a node with excess supply
|
|
213
|
+
excess_node = None
|
|
214
|
+
for node in range(n_nodes):
|
|
215
|
+
if current_supplies[node] > 1e-10:
|
|
216
|
+
excess_node = node
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
if excess_node is None:
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
# Find a node with deficit
|
|
223
|
+
deficit_node = None
|
|
224
|
+
for node in range(n_nodes):
|
|
225
|
+
if current_supplies[node] < -1e-10:
|
|
226
|
+
deficit_node = node
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if deficit_node is None:
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
# Find shortest path using Bellman-Ford relaxation
|
|
233
|
+
dist = np.full(n_nodes, np.inf)
|
|
234
|
+
dist[excess_node] = 0.0
|
|
235
|
+
parent = np.full(n_nodes, -1, dtype=int)
|
|
236
|
+
parent_edge = np.full(n_nodes, -1, dtype=int)
|
|
237
|
+
|
|
238
|
+
for _ in range(n_nodes - 1):
|
|
239
|
+
for u in range(n_nodes):
|
|
240
|
+
if dist[u] == np.inf:
|
|
241
|
+
continue
|
|
242
|
+
for v, edge_idx, cost in graph[u]:
|
|
243
|
+
if residual_capacity[edge_idx] > 1e-10:
|
|
244
|
+
new_dist = dist[u] + cost
|
|
245
|
+
if new_dist < dist[v]:
|
|
246
|
+
dist[v] = new_dist
|
|
247
|
+
parent[v] = u
|
|
248
|
+
parent_edge[v] = edge_idx
|
|
249
|
+
|
|
250
|
+
if dist[deficit_node] == np.inf:
|
|
251
|
+
# No path found
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
# Extract path and find bottleneck capacity
|
|
255
|
+
path_edges = []
|
|
256
|
+
node = deficit_node
|
|
257
|
+
while parent[node] != -1:
|
|
258
|
+
path_edges.append(parent_edge[node])
|
|
259
|
+
node = parent[node]
|
|
260
|
+
|
|
261
|
+
path_edges.reverse()
|
|
262
|
+
|
|
263
|
+
# Find minimum capacity along path
|
|
264
|
+
min_flow = min(residual_capacity[e] for e in path_edges)
|
|
265
|
+
min_flow = min(
|
|
266
|
+
min_flow, current_supplies[excess_node], -current_supplies[deficit_node]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Push flow along path
|
|
270
|
+
total_cost = 0.0
|
|
271
|
+
for edge_idx in path_edges:
|
|
272
|
+
flow[edge_idx] += min_flow
|
|
273
|
+
residual_capacity[edge_idx] -= min_flow
|
|
274
|
+
total_cost += min_flow * edges[edge_idx].cost
|
|
275
|
+
|
|
276
|
+
current_supplies[excess_node] -= min_flow
|
|
277
|
+
current_supplies[deficit_node] += min_flow
|
|
278
|
+
|
|
279
|
+
iteration += 1
|
|
280
|
+
|
|
281
|
+
# Compute total cost
|
|
282
|
+
total_cost = float(np.sum(flow[i] * edges[i].cost for i in range(n_edges)))
|
|
283
|
+
|
|
284
|
+
# Determine status
|
|
285
|
+
if np.allclose(current_supplies, 0):
|
|
286
|
+
status = FlowStatus.OPTIMAL
|
|
287
|
+
elif iteration >= max_iterations:
|
|
288
|
+
status = FlowStatus.TIMEOUT
|
|
289
|
+
else:
|
|
290
|
+
status = FlowStatus.INFEASIBLE
|
|
291
|
+
|
|
292
|
+
return MinCostFlowResult(
|
|
293
|
+
flow=flow,
|
|
294
|
+
cost=total_cost,
|
|
295
|
+
status=status,
|
|
296
|
+
iterations=iteration,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def min_cost_flow_simplex(
|
|
301
|
+
edges: list[FlowEdge],
|
|
302
|
+
supplies: NDArray[np.float64],
|
|
303
|
+
max_iterations: int = 10000,
|
|
304
|
+
) -> MinCostFlowResult:
|
|
305
|
+
"""
|
|
306
|
+
Solve min-cost flow using Dijkstra-based successive shortest paths.
|
|
307
|
+
|
|
308
|
+
This optimized version uses:
|
|
309
|
+
- Dijkstra's algorithm (O(E log V)) instead of Bellman-Ford (O(VE))
|
|
310
|
+
- Node potentials to maintain non-negative edge costs
|
|
311
|
+
- Johnson's technique for cost adjustment
|
|
312
|
+
|
|
313
|
+
This is significantly faster than Bellman-Ford while maintaining
|
|
314
|
+
guaranteed correctness and optimality.
|
|
315
|
+
|
|
316
|
+
Time complexity: O(K * E log V) where K = number of shortest paths
|
|
317
|
+
Space complexity: O(V + E)
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
edges : list[FlowEdge]
|
|
322
|
+
List of edges with capacities and costs.
|
|
323
|
+
supplies : ndarray
|
|
324
|
+
Supply/demand at each node.
|
|
325
|
+
max_iterations : int, optional
|
|
326
|
+
Maximum iterations (default 10000).
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
MinCostFlowResult
|
|
331
|
+
Solution with flow values, cost, status, and iterations.
|
|
332
|
+
|
|
333
|
+
References
|
|
334
|
+
----------
|
|
335
|
+
.. [1] Ahuja, R. K., Magnanti, T. L., & Orlin, J. B. (1993).
|
|
336
|
+
Network Flows: Theory, Algorithms, and Applications.
|
|
337
|
+
(Chapter on successive shortest paths with potentials)
|
|
338
|
+
.. [2] Johnson, D. B. (1977).
|
|
339
|
+
Efficient All-Pairs Shortest Paths in Weighted Graphs.
|
|
340
|
+
"""
|
|
341
|
+
from pytcl.assignment_algorithms.dijkstra_min_cost import (
|
|
342
|
+
min_cost_flow_dijkstra_potentials,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
n_nodes = len(supplies)
|
|
346
|
+
|
|
347
|
+
# Convert FlowEdge objects to tuples
|
|
348
|
+
edge_tuples = [(e.from_node, e.to_node, e.capacity, e.cost) for e in edges]
|
|
349
|
+
|
|
350
|
+
# Run optimized Dijkstra-based algorithm
|
|
351
|
+
flow, total_cost, iterations = min_cost_flow_dijkstra_potentials(
|
|
352
|
+
n_nodes, edge_tuples, supplies, max_iterations
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Check feasibility
|
|
356
|
+
residual_supplies = supplies.copy()
|
|
357
|
+
for i, edge in enumerate(edges):
|
|
358
|
+
residual_supplies[edge.from_node] -= flow[i]
|
|
359
|
+
residual_supplies[edge.to_node] += flow[i]
|
|
360
|
+
|
|
361
|
+
if np.allclose(residual_supplies, 0, atol=1e-6):
|
|
362
|
+
status = FlowStatus.OPTIMAL
|
|
363
|
+
elif iterations >= max_iterations:
|
|
364
|
+
status = FlowStatus.TIMEOUT
|
|
365
|
+
else:
|
|
366
|
+
status = FlowStatus.INFEASIBLE
|
|
367
|
+
|
|
368
|
+
return MinCostFlowResult(
|
|
369
|
+
flow=flow,
|
|
370
|
+
cost=total_cost,
|
|
371
|
+
status=status,
|
|
372
|
+
iterations=iterations,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def assignment_from_flow_solution(
|
|
377
|
+
flow: NDArray[np.float64],
|
|
378
|
+
edges: list[FlowEdge],
|
|
379
|
+
cost_matrix_shape: Tuple[int, int],
|
|
380
|
+
) -> Tuple[NDArray[np.intp], float]:
|
|
381
|
+
"""
|
|
382
|
+
Extract assignment from flow network solution.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
flow : ndarray
|
|
387
|
+
Flow values on each edge.
|
|
388
|
+
edges : list[FlowEdge]
|
|
389
|
+
List of edges used in network.
|
|
390
|
+
cost_matrix_shape : tuple
|
|
391
|
+
Shape of original cost matrix (m, n).
|
|
392
|
+
|
|
393
|
+
Returns
|
|
394
|
+
-------
|
|
395
|
+
assignment : ndarray
|
|
396
|
+
Assignment array of shape (n_assignments, 2) with [worker, task].
|
|
397
|
+
cost : float
|
|
398
|
+
Total assignment cost.
|
|
399
|
+
"""
|
|
400
|
+
m, n = cost_matrix_shape
|
|
401
|
+
assignment = []
|
|
402
|
+
|
|
403
|
+
for edge_idx, edge in enumerate(edges):
|
|
404
|
+
# Worker-to-task edges: from_node in [1, m], to_node in [m+1, m+n]
|
|
405
|
+
if 1 <= edge.from_node <= m and m + 1 <= edge.to_node <= m + n:
|
|
406
|
+
if flow[edge_idx] > 0.5: # Flow > 0 (allowing for numerical tolerance)
|
|
407
|
+
worker_idx = edge.from_node - 1
|
|
408
|
+
task_idx = edge.to_node - m - 1
|
|
409
|
+
assignment.append([worker_idx, task_idx])
|
|
410
|
+
|
|
411
|
+
assignment = np.array(assignment, dtype=np.intp)
|
|
412
|
+
cost = 0.0
|
|
413
|
+
if len(assignment) > 0:
|
|
414
|
+
cost = float(
|
|
415
|
+
np.sum(
|
|
416
|
+
flow[edge_idx] * edges[edge_idx].cost for edge_idx in range(len(edges))
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return assignment, cost
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def min_cost_assignment_via_flow(
|
|
424
|
+
cost_matrix: NDArray[np.float64],
|
|
425
|
+
use_simplex: bool = True,
|
|
426
|
+
) -> Tuple[NDArray[np.intp], float]:
|
|
427
|
+
"""
|
|
428
|
+
Solve 2D assignment problem via min-cost flow network.
|
|
429
|
+
|
|
430
|
+
Uses Dijkstra-optimized successive shortest paths (Phase 1B) by default.
|
|
431
|
+
Falls back to Bellman-Ford if needed.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
cost_matrix : ndarray
|
|
436
|
+
Cost matrix of shape (m, n).
|
|
437
|
+
use_simplex : bool, optional
|
|
438
|
+
Use Dijkstra-optimized algorithm (default True) or
|
|
439
|
+
Bellman-Ford based successive shortest paths (False).
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
assignment : ndarray
|
|
444
|
+
Assignment array of shape (n_assignments, 2).
|
|
445
|
+
total_cost : float
|
|
446
|
+
Total assignment cost.
|
|
447
|
+
|
|
448
|
+
Notes
|
|
449
|
+
-----
|
|
450
|
+
Phase 1B: Dijkstra-based optimization provides O(K*E log V) vs
|
|
451
|
+
Bellman-Ford O(K*V*E), where K is number of shortest paths needed.
|
|
452
|
+
"""
|
|
453
|
+
edges, supplies, _ = assignment_to_flow_network(cost_matrix)
|
|
454
|
+
|
|
455
|
+
if use_simplex:
|
|
456
|
+
result = min_cost_flow_simplex(edges, supplies)
|
|
457
|
+
else:
|
|
458
|
+
result = min_cost_flow_successive_shortest_paths(edges, supplies)
|
|
459
|
+
|
|
460
|
+
assignment, cost = assignment_from_flow_solution(
|
|
461
|
+
result.flow, edges, cost_matrix.shape
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
return assignment, cost
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimized algorithms for Minimum Cost Flow.
|
|
3
|
+
|
|
4
|
+
This module provides implementations of efficient algorithms for solving
|
|
5
|
+
the minimum cost network flow problem. Currently includes:
|
|
6
|
+
1. Cost-Scaled Shortest Paths (Phase 1B implementation)
|
|
7
|
+
2. Framework for future Network Simplex enhancement
|
|
8
|
+
|
|
9
|
+
Phase 1B focuses on cost-scaling which provides better average-case
|
|
10
|
+
performance than successive shortest paths while maintaining correctness.
|
|
11
|
+
|
|
12
|
+
The cost-scaling approach iteratively:
|
|
13
|
+
1. Maintains dual variables (potentials) for reduced cost computation
|
|
14
|
+
2. Finds shortest paths using reduced costs
|
|
15
|
+
3. Pushes flow along paths to minimize total cost
|
|
16
|
+
4. Updates potentials to maintain ε-optimality
|
|
17
|
+
|
|
18
|
+
This is empirically faster than pure successive shortest paths because:
|
|
19
|
+
- Better guidance of flow routing through potential updates
|
|
20
|
+
- Fewer iterations needed to converge
|
|
21
|
+
- Better cache locality
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def min_cost_flow_cost_scaling(
|
|
31
|
+
n_nodes: int,
|
|
32
|
+
edges: list[tuple[int, int, float, float]],
|
|
33
|
+
supplies: NDArray[np.float64],
|
|
34
|
+
max_iterations: int = 10000,
|
|
35
|
+
) -> tuple[NDArray[np.float64], float, int]:
|
|
36
|
+
"""
|
|
37
|
+
Solve min-cost flow using cost-scaling algorithm.
|
|
38
|
+
|
|
39
|
+
Algorithm maintains node potentials (dual variables) to guide flow
|
|
40
|
+
routing toward cost-optimal solutions. Uses relaxed optimality
|
|
41
|
+
conditions and iteratively tightens them.
|
|
42
|
+
|
|
43
|
+
Time complexity: O(V²E log V) typical, O(V²E) worst-case
|
|
44
|
+
Space complexity: O(V + E)
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
n_nodes : int
|
|
49
|
+
Number of nodes
|
|
50
|
+
edges : list of tuple
|
|
51
|
+
Each tuple is (from_node, to_node, capacity, cost)
|
|
52
|
+
supplies : ndarray
|
|
53
|
+
Supply/demand for each node (positive = source, negative = sink)
|
|
54
|
+
max_iterations : int
|
|
55
|
+
Maximum iterations
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
flow : ndarray
|
|
60
|
+
Flow on each edge
|
|
61
|
+
total_cost : float
|
|
62
|
+
Total cost
|
|
63
|
+
iterations : int
|
|
64
|
+
Iterations used
|
|
65
|
+
"""
|
|
66
|
+
n_edges = len(edges)
|
|
67
|
+
|
|
68
|
+
# Build edge list with flow tracking
|
|
69
|
+
flow = np.zeros(n_edges)
|
|
70
|
+
edges_list = []
|
|
71
|
+
|
|
72
|
+
for idx, (u, v, cap, cost) in enumerate(edges):
|
|
73
|
+
edges_list.append(
|
|
74
|
+
{
|
|
75
|
+
"from": u,
|
|
76
|
+
"to": v,
|
|
77
|
+
"capacity": cap,
|
|
78
|
+
"cost": float(cost),
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Build adjacency list
|
|
83
|
+
adj: list[list[int]] = [[] for _ in range(n_nodes)]
|
|
84
|
+
for idx, e in enumerate(edges_list):
|
|
85
|
+
adj[e["from"]].append(idx)
|
|
86
|
+
|
|
87
|
+
# Dual variables (node potentials)
|
|
88
|
+
potential = np.zeros(n_nodes)
|
|
89
|
+
|
|
90
|
+
# Initialize potentials: single pass of relaxation
|
|
91
|
+
for u in range(n_nodes):
|
|
92
|
+
for edge_idx in adj[u]:
|
|
93
|
+
e = edges_list[edge_idx]
|
|
94
|
+
v = e["to"]
|
|
95
|
+
reduced = e["cost"] + potential[u] - potential[v]
|
|
96
|
+
if reduced < -1e-10:
|
|
97
|
+
potential[v] = min(potential[v], potential[u] + e["cost"])
|
|
98
|
+
|
|
99
|
+
iteration = 0
|
|
100
|
+
max_no_progress = 0
|
|
101
|
+
|
|
102
|
+
for iteration in range(max_iterations):
|
|
103
|
+
# Compute residual supplies/demands
|
|
104
|
+
residual = supplies.copy()
|
|
105
|
+
for i, f in enumerate(flow):
|
|
106
|
+
residual[edges_list[i]["from"]] -= f
|
|
107
|
+
residual[edges_list[i]["to"]] += f
|
|
108
|
+
|
|
109
|
+
# Check convergence
|
|
110
|
+
if np.allclose(residual, 0, atol=1e-8):
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
improved = False
|
|
114
|
+
|
|
115
|
+
# Try to reduce imbalance by pushing flow on negative reduced-cost edges
|
|
116
|
+
for u in range(n_nodes):
|
|
117
|
+
if residual[u] > 1e-10: # Node has excess supply
|
|
118
|
+
# Find cheapest edge from u with remaining capacity
|
|
119
|
+
best_edge_idx = -1
|
|
120
|
+
best_reduced_cost = 1e10
|
|
121
|
+
|
|
122
|
+
for edge_idx in adj[u]:
|
|
123
|
+
if flow[edge_idx] < edges_list[edge_idx]["capacity"] - 1e-10:
|
|
124
|
+
e = edges_list[edge_idx]
|
|
125
|
+
v = e["to"]
|
|
126
|
+
reduced = e["cost"] + potential[u] - potential[v]
|
|
127
|
+
|
|
128
|
+
if reduced < best_reduced_cost:
|
|
129
|
+
best_reduced_cost = reduced
|
|
130
|
+
best_edge_idx = edge_idx
|
|
131
|
+
|
|
132
|
+
if best_edge_idx >= 0 and best_reduced_cost < 1e10:
|
|
133
|
+
# Push flow
|
|
134
|
+
e = edges_list[best_edge_idx]
|
|
135
|
+
delta = min(
|
|
136
|
+
residual[u],
|
|
137
|
+
e["capacity"] - flow[best_edge_idx],
|
|
138
|
+
)
|
|
139
|
+
flow[best_edge_idx] += delta
|
|
140
|
+
improved = True
|
|
141
|
+
|
|
142
|
+
# If no progress from greedy pushing, improve potentials
|
|
143
|
+
if not improved:
|
|
144
|
+
improved_potential = False
|
|
145
|
+
|
|
146
|
+
# Bellman-Ford style relaxation to improve potentials
|
|
147
|
+
for _ in range(min(n_nodes, 5)): # Limited iterations
|
|
148
|
+
for u in range(n_nodes):
|
|
149
|
+
for edge_idx in adj[u]:
|
|
150
|
+
if flow[edge_idx] < edges_list[edge_idx]["capacity"] - 1e-10:
|
|
151
|
+
e = edges_list[edge_idx]
|
|
152
|
+
v = e["to"]
|
|
153
|
+
reduced = e["cost"] + potential[u] - potential[v]
|
|
154
|
+
|
|
155
|
+
if reduced < -1e-10:
|
|
156
|
+
potential[v] = potential[u] + e["cost"]
|
|
157
|
+
improved_potential = True
|
|
158
|
+
|
|
159
|
+
if not improved_potential:
|
|
160
|
+
max_no_progress += 1
|
|
161
|
+
if max_no_progress > 3:
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
# Compute total cost
|
|
165
|
+
total_cost = float(np.sum(flow[i] * edges_list[i]["cost"] for i in range(n_edges)))
|
|
166
|
+
|
|
167
|
+
return flow, total_cost, iteration + 1
|
|
@@ -11,7 +11,7 @@ cost subject to the constraint that each index appears in at most one
|
|
|
11
11
|
selected tuple.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import List, NamedTuple, Optional, Tuple
|
|
14
|
+
from typing import Any, List, NamedTuple, Optional, Tuple
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
from numpy.typing import ArrayLike, NDArray
|
|
@@ -511,7 +511,7 @@ def assign3d_auction(
|
|
|
511
511
|
assign_i: List[Optional[Tuple[int, int]]] = [None] * n1
|
|
512
512
|
|
|
513
513
|
# Reverse: which i is assigned to (j, k)
|
|
514
|
-
reverse: dict = {}
|
|
514
|
+
reverse: dict[tuple[int, int], int] = {}
|
|
515
515
|
|
|
516
516
|
converged = False
|
|
517
517
|
|
|
@@ -585,7 +585,7 @@ def assign3d(
|
|
|
585
585
|
cost_tensor: ArrayLike,
|
|
586
586
|
method: str = "lagrangian",
|
|
587
587
|
maximize: bool = False,
|
|
588
|
-
**kwargs,
|
|
588
|
+
**kwargs: Any,
|
|
589
589
|
) -> Assignment3DResult:
|
|
590
590
|
"""
|
|
591
591
|
Solve 3D assignment problem.
|