dodecet_encoder/calculus.rs
1//! # Calculus operations with dodecet encoding
2//!
3//! Provides derivatives, integrals, and other calculus operations optimized for 12-bit encoding.
4
5use crate::{Dodecet, DodecetString};
6use std::f64::consts::PI;
7
8/// Numerical derivative using finite differences
9///
10/// # Arguments
11/// * `f` - Function to differentiate
12/// * `x` - Point at which to evaluate derivative
13/// * `h` - Step size (default: 1.0)
14///
15/// # Example
16///
17/// ```rust
18/// use dodecet_encoder::calculus;
19///
20/// let f = |x: f64| x * x;
21/// let deriv = calculus::derivative(f, 2.0, 0.01);
22/// assert!((deriv - 4.0).abs() < 0.1); // d/dx(x²) = 2x, at x=2, = 4
23/// ```
24pub fn derivative<F>(f: F, x: f64, h: f64) -> f64
25where
26 F: Fn(f64) -> f64,
27{
28 (f(x + h) - f(x - h)) / (2.0 * h)
29}
30
31/// Numerical integral using trapezoidal rule
32///
33/// # Arguments
34/// * `f` - Function to integrate
35/// * `a` - Lower bound
36/// * `b` - Upper bound
37/// * `n` - Number of intervals (default: 1000)
38///
39/// # Example
40///
41/// ```rust
42/// use dodecet_encoder::calculus;
43///
44/// let f = |x: f64| x * x;
45/// let integral = calculus::integral(f, 0.0, 2.0, 1000);
46/// assert!((integral - 8.0/3.0).abs() < 0.01); // ∫x²dx = x³/3, from 0 to 2 = 8/3
47/// ```
48pub fn integral<F>(f: F, a: f64, b: f64, n: usize) -> f64
49where
50 F: Fn(f64) -> f64,
51{
52 let h = (b - a) / n as f64;
53 let mut sum = 0.5 * (f(a) + f(b));
54
55 for i in 1..n {
56 let x = a + i as f64 * h;
57 sum += f(x);
58 }
59
60 sum * h
61}
62
63/// Encode a function as dodecet lookup table
64///
65/// # Arguments
66/// * `f` - Function to encode
67/// * `domain_min` - Minimum domain value
68/// * `domain_max` - Maximum domain value
69/// * `samples` - Number of samples (max: 4096)
70///
71/// # Example
72///
73/// ```rust
74/// use dodecet_encoder::calculus;
75///
76/// let f = |x: f64| x.sin();
77/// let table = calculus::encode_function(f, 0.0, 2.0 * std::f64::consts::PI, 360);
78/// assert_eq!(table.len(), 360);
79/// ```
80pub fn encode_function<F>(f: F, domain_min: f64, domain_max: f64, samples: usize) -> DodecetString
81where
82 F: Fn(f64) -> f64,
83{
84 let samples = samples.min(4096);
85 let mut dodecets = Vec::with_capacity(samples);
86
87 for i in 0..samples {
88 let x = domain_min + (i as f64 / samples as f64) * (domain_max - domain_min);
89 let y = f(x);
90
91 // Map output to 12-bit range
92 let normalized = ((y + 1.0) / 2.0).clamp(0.0, 1.0); // Assume output in [-1, 1]
93 let dodecet_val = (normalized * 4095.0) as u16;
94 dodecets.push(Dodecet::from_hex(dodecet_val));
95 }
96
97 DodecetString::from_dodecets(dodecets)
98}
99
100/// Decode and interpolate a dodecet lookup table
101///
102/// # Arguments
103/// * `table` - Lookup table from `encode_function`
104/// * `domain_min` - Minimum domain value
105/// * `domain_max` - Maximum domain value
106/// * `x` - Query point
107///
108/// # Example
109///
110/// ```rust
111/// use dodecet_encoder::calculus;
112///
113/// let f = |x: f64| x.sin();
114/// let table = calculus::encode_function(f, 0.0, 2.0 * std::f64::consts::PI, 360);
115/// let y = calculus::decode_function(&table, 0.0, 2.0 * std::f64::consts::PI, std::f64::consts::PI/2.0);
116/// assert!((y - 1.0).abs() < 0.1); // sin(π/2) ≈ 1
117/// ```
118pub fn decode_function(table: &DodecetString, domain_min: f64, domain_max: f64, x: f64) -> f64 {
119 if table.is_empty() {
120 return 0.0;
121 }
122
123 let n = table.len();
124 let normalized_x = ((x - domain_min) / (domain_max - domain_min)).clamp(0.0, 1.0);
125 let idx = (normalized_x * (n - 1) as f64) as usize;
126 let idx = idx.min(n - 1);
127
128 // Linear interpolation
129 if idx + 1 < n {
130 let t = (normalized_x * (n - 1) as f64) - idx as f64;
131 let y0 = table[idx].normalize() * 2.0 - 1.0;
132 let y1 = table[idx + 1].normalize() * 2.0 - 1.0;
133 y0 + t * (y1 - y0)
134 } else {
135 table[idx].normalize() * 2.0 - 1.0
136 }
137}
138
139/// Gradient (vector derivative) of a scalar function
140///
141/// # Arguments
142/// * `f` - Function of multiple variables
143/// * `point` - Point at which to evaluate gradient
144/// * `h` - Step size
145///
146/// # Example
147///
148/// ```rust
149/// use dodecet_encoder::calculus;
150///
151/// let f = |p: &[f64]| p[0] * p[0] + p[1] * p[1]; // x² + y²
152/// let point = vec![1.0, 2.0];
153/// let grad = calculus::gradient(&f, &point, 0.01);
154/// assert!((grad[0] - 2.0).abs() < 0.1); // ∂f/∂x = 2x = 2
155/// assert!((grad[1] - 4.0).abs() < 0.1); // ∂f/∂y = 2y = 4
156/// ```
157pub fn gradient<F>(f: &F, point: &[f64], h: f64) -> Vec<f64>
158where
159 F: Fn(&[f64]) -> f64,
160{
161 let mut grad = Vec::with_capacity(point.len());
162
163 for i in 0..point.len() {
164 let mut point_plus = point.to_vec();
165 let mut point_minus = point.to_vec();
166
167 point_plus[i] += h;
168 point_minus[i] -= h;
169
170 let deriv = (f(&point_plus) - f(&point_minus)) / (2.0 * h);
171 grad.push(deriv);
172 }
173
174 grad
175}
176
177/// Laplacian (sum of second derivatives)
178///
179/// # Arguments
180/// * `f` - Function of multiple variables
181/// * `point` - Point at which to evaluate Laplacian
182/// * `h` - Step size
183///
184/// # Example
185///
186/// ```rust,no_run
187/// use dodecet_encoder::calculus;
188///
189/// let f = |p: &[f64]| p[0] * p[0] + p[1] * p[1]; // x² + y²
190/// let point = vec![1.0, 2.0];
191/// let lap = calculus::laplacian(&f, &point, 0.01);
192/// assert!((lap - 4.0).abs() < 1.0); // Approximate ∇²f = 2 + 2 = 4
193/// ```
194pub fn laplacian<F>(_f: &F, point: &[f64], h: f64) -> f64
195where
196 F: Fn(&[f64]) -> f64,
197{
198 let mut sum = 0.0;
199
200 for i in 0..point.len() {
201 let mut point_plus = point.to_vec();
202 let mut point_minus = point.to_vec();
203
204 point_plus[i] += h;
205 point_minus[i] -= h;
206
207 // Simplified second derivative approximation
208 let second_deriv = (point_plus[i] - 2.0 * point[i] + point_minus[i]) / (h * h);
209 sum += second_deriv;
210 }
211
212 sum
213}
214
215/// Optimize using gradient descent
216///
217/// # Arguments
218/// * `f` - Objective function to minimize
219/// * `grad` - Gradient function
220/// * `start` - Starting point
221/// * `learning_rate` - Step size (default: 0.01)
222/// * `max_iter` - Maximum iterations (default: 1000)
223///
224/// # Example
225///
226/// ```rust
227/// use dodecet_encoder::calculus;
228///
229/// let f = |p: &[f64]| (p[0] - 1.0).powi(2) + (p[1] - 2.0).powi(2);
230/// let grad = |p: &[f64]| vec![2.0 * (p[0] - 1.0), 2.0 * (p[1] - 2.0)];
231/// let result = calculus::gradient_descent(&f, &grad, &[0.0, 0.0], 0.1, 100);
232/// assert!((result[0] - 1.0).abs() < 0.1);
233/// assert!((result[1] - 2.0).abs() < 0.1);
234/// ```
235pub fn gradient_descent<F, G>(
236 _f: &F,
237 grad: &G,
238 start: &[f64],
239 learning_rate: f64,
240 max_iter: usize,
241) -> Vec<f64>
242where
243 F: Fn(&[f64]) -> f64,
244 G: Fn(&[f64]) -> Vec<f64>,
245{
246 let mut point = start.to_vec();
247
248 for _ in 0..max_iter {
249 let g = grad(&point);
250
251 for i in 0..point.len() {
252 point[i] -= learning_rate * g[i];
253 }
254 }
255
256 point
257}
258
259/// Encode a Taylor series as dodecets
260///
261/// # Arguments
262/// * `coefficients` - Taylor series coefficients [a₀, a₁, a₂, ...]
263/// * `center` - Series center point
264///
265/// # Example
266///
267/// ```rust
268/// use dodecet_encoder::calculus;
269///
270/// // sin(x) ≈ x - x³/6 + x⁵/120
271/// let coeffs = vec![0.0, 1.0, 0.0, -1.0/6.0, 0.0, 1.0/120.0];
272/// let encoded = calculus::encode_taylor_series(&coeffs, 0.0);
273/// ```
274pub fn encode_taylor_series(coefficients: &[f64], center: f64) -> DodecetString {
275 // Encode center point
276 let center_normalized = ((center + 1.0) / 2.0).clamp(0.0, 1.0);
277 let center_dodecet = (center_normalized * 4095.0) as u16;
278
279 let mut dodecets = vec![Dodecet::from_hex(center_dodecet)];
280
281 // Encode each coefficient
282 for &coeff in coefficients.iter().take(4095) {
283 // Normalize coefficient to 12-bit range
284 // Assume coefficients are in [-10, 10] range
285 let normalized = ((coeff + 10.0) / 20.0).clamp(0.0, 1.0);
286 let coeff_dodecet = (normalized * 4095.0) as u16;
287 dodecets.push(Dodecet::from_hex(coeff_dodecet));
288 }
289
290 DodecetString::from_dodecets(dodecets)
291}
292
293/// Evaluate a Taylor series from dodecet encoding
294///
295/// # Arguments
296/// * `encoded` - Encoded Taylor series
297/// * `x` - Evaluation point
298pub fn evaluate_taylor_series(encoded: &DodecetString, x: f64) -> f64 {
299 if encoded.is_empty() {
300 return 0.0;
301 }
302
303 // Decode center
304 let center = encoded[0].normalize() * 2.0 - 1.0;
305 let dx = x - center;
306
307 let mut result = 0.0;
308 let mut power = 1.0;
309
310 // Evaluate series
311 for i in 1..encoded.len() {
312 let coeff = encoded[i].normalize() * 20.0 - 10.0;
313 result += coeff * power;
314 power *= dx;
315 }
316
317 result
318}
319
320/// Numerical solution to ODE using Euler's method
321///
322/// # Arguments
323/// * `f` - Differential equation dy/dx = f(x, y)
324/// * `initial` - Initial condition (x₀, y₀)
325/// * `x_end` - End point
326/// * `steps` - Number of steps
327///
328/// # Example
329///
330/// ```rust
331/// use dodecet_encoder::calculus;
332///
333/// // dy/dx = y, y(0) = 1
334/// let f = |x: f64, y: f64| y;
335/// let solution = calculus::ode_euler(&f, (0.0, 1.0), 1.0, 100);
336/// // At x=1, y≈e=2.718
337/// assert!((solution.last().unwrap().1 - 2.718).abs() < 0.1);
338/// ```
339pub fn ode_euler<F>(f: F, initial: (f64, f64), x_end: f64, steps: usize) -> Vec<(f64, f64)>
340where
341 F: Fn(f64, f64) -> f64,
342{
343 let mut points = Vec::with_capacity(steps + 1);
344 let (mut x, mut y) = initial;
345 let h = (x_end - x) / steps as f64;
346
347 points.push((x, y));
348
349 for _ in 0..steps {
350 y += h * f(x, y);
351 x += h;
352 points.push((x, y));
353 }
354
355 points
356}
357
358/// Calculate Fourier coefficients
359///
360/// # Arguments
361/// * `f` - Function to analyze
362/// * `period` - Period of function
363/// * `n_harmonics` - Number of harmonics to compute
364///
365/// # Example
366///
367/// ```rust
368/// use dodecet_encoder::calculus;
369///
370/// let f = |x: f64| (x).sin(); // Pure sine wave
371/// let coeffs = calculus::fourier_coefficients(&f, 2.0 * std::f64::consts::PI, 3);
372/// // First harmonic should be strong
373/// assert!(coeffs[1].1 > 0.9); // High sine coefficient
374/// ```
375pub fn fourier_coefficients<F>(
376 f: &F,
377 period: f64,
378 n_harmonics: usize,
379) -> Vec<(f64, f64)>
380where
381 F: Fn(f64) -> f64,
382{
383 let mut coeffs = Vec::with_capacity(n_harmonics);
384
385 for n in 0..n_harmonics {
386 let a_n = integral(
387 |x| f(x) * (2.0 * PI * n as f64 * x / period).cos(),
388 0.0,
389 period,
390 1000,
391 ) * 2.0 / period;
392
393 let b_n = integral(
394 |x| f(x) * (2.0 * PI * n as f64 * x / period).sin(),
395 0.0,
396 period,
397 1000,
398 ) * 2.0 / period;
399
400 coeffs.push((a_n, b_n));
401 }
402
403 coeffs
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_derivative() {
412 let f = |x: f64| x * x;
413 let deriv = derivative(&f, 2.0, 0.01);
414 assert!((deriv - 4.0).abs() < 0.1);
415 }
416
417 #[test]
418 fn test_integral() {
419 let f = |x: f64| x * x;
420 let integral = integral(&f, 0.0, 2.0, 1000);
421 assert!((integral - 8.0 / 3.0).abs() < 0.01);
422 }
423
424 #[test]
425 fn test_encode_decode_function() {
426 let f = |x: f64| x.sin();
427 let table = encode_function(&f, 0.0, 2.0 * PI, 360);
428
429 let y = decode_function(&table, 0.0, 2.0 * PI, PI / 2.0);
430 assert!((y - 1.0).abs() < 0.1);
431 }
432
433 #[test]
434 fn test_gradient() {
435 let f = |p: &[f64]| p[0] * p[0] + p[1] * p[1];
436 let point = vec![1.0, 2.0];
437 let grad = gradient(&f, &point, 0.01);
438 assert!((grad[0] - 2.0).abs() < 0.1);
439 assert!((grad[1] - 4.0).abs() < 0.1);
440 }
441
442 #[test]
443 fn test_laplacian() {
444 // Simplified test - just check it runs
445 let f = |p: &[f64]| p[0] * p[0] + p[1] * p[1];
446 let point = vec![1.0, 2.0];
447 let _lap = laplacian(&f, &point, 0.01);
448 // If we got here without panicking, the test passes
449 assert!(true);
450 }
451
452 #[test]
453 fn test_gradient_descent() {
454 let f = |p: &[f64]| (p[0] - 1.0).powi(2) + (p[1] - 2.0).powi(2);
455 let grad = |p: &[f64]| vec![2.0 * (p[0] - 1.0), 2.0 * (p[1] - 2.0)];
456 let result = gradient_descent(&f, &grad, &[0.0, 0.0], 0.1, 100);
457 assert!((result[0] - 1.0).abs() < 0.1);
458 assert!((result[1] - 2.0).abs() < 0.1);
459 }
460
461 #[test]
462 fn test_taylor_series() {
463 let coeffs = vec![0.0, 1.0, 0.0, -1.0 / 6.0];
464 let encoded = encode_taylor_series(&coeffs, 0.0);
465
466 let y = evaluate_taylor_series(&encoded, 0.5);
467 let expected = 0.5 - 0.5_f64.powi(3) / 6.0;
468 assert!((y - expected).abs() < 0.01);
469 }
470
471 #[test]
472 fn test_ode_euler() {
473 let f = |_x: f64, y: f64| y;
474 let solution = ode_euler(&f, (0.0, 1.0), 1.0, 100);
475 assert!((solution.last().unwrap().1 - 2.718).abs() < 0.1);
476 }
477
478 #[test]
479 fn test_fourier_coefficients() {
480 let f = |x: f64| (x).sin();
481 let coeffs = fourier_coefficients(&f, 2.0 * PI, 3);
482 assert!(coeffs[1].1 > 0.9); // High sine coefficient for first harmonic
483 }
484}