Implementation Details
Architecture Overview
precip-index/
├── src/
│ ├── indices.py # SPI/SPEI calculation
│ ├── compute.py # Core computation algorithms
│ ├── distributions.py # Multi-distribution fitting (Gamma, Pearson III, etc.)
│ ├── chunked.py # Memory-efficient chunked processing
│ ├── config.py # Configuration and constants
│ ├── utils.py # PET, data validation, memory utilities
│ ├── runtheory.py # Event analysis (dry & wet)
│ ├── visualization.py # Plotting functions
│ └── __init__.py # Package exports
├── input/ # User data (not in repo)
├── output/ # Generated results
│ ├── netcdf/
│ ├── csv/
│ └── plots/
└── notebook/ # Tutorials
Core Modules
1. indices.py
Purpose: Calculate SPI and SPEI indices
Key Functions:
spi()- Single-scale SPIspi_multi_scale()- Multiple scales at oncespei()- Temperature-inclusive indexspei_multi_scale()- Multiple SPEI scalesspi_global()- Global-scale SPI with automatic chunkingspei_global()- Global-scale SPEI with automatic chunkingestimate_memory_requirements()- Pre-computation memory estimation
Technology:
- NumPy vectorization for gridded operations
- Numba JIT compilation for fitting loops
- SciPy for statistical distributions
- xarray for CF-compliant NetCDF
Performance:
- 165×244 grid, 12-month scale: ~30 seconds
- Gamma distribution fitting: Numba-optimized
- Memory-efficient chunking for large datasets
2. chunked.py
Purpose: Memory-efficient processing for global-scale datasets
Key Classes/Functions:
ChunkedProcessor- Main class for chunked computationcompute_spi_chunked()- Chunked SPI calculationcompute_spei_chunked()- Chunked SPEI calculation
estimate_memory()- Estimate memory requirementsiter_chunks()- Generate spatial chunk coordinatesMemoryEstimate- Named tuple for memory information
Technology:
- Spatial tiling for memory efficiency
- Streaming I/O to disk
- Automatic garbage collection
- Progress callbacks for monitoring
Performance:
- Global CHIRPS (2160×4320): Processes with ~16GB RAM
- Chunk size auto-optimization based on available memory
- ~12x memory reduction vs. in-memory processing
3. compute.py
Purpose: Core computation algorithms for SPI/SPEI
Key Functions:
compute_index_parallel()- Main computation with optional memory-efficient mode_rolling_sum_3d()- O(n) cumulative sum algorithm_compute_gamma_params_vectorized()- Vectorized parameter fitting_transform_to_normal_vectorized()- Memory-efficient transformationcompute_index_dask()- Dask-based lazy computationcompute_index_dask_to_zarr()- Stream directly to Zarr format
Technology:
- Float32 internal processing (50% memory reduction)
- Cumulative sum for O(n) rolling windows
- Period-by-period processing to limit peak memory
4. config.py
Purpose: Configuration constants and enumerations
Key Constants:
MEMORY_MULTIPLIER- Peak memory estimation factor (12x)MEMORY_SAFETY_FACTOR- Safety margin for chunk sizing (0.7)DEFAULT_CHUNK_LAT/LON- Default chunk dimensions (500)Periodicity- Enum for monthly/daily data
5. runtheory.py
Purpose: Climate extreme event analysis using run theory (Yevjevich 1967)
Key Functions:
Event-Based:
identify_events()- Extract complete events (dry and wet)summarize_events()- Event statistics
Time-Series:
calculate_timeseries()- Month-by-month tracking- Real-time monitoring capability
Gridded Statistics:
calculate_period_statistics()- Spatial statistics for time periodcalculate_annual_statistics()- Year-by-year analysiscompare_periods()- Multi-period comparison
Implementation:
- Pure NumPy for single-location (very fast)
- xarray.apply_ufunc for gridded data
- Dual magnitude: cumulative + instantaneous
6. utils.py
Purpose: Supporting utilities and memory management
Key Functions:
calculate_pet()- Potential evapotranspiration (dispatcher)method='thornthwaite'- Uses only mean temperature (default)method='hargreaves'- Uses Tmin, Tmax, and extraterrestrial radiation
eto_thornthwaite()- Thornthwaite PET (low-level)eto_hargreaves()- Hargreaves-Samani PET (low-level)_extraterrestrial_radiation()- Daily Ra calculation (FAO-56)_monthly_mean_extraterrestrial_radiation()- Monthly Ra averagesvalidate_data()- Input checkingload_parameters()- Pre-fitted parameter loadingsave_parameters()- Parameter persistenceget_optimal_chunk_size()- Calculate optimal chunk dimensionsformat_bytes()- Human-readable byte formattingget_array_memory_size()- Calculate array memory footprintprint_memory_info()- Display current memory usage
7. visualization.py
Purpose: Publication-quality climate extremes visualization
Key Functions:
plot_index()- Time series with WMO colorsplot_events()- Event timeline with dry/wet differentiationplot_event_characteristics()- Multi-panel analysisplot_event_timeline()- 5-panel evolutionplot_spatial_stats()- Spatial mappinggenerate_location_filename()- Consistent naming
Technology:
- Matplotlib for all plots
- Color schemes from scientific literature
- Automatic folder creation
Data Flow
SPI/SPEI Calculation
Input NetCDF (precip, temp, [tmin, tmax])
↓
validate_data()
↓
[calculate_pet() if SPEI]
├─ pet_method='thornthwaite' → eto_thornthwaite(temp, lat)
└─ pet_method='hargreaves' → eto_hargreaves(temp, tmin, tmax, lat)
└─ _extraterrestrial_radiation(lat, doy)
↓
spi() or spei()
├─ Temporal aggregation (rolling sum)
├─ Distribution fitting (Numba-optimized)
└─ Probability transformation
↓
Output NetCDF (SPI/SPEI)
Climate Extreme Event Analysis
SPI/SPEI time series
↓
identify_events()
├─ Threshold crossing detection
├─ Duration calculation
├─ Magnitude (cumulative + instantaneous)
├─ Intensity (magnitude/duration)
└─ Peak identification
↓
Events DataFrame or
Period Statistics Dataset
Optimization Strategies
1. Numba JIT Compilation
Where: Distribution fitting loops in indices.py
@numba.jit(nopython=True)
def fit_gamma_numba(precip_2d):
# Fast parameter estimation
# 10-20x faster than pure NumPyBenefit: Critical for operational speed
2. Vectorization
Where: All array operations
# Vectorized temporal aggregation
precip_sum = precip.rolling(time=scale).sum()
# Vectorized threshold operations
is_event = (spi < threshold).astype(int)Benefit: Leverages NumPy/xarray efficiency
3. Chunking (Dask-ready)
Where: Large dataset processing
# Open with chunks for lazy loading
precip = xr.open_dataset('data.nc', chunks={'time': 100})
# Operations are lazy until .compute()
result = spi(precip, scale=12) # No computation yet
result.to_netcdf('output.nc') # Computes and savesBenefit: Handle datasets larger than RAM
4. Parameter Caching
Where: Operational/production systems
# Fit once on calibration period
params = spi(precip_calibration, scale=12,
calibration_start_year=1991,
calibration_end_year=2020)
# Save parameters
params.to_netcdf('spi_12_params.nc')
# Reuse for operational updates
spi_new = spi(precip_new, scale=12,
fitting_params=params)Benefit: Near-instant operational updates
Memory Management
Memory Requirements by Data Size
| Grid Size | Input Data | Peak Memory (Old) | Peak Memory (New) | Recommended Approach |
|---|---|---|---|---|
| Single Location | <1 MB | <10 MB | <10 MB | In-memory |
| Small (100×100) | ~50 MB | ~500 MB | ~100 MB | In-memory |
| Medium (500×500) | ~500 MB | ~5 GB | ~1 GB | In-memory or chunked |
| Large (1000×1000) | ~2 GB | ~20 GB | ~4 GB | Chunked |
| Global (2160×4320) | ~36 GB | ~400 GB | ~16 GB | Chunked (required) |
Memory-Efficient Processing (New in 2026.1)
The new chunked processing module reduces peak memory by ~12x through:
- Float32 Internal Processing
- Uses float32 instead of float64 internally
- 50% memory reduction for intermediate arrays
- Output precision maintained
- Cumulative Sum Algorithm
- O(n) rolling sum instead of O(n×scale)
- Single pass through time dimension
- No intermediate rolling arrays
- Period-by-Period Transformation
- Process one calendar period at a time
- Garbage collection between periods
- Limits peak memory during standardization
- Spatial Chunking
- Process data in spatial tiles
- Stream results to disk
- Memory bounded by chunk size, not total grid
Chunk Size Guidelines
| Available RAM | Recommended Chunk Size | Notes |
|---|---|---|
| 16 GB | 200 × 200 | Minimum recommended |
| 32 GB | 300 × 300 | Good for workstations |
| 64 GB | 400 × 400 | Standard servers |
| 128 GB | 600 × 600 | High-memory systems |
| 256 GB+ | 800 × 800 | Large servers |
Usage Examples
In-memory (small data):
from indices import spi
spi_12 = spi(precip, scale=12)Chunked processing (large data):
from indices import spi_global
result = spi_global(
'global_precip.nc',
'spi_12_global.nc',
scale=12,
chunk_size=500
)Memory estimation before processing:
from indices import estimate_memory_requirements
mem = estimate_memory_requirements('global_precip.nc')
print(mem) # Shows input size, peak memory, recommendationsDask-based streaming to Zarr:
from compute import compute_index_dask_to_zarr
compute_index_dask_to_zarr(
precip_da,
'output.zarr',
scale=12,
n_workers=8
)Output Standards
NetCDF Files
Compliance: CF Convention 1.8
Required Attributes:
attrs = {
'standard_name': 'standardized_precipitation_index',
'long_name': 'SPI-12 (Gamma distribution)',
'units': '1',
'scale': 12,
'distribution': 'gamma',
'calibration_period': '1991-2020',
'threshold': -1.0
}Coordinates:
time: Datetime64 with monthly frequencylat: WGS84 latitudelon: WGS84 longitude
CSV Files
Format: Standard pandas DataFrame export
Columns:
- Event-based: event_id, start_date, end_date, duration, magnitude, etc.
- Time-series: time, index_value, is_event, duration, magnitude_cumulative, etc.
Plots
Format: PNG (300 dpi, recommended)
Naming: Location/period-based
plot_*_lat*.##_lon*.##.pngplot_spatial_*_YYYY-YYYY.png
Testing Strategy
Unit Tests (Future)
- Individual function validation
- Edge case handling
- Parameter validation
Integration Tests
test_drought_characteristics.py- End-to-end workflow
- Real data validation
Validation
- Compare with published results
- Cross-check with original climate-indices
- Visual inspection of outputs
Dependencies
Required
numpy>=1.20.0 # Array operations
scipy>=1.7.0 # Statistical distributions
xarray>=0.19.0 # NetCDF handling
netCDF4>=1.5.7 # NetCDF I/O
numba>=0.54.0 # JIT compilation
matplotlib>=3.4.0 # Visualization
pandas>=1.1.0 # DataFrame operations
Required for Global Processing
dask[complete]>=2021.10.0 # Parallel/distributed computing
zarr>=2.10.0 # Efficient chunked array storage
psutil>=5.8.0 # Memory detection and monitoring
Optional
cartopy>=0.20.0 # Map projections for visualization
jupyter>=1.0.0 # Notebook support
Error Handling
Data Validation
- Check for NaN values
- Verify time dimension
- Ensure positive precipitation
- Validate coordinate systems
Distribution Fitting
- Fallback to default parameters if fitting fails
- Warning messages for problematic locations
- NaN preservation in outputs
File Operations
- Auto-create output directories
- Handle existing file overwrites
- Validate paths
Performance Benchmarks
Standard Processing (Laptop: 4 cores, 16 GB RAM)
| Operation | Grid Size | Time | Memory |
|---|---|---|---|
| SPI-12 calculation | 165×244×408 | 30 sec | 500 MB |
| Drought events (single) | 408 months | <0.1 sec | <1 MB |
| Period statistics | 165×244, 5 years | 30 sec | 200 MB |
| Annual statistics | 165×244, 10 years | 5 min | 300 MB |
| Plot generation | Single location | 2 sec | 50 MB |
Global-Scale Processing (Workstation: 128 GB RAM)
| Operation | Dataset | Grid Size | Time Steps | Chunks | Total Time |
|---|---|---|---|---|---|
| SPI-12 (Gamma) | CHIRPS v3 (0.05°) | 2400 × 7200 (17.3M cells) | 539 months | 12 | ~2h 47m |
| SPEI-12 (Pearson III) | TerraClimate (0.04°) | 4320 × 8640 (37.3M cells) | 804 months | 32 | ~26h |
Why Does TerraClimate SPEI Take ~9.5× Longer?
The difference comes from compounding factors across every dimension:
| Factor | CHIRPS SPI-12 | TerraClimate SPEI-12 | Impact |
|---|---|---|---|
| Grid cells | 17.3M | 37.3M | 2.2× more cells |
| Time steps | 539 months (1981–2025) | 804 months (1958–2024) | 1.5× longer series |
| Dataset size | ~69 GB | ~224 GB | 3.2× more I/O |
| Distribution | Gamma (Method of Moments) | Pearson III (MLE) | ~2× slower fitting |
| Extra computation | — | PET calculation | Additional overhead |
| Spatial chunks | 12 | 32 | 2.7× more I/O cycles |
Key takeaway: Gamma fitting uses a closed-form Method of Moments solution (fast), while Pearson III uses iterative Maximum Likelihood Estimation (slower per cell). Combined with 2× more grid cells and 1.5× longer time series, the total work scales roughly as 2.2 × 1.5 × 2 ≈ 6.6×. Add the I/O overhead of 32 chunks (each requiring a full read/write cycle) and PET computation, and the ~9.5× ratio is expected.
These benchmarks are for global-scale datasets. Country or regional subsets (e.g., Indonesia, East Africa) typically complete in seconds to minutes on a standard laptop with 16 GB RAM.
Chunk Size vs. Performance Trade-offs
| Chunk Size | Memory per Chunk | I/O Operations | Speed |
|---|---|---|---|
| 200×200 | ~2 GB | Many (slow I/O) | Slower |
| 500×500 | ~8 GB | Moderate | Balanced |
| 800×800 | ~20 GB | Few (fast I/O) | Faster |
Recommendation: Use the largest chunk size your system can handle for best performance.
Scalability
Horizontal Scaling
- Process regions in parallel
- Independent grid cells
- Batch processing capability
Vertical Scaling
- Dask for large grids
- Chunked I/O for huge datasets
- Memory-mapped arrays for extreme sizes
Future Optimizations
- GPU Acceleration - CuPy for distribution fitting
- Parallel Processing - Multiprocessing for annual statistics
- Caching - Redis for parameter storage
- Streaming - Process data as it arrives
Code Style
Follows:
- PEP 8 conventions
- NumPy docstring format
- Type hints (future)
Standards:
- Functions: snake_case
- Classes: PascalCase (minimal usage)
- Constants: UPPER_CASE
- Private: _leading_underscore
Version Control
Current: 2026.1 (January 2026)
Key Changes:
- 2026.1: Complete package with SPI/SPEI, run theory, and dual magnitude
See: CHANGELOG for details
References
- McKee, T.B., et al. (1993). SPI methodology
- Vicente-Serrano, S.M., et al. (2010). SPEI methodology
- Yevjevich, V. (1967). Run theory
- Adams, J. (2017). Original climate-indices package
Contributing
To extend functionality:
- Add function to appropriate module
- Export in
__init__.py - Document in user guide
- Add test case
- Update CHANGELOG
Maintain:
- CF Convention compliance
- NumPy vectorization
- Clear documentation
- Consistent API
See Also
- Probability Distributions - Distribution selection and fitting methods
- Validation & Test Results - Quality verification and test coverage
- Methodology - Scientific background
- API Reference - Function documentation
- User Guides - Practical usage