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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
//! Interpolation and extrapolation classes - exact 1:1 port from Python colour-science
//! Line-by-line port with exact behavior matching
use crate::error::Result;
/// Linear interpolator - exact 1:1 port from Python colour-science LinearInterpolator
/// This class wraps numpy.interp functionality
pub struct LinearInterpolator {
x: Vec<f64>,
y: Vec<f64>,
}
impl LinearInterpolator {
/// Create a new LinearInterpolator
/// Exact 1:1 port from Python LinearInterpolator.__init__
pub fn new(x: Vec<f64>, y: Vec<f64>) -> Result<Self> {
// Python: attest(x.ndim == 1, '"x" independent variable must have exactly one dimension!')
// Python: attest(y.ndim == 1, '"y" dependent variable must have exactly one dimension!')
// Python: validate dimensions match
if x.len() != y.len() {
return Err(crate::error::MunsellError::ConversionError {
message: format!("x and y dimensions must match: {} != {}", x.len(), y.len())
});
}
Ok(Self { x, y })
}
/// Evaluate the interpolating polynomial at given point(s)
/// Exact 1:1 port from Python LinearInterpolator.__call__
pub fn interpolate(&self, x: f64) -> f64 {
// Python: return np.interp(x, self._x, self._y)
// np.interp behavior:
// - Returns y[0] if x <= x[0]
// - Returns y[-1] if x >= x[-1]
// - Linear interpolation between points
if x <= self.x[0] {
return self.y[0];
}
if x >= self.x[self.x.len() - 1] {
return self.y[self.y.len() - 1];
}
// Find the interval containing x
for i in 0..self.x.len() - 1 {
if x >= self.x[i] && x <= self.x[i + 1] {
// Linear interpolation formula: y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
let t = (x - self.x[i]) / (self.x[i + 1] - self.x[i]);
return self.y[i] + t * (self.y[i + 1] - self.y[i]);
}
}
// Should not reach here if x is in range
self.y[self.y.len() - 1]
}
/// Interpolate multiple points
/// Helper for batch interpolation
pub fn interpolate_many(&self, xs: &[f64]) -> Vec<f64> {
xs.iter().map(|&x| self.interpolate(x)).collect()
}
/// Get x values
pub fn x(&self) -> &[f64] {
&self.x
}
/// Get y values
pub fn y(&self) -> &[f64] {
&self.y
}
}
/// Extrapolator - exact 1:1 port from Python colour-science Extrapolator
/// Extrapolates 1-D function using Linear or Constant methods
pub struct Extrapolator {
interpolator: LinearInterpolator,
method: ExtrapolationMethod,
left: Option<f64>,
right: Option<f64>,
}
/// Extrapolation method
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExtrapolationMethod {
Linear,
Constant,
}
impl Extrapolator {
/// Create a new Extrapolator
/// Exact 1:1 port from Python Extrapolator.__init__
pub fn new(
interpolator: LinearInterpolator,
method: ExtrapolationMethod,
left: Option<f64>,
right: Option<f64>,
) -> Self {
Self {
interpolator,
method,
left,
right,
}
}
/// Evaluate the Extrapolator at given point
/// Exact 1:1 port from Python Extrapolator.__call__ and _evaluate
pub fn extrapolate(&self, x: f64) -> f64 {
let xi = self.interpolator.x();
let yi = self.interpolator.y();
// Check if x is in interpolation range
if x >= xi[0] && x <= xi[xi.len() - 1] {
// Within range - use interpolator
return self.interpolator.interpolate(x);
}
// Handle extrapolation
if x < xi[0] {
// Below range
if let Some(left_val) = self.left {
// Python: if self._left is not None: y[x < xi[0]] = self._left
return left_val;
}
match self.method {
ExtrapolationMethod::Linear => {
// Python: y[x < xi[0]] = yi[0] + (x[x < xi[0]] - xi[0]) * sdiv(yi[1] - yi[0], xi[1] - xi[0])
let slope = if (xi[1] - xi[0]).abs() < 1e-10 {
0.0
} else {
(yi[1] - yi[0]) / (xi[1] - xi[0])
};
yi[0] + (x - xi[0]) * slope
}
ExtrapolationMethod::Constant => {
// Python: y[x < xi[0]] = yi[0]
yi[0]
}
}
} else {
// Above range (x > xi[-1])
if let Some(right_val) = self.right {
// Python: if self._right is not None: y[x > xi[-1]] = self._right
return right_val;
}
let n = xi.len();
match self.method {
ExtrapolationMethod::Linear => {
// Python: y[x > xi[-1]] = yi[-1] + (x[x > xi[-1]] - xi[-1]) * sdiv(yi[-1] - yi[-2], xi[-1] - xi[-2])
let slope = if (xi[n - 1] - xi[n - 2]).abs() < 1e-10 {
0.0
} else {
(yi[n - 1] - yi[n - 2]) / (xi[n - 1] - xi[n - 2])
};
yi[n - 1] + (x - xi[n - 1]) * slope
}
ExtrapolationMethod::Constant => {
// Python: y[x > xi[-1]] = yi[-1]
yi[n - 1]
}
}
}
}
/// Extrapolate multiple points
/// Helper for batch extrapolation
pub fn extrapolate_many(&self, xs: &[f64]) -> Vec<f64> {
xs.iter().map(|&x| self.extrapolate(x)).collect()
}
}
/// Create a simple linear interpolator from two arrays
/// Helper function matching Python's common usage pattern
pub fn linear_interp(x: &[f64], y: &[f64], xi: f64) -> f64 {
// This matches np.interp behavior
if xi <= x[0] {
return y[0];
}
if xi >= x[x.len() - 1] {
return y[y.len() - 1];
}
// Find interval and interpolate
for i in 0..x.len() - 1 {
if xi >= x[i] && xi <= x[i + 1] {
let t = (xi - x[i]) / (x[i + 1] - x[i]);
return y[i] + t * (y[i + 1] - y[i]);
}
}
y[y.len() - 1]
}
/// Clamp linear interpolation (ensure result is within y bounds)
/// This matches np.interp with bounds checking
pub fn linear_interp_clamped(x: &[f64], y: &[f64], xi: f64) -> f64 {
let result = linear_interp(x, y, xi);
// Find min and max of y values
let y_min = y.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max = y.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
// Clamp result to y range
result.max(y_min).min(y_max)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_interpolator() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![0.0, 2.0, 4.0, 6.0];
let interp = LinearInterpolator::new(x, y).unwrap();
// Test exact points
assert_eq!(interp.interpolate(0.0), 0.0);
assert_eq!(interp.interpolate(1.0), 2.0);
assert_eq!(interp.interpolate(2.0), 4.0);
assert_eq!(interp.interpolate(3.0), 6.0);
// Test interpolation
assert_eq!(interp.interpolate(0.5), 1.0);
assert_eq!(interp.interpolate(1.5), 3.0);
assert_eq!(interp.interpolate(2.5), 5.0);
// Test extrapolation (np.interp clamps)
assert_eq!(interp.interpolate(-1.0), 0.0);
assert_eq!(interp.interpolate(4.0), 6.0);
}
#[test]
fn test_extrapolator_linear() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.0, 2.0, 3.0];
let interp = LinearInterpolator::new(x, y).unwrap();
let extrap = Extrapolator::new(interp, ExtrapolationMethod::Linear, None, None);
// Test interpolation range
assert_eq!(extrap.extrapolate(1.5), 1.5);
assert_eq!(extrap.extrapolate(2.0), 2.0);
assert_eq!(extrap.extrapolate(2.5), 2.5);
// Test linear extrapolation
assert_eq!(extrap.extrapolate(0.0), 0.0); // Extends line backwards
assert_eq!(extrap.extrapolate(4.0), 4.0); // Extends line forwards
}
#[test]
fn test_extrapolator_constant() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.0, 2.0, 3.0];
let interp = LinearInterpolator::new(x, y).unwrap();
let extrap = Extrapolator::new(interp, ExtrapolationMethod::Constant, None, None);
// Test constant extrapolation
assert_eq!(extrap.extrapolate(0.0), 1.0); // Uses first value
assert_eq!(extrap.extrapolate(4.0), 3.0); // Uses last value
}
#[test]
fn test_extrapolator_with_bounds() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.0, 2.0, 3.0];
let interp = LinearInterpolator::new(x, y).unwrap();
let extrap = Extrapolator::new(
interp,
ExtrapolationMethod::Linear,
Some(0.0), // Left bound
Some(5.0), // Right bound
);
// Test custom bounds override extrapolation
assert_eq!(extrap.extrapolate(0.0), 0.0); // Uses left bound
assert_eq!(extrap.extrapolate(4.0), 5.0); // Uses right bound
}
#[test]
fn test_linear_interp_function() {
let x = [0.0, 1.0, 2.0];
let y = [0.0, 2.0, 4.0];
assert_eq!(linear_interp(&x, &y, 0.5), 1.0);
assert_eq!(linear_interp(&x, &y, 1.5), 3.0);
assert_eq!(linear_interp(&x, &y, -1.0), 0.0); // Clamps
assert_eq!(linear_interp(&x, &y, 3.0), 4.0); // Clamps
}
}