Skip to content

Commit b3a909d

Browse files
authored
fix: improve table merge speed by pre-grouping and vectorizing operat…
2 parents 01627e4 + 2678370 commit b3a909d

1 file changed

Lines changed: 83 additions & 40 deletions

File tree

‎loopresources/drillhole/resample.py‎

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)