@@ -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