@@ -73,7 +73,7 @@ def _compute_slope_aspect_topocalc(dem_arr, dx, dy):
7373 """Compute slope and aspect using topocalc backend."""
7474 return gradient .gradient_d8 (dem_arr , dx , dy )
7575
76- def _compute_svf_horayzon (dem_arr , dx , azimuth_inc = 5 ):
76+ def _compute_svf_horayzon (dem_arr , dx , dy , azimuth_inc = 5 ):
7777 """
7878 Compute sky view factor using HORAYZON backend with optimized parameters.
7979
@@ -99,7 +99,7 @@ def _compute_svf_topocalc(dem_arr, dx):
9999 """Compute sky view factor using topocalc backend."""
100100 return viewf .viewf (np .double (dem_arr ), dx )[0 ]
101101
102- def _compute_horizon_horayzon (dem_arr , dx , azimuth , num_threads = None ):
102+ def _compute_horizon_horayzon (dem_arr , dx , dy , azimuth , num_threads = None ):
103103 """
104104 Compute horizon angles using HORAYZON backend.
105105
@@ -356,65 +356,81 @@ def open_dem(dem_file):
356356
357357
358358
359- def compute_dem_param (dem_file , fname = 'ds_param.nc' , project_directory = Path ('./' ), output_folder = 'outputs' ):
359+ def compute_dem_param (dem_file , fname = 'ds_param.nc' , project_directory = Path ('./' ),
360+ output_folder = 'outputs' , format = 'zarr' ):
360361 """
361- Function to compute and derive DEM parameters: slope, aspect, sky view factor
362+ Function to compute and derive DEM parameters: slope, aspect, sky view factor.
363+
364+ Parameters
365+ ----------
366+ dem_file : str or Path
367+ Path to raster file (geotiff). Must be in a local cartesian coordinate system (e.g. UTM).
368+ fname : str
369+ Output filename (extension ignored when format='zarr'; .zarr is forced).
370+ project_directory : Path
371+ Project root directory.
372+ output_folder : str
373+ Subdirectory for outputs.
374+ format : {'zarr', 'netcdf'}
375+ Output format. Zarr is the default for faster reads and parallel access.
376+
377+ Returns
378+ -------
379+ xarray.Dataset
380+ Dataset containing elevation, slope, aspect, aspect_cos, aspect_sin, svf.
381+ """
382+ pdir = Path (project_directory )
362383
363- Args:
364- dem_file (str): path to raster file (geotif). Raster must be in local cartesian coordinate system (e.g. UTM)
384+ # Force extension based on format
385+ base_path = pdir / output_folder / Path (fname ).with_suffix ('' )
386+ zarr_path = base_path .with_suffix ('.zarr' )
387+ nc_path = base_path .with_suffix ('.nc' )
365388
366- Returns:
367- dataset: x, y, elev, slope, aspect, svf
389+ file_ds = zarr_path if format .lower () == 'zarr' else nc_path
368390
369- """
370- pdir = project_directory
371- file_ds = pdir / output_folder / fname
372- if file_ds .is_file ():
373- print (f'\n ---> Dataset { fname } found.' )
391+ if file_ds .exists ():
392+ print (f'\n ---> Dataset { file_ds .name } found.' )
374393 try :
375- ds = xr . open_dataset ( file_ds )
376- # Test if we can actually read the elevation data
377- _ = ds .elevation .values
378- except ( RuntimeError , OSError ) as e :
379- if "NetCDF: HDF error" in str ( e ) or "filter returned failure" in str ( e ):
380- print ( f' \n ---> Dataset { fname } corrupted (HDF/compression error). Trying h5netcdf backend...' )
394+ if format . lower () == 'zarr' :
395+ ds = xr . open_zarr ( str ( file_ds ), consolidated = True )
396+ ds = ds .compute () if hasattr ( ds . elevation .data , 'compute' ) else ds
397+ else :
398+ ds = xr . open_dataset ( file_ds )
399+ # Also try h5netcdf in case of compression issues
381400 try :
401+ _ = ds .elevation .values
402+ except (RuntimeError , OSError ):
382403 ds = xr .open_dataset (file_ds , engine = 'h5netcdf' )
383404 _ = ds .elevation .values
384- except Exception :
385- print (f'\n ---> h5netcdf backend failed. Regenerating dataset from DEM...' )
386- if Path (dem_file ).is_file ():
387- ds = open_dem (dem_file )
388- else :
389- raise ValueError (f'ERROR: Dataset corrupted and no DEM available to regenerate' )
405+ except Exception as e :
406+ print (f'\n ---> Dataset { file_ds .name } could not be read: { e } ' )
407+ if Path (dem_file ).is_file ():
408+ print (f'\n ---> Regenerating dataset from DEM...' )
409+ ds = open_dem (dem_file )
390410 else :
391- raise e
392-
411+ raise ValueError (f'ERROR: Dataset corrupted/unreadable and no DEM available to regenerate' )
393412 else :
394413 if Path (dem_file ).is_file ():
395- print (f'\n ---> No { fname } Dataset found. DEM { dem_file } available.' )
414+ print (f'\n ---> No { file_ds . name } dataset found. DEM { dem_file } available.' )
396415 ds = open_dem (dem_file )
397-
398416 else :
399417 raise ValueError (f'ERROR: No DEM or dataset available' )
400418
401419 var_in = list (ds .variables .keys ())
402420 print ('\n ---> Extracting DEM parameters (slope, aspect, svf)' )
403421 dx = ds .x .diff ('x' ).median ().values
404422 dy = ds .y .diff ('y' ).median ().values
405-
406- # Safely access elevation data with error handling
423+
424+ # Safely access elevation data
407425 try :
408426 dem_arr = ds .elevation .values
409427 except (RuntimeError , OSError ) as e :
410428 if "NetCDF: HDF error" in str (e ) or "filter returned failure" in str (e ):
411429 print (f'---> Error reading elevation data: { str (e )} ' )
412430 print ('---> Attempting to load elevation data with different method...' )
413431 try :
414- # Try loading the data chunk by chunk or using compute()
415432 dem_arr = ds .elevation .load ().values
416433 except Exception :
417- # If all else fails, regenerate from DEM file
418434 print ('---> All methods failed. Regenerating from original DEM...' )
419435 if Path (dem_file ).is_file ():
420436 ds_new = open_dem (dem_file )
@@ -432,7 +448,7 @@ def compute_dem_param(dem_file, fname='ds_param.nc', project_directory=Path('./'
432448 slope , aspect = _compute_slope_aspect_horayzon (dem_arr , dx , dy )
433449 else :
434450 slope , aspect = _compute_slope_aspect_topocalc (dem_arr , dx , dy )
435-
451+
436452 ds ['slope' ] = (["y" , "x" ], np .deg2rad (slope ))
437453 ds ['aspect' ] = (["y" , "x" ], np .deg2rad (aspect ))
438454 if 'aspect_cos' not in var_in :
@@ -443,13 +459,14 @@ def compute_dem_param(dem_file, fname='ds_param.nc', project_directory=Path('./'
443459 if 'svf' not in var_in :
444460 print ('Computing svf ...' )
445461 if HORAYZON_AVAILABLE :
446- svf = _compute_svf_horayzon (dem_arr , dx , azimuth_inc = 5 )
462+ svf = _compute_svf_horayzon (dem_arr , dx , dy , azimuth_inc = 5 )
447463 else :
448464 svf = _compute_svf_topocalc (dem_arr , dx )
449465 ds ['svf' ] = (["y" , "x" ], svf )
450466
451467 ds .attrs = dict (description = "DEM input parameters to TopoSub" ,
452- author = "TopoPyScale, https://github.com/ArcticSnow/TopoPyScale" )
468+ author = "TopoPyScale, https://github.com/ArcticSnow/TopoPyScale" ,
469+ format = format )
453470 ds .x .attrs = {'units' : 'm' }
454471 ds .y .attrs = {'units' : 'm' }
455472 ds .elevation .attrs = {'units' : 'm' }
@@ -459,19 +476,26 @@ def compute_dem_param(dem_file, fname='ds_param.nc', project_directory=Path('./'
459476 ds .aspect_sin .attrs = {'units' : 'sinus' }
460477 ds .svf .attrs = {'units' : 'ratio' , 'standard_name' : 'svf' , 'long_name' : 'Sky view factor' }
461478
462- if file_ds .is_file ():
463- te .to_netcdf (ds , fname = pdir / output_folder / 'tmp' / fname )
464- ds = None
465- shutil .move (pdir / output_folder / 'tmp' / fname , file_ds )
466- ds = xr .open_dataset (file_ds )
479+ print (f'---> Saving DEM parameters to { format } : { file_ds } ' )
480+ if format .lower () == 'zarr' :
481+ try :
482+ from zarr .codecs import BloscCodec
483+ except ImportError :
484+ from zarr import Blosc as BloscCodec
485+ encoding = {v : {'compressor' : BloscCodec (cname = 'zstd' , clevel = 3 , shuffle = 'bitshuffle' , blocksize = 0 )}
486+ for v in ds .data_vars }
487+ # Use reasonable chunk sizes for spatial data
488+ ds = ds .chunk ({'y' : min (512 , ds .y .size ), 'x' : min (512 , ds .x .size )})
489+ ds .to_zarr (str (file_ds ), mode = 'w' , encoding = encoding , zarr_format = 3 , consolidated = True )
490+ ds = xr .open_zarr (str (file_ds ), consolidated = True )
467491 else :
468492 te .to_netcdf (ds , fname = file_ds )
469493
470494 return ds
471495
472496
473497def compute_horizon (dem_file , azimuth_inc = 30 , num_threads = None , fname = 'da_horizon.nc' ,
474- output_directory = Path ('./outputs' ), format = 'netcdf ' ):
498+ output_directory = Path ('./outputs' ), format = 'zarr ' ):
475499 """
476500 Function to compute horizon angles using the best available backend (HORAYZON preferred).
477501
@@ -500,7 +524,7 @@ def compute_horizon(dem_file, azimuth_inc=30, num_threads=None, fname='da_horizo
500524
501525 # HORAYZON can compute all azimuths at once - much faster!
502526 horizon_cos_elev = _compute_horizon_horayzon (
503- ds .elevation .values , dx , azimuth , num_threads
527+ ds .elevation .values , dx , dy , azimuth , num_threads
504528 )
505529
506530 # Convert back to horizon elevation angles
0 commit comments