nrl-tracker 1.6.0__py3-none-any.whl → 1.7.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.
@@ -0,0 +1,361 @@
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 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, NDArray, NDArray]:
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(FlowEdge(from_node=task_node, to_node=sink, capacity=1.0, cost=0.0))
144
+
145
+ # Supply/demand: source supplies m units, sink demands m units
146
+ supplies = np.zeros(n_nodes)
147
+ supplies[source] = float(m)
148
+ supplies[sink] = float(-m)
149
+
150
+ node_names = np.array(
151
+ ["source"]
152
+ + [f"worker_{i}" for i in range(m)]
153
+ + [f"task_{j}" for j in range(n)]
154
+ + ["sink"]
155
+ )
156
+
157
+ return edges, supplies, node_names
158
+
159
+
160
+ def min_cost_flow_successive_shortest_paths(
161
+ edges: list,
162
+ supplies: NDArray[np.float64],
163
+ max_iterations: int = 1000,
164
+ ) -> MinCostFlowResult:
165
+ """
166
+ Solve min-cost flow using successive shortest paths.
167
+
168
+ Algorithm:
169
+ 1. While there is excess supply:
170
+ - Find shortest path from a supply node to a demand node
171
+ - Push maximum feasible flow along path
172
+ - Update supplies and residual capacities
173
+
174
+ Parameters
175
+ ----------
176
+ edges : list[FlowEdge]
177
+ List of edges with capacities and costs.
178
+ supplies : ndarray
179
+ Supply/demand at each node.
180
+ max_iterations : int, optional
181
+ Maximum iterations (default 1000).
182
+
183
+ Returns
184
+ -------
185
+ MinCostFlowResult
186
+ Solution with flow values, cost, status, and iterations.
187
+
188
+ Notes
189
+ -----
190
+ This is a simplified implementation using Bellman-Ford for shortest
191
+ paths. Production code would use more efficient implementations.
192
+ """
193
+ n_nodes = len(supplies)
194
+ n_edges = len(edges)
195
+
196
+ # Build adjacency lists for residual graph
197
+ graph: list[list[tuple[int, int, float]]] = [[] for _ in range(n_nodes)]
198
+ flow = np.zeros(n_edges)
199
+ residual_capacity = np.array([e.capacity for e in edges])
200
+
201
+ for edge_idx, edge in enumerate(edges):
202
+ graph[edge.from_node].append((edge.to_node, edge_idx, edge.cost))
203
+ # Add reverse edge with negative cost
204
+ graph[edge.to_node].append((edge.from_node, edge_idx, -edge.cost))
205
+
206
+ current_supplies = supplies.copy()
207
+ iteration = 0
208
+
209
+ while iteration < max_iterations:
210
+ # Find a node with excess supply
211
+ excess_node = None
212
+ for node in range(n_nodes):
213
+ if current_supplies[node] > 1e-10:
214
+ excess_node = node
215
+ break
216
+
217
+ if excess_node is None:
218
+ break
219
+
220
+ # Find a node with deficit
221
+ deficit_node = None
222
+ for node in range(n_nodes):
223
+ if current_supplies[node] < -1e-10:
224
+ deficit_node = node
225
+ break
226
+
227
+ if deficit_node is None:
228
+ break
229
+
230
+ # Find shortest path using Bellman-Ford relaxation
231
+ dist = np.full(n_nodes, np.inf)
232
+ dist[excess_node] = 0.0
233
+ parent = np.full(n_nodes, -1, dtype=int)
234
+ parent_edge = np.full(n_nodes, -1, dtype=int)
235
+
236
+ for _ in range(n_nodes - 1):
237
+ for u in range(n_nodes):
238
+ if dist[u] == np.inf:
239
+ continue
240
+ for v, edge_idx, cost in graph[u]:
241
+ if residual_capacity[edge_idx] > 1e-10:
242
+ new_dist = dist[u] + cost
243
+ if new_dist < dist[v]:
244
+ dist[v] = new_dist
245
+ parent[v] = u
246
+ parent_edge[v] = edge_idx
247
+
248
+ if dist[deficit_node] == np.inf:
249
+ # No path found
250
+ break
251
+
252
+ # Extract path and find bottleneck capacity
253
+ path_edges = []
254
+ node = deficit_node
255
+ while parent[node] != -1:
256
+ path_edges.append(parent_edge[node])
257
+ node = parent[node]
258
+
259
+ path_edges.reverse()
260
+
261
+ # Find minimum capacity along path
262
+ min_flow = min(residual_capacity[e] for e in path_edges)
263
+ min_flow = min(min_flow, current_supplies[excess_node], -current_supplies[deficit_node])
264
+
265
+ # Push flow along path
266
+ total_cost = 0.0
267
+ for edge_idx in path_edges:
268
+ flow[edge_idx] += min_flow
269
+ residual_capacity[edge_idx] -= min_flow
270
+ total_cost += min_flow * edges[edge_idx].cost
271
+
272
+ current_supplies[excess_node] -= min_flow
273
+ current_supplies[deficit_node] += min_flow
274
+
275
+ iteration += 1
276
+
277
+ # Compute total cost
278
+ total_cost = float(np.sum(flow[i] * edges[i].cost for i in range(n_edges)))
279
+
280
+ # Determine status
281
+ if np.allclose(current_supplies, 0):
282
+ status = FlowStatus.OPTIMAL
283
+ elif iteration >= max_iterations:
284
+ status = FlowStatus.TIMEOUT
285
+ else:
286
+ status = FlowStatus.INFEASIBLE
287
+
288
+ return MinCostFlowResult(
289
+ flow=flow,
290
+ cost=total_cost,
291
+ status=status,
292
+ iterations=iteration,
293
+ )
294
+
295
+
296
+ def assignment_from_flow_solution(
297
+ flow: NDArray[np.float64],
298
+ edges: list,
299
+ cost_matrix_shape: Tuple[int, int],
300
+ ) -> Tuple[NDArray[np.intp], float]:
301
+ """
302
+ Extract assignment from flow network solution.
303
+
304
+ Parameters
305
+ ----------
306
+ flow : ndarray
307
+ Flow values on each edge.
308
+ edges : list[FlowEdge]
309
+ List of edges used in network.
310
+ cost_matrix_shape : tuple
311
+ Shape of original cost matrix (m, n).
312
+
313
+ Returns
314
+ -------
315
+ assignment : ndarray
316
+ Assignment array of shape (n_assignments, 2) with [worker, task].
317
+ cost : float
318
+ Total assignment cost.
319
+ """
320
+ m, n = cost_matrix_shape
321
+ assignment = []
322
+
323
+ for edge_idx, edge in enumerate(edges):
324
+ # Worker-to-task edges: from_node in [1, m], to_node in [m+1, m+n]
325
+ if 1 <= edge.from_node <= m and m + 1 <= edge.to_node <= m + n:
326
+ if flow[edge_idx] > 0.5: # Flow > 0 (allowing for numerical tolerance)
327
+ worker_idx = edge.from_node - 1
328
+ task_idx = edge.to_node - m - 1
329
+ assignment.append([worker_idx, task_idx])
330
+
331
+ assignment = np.array(assignment, dtype=np.intp)
332
+ cost = 0.0
333
+ if len(assignment) > 0:
334
+ cost = float(np.sum(flow[edge_idx] * edges[edge_idx].cost for edge_idx in range(len(edges))))
335
+
336
+ return assignment, cost
337
+
338
+
339
+ def min_cost_assignment_via_flow(
340
+ cost_matrix: NDArray[np.float64],
341
+ ) -> Tuple[NDArray[np.intp], float]:
342
+ """
343
+ Solve 2D assignment problem via min-cost flow network.
344
+
345
+ Parameters
346
+ ----------
347
+ cost_matrix : ndarray
348
+ Cost matrix of shape (m, n).
349
+
350
+ Returns
351
+ -------
352
+ assignment : ndarray
353
+ Assignment array of shape (n_assignments, 2).
354
+ total_cost : float
355
+ Total assignment cost.
356
+ """
357
+ edges, supplies, _ = assignment_to_flow_network(cost_matrix)
358
+ result = min_cost_flow_successive_shortest_paths(edges, supplies)
359
+ assignment, cost = assignment_from_flow_solution(result.flow, edges, cost_matrix.shape)
360
+
361
+ return assignment, cost
@@ -130,6 +130,24 @@ from pytcl.astronomical.relativity import (
130
130
  schwarzschild_radius,
131
131
  shapiro_delay,
132
132
  )
133
+ from pytcl.astronomical.special_orbits import (
134
+ OrbitType,
135
+ classify_orbit,
136
+ eccentricity_vector,
137
+ escape_velocity_at_radius,
138
+ hyperbolic_anomaly_to_true_anomaly,
139
+ hyperbolic_asymptote_angle,
140
+ hyperbolic_deflection_angle,
141
+ hyperbolic_excess_velocity,
142
+ mean_to_parabolic_anomaly,
143
+ mean_to_true_anomaly_parabolic,
144
+ parabolic_anomaly_to_true_anomaly,
145
+ radius_parabolic,
146
+ semi_major_axis_from_energy,
147
+ true_anomaly_to_hyperbolic_anomaly,
148
+ true_anomaly_to_parabolic_anomaly,
149
+ velocity_parabolic,
150
+ )
133
151
  from pytcl.astronomical.sgp4 import (
134
152
  SGP4Satellite,
135
153
  SGP4State,
@@ -337,4 +355,21 @@ __all__ = [
337
355
  "geodetic_precession",
338
356
  "lense_thirring_precession",
339
357
  "relativistic_range_correction",
358
+ # Special orbits - Parabolic and hyperbolic
359
+ "OrbitType",
360
+ "classify_orbit",
361
+ "mean_to_parabolic_anomaly",
362
+ "parabolic_anomaly_to_true_anomaly",
363
+ "true_anomaly_to_parabolic_anomaly",
364
+ "mean_to_true_anomaly_parabolic",
365
+ "radius_parabolic",
366
+ "velocity_parabolic",
367
+ "hyperbolic_anomaly_to_true_anomaly",
368
+ "true_anomaly_to_hyperbolic_anomaly",
369
+ "escape_velocity_at_radius",
370
+ "hyperbolic_excess_velocity",
371
+ "semi_major_axis_from_energy",
372
+ "hyperbolic_asymptote_angle",
373
+ "hyperbolic_deflection_angle",
374
+ "eccentricity_vector",
340
375
  ]
@@ -1302,6 +1302,106 @@ def itrf_to_tod(
1302
1302
  return R.T @ (W.T @ r_itrf)
1303
1303
 
1304
1304
 
1305
+ def gcrf_to_pef(
1306
+ r_gcrf: NDArray[np.floating],
1307
+ jd_ut1: float,
1308
+ jd_tt: float,
1309
+ ) -> NDArray[np.floating]:
1310
+ """
1311
+ Transform position from GCRF (inertial) to PEF (Earth-fixed, rotation only).
1312
+
1313
+ PEF (Pseudo-Earth Fixed) is an intermediate reference frame between
1314
+ GCRF and ITRF. It includes precession, nutation, and Earth rotation,
1315
+ but excludes polar motion.
1316
+
1317
+ Parameters
1318
+ ----------
1319
+ r_gcrf : ndarray
1320
+ Position in GCRF (km), shape (3,).
1321
+ jd_ut1 : float
1322
+ Julian date in UT1.
1323
+ jd_tt : float
1324
+ Julian date in TT.
1325
+
1326
+ Returns
1327
+ -------
1328
+ r_pef : ndarray
1329
+ Position in PEF (km), shape (3,).
1330
+
1331
+ Notes
1332
+ -----
1333
+ The transformation chain is: GCRF -> MOD -> TOD -> PEF
1334
+ - Precession: GCRF -> MOD
1335
+ - Nutation: MOD -> TOD
1336
+ - Sidereal rotation: TOD -> PEF
1337
+
1338
+ See Also
1339
+ --------
1340
+ pef_to_gcrf : Inverse transformation
1341
+ gcrf_to_itrf : Includes polar motion
1342
+
1343
+ References
1344
+ ----------
1345
+ .. [1] Vallado et al., "Fundamentals of Astrodynamics and Applications", 4th ed.
1346
+ """
1347
+ # Precession: GCRF -> MOD
1348
+ P = precession_matrix_iau76(jd_tt)
1349
+ r_mod = P @ r_gcrf
1350
+
1351
+ # Nutation: MOD -> TOD
1352
+ N = nutation_matrix(jd_tt)
1353
+ r_tod = N @ r_mod
1354
+
1355
+ # Sidereal rotation: TOD -> PEF
1356
+ gast = gast_iau82(jd_ut1, jd_tt)
1357
+ R = sidereal_rotation_matrix(gast)
1358
+ r_pef = R @ r_tod
1359
+
1360
+ return r_pef
1361
+
1362
+
1363
+ def pef_to_gcrf(
1364
+ r_pef: NDArray[np.floating],
1365
+ jd_ut1: float,
1366
+ jd_tt: float,
1367
+ ) -> NDArray[np.floating]:
1368
+ """
1369
+ Transform position from PEF (Earth-fixed, rotation only) to GCRF (inertial).
1370
+
1371
+ Inverse of gcrf_to_pef.
1372
+
1373
+ Parameters
1374
+ ----------
1375
+ r_pef : ndarray
1376
+ Position in PEF (km), shape (3,).
1377
+ jd_ut1 : float
1378
+ Julian date in UT1.
1379
+ jd_tt : float
1380
+ Julian date in TT.
1381
+
1382
+ Returns
1383
+ -------
1384
+ r_gcrf : ndarray
1385
+ Position in GCRF (km), shape (3,).
1386
+
1387
+ See Also
1388
+ --------
1389
+ gcrf_to_pef : Forward transformation
1390
+ """
1391
+ # Compute rotation matrices
1392
+ P = precession_matrix_iau76(jd_tt)
1393
+ N = nutation_matrix(jd_tt)
1394
+ gast = gast_iau82(jd_ut1, jd_tt)
1395
+ R = sidereal_rotation_matrix(gast)
1396
+
1397
+ # Inverse transformation: GCRF = P.T * N.T * R.T * PEF
1398
+ r_tod = R.T @ r_pef
1399
+ r_mod = N.T @ r_tod
1400
+ r_gcrf = P.T @ r_mod
1401
+
1402
+ return r_gcrf
1403
+
1404
+
1305
1405
  def clear_transformation_cache() -> None:
1306
1406
  """Clear cached transformation matrices.
1307
1407
 
@@ -1351,6 +1451,8 @@ __all__ = [
1351
1451
  # Full transformations
1352
1452
  "gcrf_to_itrf",
1353
1453
  "itrf_to_gcrf",
1454
+ "gcrf_to_pef",
1455
+ "pef_to_gcrf",
1354
1456
  "eci_to_ecef",
1355
1457
  "ecef_to_eci",
1356
1458
  # Ecliptic/equatorial