1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//! Pure-Rust numerical kernels for `climate_indices`.
//!
//! The public modules expose numerical kernels, not a complete SPI/SPEI workflow.
//! Callers prepare calendars, select the Calibration Period, validate physical
//! ranges, and apply any required aggregation and final index transformation.
//! Kernels check the structural constraints documented on each function.
//! This crate has no Python, I/O, logging, or metadata dependency.
//!
//! # Arrays and compatibility
//!
//! Array inputs borrow `ndarray` views; array outputs own their data.
//! Other kernels accept scalars or slices and return their documented result types.
//! Strided views are supported unless a function documents a restriction.
//! `ndarray` is a public dependency, not a re-export: callers use a compatible
//! `ndarray` dependency.
//! An incompatible `ndarray` upgrade requires a breaking crate release.
//! The crate has no feature flags and no optional runtime dependency.
//!
//! # Shapes, calendars, and units
//!
//! All array indices are zero-based. A daily block has shape `(days, cells)`.
//! One series uses one cell; a grid uses one column per independent cell.
//! Distribution fits use `(years, columns)`, with one column per calendar step
//! and cell. A caller can use `column = step * cells + cell` consistently for
//! values and fit parameters. Monthly records have 12 calendar steps per year;
//! the prepared all-leap daily layout has 366, including a February 29 position
//! in every year. Kernels do not infer calendar dates or insert missing days.
//!
//! Precipitation and PET kernels use mm, temperatures use degrees C, and daily
//! window lengths count calendar days. PET latitude inputs use radians in
//! [`eto`] and degrees in [`pm_eto`]. Palmer water-balance inputs instead use
//! inches. Function documentation specifies other units and preconditions.
//! NaN handling is kernel-specific; NaN is not a substitute for shape validation.
//!
//! # One series and a grid
//!
//! [`flood::effective_precipitation`] accepts both layouts without a wrapper or
//! a copy of the input. Its output has the same shape as the input.
//!
//! ```
//! use climate_indices::{ClimateError, flood::effective_precipitation};
//! use ndarray::{Axis, array};
//!
//! let series_mm = array![1.0, 2.0, 3.0];
//! let series = effective_precipitation(series_mm.view().insert_axis(Axis(1)), 2)?;
//! assert_eq!(series.dim(), (3, 1));
//! assert!(series[[0, 0]].is_nan());
//! assert_eq!(series[[1, 0]], 3.5);
//!
//! let grid_mm = array![[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]];
//! let grid = effective_precipitation(grid_mm.view(), 2)?;
//! assert_eq!(grid.dim(), (3, 2));
//! assert_eq!(grid[[1, 0]], series[[1, 0]]);
//! assert_eq!(grid[[1, 1]], 35.0);
//! # Ok::<(), ClimateError>(())
//! ```
//!
//! # Calendar-column fitting
//!
//! [`gamma::gamma_parameters`] fits each column independently. This small
//! synthetic sample illustrates the layout, not a sufficient Calibration Period.
//! Gamma shape is dimensionless; gamma scale has the input's units (here mm).
//! The result below is a probability, not SPI.
//!
//! ```
//! use climate_indices::{ClimateError, gamma::{gamma_parameters, gamma_probabilities}};
//! use ndarray::{Array1, Array2};
//!
//! let (years, steps, cells) = (3, 12, 2);
//! let rain_mm = Array2::from_shape_fn((years, steps * cells), |(year, column)| {
//! 10.0 * (year + column + 1) as f64
//! });
//! let (shape, scale_mm) = gamma_parameters(rain_mm.view());
//! let probability_of_zero = Array1::zeros(steps * cells);
//! let probabilities = gamma_probabilities(
//! rain_mm.view(), shape.view(), scale_mm.view(), probability_of_zero.view(),
//! )?;
//! let (year, step, cell) = (1, 5, 1);
//! assert!((0.0..=1.0).contains(&probabilities[[year, step * cells + cell]]));
//! # Ok::<(), ClimateError>(())
//! ```
//!
//! # Errors and numerical behavior
//!
//! Match [`ClimateError`] variants and fields, not diagnostic text. Public enums
//! are non-exhaustive, so downstream matches need a wildcard arm. Display text
//! and diagnostic strings are not stable API and carry no Python-message promise.
//! `climate-py` owns Python exception translation. The Python implementations
//! remain the numerical parity oracles. Each public kernel documents its numerical
//! contract; the measured evidence below distinguishes workflow and scalar errors.
//! No kernel's operation order changes with this surface.
use fmt;
// compiles and runs the README example under `cargo test --doc`
;
/// Version of this crate, re-exported by the Python extension as `__version__`.
pub const VERSION: &str = env!;
/// A structural input error or a numerical failure reported by a kernel.
///
/// Variants and their typed fields form the error contract. Diagnostic strings,
/// including [`Display`](fmt::Display) output, are not stable identifiers.
///
/// ```
/// use climate_indices::{ClimateError, flood::effective_precipitation};
/// use ndarray::array;
///
/// let rain = array![[1.0]];
/// match effective_precipitation(rain.view(), 0) {
/// Err(ClimateError::EmptyPeriod { argument }) => assert_eq!(argument, "duration"),
/// Err(_) => panic!("unexpected error"),
/// Ok(_) => panic!("a zero-day window must fail"),
/// }
/// ```