Skip to main content

oxigeo_algorithms/dsl/
functions.rs

1//! Built-in functions for the Raster Algebra DSL
2//!
3//! This module provides a comprehensive library of built-in functions including:
4//! - Mathematical functions (sin, cos, sqrt, etc.)
5//! - Statistical functions (mean, median, percentile, etc.)
6//! - Spatial functions (focal operations)
7//! - Logical functions
8//! - Type conversion functions
9
10use super::variables::Value;
11use crate::error::{AlgorithmError, Result};
12use crate::raster::{gaussian_blur, median_filter};
13
14#[cfg(not(feature = "std"))]
15use alloc::{boxed::Box, string::String, vec::Vec};
16
17/// Built-in function type
18pub type BuiltinFn = fn(&[Value]) -> Result<Value>;
19
20/// Registry of built-in functions
21pub struct FunctionRegistry {
22    functions: Vec<(&'static str, BuiltinFn, usize)>, // (name, function, arity)
23}
24
25impl Default for FunctionRegistry {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl FunctionRegistry {
32    /// Creates a new function registry with all built-in functions
33    pub fn new() -> Self {
34        let mut registry = Self {
35            functions: Vec::new(),
36        };
37
38        // Mathematical functions (1 arg)
39        registry.register("sqrt", fn_sqrt, 1);
40        registry.register("abs", fn_abs, 1);
41        registry.register("floor", fn_floor, 1);
42        registry.register("ceil", fn_ceil, 1);
43        registry.register("round", fn_round, 1);
44        registry.register("log", fn_log, 1);
45        registry.register("log10", fn_log10, 1);
46        registry.register("log2", fn_log2, 1);
47        registry.register("exp", fn_exp, 1);
48        registry.register("sin", fn_sin, 1);
49        registry.register("cos", fn_cos, 1);
50        registry.register("tan", fn_tan, 1);
51        registry.register("asin", fn_asin, 1);
52        registry.register("acos", fn_acos, 1);
53        registry.register("atan", fn_atan, 1);
54        registry.register("sinh", fn_sinh, 1);
55        registry.register("cosh", fn_cosh, 1);
56        registry.register("tanh", fn_tanh, 1);
57
58        // Mathematical functions (2 args)
59        registry.register("atan2", fn_atan2, 2);
60        registry.register("pow", fn_pow, 2);
61        registry.register("hypot", fn_hypot, 2);
62
63        // Min/Max (variable args)
64        registry.register("min", fn_min, 0);
65        registry.register("max", fn_max, 0);
66
67        // Statistical functions (1 arg - raster)
68        registry.register("mean", fn_mean, 1);
69        registry.register("median", fn_median, 1);
70        registry.register("mode", fn_mode, 1);
71        registry.register("stddev", fn_stddev, 1);
72        registry.register("variance", fn_variance, 1);
73        registry.register("sum", fn_sum, 1);
74        registry.register("product", fn_product, 1);
75
76        // Percentile functions
77        registry.register("percentile", fn_percentile, 2);
78
79        // Spatial filters
80        registry.register("gaussian", fn_gaussian, 2);
81        registry.register("median_filter", fn_median_filt, 2);
82
83        // Logical functions
84        registry.register("and", fn_and, 2);
85        registry.register("or", fn_or, 2);
86        registry.register("not", fn_not, 1);
87        registry.register("xor", fn_xor, 2);
88
89        // Comparison functions
90        registry.register("eq", fn_eq, 2);
91        registry.register("ne", fn_ne, 2);
92        registry.register("lt", fn_lt, 2);
93        registry.register("le", fn_le, 2);
94        registry.register("gt", fn_gt, 2);
95        registry.register("ge", fn_ge, 2);
96
97        // Type conversion
98        registry.register("to_number", fn_to_number, 1);
99        registry.register("to_bool", fn_to_bool, 1);
100
101        // Utility functions
102        registry.register("clamp", fn_clamp, 3);
103        registry.register("select", fn_select, 3);
104
105        registry
106    }
107
108    /// Registers a function
109    pub fn register(&mut self, name: &'static str, func: BuiltinFn, arity: usize) {
110        self.functions.push((name, func, arity));
111    }
112
113    /// Looks up a function by name
114    pub fn lookup(&self, name: &str) -> Option<(BuiltinFn, usize)> {
115        self.functions
116            .iter()
117            .find(|(n, _, _)| *n == name)
118            .map(|(_, f, a)| (*f, *a))
119    }
120
121    /// Checks if a function exists
122    pub fn exists(&self, name: &str) -> bool {
123        self.functions.iter().any(|(n, _, _)| *n == name)
124    }
125
126    /// Gets all function names
127    pub fn function_names(&self) -> Vec<&'static str> {
128        self.functions.iter().map(|(n, _, _)| *n).collect()
129    }
130}
131
132// Mathematical functions
133
134/// Helper to apply a unary function to either a scalar or raster
135fn apply_unary_fn<F>(value: &Value, f: F) -> Result<Value>
136where
137    F: Fn(f64) -> f64,
138{
139    match value {
140        Value::Number(x) => Ok(Value::Number(f(*x))),
141        Value::Raster(raster) => {
142            use oxigeo_core::types::RasterDataType;
143            let width = raster.width();
144            let height = raster.height();
145            let mut result =
146                oxigeo_core::buffer::RasterBuffer::zeros(width, height, RasterDataType::Float32);
147
148            for y in 0..height {
149                for x in 0..width {
150                    let pixel = raster
151                        .get_pixel(x, y)
152                        .map_err(crate::error::AlgorithmError::Core)?;
153                    let new_val = f(pixel);
154                    result
155                        .set_pixel(x, y, new_val)
156                        .map_err(crate::error::AlgorithmError::Core)?;
157                }
158            }
159
160            Ok(Value::Raster(Box::new(result)))
161        }
162        _ => Err(AlgorithmError::InvalidParameter {
163            parameter: "value",
164            message: "Expected number or raster".to_string(),
165        }),
166    }
167}
168
169/// Helper to apply a binary function to scalars or rasters
170fn apply_binary_fn<F>(left: &Value, right: &Value, f: F) -> Result<Value>
171where
172    F: Fn(f64, f64) -> f64,
173{
174    match (left, right) {
175        (Value::Number(l), Value::Number(r)) => Ok(Value::Number(f(*l, *r))),
176        (Value::Raster(raster), Value::Number(scalar))
177        | (Value::Number(scalar), Value::Raster(raster)) => {
178            use oxigeo_core::types::RasterDataType;
179            let width = raster.width();
180            let height = raster.height();
181            let mut result =
182                oxigeo_core::buffer::RasterBuffer::zeros(width, height, RasterDataType::Float32);
183
184            for y in 0..height {
185                for x in 0..width {
186                    let pixel = raster
187                        .get_pixel(x, y)
188                        .map_err(crate::error::AlgorithmError::Core)?;
189                    let new_val = f(pixel, *scalar);
190                    result
191                        .set_pixel(x, y, new_val)
192                        .map_err(crate::error::AlgorithmError::Core)?;
193                }
194            }
195
196            Ok(Value::Raster(Box::new(result)))
197        }
198        (Value::Raster(left_raster), Value::Raster(right_raster)) => {
199            use oxigeo_core::types::RasterDataType;
200            let width = left_raster.width();
201            let height = left_raster.height();
202
203            if right_raster.width() != width || right_raster.height() != height {
204                return Err(AlgorithmError::InvalidDimensions {
205                    message: "Rasters must have same dimensions",
206                    actual: right_raster.width() as usize,
207                    expected: width as usize,
208                });
209            }
210
211            let mut result =
212                oxigeo_core::buffer::RasterBuffer::zeros(width, height, RasterDataType::Float32);
213
214            for y in 0..height {
215                for x in 0..width {
216                    let left_pixel = left_raster
217                        .get_pixel(x, y)
218                        .map_err(crate::error::AlgorithmError::Core)?;
219                    let right_pixel = right_raster
220                        .get_pixel(x, y)
221                        .map_err(crate::error::AlgorithmError::Core)?;
222                    let new_val = f(left_pixel, right_pixel);
223                    result
224                        .set_pixel(x, y, new_val)
225                        .map_err(crate::error::AlgorithmError::Core)?;
226                }
227            }
228
229            Ok(Value::Raster(Box::new(result)))
230        }
231        _ => Err(AlgorithmError::InvalidParameter {
232            parameter: "value",
233            message: "Expected number or raster".to_string(),
234        }),
235    }
236}
237
238fn fn_sqrt(args: &[Value]) -> Result<Value> {
239    apply_unary_fn(&args[0], |x| x.sqrt())
240}
241
242fn fn_abs(args: &[Value]) -> Result<Value> {
243    apply_unary_fn(&args[0], |x| x.abs())
244}
245
246fn fn_floor(args: &[Value]) -> Result<Value> {
247    apply_unary_fn(&args[0], |x| x.floor())
248}
249
250fn fn_ceil(args: &[Value]) -> Result<Value> {
251    apply_unary_fn(&args[0], |x| x.ceil())
252}
253
254fn fn_round(args: &[Value]) -> Result<Value> {
255    apply_unary_fn(&args[0], |x| x.round())
256}
257
258fn fn_log(args: &[Value]) -> Result<Value> {
259    apply_unary_fn(&args[0], |x| x.ln())
260}
261
262fn fn_log10(args: &[Value]) -> Result<Value> {
263    apply_unary_fn(&args[0], |x| x.log10())
264}
265
266fn fn_log2(args: &[Value]) -> Result<Value> {
267    apply_unary_fn(&args[0], |x| x.log2())
268}
269
270fn fn_exp(args: &[Value]) -> Result<Value> {
271    apply_unary_fn(&args[0], |x| x.exp())
272}
273
274fn fn_sin(args: &[Value]) -> Result<Value> {
275    apply_unary_fn(&args[0], |x| x.sin())
276}
277
278fn fn_cos(args: &[Value]) -> Result<Value> {
279    apply_unary_fn(&args[0], |x| x.cos())
280}
281
282fn fn_tan(args: &[Value]) -> Result<Value> {
283    apply_unary_fn(&args[0], |x| x.tan())
284}
285
286fn fn_asin(args: &[Value]) -> Result<Value> {
287    apply_unary_fn(&args[0], |x| x.asin())
288}
289
290fn fn_acos(args: &[Value]) -> Result<Value> {
291    apply_unary_fn(&args[0], |x| x.acos())
292}
293
294fn fn_atan(args: &[Value]) -> Result<Value> {
295    apply_unary_fn(&args[0], |x| x.atan())
296}
297
298fn fn_sinh(args: &[Value]) -> Result<Value> {
299    apply_unary_fn(&args[0], |x| x.sinh())
300}
301
302fn fn_cosh(args: &[Value]) -> Result<Value> {
303    apply_unary_fn(&args[0], |x| x.cosh())
304}
305
306fn fn_tanh(args: &[Value]) -> Result<Value> {
307    apply_unary_fn(&args[0], |x| x.tanh())
308}
309
310fn fn_atan2(args: &[Value]) -> Result<Value> {
311    apply_binary_fn(&args[0], &args[1], |y, x| y.atan2(x))
312}
313
314fn fn_pow(args: &[Value]) -> Result<Value> {
315    apply_binary_fn(&args[0], &args[1], |base, exp| base.powf(exp))
316}
317
318fn fn_hypot(args: &[Value]) -> Result<Value> {
319    apply_binary_fn(&args[0], &args[1], |x, y| x.hypot(y))
320}
321
322fn fn_min(args: &[Value]) -> Result<Value> {
323    if args.is_empty() {
324        return Err(AlgorithmError::InvalidParameter {
325            parameter: "min",
326            message: "Expected at least 1 argument".to_string(),
327        });
328    }
329
330    // Use NaN-aware `f64::min`, which returns the non-NaN operand. Raw `<`
331    // comparisons make the result depend on argument order because every
332    // comparison against NaN is false (min(NaN, 5) would give NaN but
333    // min(5, NaN) would give 5). This mirrors the raster-calculator front-end
334    // (raster/calculator/evaluator.rs) which folds with f64::min.
335    let mut min_val = args[0].as_number()?;
336    for arg in &args[1..] {
337        min_val = min_val.min(arg.as_number()?);
338    }
339    Ok(Value::Number(min_val))
340}
341
342fn fn_max(args: &[Value]) -> Result<Value> {
343    if args.is_empty() {
344        return Err(AlgorithmError::InvalidParameter {
345            parameter: "max",
346            message: "Expected at least 1 argument".to_string(),
347        });
348    }
349
350    // Use NaN-aware `f64::max` for order-independent NoData semantics, matching
351    // the raster-calculator front-end (see fn_min above).
352    let mut max_val = args[0].as_number()?;
353    for arg in &args[1..] {
354        max_val = max_val.max(arg.as_number()?);
355    }
356    Ok(Value::Number(max_val))
357}
358
359// Statistical functions
360
361fn fn_mean(args: &[Value]) -> Result<Value> {
362    let raster = args[0].as_raster()?;
363    let mut sum = 0.0;
364    let mut count = 0u64;
365
366    for y in 0..raster.height() {
367        for x in 0..raster.width() {
368            if let Ok(val) = raster.get_pixel(x, y) {
369                if val.is_finite() {
370                    sum += val;
371                    count += 1;
372                }
373            }
374        }
375    }
376
377    if count == 0 {
378        return Err(AlgorithmError::EmptyInput { operation: "mean" });
379    }
380
381    Ok(Value::Number(sum / count as f64))
382}
383
384fn fn_median(args: &[Value]) -> Result<Value> {
385    let raster = args[0].as_raster()?;
386    let mut values: Vec<f64> = Vec::with_capacity((raster.width() * raster.height()) as usize);
387
388    for y in 0..raster.height() {
389        for x in 0..raster.width() {
390            if let Ok(val) = raster.get_pixel(x, y) {
391                if val.is_finite() {
392                    values.push(val);
393                }
394            }
395        }
396    }
397
398    if values.is_empty() {
399        return Err(AlgorithmError::EmptyInput {
400            operation: "median",
401        });
402    }
403
404    // Sort using total ordering on finite f64 values (all NaN/inf already excluded above)
405    values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
406
407    let mid = values.len() / 2;
408    let median = if values.len().is_multiple_of(2) {
409        (values[mid - 1] + values[mid]) / 2.0
410    } else {
411        values[mid]
412    };
413
414    Ok(Value::Number(median))
415}
416
417fn fn_mode(args: &[Value]) -> Result<Value> {
418    let raster = args[0].as_raster()?;
419
420    // Use a frequency map keyed by integer bit pattern for exact equality
421    // (suitable for raster data that is typically quantized)
422    use std::collections::HashMap;
423    let mut freq: HashMap<u64, (f64, u64)> = HashMap::new();
424
425    for y in 0..raster.height() {
426        for x in 0..raster.width() {
427            if let Ok(val) = raster.get_pixel(x, y) {
428                if val.is_finite() {
429                    let key = val.to_bits();
430                    let entry = freq.entry(key).or_insert((val, 0));
431                    entry.1 += 1;
432                }
433            }
434        }
435    }
436
437    if freq.is_empty() {
438        return Err(AlgorithmError::EmptyInput { operation: "mode" });
439    }
440
441    // Find the value with the highest frequency; break ties by smallest value
442    let mode = freq
443        .values()
444        .max_by(|a, b| {
445            a.1.cmp(&b.1)
446                .then_with(|| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal))
447        })
448        .map(|(val, _)| *val)
449        .ok_or(AlgorithmError::EmptyInput { operation: "mode" })?;
450
451    Ok(Value::Number(mode))
452}
453
454fn fn_stddev(args: &[Value]) -> Result<Value> {
455    let raster = args[0].as_raster()?;
456    let mut sum = 0.0;
457    let mut sum_sq = 0.0;
458    let mut count = 0u64;
459
460    for y in 0..raster.height() {
461        for x in 0..raster.width() {
462            if let Ok(val) = raster.get_pixel(x, y) {
463                if val.is_finite() {
464                    sum += val;
465                    sum_sq += val * val;
466                    count += 1;
467                }
468            }
469        }
470    }
471
472    if count == 0 {
473        return Err(AlgorithmError::EmptyInput {
474            operation: "stddev",
475        });
476    }
477
478    let mean = sum / count as f64;
479    let variance = (sum_sq / count as f64) - (mean * mean);
480    Ok(Value::Number(variance.sqrt()))
481}
482
483fn fn_variance(args: &[Value]) -> Result<Value> {
484    let raster = args[0].as_raster()?;
485    let mut sum = 0.0;
486    let mut sum_sq = 0.0;
487    let mut count = 0u64;
488
489    for y in 0..raster.height() {
490        for x in 0..raster.width() {
491            if let Ok(val) = raster.get_pixel(x, y) {
492                if val.is_finite() {
493                    sum += val;
494                    sum_sq += val * val;
495                    count += 1;
496                }
497            }
498        }
499    }
500
501    if count == 0 {
502        return Err(AlgorithmError::EmptyInput {
503            operation: "variance",
504        });
505    }
506
507    let mean = sum / count as f64;
508    let variance = (sum_sq / count as f64) - (mean * mean);
509    Ok(Value::Number(variance))
510}
511
512fn fn_sum(args: &[Value]) -> Result<Value> {
513    let raster = args[0].as_raster()?;
514    let mut sum = 0.0;
515
516    for y in 0..raster.height() {
517        for x in 0..raster.width() {
518            if let Ok(val) = raster.get_pixel(x, y) {
519                if val.is_finite() {
520                    sum += val;
521                }
522            }
523        }
524    }
525
526    Ok(Value::Number(sum))
527}
528
529fn fn_product(args: &[Value]) -> Result<Value> {
530    let raster = args[0].as_raster()?;
531    let mut product = 1.0;
532
533    for y in 0..raster.height() {
534        for x in 0..raster.width() {
535            if let Ok(val) = raster.get_pixel(x, y) {
536                if val.is_finite() {
537                    product *= val;
538                }
539            }
540        }
541    }
542
543    Ok(Value::Number(product))
544}
545
546fn fn_percentile(args: &[Value]) -> Result<Value> {
547    let raster = args[0].as_raster()?;
548    let p = args[1].as_number()?;
549
550    if !(0.0..=100.0).contains(&p) {
551        return Err(AlgorithmError::InvalidParameter {
552            parameter: "percentile",
553            message: format!("Percentile must be in [0, 100], got {p}"),
554        });
555    }
556
557    let mut values: Vec<f64> = Vec::with_capacity((raster.width() * raster.height()) as usize);
558
559    for y in 0..raster.height() {
560        for x in 0..raster.width() {
561            if let Ok(val) = raster.get_pixel(x, y) {
562                if val.is_finite() {
563                    values.push(val);
564                }
565            }
566        }
567    }
568
569    if values.is_empty() {
570        return Err(AlgorithmError::EmptyInput {
571            operation: "percentile",
572        });
573    }
574
575    values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
576
577    let n = values.len();
578    if n == 1 {
579        return Ok(Value::Number(values[0]));
580    }
581
582    // Linear interpolation method (same as numpy's default)
583    let rank = p / 100.0 * (n - 1) as f64;
584    let lower = rank.floor() as usize;
585    let upper = (lower + 1).min(n - 1);
586    let frac = rank - lower as f64;
587    let result = values[lower] + frac * (values[upper] - values[lower]);
588
589    Ok(Value::Number(result))
590}
591
592// Spatial filters
593
594fn fn_gaussian(args: &[Value]) -> Result<Value> {
595    let raster = args[0].as_raster()?;
596    let sigma = args[1].as_number()?;
597
598    let result = gaussian_blur(raster, sigma, None)?;
599    Ok(Value::Raster(Box::new(result)))
600}
601
602fn fn_median_filt(args: &[Value]) -> Result<Value> {
603    let raster = args[0].as_raster()?;
604    let radius = args[1].as_number()? as usize;
605
606    let result = median_filter(raster, radius)?;
607    Ok(Value::Raster(Box::new(result)))
608}
609
610// Logical functions
611
612fn fn_and(args: &[Value]) -> Result<Value> {
613    let a = args[0].as_bool()?;
614    let b = args[1].as_bool()?;
615    Ok(Value::Bool(a && b))
616}
617
618fn fn_or(args: &[Value]) -> Result<Value> {
619    let a = args[0].as_bool()?;
620    let b = args[1].as_bool()?;
621    Ok(Value::Bool(a || b))
622}
623
624fn fn_not(args: &[Value]) -> Result<Value> {
625    let a = args[0].as_bool()?;
626    Ok(Value::Bool(!a))
627}
628
629fn fn_xor(args: &[Value]) -> Result<Value> {
630    let a = args[0].as_bool()?;
631    let b = args[1].as_bool()?;
632    Ok(Value::Bool(a ^ b))
633}
634
635// Comparison functions
636
637fn fn_eq(args: &[Value]) -> Result<Value> {
638    let a = args[0].as_number()?;
639    let b = args[1].as_number()?;
640    Ok(Value::Bool((a - b).abs() < f64::EPSILON))
641}
642
643fn fn_ne(args: &[Value]) -> Result<Value> {
644    let a = args[0].as_number()?;
645    let b = args[1].as_number()?;
646    Ok(Value::Bool((a - b).abs() >= f64::EPSILON))
647}
648
649fn fn_lt(args: &[Value]) -> Result<Value> {
650    let a = args[0].as_number()?;
651    let b = args[1].as_number()?;
652    Ok(Value::Bool(a < b))
653}
654
655fn fn_le(args: &[Value]) -> Result<Value> {
656    let a = args[0].as_number()?;
657    let b = args[1].as_number()?;
658    Ok(Value::Bool(a <= b))
659}
660
661fn fn_gt(args: &[Value]) -> Result<Value> {
662    let a = args[0].as_number()?;
663    let b = args[1].as_number()?;
664    Ok(Value::Bool(a > b))
665}
666
667fn fn_ge(args: &[Value]) -> Result<Value> {
668    let a = args[0].as_number()?;
669    let b = args[1].as_number()?;
670    Ok(Value::Bool(a >= b))
671}
672
673// Type conversion
674
675fn fn_to_number(args: &[Value]) -> Result<Value> {
676    args[0].as_number().map(Value::Number)
677}
678
679fn fn_to_bool(args: &[Value]) -> Result<Value> {
680    args[0].as_bool().map(Value::Bool)
681}
682
683// Utility functions
684
685fn fn_clamp(args: &[Value]) -> Result<Value> {
686    let value = args[0].as_number()?;
687    let min = args[1].as_number()?;
688    let max = args[2].as_number()?;
689
690    let clamped = if value < min {
691        min
692    } else if value > max {
693        max
694    } else {
695        value
696    };
697
698    Ok(Value::Number(clamped))
699}
700
701fn fn_select(args: &[Value]) -> Result<Value> {
702    let cond = args[0].as_bool()?;
703    if cond {
704        Ok(args[1].clone())
705    } else {
706        Ok(args[2].clone())
707    }
708}
709
710#[cfg(test)]
711#[allow(clippy::panic)]
712mod tests {
713    use super::*;
714    use oxigeo_core::buffer::RasterBuffer;
715    use oxigeo_core::types::RasterDataType;
716
717    #[test]
718    fn test_function_registry() {
719        let registry = FunctionRegistry::new();
720        assert!(registry.exists("sqrt"));
721        assert!(registry.exists("sin"));
722        assert!(registry.exists("mean"));
723        assert!(!registry.exists("nonexistent"));
724    }
725
726    #[test]
727    fn test_math_functions() {
728        let args = vec![Value::Number(16.0)];
729        let result = fn_sqrt(&args).expect("Should work");
730        if let Value::Number(n) = result {
731            assert!((n - 4.0).abs() < 1e-10);
732        } else {
733            panic!("Expected number");
734        }
735    }
736
737    #[test]
738    fn test_min_max() {
739        let args = vec![
740            Value::Number(3.0),
741            Value::Number(1.0),
742            Value::Number(4.0),
743            Value::Number(1.0),
744            Value::Number(5.0),
745        ];
746
747        let min_result = fn_min(&args).expect("Should work");
748        if let Value::Number(n) = min_result {
749            assert!((n - 1.0).abs() < 1e-10);
750        }
751
752        let max_result = fn_max(&args).expect("Should work");
753        if let Value::Number(n) = max_result {
754            assert!((n - 5.0).abs() < 1e-10);
755        }
756    }
757
758    #[test]
759    fn test_min_max_nan_order_independent() {
760        // NaN (the DSL NoData sentinel) must be skipped regardless of position,
761        // so min/max return the same finite result whether NaN comes first or last.
762        let nan_first = vec![Value::Number(f64::NAN), Value::Number(5.0)];
763        let nan_last = vec![Value::Number(5.0), Value::Number(f64::NAN)];
764
765        let min_a = fn_min(&nan_first).expect("min should work");
766        let min_b = fn_min(&nan_last).expect("min should work");
767        if let (Value::Number(a), Value::Number(b)) = (min_a, min_b) {
768            assert!(a.is_finite() && b.is_finite(), "NaN must not poison min");
769            assert!((a - 5.0).abs() < 1e-10 && (b - 5.0).abs() < 1e-10);
770        } else {
771            panic!("expected numbers");
772        }
773
774        let max_a = fn_max(&nan_first).expect("max should work");
775        let max_b = fn_max(&nan_last).expect("max should work");
776        if let (Value::Number(a), Value::Number(b)) = (max_a, max_b) {
777            assert!(a.is_finite() && b.is_finite(), "NaN must not poison max");
778            assert!((a - 5.0).abs() < 1e-10 && (b - 5.0).abs() < 1e-10);
779        } else {
780            panic!("expected numbers");
781        }
782    }
783
784    #[test]
785    fn test_mean() {
786        let mut raster = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
787        for y in 0..10 {
788            for x in 0..10 {
789                let _ = raster.set_pixel(x, y, (x + y) as f64);
790            }
791        }
792
793        let args = vec![Value::Raster(Box::new(raster))];
794        let result = fn_mean(&args);
795        assert!(result.is_ok());
796    }
797
798    #[test]
799    fn test_logical_functions() {
800        let args_true = vec![Value::Bool(true), Value::Bool(true)];
801        let result = fn_and(&args_true).expect("Should work");
802        assert!(matches!(result, Value::Bool(true)));
803
804        let args_false = vec![Value::Bool(true), Value::Bool(false)];
805        let result = fn_and(&args_false).expect("Should work");
806        assert!(matches!(result, Value::Bool(false)));
807    }
808
809    #[test]
810    fn test_clamp() {
811        let args = vec![Value::Number(15.0), Value::Number(0.0), Value::Number(10.0)];
812        let result = fn_clamp(&args).expect("Should work");
813        if let Value::Number(n) = result {
814            assert!((n - 10.0).abs() < 1e-10);
815        }
816    }
817}