Source code for torx.geometry.line_geometry_m

"""Contains functions for basic geometry with lines that are given by points."""
import numpy as np
from scipy.spatial import cKDTree
from torx.autodoc_decorators_m import autodoc_function

[docs] @autodoc_function def intersect_line_segments(x1, y1, x2, y2, x3, y3, x4, y4): """ Intersect two line segments that are given by their start and end points. Here 1 and 2 belongs to the first segment, 3 and 4 to the second. Returns the intersection point if it exists, otherwise a NaN point. Note ---- Works in 2D. """ denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) # We need to eliminate the sign of denom because in the condition below # the <= would have to be switched to >= if denom is negative. We avoid # this by multiplying with the sign of denom here. Note that this does # not change the result since a division by denom is required anyway. t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) * np.sign(denom) u = ((x1 - x3) * (y1 - y2) - (y1 - y3) * (x1 - x2)) * np.sign(denom) denom *= np.sign(denom) # Checks for intersection can be made without division if(t <= denom and t >= 0 and u <= denom and u >= 0 and denom != 0): px = x1 + (t / denom) * (x2 - x1) py = y1 + (t / denom) * (y2 - y1) return [px, py] else: return [np.nan, np.nan]
[docs] @autodoc_function def intersect_lines(l1x, l1y, l2x, l2y, intersec_mode:str="first"): """ Intersect two lines that are given by points on the curve. The intersection is performed by checking all combinations of line segments. Returns the intersection point if it exists, otherwise a NaN point. Depending on the intersec_mode parameter, different intersection points are returned. Current options are "first", "last", "all" and "closest" (w.r.t. first point of l1). Default is "first". Note that "first" and "closest" are different in the case of multiple intersections within a segment, while for a single intersection they return the same. Note ---- Works in 2D. """ p_inter_collection = [] for i in range(l1x.size - 1): for j in range(l2x.size - 1): p_inter = intersect_line_segments(l1x[i ], l1y[i ], \ l1x[i + 1], l1y[i + 1], \ l2x[j ], l2y[j ], \ l2x[j + 1], l2y[j + 1]) if not (np.isnan(p_inter[0]) and np.isnan(p_inter[1])): if intersec_mode == "first": return p_inter p_inter_collection.append(p_inter) if(not p_inter_collection): return [np.nan, np.nan] if(len(p_inter_collection) == 1): return p_inter_collection[0] if(intersec_mode=="all"): return p_inter_collection if(intersec_mode=="last"): return p_inter_collection[-1] if(intersec_mode=="closest"): p_inter_collection = np.array(p_inter_collection) dist = np.sqrt((p_inter_collection[:, 0] - l1x[0])**2 + \ (p_inter_collection[:, 1] - l1y[0])**2) i_closest = np.argmin(dist) return p_inter_collection[i_closest, :]
[docs] @autodoc_function def compute_arc_length(r_points: np.ndarray, z_points: np.ndarray, phi_points: np.ndarray=None, skipna: bool=False): """ Compute the arc length of a line given by points via triangularization. Note ---- The skipna implementation is useful but not rigorous. It can handle single block of NaNs at the beginning or the end of the array. It can also handle a single block of values surrounded by NaNs. It will produce inaccurate results for anything more complex involving NaNs. """ if phi_points is None: return _arc_length_2d(r_points, z_points, skipna) else: return _arc_length_3d(r_points, z_points, phi_points, skipna)
def _arc_length_2d(r_points, z_points, skipna): """Compute the arc length of a line via triangularization in 2D.""" # Only perform the cumulative sum where the coordinate values are not NaNs if skipna: isnan = np.isnan(r_points) assert np.all(np.isnan(z_points) == np.isnan(r_points)), \ "Inconsistent NaN values between 'r_points' and 'z_points'." assert len(~isnan) > 1, \ "Cannot compute arc length with <2 points remaining." r_points = r_points[~isnan] z_points = z_points[~isnan] # Perform triangularization and add zero value at the beginning r_diff = np.diff(r_points) z_diff = np.diff(z_points) x_vals = np.cumsum(np.sqrt(r_diff**2 + z_diff**2)) x_vals = np.insert(x_vals, 0, 0.0) # Put NaNs into to the returned array, except for the NaN next to the values if skipna: x_vals_full = np.full(len(isnan), np.nan) x_vals_full[~isnan] = x_vals else: x_vals_full = x_vals return x_vals_full def _arc_length_3d(r_points, z_points, phi_points, skipna): """Compute the arc length of a line via triangularization in 3D.""" # Only perform the cumulative sum where the coordinate values are not NaNs if skipna: isnan = np.isnan(r_points) assert ( np.all(np.isnan(z_points) == np.isnan(r_points)) and np.all(np.isnan(r_points) == np.isnan(phi_points)) ), \ "Inconsistent NaN values between 'phi_points', 'r_points' \ and 'z_points'." assert len(~isnan) > 1, \ "Cannot compute arc length with <2 points remaining." r_points = r_points[~isnan] z_points = z_points[~isnan] phi_points = phi_points[~isnan] # Perform triangularization and add zero value at the beginning r_diff = np.diff(r_points) z_diff = np.diff(z_points) phi_diff = np.diff(phi_points) # Use the toroidal coordinates, i.e. phi_diff = r_mean * dphi r_stag = r_points[:-1] + r_diff x_vals = np.cumsum(np.sqrt(r_diff**2 + z_diff**2 + (r_stag*phi_diff)**2)) x_vals = np.insert(x_vals, 0, 0.0) # Put NaNs into to the returned array, except for the NaN next to the values if skipna: x_vals_full = np.full(len(isnan), np.nan) x_vals_full[~isnan] = x_vals else: x_vals_full = x_vals return x_vals_full
[docs] @autodoc_function def order_points(x_points, y_points, start_idx: int=0, k: int=20): """ Order the x, y points using a nearest-neighbor algorithm. Starting from the point with index start_idx and looking for up to k nearest neighbors. Falls back to a linear search if no unvisited neighbor is found among the k nearest neighbors. """ pts = np.column_stack([x_points, y_points]) n_pts = x_points.size tree = cKDTree(pts) visited = np.zeros(n_pts, dtype=bool) order = np.empty(n_pts, dtype=int) order[0] = start_idx cur = start_idx visited[cur] = True for i in range(1, n_pts): # Get up to k nearest neighbors dists, idxs = tree.query(pts[cur], k=min(k, n_pts)) # Handle k=1 idxs = np.atleast_1d(idxs) # Find first unvisited neighbor unvisited = idxs[~visited[idxs]] if unvisited.size > 0: nxt = unvisited[0] else: # Fallback: linear search for nearest unvisited d_all = np.linalg.norm(pts - pts[cur], axis=1) d_all[visited] = np.inf nxt = np.argmin(d_all) order[i] = nxt visited[nxt] = True cur = nxt # Apply order pts = pts[order] return pts[:, 0], pts[:, 1]