Skip to main content

oxihuman_core/
transform_pipe.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Data transform pipeline: chain of named f32 transforms.
6
7/// A named transform stage (maps f32 -> f32).
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct TransformStage {
11    pub name: String,
12    pub kind: TransformKind,
13}
14
15/// Built-in transform kinds.
16#[allow(dead_code)]
17#[derive(Debug, Clone, PartialEq)]
18pub enum TransformKind {
19    Scale(f32),
20    Offset(f32),
21    Clamp(f32, f32),
22    Abs,
23    Negate,
24    Reciprocal,
25    Sin,
26    Cos,
27}
28
29impl TransformKind {
30    #[allow(dead_code)]
31    fn apply(&self, x: f32) -> f32 {
32        match self {
33            TransformKind::Scale(s) => x * s,
34            TransformKind::Offset(o) => x + o,
35            TransformKind::Clamp(lo, hi) => x.clamp(*lo, *hi),
36            TransformKind::Abs => x.abs(),
37            TransformKind::Negate => -x,
38            TransformKind::Reciprocal => {
39                if x.abs() > 1e-9 {
40                    1.0 / x
41                } else {
42                    0.0
43                }
44            }
45            TransformKind::Sin => x.sin(),
46            TransformKind::Cos => x.cos(),
47        }
48    }
49}
50
51/// A pipeline of transform stages.
52#[allow(dead_code)]
53#[derive(Debug, Clone, Default)]
54pub struct TransformPipe {
55    stages: Vec<TransformStage>,
56}
57
58/// Create an empty `TransformPipe`.
59#[allow(dead_code)]
60pub fn new_transform_pipe() -> TransformPipe {
61    TransformPipe::default()
62}
63
64/// Add a stage to the pipe.
65#[allow(dead_code)]
66pub fn tp_add(pipe: &mut TransformPipe, name: &str, kind: TransformKind) {
67    pipe.stages.push(TransformStage {
68        name: name.to_string(),
69        kind,
70    });
71}
72
73/// Apply the full pipeline to a value.
74#[allow(dead_code)]
75pub fn tp_apply(pipe: &TransformPipe, mut val: f32) -> f32 {
76    for stage in &pipe.stages {
77        val = stage.kind.apply(val);
78    }
79    val
80}
81
82/// Number of stages.
83#[allow(dead_code)]
84pub fn tp_len(pipe: &TransformPipe) -> usize {
85    pipe.stages.len()
86}
87
88/// Whether the pipeline is empty.
89#[allow(dead_code)]
90pub fn tp_is_empty(pipe: &TransformPipe) -> bool {
91    pipe.stages.is_empty()
92}
93
94/// Remove the last stage.
95#[allow(dead_code)]
96pub fn tp_pop(pipe: &mut TransformPipe) -> Option<TransformStage> {
97    pipe.stages.pop()
98}
99
100/// Clear all stages.
101#[allow(dead_code)]
102pub fn tp_clear(pipe: &mut TransformPipe) {
103    pipe.stages.clear();
104}
105
106/// Get stage by index.
107#[allow(dead_code)]
108pub fn tp_get(pipe: &TransformPipe, idx: usize) -> Option<&TransformStage> {
109    pipe.stages.get(idx)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::f32::consts::PI;
116
117    #[test]
118    fn test_empty_pipe_passthrough() {
119        let pipe = new_transform_pipe();
120        assert!((tp_apply(&pipe, 3.5) - 3.5).abs() < 1e-6);
121    }
122
123    #[test]
124    fn test_scale() {
125        let mut pipe = new_transform_pipe();
126        tp_add(&mut pipe, "x2", TransformKind::Scale(2.0));
127        assert!((tp_apply(&pipe, 3.0) - 6.0).abs() < 1e-6);
128    }
129
130    #[test]
131    fn test_offset() {
132        let mut pipe = new_transform_pipe();
133        tp_add(&mut pipe, "shift", TransformKind::Offset(1.0));
134        assert!((tp_apply(&pipe, 4.0) - 5.0).abs() < 1e-6);
135    }
136
137    #[test]
138    fn test_clamp() {
139        let mut pipe = new_transform_pipe();
140        tp_add(&mut pipe, "clamp", TransformKind::Clamp(0.0, 1.0));
141        assert!((tp_apply(&pipe, 2.5) - 1.0).abs() < 1e-6);
142    }
143
144    #[test]
145    fn test_abs() {
146        let mut pipe = new_transform_pipe();
147        tp_add(&mut pipe, "abs", TransformKind::Abs);
148        assert!((tp_apply(&pipe, -7.0) - 7.0).abs() < 1e-6);
149    }
150
151    #[test]
152    fn test_negate() {
153        let mut pipe = new_transform_pipe();
154        tp_add(&mut pipe, "neg", TransformKind::Negate);
155        assert!((tp_apply(&pipe, 5.0) - (-5.0)).abs() < 1e-6);
156    }
157
158    #[test]
159    fn test_chain() {
160        let mut pipe = new_transform_pipe();
161        tp_add(&mut pipe, "scale", TransformKind::Scale(2.0));
162        tp_add(&mut pipe, "offset", TransformKind::Offset(-1.0));
163        assert!((tp_apply(&pipe, 3.0) - 5.0).abs() < 1e-6);
164    }
165
166    #[test]
167    fn test_sin_at_pi() {
168        let mut pipe = new_transform_pipe();
169        tp_add(&mut pipe, "sin", TransformKind::Sin);
170        assert!(tp_apply(&pipe, PI).abs() < 1e-5);
171    }
172
173    #[test]
174    fn test_pop_removes_stage() {
175        let mut pipe = new_transform_pipe();
176        tp_add(&mut pipe, "a", TransformKind::Abs);
177        tp_pop(&mut pipe);
178        assert!(tp_is_empty(&pipe));
179    }
180
181    #[test]
182    fn test_clear() {
183        let mut pipe = new_transform_pipe();
184        tp_add(&mut pipe, "a", TransformKind::Abs);
185        tp_clear(&mut pipe);
186        assert_eq!(tp_len(&pipe), 0);
187    }
188}