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.
Files changed (86) hide show
  1. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/METADATA +57 -10
  2. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/RECORD +86 -69
  3. pytcl/__init__.py +4 -3
  4. pytcl/assignment_algorithms/__init__.py +28 -0
  5. pytcl/assignment_algorithms/dijkstra_min_cost.py +184 -0
  6. pytcl/assignment_algorithms/gating.py +10 -10
  7. pytcl/assignment_algorithms/jpda.py +40 -40
  8. pytcl/assignment_algorithms/nd_assignment.py +379 -0
  9. pytcl/assignment_algorithms/network_flow.py +464 -0
  10. pytcl/assignment_algorithms/network_simplex.py +167 -0
  11. pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
  12. pytcl/astronomical/__init__.py +104 -3
  13. pytcl/astronomical/ephemerides.py +14 -11
  14. pytcl/astronomical/reference_frames.py +865 -56
  15. pytcl/astronomical/relativity.py +6 -5
  16. pytcl/astronomical/sgp4.py +710 -0
  17. pytcl/astronomical/special_orbits.py +532 -0
  18. pytcl/astronomical/tle.py +558 -0
  19. pytcl/atmosphere/__init__.py +43 -1
  20. pytcl/atmosphere/ionosphere.py +512 -0
  21. pytcl/atmosphere/nrlmsise00.py +809 -0
  22. pytcl/clustering/dbscan.py +2 -2
  23. pytcl/clustering/gaussian_mixture.py +3 -3
  24. pytcl/clustering/hierarchical.py +15 -15
  25. pytcl/clustering/kmeans.py +4 -4
  26. pytcl/containers/__init__.py +24 -0
  27. pytcl/containers/base.py +219 -0
  28. pytcl/containers/cluster_set.py +12 -2
  29. pytcl/containers/covertree.py +26 -29
  30. pytcl/containers/kd_tree.py +94 -29
  31. pytcl/containers/rtree.py +200 -1
  32. pytcl/containers/vptree.py +21 -28
  33. pytcl/coordinate_systems/conversions/geodetic.py +272 -5
  34. pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
  35. pytcl/coordinate_systems/projections/__init__.py +1 -1
  36. pytcl/coordinate_systems/projections/projections.py +2 -2
  37. pytcl/coordinate_systems/rotations/rotations.py +10 -6
  38. pytcl/core/__init__.py +18 -0
  39. pytcl/core/validation.py +333 -2
  40. pytcl/dynamic_estimation/__init__.py +26 -0
  41. pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
  42. pytcl/dynamic_estimation/imm.py +14 -14
  43. pytcl/dynamic_estimation/kalman/__init__.py +30 -0
  44. pytcl/dynamic_estimation/kalman/constrained.py +382 -0
  45. pytcl/dynamic_estimation/kalman/extended.py +8 -8
  46. pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
  47. pytcl/dynamic_estimation/kalman/square_root.py +60 -573
  48. pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
  49. pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
  50. pytcl/dynamic_estimation/kalman/unscented.py +8 -6
  51. pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
  52. pytcl/dynamic_estimation/rbpf.py +589 -0
  53. pytcl/gravity/egm.py +13 -0
  54. pytcl/gravity/spherical_harmonics.py +98 -37
  55. pytcl/gravity/tides.py +6 -6
  56. pytcl/logging_config.py +328 -0
  57. pytcl/magnetism/__init__.py +7 -0
  58. pytcl/magnetism/emm.py +10 -3
  59. pytcl/magnetism/wmm.py +260 -23
  60. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
  61. pytcl/mathematical_functions/geometry/geometry.py +5 -5
  62. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
  63. pytcl/mathematical_functions/signal_processing/detection.py +24 -24
  64. pytcl/mathematical_functions/signal_processing/filters.py +14 -14
  65. pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
  66. pytcl/mathematical_functions/special_functions/bessel.py +15 -3
  67. pytcl/mathematical_functions/special_functions/debye.py +136 -26
  68. pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
  69. pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
  70. pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
  71. pytcl/mathematical_functions/transforms/fourier.py +8 -8
  72. pytcl/mathematical_functions/transforms/stft.py +12 -12
  73. pytcl/mathematical_functions/transforms/wavelets.py +9 -9
  74. pytcl/navigation/geodesy.py +246 -160
  75. pytcl/navigation/great_circle.py +101 -19
  76. pytcl/plotting/coordinates.py +7 -7
  77. pytcl/plotting/tracks.py +2 -2
  78. pytcl/static_estimation/maximum_likelihood.py +16 -14
  79. pytcl/static_estimation/robust.py +5 -5
  80. pytcl/terrain/loaders.py +5 -5
  81. pytcl/trackers/hypothesis.py +1 -1
  82. pytcl/trackers/mht.py +9 -9
  83. pytcl/trackers/multi_target.py +1 -1
  84. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/LICENSE +0 -0
  85. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/WHEEL +0 -0
  86. {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.