import pandas as pd import numpy as np import matplotlib.pyplot as plt from itertools import combinations from scipy.spatial import cKDTree import math import time import os import warnings # Ignore RuntimeWarnings that can sometimes occur in pandas/numpy operations warnings.filterwarnings("ignore", category=RuntimeWarning) # ----------------------------- # Helper functions # ----------------------------- def are_parallel(coords1, coords2, tolerance_degrees=30.0): """ Checks if two line segments (defined by their start and end coordinates) are approximately parallel based on the angle between them. Args: coords1 (np.array): Coordinates for the first line. coords2 (np.array): Coordinates for the second line. tolerance_degrees (float): Maximum angle difference (in degrees) to consider lines parallel. Returns: bool: True if parallel, False otherwise. """ if len(coords1) < 2 or len(coords2) < 2: return False # Calculate vector components for line 1 dx1 = coords1[-1][0] - coords1[0][0] dy1 = coords1[-1][1] - coords1[0][1] # Calculate vector components for line 2 dx2 = coords2[-1][0] - coords2[0][0] dy2 = coords2[-1][1] - coords2[0][1] if (dx1 == 0 and dy1 == 0) or (dx2 == 0 and dy2 == 0): return False # Calculate angles in radians angle1 = math.atan2(dy1, dx1) angle2 = math.atan2(dy2, dx2) # Calculate the smaller angle between the two lines (0 to pi/2) diff_rad = abs(angle1 - angle2) % math.pi if diff_rad > math.pi / 2: diff_rad = math.pi - diff_rad diff_deg = math.degrees(diff_rad) return diff_deg <= tolerance_degrees # ----------------------------- # Main Processing Function # ----------------------------- def mark_intersections_kdtree(input_csv_path, radius=1.0, parallel_tolerance_deg=15.0, output_path=None): """ Identifies intersection points between survey lines using a cKDTree spatial search, marks them, and calculates min/max mean signal and altitude flags. Args: input_csv_path (str): Path to the input CSV file. radius (float): Search radius (in coordinate units) for intersection. parallel_tolerance_deg (float): Angle threshold to skip parallel lines. output_path (str, optional): Directory path where the final CSV should be saved. If None, the file is saved in the current working directory. """ start_time = time.time() print(f"Starting KD-Tree intersection marking for {os.path.basename(input_csv_path)}...") # Define column names for mean calculation SIGNAL_COL = "Signal" ALT_COL = "Alt" # 1. Load and prepare data df = pd.read_csv(input_csv_path) df = df.dropna(subset=["X", "Y"]).reset_index(drop=True) df['row_id'] = df.index.astype(int) # Ensure row_id is contiguous after dropping NaNs # Check for required columns if SIGNAL_COL not in df.columns or ALT_COL not in df.columns: raise ValueError(f"Input CSV must contain '{SIGNAL_COL}' and '{ALT_COL}' columns for mean calculation.") line_ids = df["Line"].unique().tolist() print(f"\n{len(line_ids)} unique lines detected.") line_coords = {} line_info = {} # Stores row_id, indexed by the line order # Pre-process coordinates and row info by line for line in line_ids: group = df[df["Line"] == line].sort_values(by='row_id') # Ensure order is consistent line_coords[line] = group[["X", "Y"]].to_numpy() # Store just the row_id for easy lookup line_info[line] = group["row_id"].to_numpy() # 2. KD-Tree Search for Intersections and Mapping # Stores tuples of (row_id, unique_intersection_key, line_id) intersection_map = [] total_match_pairs = 0 print("Computing line intersections using cKDTree...") for line1, line2 in combinations(line_ids, 2): A_coords = line_coords[line1] B_coords = line_coords[line2] A_ids = line_info[line1] B_ids = line_info[line2] # Skip parallel lines early if are_parallel(A_coords, B_coords, tolerance_degrees=parallel_tolerance_deg): continue # Build tree on the second line's points tree = cKDTree(B_coords) # Query: find all points in A within radius 'r' of any point in B matches = tree.query_ball_point(A_coords, r=radius) n_matches = sum(len(js) for js in matches) if n_matches == 0: continue total_match_pairs += n_matches # Extract and store row IDs and the intersection key intersection_key = '_'.join(sorted([line1, line2])) for i, js in enumerate(matches): if js: # Point from Line 1 (A) intersection_map.append((A_ids[i], intersection_key, line1)) # Points from Line 2 (B) that match A[i] for j in js: intersection_map.append((B_ids[j], intersection_key, line2)) # Define final flag column names # CHANGED: Lower signal (MIN) is 1 FLAG_SIG = "is_minimum_signal_line" # CHANGED: Higher altitude (MAX) is 1 FLAG_ALT = "is_maximum_alt_line" if not intersection_map: print("\nNo intersecting points found. Skipping mean calculation and flagging.") df["is_intersection"] = 0 df[FLAG_SIG] = 0 df[FLAG_ALT] = 0 else: # 3. Create Intersection Map DataFrame and Merge Data # df_map has columns: row_id, key (intersection_key), Line df_map = pd.DataFrame(intersection_map, columns=['row_id', 'key', 'Line']).drop_duplicates() print(f"\nFound {total_match_pairs} point-pair matches in total.") print(f"Marking {df_map['row_id'].nunique()} unique survey points as intersecting.") # Merge signal and alt data from the main df df_map = df_map.merge(df[['row_id', SIGNAL_COL, ALT_COL]], on='row_id', how='left') # 4. Calculate Means and Min/Max Flags # Calculate means per line per intersection key line_means = df_map.groupby(['key', 'Line']).agg( mean_signal=(SIGNAL_COL, 'mean'), mean_alt=(ALT_COL, 'mean') ).reset_index() # Determine line with MINIMUM mean Signal for each key (User requested: lower signal = 1) line_means["min_signal"] = line_means.groupby("key")["mean_signal"].transform("min") line_means[FLAG_SIG] = (line_means["mean_signal"] == line_means["min_signal"]).astype(int) # Determine line with MAXIMUM mean Alt for each key (User requested: higher altitude = 1) line_means["max_alt"] = line_means.groupby("key")["mean_alt"].transform("max") line_means[FLAG_ALT] = (line_means["mean_alt"] == line_means["max_alt"]).astype(int) # 5. Determine the final point-level flags based on intersection results # Merge line_means (intersection results) back into df_map (point-to-key mapping) # This assigns the per-intersection flag back to the individual points. df_flagged_points = df_map.merge(line_means, on=['key', 'Line'], how='left') # Get the row_ids that should have FLAG_SIG = 1 and FLAG_ALT = 1 # is_minimum_signal_line (lower signal) sig_row_ids = df_flagged_points[df_flagged_points[FLAG_SIG] == 1]['row_id'].unique() # is_maximum_alt_line (higher altitude) alt_row_ids = df_flagged_points[df_flagged_points[FLAG_ALT] == 1]['row_id'].unique() # 6. Mark the original DataFrame FLAG_COLUMN_INT = "is_intersection" # Initialize the flags df[FLAG_COLUMN_INT] = 0 df[FLAG_SIG] = 0 df[FLAG_ALT] = 0 # Mark all points that belong to ANY intersection intersection_row_ids = df_map['row_id'].unique() df.loc[df['row_id'].isin(intersection_row_ids), FLAG_COLUMN_INT] = 1 # Mark the signal flags (only points in sig_row_ids get 1, others remain 0) df.loc[df['row_id'].isin(sig_row_ids), FLAG_SIG] = 1 # Mark the altitude flags (only points in alt_row_ids get 1, others remain 0) df.loc[df['row_id'].isin(alt_row_ids), FLAG_ALT] = 1 # 7. Save Output base = os.path.splitext(os.path.basename(input_csv_path))[0] # UPDATED FILENAME to reflect MINSIG and MAXALT filename = f"{base}_marked_kdtree_{radius}m_MEAN_FLAGS_MINSIG_MAXALT.csv" # Determine output path: use specified path or save to current directory if output_path: # Ensure the output directory exists before saving os.makedirs(output_path, exist_ok=True) outpath = os.path.join(output_path, filename) else: outpath = filename # Saves to current working directory # Save the full dataframe with the new intersection columns df.to_csv(outpath, index=False) end_time = time.time() elapsed = (end_time - start_time) / 60.0 print(f"\n✔ Process complete. Saved: {outpath}") print(f"Total time taken: {elapsed:.2f} minutes.") return df # ----------------------------- # Script Execution # ----------------------------- if __name__ == "__main__": # --- Configuration --- # NOTE: The radius here is the maximum distance between points on two lines # to be considered part of an intersection zone. RADIUS_M = 0.5 # Set your desired radius here PARALLEL_TOLERANCE_DEG = 10.0 # Keep lines crossing at less than 10 degrees (or whatever you prefer) # --- Input Path --- #input_csv_path = "MAG_Channel_Turning_Processed_Data_Split_A0.csv" input_csv_path = r"C:\Users\NivedithaPoolla\Documents\P110-2025\P304-2024_Processed_Data_Split_A0.csv" # --- Output Path --- # Setting the user-requested output folder OUTPUT_FOLDER = r"C:\Users\NivedithaPoolla\Documents\P110-2025" try: df_marked = mark_intersections_kdtree( input_csv_path=input_csv_path, radius=RADIUS_M, parallel_tolerance_deg=PARALLEL_TOLERANCE_DEG, output_path=OUTPUT_FOLDER ) except FileNotFoundError: print(f"\n--- ERROR ---") print(f"File not found at path: {input_csv_path}") print("Please ensure the CSV file is in the correct directory.") except Exception as e: print(f"\nAn unexpected error occurred during processing: {e}")