Skip to content

Commit 625b896

Browse files
committed
Add function to project 1D to 2D
1 parent 4c6365e commit 625b896

File tree

1 file changed

+73
-0
lines changed

1 file changed

+73
-0
lines changed

src/track_linearization/core.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,3 +821,76 @@ def get_linearized_position(
821821
"projected_y_position": projected_y_position,
822822
}
823823
)
824+
825+
826+
827+
def project_1d_to_2d(
828+
linear_position: np.ndarray,
829+
track_graph: nx.Graph,
830+
edge_order: List[Edge],
831+
edge_spacing: Union[float, List[float]] = 0.0,
832+
) -> np.ndarray:
833+
"""
834+
Map 1-D linear positions back to 2-D coordinates on the track graph.
835+
836+
Parameters
837+
----------
838+
linear_position : np.ndarray, shape (n_time,)
839+
track_graph : networkx.Graph
840+
Same graph you passed to `get_linearized_position`.
841+
Nodes must have `"pos"`; edges must have `"distance"`.
842+
edge_order : list[tuple(node, node)]
843+
Same order you used for linearisation.
844+
edge_spacing : float or list of float, optional
845+
Controls the spacing between track segments in 1D position.
846+
If float, applied uniformly. If list, length must be `len(edge_order) - 1`.
847+
848+
Returns
849+
-------
850+
coords : np.ndarray, shape (n_time, n_space)
851+
2-D (or 3-D) coordinates corresponding to each 1-D input.
852+
Positions that fall beyond the last edge are clipped to the last node.
853+
NaNs in `linear_position` propagate to rows of NaNs.
854+
"""
855+
linear_position = np.asarray(linear_position, dtype=float)
856+
n_edges = len(edge_order)
857+
858+
# --- edge lengths & spacing ------------------------------------------------
859+
edge_lengths = np.array([track_graph.edges[e]["distance"] for e in edge_order],
860+
dtype=float)
861+
862+
if isinstance(edge_spacing, (int, float)):
863+
gaps = np.full(max(0, n_edges-1), float(edge_spacing))
864+
else:
865+
gaps = np.asarray(edge_spacing, dtype=float)
866+
if gaps.size != max(0, n_edges-1):
867+
raise ValueError("edge_spacing length must be len(edge_order)‑1")
868+
869+
# cumulative start position of each edge
870+
cumulative = np.concatenate([
871+
[0.0],
872+
np.cumsum(edge_lengths[:-1] + gaps)
873+
]) # shape (n_edges,)
874+
875+
# --- vectorised lookup -----------------------------------------------------
876+
idx = np.searchsorted(cumulative, linear_position, side="right") - 1
877+
idx = np.clip(idx, 0, n_edges-1) # clamp to valid edge index
878+
879+
# handle NaNs early so they don't pollute math
880+
nan_mask = ~np.isfinite(linear_position)
881+
idx[nan_mask] = 0 # dummy index, will overwrite later
882+
883+
# param along each chosen edge
884+
t = (linear_position - cumulative[idx]) / edge_lengths[idx]
885+
t = np.clip(t, 0.0, 1.0) # project extremes onto endpoints
886+
887+
# gather endpoint coordinates
888+
node_pos = nx.get_node_attributes(track_graph, "pos")
889+
u = np.array([node_pos[edge_order[i][0]] for i in idx])
890+
v = np.array([node_pos[edge_order[i][1]] for i in idx])
891+
892+
coords = (1.0 - t[:, None]) * u + t[:, None] * v
893+
894+
# propagate NaNs from the input
895+
coords[nan_mask] = np.nan
896+
return coords

0 commit comments

Comments
 (0)