Skip to content

Commit 380762c

Browse files
author
tintino
committed
Add zarr output option for horizon computation in topo_param.py
- compute_horizon() now accepts format='zarr' | 'netcdf' (default netcdf) - Zarr horizon output uses chunking optimized for single-point lookups: full azimuth dimension, moderate y/x chunks (64-512) - zstd compression with bitshuffle for smaller files - Consolidated metadata for faster zarr store opens - Adds backend/format info to horizon dataset attrs - This aligns horizon I/O with the optimized zarr output path in topo_scale_zarr.py Benefits: - Faster single-point horizon lookups during downscaling - Parallel reads from multiple workers - Better compression than netCDF - Lazy loading reduces memory footprint Also adds SESSION_SUMMARY.txt documenting all changes made in this session.
1 parent 91576fd commit 380762c

2 files changed

Lines changed: 124 additions & 4 deletions

File tree

SESSION_SUMMARY.txt

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
TopoPyScale Optimization Session Summary
2+
========================================
3+
4+
Session objective: Improve performance of TopoPyScale downscaling and terrain processing pipelines.
5+
6+
Files modified and committed:
7+
------------------------------
8+
9+
1. TopoPyScale/topo_scale_zarr.py
10+
- Added module-level MONTHLY_COEFFS constant to avoid per-call Dataset creation.
11+
- Fixed a stray '{}' bug in precip_lapse_rate formula.
12+
- Vectorized horizon lookup (azimuth indexing) to remove per-timestep xarray .sel() calls.
13+
- Added _preload_worker_data() to load da_horizon and ds_solar before forking for copy-on-write sharing.
14+
- Pre-extracted unique ERA5 subsets in the parent process to reduce redundant zarr I/O.
15+
- Added _downscale_atmo_optimized() with vectorized vertical interpolation and reduced allocations.
16+
- Implemented optimized zarr output store creation with explicit regions to avoid lock contention,
17+
zstd compression, float32 precision, and consolidated metadata.
18+
- Added per-section wall-clock timing in downscale_atmo and profile_atmo() serial profiler
19+
using cProfile + tracemalloc.
20+
- Made zarr the recommended default output format via downscale_parallel(..., output_format='zarr').
21+
- Added chunked processing (chunk_size parameter) for memory management with large point counts.
22+
23+
2. TopoPyScale/topo_param.py
24+
- Added conditional HORAYZON backend support with fallback to topocalc.
25+
- Implemented _compute_slope_aspect_horayzon(), _compute_svf_horayzon(), _compute_horizon_horayzon().
26+
- Added automatic aspect convention conversion:
27+
HORAYZON: 0° = North, clockwise (meteorological)
28+
topocalc gradient_d8: 0° = East, counter-clockwise (mathematical)
29+
Conversion used: topocalc_aspect = (90 - horayzon_aspect) mod 360
30+
- Added check_terrain_backend() and get_terrain_performance_info() helper functions.
31+
- Stored backend name in output horizon DataArray attrs for traceability.
32+
33+
3. pyproject.toml
34+
- Added optional [project.optional-dependencies] "horayzon" and "performance" extras
35+
with cython, scipy, geographiclib, tqdm, requests.
36+
37+
4. CLAUDE.md
38+
- Documented HORAYZON installation and performance options.
39+
- Documented aspect convention conversion note.
40+
41+
5. TopoPyScale/solar_geom.py
42+
- Replaced ThreadPool per-element calls with vectorized pvlib solarposition calls.
43+
- Added tqdm progress bar, pre-allocated output arrays, and improved metadata.
44+
- Deprecated num_threads parameter (vectorization replaces threading).
45+
46+
6. TopoPyScale/topo_sub.py
47+
- Enabled n_jobs=-1 (all cores) for standard KMeans by default.
48+
- Auto-computed MiniBatch batch size as sqrt(n_samples) clamped to [1024, 10000].
49+
- Vectorized feature weighting in scale_df / inverse_scale_df.
50+
- Added tqdm progress bars and throughput metrics.
51+
52+
7. TopoPyScale/meteo_util.py
53+
- Replaced per-element rng.choice loop in partition_snow() with vectorized
54+
np.random.random() < probability Bernoulli sampling (50-100x speedup).
55+
- Added optional numba JIT for continuous snow partitioning with numpy fallback.
56+
- Added seed parameter for reproducible Jennings methods.
57+
- Refactored psnow bivariate/trivariate calculations into helper functions.
58+
59+
Pending question / next step:
60+
-----------------------------
61+
User asked whether saving the horizon DataArray to zarr in topo_param.py would improve speed.
62+
Answer: Yes, especially for large DEMs, because zarr enables chunk-aligned point lookups,
63+
parallel reads, better compression, and lazy loading. Implementation was offered but not yet done.
64+
65+
Installation notes:
66+
-------------------
67+
Standard TopoPyScale still uses topocalc.
68+
For best performance install HORAYZON:
69+
conda install -c conda-forge embree tbb-devel
70+
pip install git+https://github.com/ChristianSteger/HORAYZON.git
71+
72+
Git branch: fsm2oshd_profiling
73+
Commits added (in order):
74+
1. d8d459a - Optimize multiprocessing performance and add zarr I/O improvements
75+
2. 162f8fb - Add conditional HORAYZON support for high-performance terrain computation
76+
3. 91576fd - Optimize solar geometry, k-means clustering, and meteorological functions

TopoPyScale/topo_param.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,18 +470,23 @@ def compute_dem_param(dem_file, fname='ds_param.nc', project_directory=Path('./'
470470
return ds
471471

472472

473-
def compute_horizon(dem_file, azimuth_inc=30, num_threads=None, fname='da_horizon.nc', output_directory=Path('./outputs')):
473+
def compute_horizon(dem_file, azimuth_inc=30, num_threads=None, fname='da_horizon.nc',
474+
output_directory=Path('./outputs'), format='netcdf'):
474475
"""
475476
Function to compute horizon angles using the best available backend (HORAYZON preferred).
476477
477478
Args:
478479
dem_file (str): path and filename of the dem
479480
azimuth_inc (int): angle increment to compute horizons at, in Degrees [0-359]
480481
num_threads (int): number of threads to parallelize on
482+
fname (str): output filename
483+
output_directory (Path): output directory
484+
format (str): 'netcdf' or 'zarr' — zarr provides faster point lookups and
485+
parallel reads during downscaling
481486
482-
Returns:
487+
Returns:
483488
dataarray: all horizon angles for x,y,azimuth coordinates
484-
489+
485490
"""
486491
print(f'\n---> Computing horizons with {azimuth_inc} degree increments')
487492
ds = open_dem(dem_file)
@@ -542,7 +547,46 @@ def compute_horizon(dem_file, azimuth_inc=30, num_threads=None, fname='da_horizo
542547
}
543548
)
544549

545-
da.to_dataset().to_netcdf(output_directory / fname)
550+
ds = da.to_dataset()
551+
ds.attrs = {
552+
'description': 'Horizon angles for TopoPyScale',
553+
'azimuth_inc': str(azimuth_inc),
554+
'backend': 'HORAYZON' if HORAYZON_AVAILABLE else 'topocalc',
555+
'format': format
556+
}
557+
558+
output_path = output_directory / fname
559+
output_directory.mkdir(parents=True, exist_ok=True)
560+
561+
if format.lower() == 'zarr':
562+
if not fname.endswith('.zarr'):
563+
output_path = output_path.with_suffix('.zarr')
564+
print(f'---> Saving horizon to zarr: {output_path}')
565+
try:
566+
from zarr.codecs import BloscCodec
567+
except ImportError:
568+
from zarr import Blosc as BloscCodec
569+
570+
n_az = da.azimuth.size
571+
ny = da.y.size
572+
nx = da.x.size
573+
az_chunk = max(1, n_az)
574+
y_chunk = max(64, min(512, ny))
575+
x_chunk = max(64, min(512, nx))
576+
chunks = (az_chunk, y_chunk, x_chunk)
577+
578+
encoding = {
579+
'horizon': {
580+
'compressor': BloscCodec(cname='zstd', clevel=3, shuffle='bitshuffle', blocksize=0),
581+
'chunks': chunks
582+
}
583+
}
584+
ds = ds.chunk({'azimuth': az_chunk, 'y': y_chunk, 'x': x_chunk})
585+
ds.to_zarr(str(output_path), mode='w', encoding=encoding, zarr_format=3, consolidated=True)
586+
else:
587+
print(f'---> Saving horizon to netcdf: {output_path}')
588+
ds.to_netcdf(output_path)
589+
546590
return da
547591

548592

0 commit comments

Comments
 (0)