1#![allow(dead_code)]
5
6use rayon::prelude::*;
7
8pub const LOGICLE_IMPL_VERSION: &str = "logicle_v1";
9
10const TAYLOR_LENGTH: usize = 16;
11const DEFAULT_FORWARD_LUT_BINS: usize = 16_384;
12const DEFAULT_INVERSE_LUT_BINS: usize = 8_192;
13const MAX_FORWARD_LUT_BINS: usize = 32_768;
14const LUT_ERROR_THRESHOLD: f64 = 1e-4;
15const MIN_SPAN: f64 = 1e-12;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct LogicleParams {
19 pub t: f64,
20 pub w: f64,
21 pub m: f64,
22 pub a: f64,
23}
24
25impl Default for LogicleParams {
26 fn default() -> Self {
27 Self {
28 t: 262_144.0,
29 w: 0.5,
30 m: 4.5,
31 a: 0.0,
32 }
33 }
34}
35
36impl LogicleParams {
37 pub fn validate(self) -> Result<Self, String> {
38 if !self.t.is_finite() || self.t <= 0.0 {
39 return Err("Logicle parameter T must be finite and > 0".to_string());
40 }
41 if !self.m.is_finite() || self.m <= 0.0 {
42 return Err("Logicle parameter M must be finite and > 0".to_string());
43 }
44 if !self.w.is_finite() || self.w < 0.0 {
45 return Err("Logicle parameter W must be finite and >= 0".to_string());
46 }
47 if !self.a.is_finite() {
48 return Err("Logicle parameter A must be finite".to_string());
49 }
50 if 2.0 * self.w > self.m {
51 return Err("Logicle parameter W is too large for M".to_string());
52 }
53 if -self.a > self.w || (self.a + self.w) > (self.m - self.w) {
54 return Err("Logicle parameters violate A/W/M constraints".to_string());
55 }
56 Ok(self)
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum AxisTickKind {
62 Major,
63 Minor,
64 Zero,
65}
66
67#[derive(Debug, Clone)]
68pub struct AxisTick {
69 pub signal_value: f64,
70 pub transformed_position: f64,
71 pub kind: AxisTickKind,
72}
73
74#[derive(Debug, Clone)]
75struct LogicleContext {
76 params: LogicleParams,
77 a_coef: f64,
80 b_coef: f64,
81 c_coef: f64,
82 d_coef: f64,
83 f_coef: f64,
84 x0: f64,
85 x1: f64,
86 x_taylor: f64,
87 taylor: [f64; TAYLOR_LENGTH],
88}
89
90impl LogicleContext {
91 fn new(params: LogicleParams) -> Result<Self, String> {
92 let params = params.validate()?;
93
94 let w = params.w / (params.m + params.a);
96 let x2 = params.a / (params.m + params.a);
97 let x1 = x2 + w;
98 let x0 = x2 + 2.0 * w;
99
100 let b_coef = (params.m + params.a) * std::f64::consts::LN_10;
101 let d_coef = solve_d(b_coef, w)?;
102 let c_over_a = (x0 * (b_coef + d_coef)).exp();
103 let m_over_a = (b_coef * x1).exp() - c_over_a / (d_coef * x1).exp();
104 let a_coef = params.t / (((b_coef).exp() - m_over_a) - c_over_a / (d_coef).exp());
105 let c_coef = c_over_a * a_coef;
106 let f_coef = -m_over_a * a_coef;
107
108 let x_taylor = x1 + w / 4.0;
109 let mut taylor = [0.0_f64; TAYLOR_LENGTH];
110 let mut pos_coef = a_coef * (b_coef * x1).exp();
111 let mut neg_coef = -c_coef / (d_coef * x1).exp();
112 for (i, slot) in taylor.iter_mut().enumerate() {
113 pos_coef *= b_coef / (i as f64 + 1.0);
114 neg_coef *= -d_coef / (i as f64 + 1.0);
115 *slot = pos_coef + neg_coef;
116 }
117 taylor[1] = 0.0;
119
120 Ok(Self {
121 params,
122 a_coef,
123 b_coef,
124 c_coef,
125 d_coef,
126 f_coef,
127 x0,
128 x1,
129 x_taylor,
130 taylor,
131 })
132 }
133
134 fn params(&self) -> LogicleParams {
135 self.params
136 }
137
138 fn series_biexponential(&self, scale: f64) -> f64 {
139 let x = scale - self.x1;
141 let mut sum = self.taylor[TAYLOR_LENGTH - 1] * x;
142 for i in (2..=(TAYLOR_LENGTH - 2)).rev() {
143 sum = (sum + self.taylor[i]) * x;
144 }
145 (sum * x + self.taylor[0]) * x
146 }
147
148 fn inverse_positive(&self, scale: f64) -> f64 {
149 if scale < self.x_taylor {
150 self.series_biexponential(scale)
151 } else {
152 (self.a_coef * (self.b_coef * scale).exp() + self.f_coef)
153 - self.c_coef / (self.d_coef * scale).exp()
154 }
155 }
156
157 fn eval_forward_function(&self, scale: f64, signal: f64) -> f64 {
158 self.inverse_positive(scale) - signal
159 }
160
161 fn forward_positive_halley(&self, signal: f64) -> Option<f64> {
162 let mut x = if signal < self.f_coef {
163 self.x1 + signal / self.taylor[0]
164 } else {
165 (signal / self.a_coef).ln() / self.b_coef
166 };
167
168 let mut tolerance = 3.0 * f64::EPSILON;
169 if x > 1.0 {
170 tolerance = 3.0 * x * f64::EPSILON;
171 }
172
173 for _ in 0..16 {
174 let ae2bx = self.a_coef * (self.b_coef * x).exp();
175 let ce2mdx = self.c_coef / (self.d_coef * x).exp();
176 let y = if x < self.x_taylor {
177 self.series_biexponential(x) - signal
178 } else {
179 (ae2bx + self.f_coef) - (ce2mdx + signal)
180 };
181 let dy = self.b_coef * ae2bx + self.d_coef * ce2mdx;
182 if !dy.is_finite() || dy.abs() < f64::EPSILON {
183 return None;
184 }
185 let ddy = self.b_coef * self.b_coef * ae2bx - self.d_coef * self.d_coef * ce2mdx;
186 let denom = dy * (1.0 - y * ddy / (2.0 * dy * dy));
187 if !denom.is_finite() || denom.abs() < f64::EPSILON {
188 return None;
189 }
190 let delta = y / denom;
191 if !delta.is_finite() {
192 return None;
193 }
194 x -= delta;
195 if delta.abs() < tolerance {
196 return Some(x);
197 }
198 }
199 None
200 }
201
202 fn forward_positive_bisection(&self, signal: f64) -> Option<f64> {
203 let mut lo = self.x1;
205 let mut hi = (self.x1 + 1.0).max(1.0);
206
207 let mut f_lo = self.eval_forward_function(lo, signal);
208 if !f_lo.is_finite() {
209 return None;
210 }
211 if f_lo >= 0.0 {
212 return Some(lo);
213 }
214
215 let mut f_hi = self.eval_forward_function(hi, signal);
216 let mut guard = 0;
217 while (!f_hi.is_finite() || f_hi < 0.0) && guard < 64 {
218 let span = (hi - lo).max(0.5);
219 hi += span * 1.8;
220 f_hi = self.eval_forward_function(hi, signal);
221 guard += 1;
222 }
223 if !f_hi.is_finite() || f_hi < 0.0 {
224 return None;
225 }
226
227 for _ in 0..80 {
228 let mid = 0.5 * (lo + hi);
229 let f_mid = self.eval_forward_function(mid, signal);
230 if !f_mid.is_finite() {
231 return None;
232 }
233 if f_mid.abs() < 1e-12 || (hi - lo).abs() < 1e-12 {
234 return Some(mid);
235 }
236 if f_mid < 0.0 {
237 lo = mid;
238 f_lo = f_mid;
239 } else {
240 hi = mid;
241 f_hi = f_mid;
242 }
243 if (f_hi - f_lo).abs() < 1e-20 {
244 break;
245 }
246 }
247 Some(0.5 * (lo + hi))
248 }
249
250 fn forward_positive(&self, signal: f64) -> f64 {
251 if signal == 0.0 {
252 return self.x1;
253 }
254 if let Some(value) = self.forward_positive_halley(signal) {
255 return value;
256 }
257 self.forward_positive_bisection(signal).unwrap_or(self.x1)
258 }
259
260 fn forward(&self, value: f64) -> f64 {
261 if !value.is_finite() {
262 return f64::NAN;
263 }
264 if value == 0.0 {
265 return self.x1;
266 }
267 if value < 0.0 {
268 let pos = self.forward_positive(-value);
269 return 2.0 * self.x1 - pos;
270 }
271 self.forward_positive(value)
272 }
273
274 fn inverse(&self, value: f64) -> f64 {
275 if !value.is_finite() {
276 return f64::NAN;
277 }
278 let negative = value < self.x1;
279 let reflected = if negative {
280 2.0 * self.x1 - value
281 } else {
282 value
283 };
284 let inverse = self.inverse_positive(reflected);
285 if negative {
286 -inverse
287 } else {
288 inverse
289 }
290 }
291
292 fn linear_half_signal(&self) -> f64 {
293 self.inverse(self.x0).abs()
294 }
295}
296
297fn solve_d(b: f64, w: f64) -> Result<f64, String> {
298 if w == 0.0 {
300 return Ok(b);
301 }
302 let tolerance = 2.0 * b * f64::EPSILON;
303 let mut d_lo = 0.0_f64;
304 let mut d_hi = b;
305 let mut d = 0.5 * (d_lo + d_hi);
306 let mut last_delta = d_hi - d_lo;
307 let f_b = -2.0 * b.ln() + w * b;
308 let mut f = 2.0 * d.ln() + w * d + f_b;
309 let mut last_f = f64::NAN;
310
311 for _ in 0..40 {
312 let df = 2.0 / d + w;
313 let newton_unsafe = ((d - d_hi) * df - f) * ((d - d_lo) * df - f) >= 0.0
314 || (1.9 * f).abs() > (last_delta * df).abs();
315
316 let delta = if newton_unsafe {
317 let delta = 0.5 * (d_hi - d_lo);
318 d = d_lo + delta;
319 delta
320 } else {
321 let delta = f / df;
322 d -= delta;
323 delta
324 };
325
326 if !delta.is_finite() {
327 return Err("Logicle d solve failed: non-finite update".to_string());
328 }
329 if delta.abs() < tolerance {
330 return Ok(d);
331 }
332 last_delta = delta;
333
334 f = 2.0 * d.ln() + w * d + f_b;
335 if f == 0.0 || f == last_f {
336 return Ok(d);
337 }
338 last_f = f;
339
340 if f < 0.0 {
341 d_lo = d;
342 } else {
343 d_hi = d;
344 }
345 }
346
347 Err("Logicle d solve failed to converge".to_string())
348}
349
350#[derive(Debug, Clone)]
351pub struct LogicleLut {
352 context: LogicleContext,
353 signal_min: f64,
354 signal_max: f64,
355 signal_step: f64,
356 forward_lut: Vec<f32>,
357 domain_min: f64,
358 domain_max: f64,
359 domain_step: f64,
360 inverse_lut: Vec<f32>,
361}
362
363impl LogicleLut {
364 pub fn build(
365 params: LogicleParams,
366 signal_min: f64,
367 signal_max: f64,
368 forward_bins: usize,
369 inverse_bins: usize,
370 ) -> Result<Self, String> {
371 let context = LogicleContext::new(params)?;
372 let lo = signal_min.min(signal_max);
373 let mut hi = signal_min.max(signal_max);
374 if (hi - lo).abs() < MIN_SPAN {
375 hi = lo + MIN_SPAN;
376 }
377
378 let forward_bins = forward_bins.max(2);
379 let signal_step = (hi - lo) / (forward_bins as f64 - 1.0);
380 let mut forward_lut = vec![0.0_f32; forward_bins];
381 for (i, slot) in forward_lut.iter_mut().enumerate() {
382 let signal = lo + signal_step * i as f64;
383 *slot = context.forward(signal) as f32;
384 }
385
386 let mut domain_min = forward_lut.first().copied().unwrap_or(0.0) as f64;
387 let mut domain_max = forward_lut.last().copied().unwrap_or(1.0) as f64;
388 if !domain_min.is_finite()
389 || !domain_max.is_finite()
390 || (domain_max - domain_min).abs() < MIN_SPAN
391 {
392 domain_min = context.forward(lo);
393 domain_max = context.forward(hi);
394 if (domain_max - domain_min).abs() < MIN_SPAN {
395 domain_max = domain_min + MIN_SPAN;
396 }
397 }
398
399 let inverse_bins = inverse_bins.max(2);
400 let domain_step = (domain_max - domain_min) / (inverse_bins as f64 - 1.0);
401 let mut inverse_lut = vec![0.0_f32; inverse_bins];
402 for (i, slot) in inverse_lut.iter_mut().enumerate() {
403 let domain = domain_min + domain_step * i as f64;
404 *slot = context.inverse(domain) as f32;
405 }
406
407 Ok(Self {
408 context,
409 signal_min: lo,
410 signal_max: hi,
411 signal_step,
412 forward_lut,
413 domain_min,
414 domain_max,
415 domain_step,
416 inverse_lut,
417 })
418 }
419
420 pub fn build_adaptive(
421 params: LogicleParams,
422 signal_min: f64,
423 signal_max: f64,
424 ) -> Result<Self, String> {
425 let mut lut = Self::build(
426 params,
427 signal_min,
428 signal_max,
429 DEFAULT_FORWARD_LUT_BINS,
430 DEFAULT_INVERSE_LUT_BINS,
431 )?;
432 if lut.max_forward_abs_error_sampled(256) > LUT_ERROR_THRESHOLD {
433 lut = Self::build(
434 params,
435 signal_min,
436 signal_max,
437 MAX_FORWARD_LUT_BINS,
438 DEFAULT_INVERSE_LUT_BINS,
439 )?;
440 }
441 Ok(lut)
442 }
443
444 pub fn params(&self) -> LogicleParams {
445 self.context.params()
446 }
447
448 pub fn forward(&self, signal: f64) -> f64 {
449 if !signal.is_finite() {
450 return f64::NAN;
451 }
452 if signal < self.signal_min || signal > self.signal_max {
453 return self.context.forward(signal);
454 }
455 let idx_f = (signal - self.signal_min) / self.signal_step;
456 interpolate_lut(idx_f, &self.forward_lut)
457 }
458
459 pub fn inverse(&self, domain: f64) -> f64 {
460 if !domain.is_finite() {
461 return f64::NAN;
462 }
463 if domain < self.domain_min || domain > self.domain_max {
464 return self.context.inverse(domain);
465 }
466 let idx_f = (domain - self.domain_min) / self.domain_step;
467 interpolate_lut(idx_f, &self.inverse_lut)
468 }
469
470 pub fn max_forward_abs_error_sampled(&self, samples: usize) -> f64 {
471 let sample_count = samples.max(16);
472 let span = (self.signal_max - self.signal_min).max(MIN_SPAN);
473 let mut max_err = 0.0_f64;
474 for i in 0..sample_count {
475 let signal = self.signal_min + span * (i as f64 / (sample_count - 1) as f64);
476 let direct = self.context.forward(signal);
477 let approx = self.forward(signal);
478 let err = (direct - approx).abs();
479 if err > max_err {
480 max_err = err;
481 }
482 }
483 max_err
484 }
485}
486
487fn interpolate_lut(index: f64, lut: &[f32]) -> f64 {
488 if lut.is_empty() || !index.is_finite() {
489 return f64::NAN;
490 }
491 if lut.len() == 1 {
492 return lut[0] as f64;
493 }
494 let low = index.floor().clamp(0.0, (lut.len() - 1) as f64) as usize;
495 let high = (low + 1).min(lut.len() - 1);
496 let frac = (index - low as f64).clamp(0.0, 1.0);
497 let a = lut[low] as f64;
498 let b = lut[high] as f64;
499 a + (b - a) * frac
500}
501
502pub fn logicle_forward(value: f64, params: LogicleParams) -> f64 {
503 match LogicleContext::new(params) {
504 Ok(ctx) => ctx.forward(value),
505 Err(_) => f64::NAN,
506 }
507}
508
509pub fn logicle_inverse(value: f64, params: LogicleParams) -> f64 {
510 match LogicleContext::new(params) {
511 Ok(ctx) => ctx.inverse(value),
512 Err(_) => f64::NAN,
513 }
514}
515
516pub fn apply_transform_logicle(values: &[f32], params: LogicleParams, out: &mut [f32]) {
517 if values.len() != out.len() {
518 return;
519 }
520 if values.is_empty() {
521 return;
522 }
523
524 let mut min_signal = f64::INFINITY;
525 let mut max_signal = f64::NEG_INFINITY;
526 for value in values.iter().copied() {
527 let v = value as f64;
528 if !v.is_finite() {
529 continue;
530 }
531 min_signal = min_signal.min(v);
532 max_signal = max_signal.max(v);
533 }
534 if !min_signal.is_finite() || !max_signal.is_finite() {
535 out.fill(f32::NAN);
536 return;
537 }
538
539 let lut = match LogicleLut::build_adaptive(params, min_signal, max_signal) {
540 Ok(lut) => lut,
541 Err(_) => {
542 out.fill(f32::NAN);
543 return;
544 }
545 };
546
547 out.par_iter_mut()
548 .zip(values.par_iter())
549 .for_each(|(dst, src)| {
550 let value = *src as f64;
551 *dst = if value.is_finite() {
552 lut.forward(value) as f32
553 } else {
554 f32::NAN
555 };
556 });
557}
558
559fn nice_linear_step(span: f64, target_ticks: usize) -> f64 {
560 let target = target_ticks.max(2) as f64;
561 let rough = (span / target).abs().max(MIN_SPAN);
562 let mag = 10.0_f64.powf(rough.log10().floor());
563 let residual = rough / mag;
564 let nice = if residual <= 1.0 {
565 1.0
566 } else if residual <= 2.0 {
567 2.0
568 } else if residual <= 5.0 {
569 5.0
570 } else {
571 10.0
572 };
573 nice * mag
574}
575
576fn add_log_decade_ticks(
577 min_signal: f64,
578 max_signal: f64,
579 sign: f64,
580 out: &mut Vec<(f64, AxisTickKind)>,
581) {
582 if min_signal <= 0.0 || max_signal <= 0.0 || max_signal < min_signal {
583 return;
584 }
585 let start_pow = min_signal.log10().floor() as i32;
586 let end_pow = max_signal.log10().ceil() as i32;
587 let range_lo = if sign >= 0.0 { min_signal } else { -max_signal };
588 let range_hi = if sign >= 0.0 { max_signal } else { -min_signal };
589 for p in start_pow..=end_pow {
590 let base = 10.0_f64.powi(p);
591 for (mult, kind) in [
592 (1.0_f64, AxisTickKind::Major),
593 (2.0_f64, AxisTickKind::Minor),
594 (5.0_f64, AxisTickKind::Minor),
595 ] {
596 let candidate = sign * base * mult;
597 if candidate >= range_lo && candidate <= range_hi {
598 out.push((candidate, kind));
599 }
600 }
601 }
602}
603
604pub fn logicle_axis_ticks(
605 params: LogicleParams,
606 domain_min: f64,
607 domain_max: f64,
608) -> Result<Vec<AxisTick>, String> {
609 let ctx = LogicleContext::new(params)?;
610 let d_lo = domain_min.min(domain_max);
611 let d_hi = domain_min.max(domain_max);
612 if !d_lo.is_finite() || !d_hi.is_finite() || (d_hi - d_lo).abs() < MIN_SPAN {
613 return Ok(Vec::new());
614 }
615
616 let signal_a = ctx.inverse(d_lo);
617 let signal_b = ctx.inverse(d_hi);
618 if !signal_a.is_finite() || !signal_b.is_finite() {
619 return Ok(Vec::new());
620 }
621 let s_lo = signal_a.min(signal_b);
622 let s_hi = signal_a.max(signal_b);
623
624 let mut candidates: Vec<(f64, AxisTickKind)> = Vec::new();
625
626 if s_lo <= 0.0 && s_hi >= 0.0 {
627 candidates.push((0.0, AxisTickKind::Zero));
628 }
629 if s_hi > 0.0 {
630 add_log_decade_ticks(s_lo.max(MIN_SPAN), s_hi, 1.0, &mut candidates);
631 }
632 if s_lo < 0.0 {
633 add_log_decade_ticks((-s_hi).max(MIN_SPAN), -s_lo, -1.0, &mut candidates);
634 }
635
636 let linear_half = ctx.linear_half_signal().max(1.0);
638 let linear_extent = linear_half.min(s_hi.abs().max(s_lo.abs()));
639 let linear_step = nice_linear_step(2.0 * linear_extent, 8);
640 for i in -4..=4 {
641 let signal = i as f64 * linear_step;
642 if signal >= s_lo && signal <= s_hi {
643 let kind = if signal == 0.0 {
644 AxisTickKind::Zero
645 } else {
646 AxisTickKind::Minor
647 };
648 candidates.push((signal, kind));
649 }
650 }
651
652 let mut ticks = Vec::new();
653 for (signal, kind) in candidates {
654 let transformed = ctx.forward(signal);
655 if !transformed.is_finite() {
656 continue;
657 }
658 if transformed < d_lo - 1e-9 || transformed > d_hi + 1e-9 {
659 continue;
660 }
661 ticks.push(AxisTick {
662 signal_value: signal,
663 transformed_position: transformed,
664 kind,
665 });
666 }
667
668 ticks.sort_by(|a, b| {
669 let by_pos = a
670 .transformed_position
671 .partial_cmp(&b.transformed_position)
672 .unwrap_or(std::cmp::Ordering::Equal);
673 if by_pos != std::cmp::Ordering::Equal {
674 return by_pos;
675 }
676 axis_kind_priority(&a.kind).cmp(&axis_kind_priority(&b.kind))
677 });
678 ticks.dedup_by(|a, b| {
679 (a.transformed_position - b.transformed_position).abs() < 1e-7
680 || (a.signal_value - b.signal_value).abs() < 1e-7
681 });
682
683 Ok(ticks)
684}
685
686fn axis_kind_priority(kind: &AxisTickKind) -> i32 {
687 match kind {
688 AxisTickKind::Zero => 0,
689 AxisTickKind::Major => 1,
690 AxisTickKind::Minor => 2,
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test]
699 fn round_trip_forward_inverse_is_stable() {
700 let p = LogicleParams::default();
701 let values = [
702 -10_000.0, -2_000.0, -250.0, -5.0, 0.0, 5.0, 250.0, 5_000.0, 50_000.0, 250_000.0,
703 ];
704 for x in values {
705 let y = logicle_forward(x, p);
706 let x2 = logicle_inverse(y, p);
707 let abs_err = (x - x2).abs();
708 let rel = abs_err / x.abs().max(1.0);
709 assert!(rel < 1e-6, "round-trip error too high at {x}: got {x2}");
710 }
711 }
712
713 #[test]
714 fn forward_is_monotonic() {
715 let ctx = LogicleContext::new(LogicleParams::default()).expect("context");
716 let mut last = f64::NEG_INFINITY;
717 for i in -10_000..=10_000 {
718 let signal = i as f64 * 30.0;
719 let y = ctx.forward(signal);
720 assert!(y >= last, "non-monotonic at {signal}: {y} < {last}");
721 last = y;
722 }
723 }
724
725 #[test]
726 fn lut_error_is_small() {
727 let params = LogicleParams::default();
728 let lut = LogicleLut::build_adaptive(params, -25_000.0, 262_144.0).expect("lut");
729 assert!(lut.max_forward_abs_error_sampled(512) < 1e-3);
730 }
731}