@@ -452,6 +452,102 @@ def run_real_world(files=None, resamplings=None):
452452 f'| { stats ["exact" ][1 ]:>12.6f} |' )
453453
454454
455+ def run_merge (sizes = None ):
456+ """Benchmark xrspatial.merge vs rioxarray.merge_arrays.
457+
458+ Creates 4 overlapping rasters in a 2x2 grid arrangement and merges
459+ them into a single mosaic with each library.
460+ """
461+ from rioxarray .merge import merge_arrays as rio_merge_arrays
462+
463+ from xrspatial .reproject import merge as xrs_merge
464+
465+ sizes = sizes or [(512 , 512 ), (1024 , 1024 ), (2048 , 2048 )]
466+
467+ print ()
468+ print ('=' * 100 )
469+ print ('MERGE BENCHMARK: xrspatial.merge vs rioxarray.merge_arrays (4 overlapping tiles)' )
470+ print ('=' * 100 )
471+ print ()
472+ print (f'| { "Tile size" :>12} '
473+ f'| { "xrs merge" :>11} | { "rio merge" :>11} '
474+ f'| { "xrs/rio" :>7} '
475+ f'| { "RMSE" :>10} | { "MaxErr" :>10} '
476+ f'| { "Valid px" :>10} | { "NaN agree" :>9} |' )
477+ print (f'|{ "-" * 14 } '
478+ f'|{ "-" * 13 } |{ "-" * 13 } '
479+ f'|{ "-" * 9 } '
480+ f'|{ "-" * 12 } |{ "-" * 12 } '
481+ f'|{ "-" * 12 } |{ "-" * 11 } |' )
482+
483+ for h , w in sizes :
484+ # Build 4 overlapping tiles in a 2x2 grid.
485+ # Each tile spans 10 degrees; overlap is 2 degrees on each shared edge.
486+ # Total coverage: 18 x 18 degrees (from -9 to 9 lon, 41 to 59 lat).
487+ tile_specs = [
488+ # (x_range, y_range) -- 2-degree overlap between neighbours
489+ ((- 9 , 1 ), (49 , 59 )), # top-left
490+ ((- 1 , 9 ), (49 , 59 )), # top-right
491+ ((- 9 , 1 ), (41 , 51 )), # bottom-left
492+ ((- 1 , 9 ), (41 , 51 )), # bottom-right
493+ ]
494+
495+ tiles_xrs = []
496+ tiles_rio = []
497+ for x_range , y_range in tile_specs :
498+ da = _make_raster (h , w , crs = 'EPSG:4326' ,
499+ x_range = x_range , y_range = y_range )
500+ tiles_xrs .append (da )
501+ tiles_rio .append (_make_rio_raster (da , 'EPSG:4326' ))
502+
503+ # Benchmark xrspatial merge
504+ xrs_time , xrs_result = _timer (
505+ lambda : xrs_merge (tiles_xrs ),
506+ warmup = 1 , runs = 3 ,
507+ )
508+
509+ # Benchmark rioxarray merge
510+ rio_time , rio_result = _timer (
511+ lambda : rio_merge_arrays (tiles_rio ),
512+ warmup = 1 , runs = 3 ,
513+ )
514+
515+ xrs_vals = xrs_result .values
516+ rio_vals = rio_result .values
517+
518+ # Crop to common shape if they differ
519+ common_h = min (xrs_vals .shape [0 ], rio_vals .shape [0 ])
520+ common_w = min (xrs_vals .shape [1 ], rio_vals .shape [1 ])
521+ xrs_vals = xrs_vals [:common_h , :common_w ]
522+ rio_vals = rio_vals [:common_h , :common_w ]
523+
524+ # Compare where both have valid data
525+ xrs_nan = np .isnan (xrs_vals )
526+ rio_nan = np .isnan (rio_vals )
527+ both_valid = ~ xrs_nan & ~ rio_nan
528+ n_valid = int (both_valid .sum ())
529+ nan_agree = np .mean (xrs_nan == rio_nan ) * 100
530+
531+ if n_valid > 0 :
532+ diff = xrs_vals [both_valid ] - rio_vals [both_valid ]
533+ rmse = np .sqrt (np .mean (diff ** 2 ))
534+ max_err = np .max (np .abs (diff ))
535+ rmse_str = f'{ rmse :.6f} '
536+ max_str = f'{ max_err :.6f} '
537+ else :
538+ rmse_str = 'N/A'
539+ max_str = 'N/A'
540+
541+ ratio = xrs_time / rio_time if rio_time > 0 else float ('inf' )
542+
543+ print (f'| { _fmt_shape ((h , w )):>12} '
544+ f'| { _fmt_time (xrs_time ):>11} '
545+ f'| { _fmt_time (rio_time ):>11} '
546+ f'| { ratio :>6.2f} x '
547+ f'| { rmse_str :>10} | { max_str :>10} '
548+ f'| { n_valid :>10} | { nan_agree :>8.1f} % |' )
549+
550+
455551def main ():
456552 if not HAS_PYPROJ :
457553 print ('ERROR: pyproj is required for reprojection benchmarks' )
@@ -476,6 +572,7 @@ def main():
476572 run_consistency ()
477573 run_performance ()
478574 run_real_world ()
575+ run_merge ()
479576
480577
481578if __name__ == '__main__' :
0 commit comments