@@ -65,12 +65,6 @@ def resample_point(
6565 > r [DhConfig .depth ].to_numpy ()[:, None ],
6666 )
6767 )
68- _exact_from = np .where (
69- table [DhConfig .sample_from ].to_numpy ()[None , :] == r [DhConfig .depth ].to_numpy ()[:, None ]
70- )
71- _exact_to = np .where (
72- table [DhConfig .sample_to ].to_numpy ()[None , :] == r [DhConfig .depth ].to_numpy ()[:, None ]
73- )
7468 cols_in = [col for col in cols if col in table .columns ]
7569 r [cols_in ] = np .nan
7670 r .loc [r .index [i ], cols_in ] = table .loc [table .index [j ], cols_in ].to_numpy ()
@@ -170,9 +164,9 @@ def resample_interval(
170164
171165 # Initialize column with appropriate type
172166 if table [col ].dtype == "object" or pd .api .types .is_string_dtype (table [col ]):
173- new_columns [col ] = np .array ([ None ] * len (r ), dtype = object )
167+ new_columns [col ] = np .full ( len (r ), None , dtype = object )
174168 else :
175- new_columns [col ] = np .array ([ np . nan ] * len (r ))
169+ new_columns [col ] = np .full ( len (r ), np . nan )
176170
177171 # find values in the intervals and assign them to the survey
178172 new_columns [col ][r .index [i ]] = table .loc [table .index [j ], col ].to_numpy ()
@@ -326,49 +320,98 @@ def merge_interval_tables(tables: List[pd.DataFrame]) -> pd.DataFrame:
326320 for t in processed_tables :
327321 hole_ids .update (t [DhConfig .holeid ].unique ())
328322
323+ # Pre-group tables by hole_id to avoid repeated filtering
324+ tables_by_hole = {}
325+ for hole in hole_ids :
326+ tables_by_hole [hole ] = []
327+ for t in processed_tables :
328+ sub = t [t [DhConfig .holeid ] == hole ]
329+ if not sub .empty :
330+ tables_by_hole [hole ].append (sub )
331+
329332 merged_rows = []
330333
331334 # Process each hole independently
332335 for hole in sorted (hole_ids ):
336+ hole_tables = tables_by_hole [hole ]
337+ if not hole_tables :
338+ continue
339+
333340 # collect all unique boundaries for this hole
334341 boundaries = set ()
335- for t in processed_tables :
336- sub = t [t [DhConfig .holeid ] == hole ]
337- if sub .empty :
338- continue
339- boundaries .update (sub [DhConfig .sample_from ].tolist ())
340- boundaries .update (sub [DhConfig .sample_to ].tolist ())
342+ for sub in hole_tables :
343+ boundaries .update (sub [DhConfig .sample_from ].to_numpy ())
344+ boundaries .update (sub [DhConfig .sample_to ].to_numpy ())
345+
341346 if not boundaries :
342347 continue
343348 sorted_bounds = sorted (boundaries )
344349
350+ # Pre-convert tables to numpy arrays for faster access
351+ table_arrays = []
352+ for sub in hole_tables :
353+ from_arr = sub [DhConfig .sample_from ].to_numpy ()
354+ to_arr = sub [DhConfig .sample_to ].to_numpy ()
355+ data_cols = [c for c in sub .columns if c not in required ]
356+ table_arrays .append ((from_arr , to_arr , sub , data_cols ))
357+
345358 # build atomic segments between consecutive boundaries
346- for a , b in zip (sorted_bounds [:- 1 ], sorted_bounds [1 :]):
347- if b <= a :
348- continue
349- row = {
350- DhConfig .holeid : hole ,
351- DhConfig .sample_from : a ,
352- DhConfig .sample_to : b ,
353- }
354-
355- # For each processed table, find the interval that covers [a,b]
356- for t in processed_tables :
357- sub = t [t [DhConfig .holeid ] == hole ]
358- if sub .empty :
359- continue
360- cover = sub [(sub [DhConfig .sample_from ] <= a ) & (sub [DhConfig .sample_to ] >= b )]
361- if cover .empty :
362- # no covering interval -> leave values as NaN
363- continue
364- # If multiple matches take the first (shouldn't happen if inputs valid)
365- cover_row = cover .iloc [0 ]
366- for c in cover_row .index :
367- if c in {DhConfig .holeid , DhConfig .sample_from , DhConfig .sample_to }:
368- continue
369- # only set value if not already present (earlier table wins for same-named column)
370- if c not in row :
371- row [c ] = cover_row [c ]
359+ n_segments = len (sorted_bounds ) - 1
360+ if n_segments <= 0 :
361+ continue
362+
363+ # Create base arrays for the segments
364+ segment_from = np .array (sorted_bounds [:- 1 ])
365+ segment_to = np .array (sorted_bounds [1 :])
366+
367+ # Filter out zero-length segments
368+ valid_mask = segment_to > segment_from
369+ segment_from = segment_from [valid_mask ]
370+ segment_to = segment_to [valid_mask ]
371+ n_valid = len (segment_from )
372+
373+ if n_valid == 0 :
374+ continue
375+
376+ # Initialize rows array
377+ rows_data = {
378+ DhConfig .holeid : np .full (n_valid , hole ),
379+ DhConfig .sample_from : segment_from ,
380+ DhConfig .sample_to : segment_to ,
381+ }
382+
383+ # For each table, vectorize the interval matching using full broadcasting
384+ for from_arr , to_arr , sub , data_cols in table_arrays :
385+ # Broadcast segment boundaries and interval boundaries for vectorized comparison
386+ # segment [a,b] is covered by interval [from,to] if from <= a AND to >= b
387+ segment_from_2d = segment_from [:, None ] # Shape: (n_valid, 1)
388+ segment_to_2d = segment_to [:, None ] # Shape: (n_valid, 1)
389+ from_arr_2d = from_arr [None , :] # Shape: (1, n_intervals)
390+ to_arr_2d = to_arr [None , :] # Shape: (1, n_intervals)
391+
392+ # Check coverage for all segment-interval combinations at once
393+ # Shape: (n_valid, n_intervals) - True where interval covers segment
394+ covering = (from_arr_2d <= segment_from_2d ) & (to_arr_2d >= segment_to_2d )
395+
396+ # For each segment, find first covering interval
397+ # Use argmax to find first True (returns 0 if all False)
398+ cover_indices = np .argmax (covering , axis = 1 )
399+ # Verify that coverage actually exists (argmax returns 0 even if no True)
400+ has_cover = covering [np .arange (n_valid ), cover_indices ]
401+
402+ # Vectorized data copying for all segments at once
403+ for c in data_cols :
404+ if c not in rows_data :
405+ # Initialize column with NaN
406+ rows_data [c ] = np .full (n_valid , np .nan , dtype = object )
407+ # Only copy where coverage exists and cell is still NaN
408+ mask = has_cover & pd .isna (rows_data [c ])
409+ if mask .any ():
410+ rows_data [c ][mask ] = sub [c ].iloc [cover_indices [mask ]].values
411+
412+ # Convert to list of dicts for DataFrame construction
413+ for idx in range (n_valid ):
414+ row = {k : v [idx ] if isinstance (v , np .ndarray ) else v for k , v in rows_data .items ()}
372415 merged_rows .append (row )
373416
374417 if not merged_rows :
0 commit comments