use crate::error::{HoronError, HoronResult};
use crate::format::{DIM_USER_DEFINED_START, FLAG_GACL, FLAG_QUANTIZED_SEMANTIC};
use crate::header::GeoHeader;
pub const TQ19_SCALE: i128 = 19_683;
pub const TQ19_MAX_RAW: i16 = 29_524;
pub fn quantize_raw(raw: i128) -> Option<i16> {
if raw.unsigned_abs() > 2u128 << 64 {
return None;
}
let scaled = raw * TQ19_SCALE;
let half = 1i128 << 63;
let q = if scaled >= 0 {
(scaled + half) >> 64
} else {
-((-scaled + half) >> 64)
};
if q.unsigned_abs() > TQ19_MAX_RAW as u128 {
None
} else {
Some(q as i16)
}
}
pub fn dequantize_raw(q: i16) -> i128 {
let x = (q as i128) * (1i128 << 64);
let neg = x < 0;
let mag = x.unsigned_abs();
let div = TQ19_SCALE as u128;
let (quot, rem) = (mag / div, mag % div);
let quot = if rem * 2 >= div { quot + 1 } else { quot };
if neg {
-(quot as i128)
} else {
quot as i128
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SemLayout {
pub dims: usize,
pub quantized: bool,
pub gacl: bool,
}
impl SemLayout {
pub fn from_header(h: &GeoHeader) -> Self {
Self {
dims: h.semantic_dims as usize,
quantized: h.flags & FLAG_QUANTIZED_SEMANTIC != 0,
gacl: h.flags & FLAG_GACL != 0,
}
}
pub fn plain(dims: usize) -> Self {
Self { dims, quantized: false, gacl: false }
}
pub fn mem_bytes(&self) -> usize {
self.dims * 16
}
pub fn reserved_dims(&self) -> usize {
self.dims.min(DIM_USER_DEFINED_START)
}
pub fn user_dims(&self) -> usize {
self.dims.saturating_sub(DIM_USER_DEFINED_START)
}
pub fn disk_bytes(&self) -> usize {
if !self.quantized {
self.mem_bytes()
} else {
let reserved = if self.gacl { self.reserved_dims() * 16 } else { 0 };
reserved + self.user_dims() * 2
}
}
fn dim_raw(full: &[u8], d: usize) -> i128 {
let start = d * 16;
let end = start + 16;
if full.len() >= end {
i128::from_le_bytes(full[start..end].try_into().unwrap())
} else {
0
}
}
pub fn encode_tail(&self, full: &[u8]) -> HoronResult<Vec<u8>> {
debug_assert!(self.quantized);
let mut out = Vec::with_capacity(self.disk_bytes());
for d in 0..self.reserved_dims() {
let raw = Self::dim_raw(full, d);
if self.gacl {
out.extend_from_slice(&raw.to_le_bytes());
} else if raw != 0 {
return Err(HoronError::InvalidOperation(format!(
"quantized file without GACL stores no reserved dims — dim {} must be zero",
d
)));
}
}
for u in 0..self.user_dims() {
let d = DIM_USER_DEFINED_START + u;
let raw = Self::dim_raw(full, d);
let q = quantize_raw(raw).ok_or_else(|| {
HoronError::InvalidOperation(format!(
"semantic dim {} is outside the TQ1.9 range ±29524/19683 (≈±1.49987)",
d
))
})?;
out.extend_from_slice(&q.to_le_bytes());
}
Ok(out)
}
pub fn decode_tail(&self, disk: &[u8]) -> HoronResult<Vec<u8>> {
debug_assert!(self.quantized);
if disk.len() != self.disk_bytes() {
return Err(HoronError::InvalidFormat(format!(
"quantized semantic tail is {} bytes, expected {}",
disk.len(),
self.disk_bytes()
)));
}
let mut out = vec![0u8; self.mem_bytes()];
let mut pos = 0usize;
if self.gacl {
for d in 0..self.reserved_dims() {
out[d * 16..d * 16 + 16].copy_from_slice(&disk[pos..pos + 16]);
pos += 16;
}
}
for u in 0..self.user_dims() {
let d = DIM_USER_DEFINED_START + u;
let q = i16::from_le_bytes(disk[pos..pos + 2].try_into().unwrap());
pos += 2;
out[d * 16..d * 16 + 16].copy_from_slice(&dequantize_raw(q).to_le_bytes());
}
Ok(out)
}
pub fn canonicalize(&self, coords: &mut Vec<u8>) -> HoronResult<()> {
debug_assert!(self.quantized);
if coords.len() > self.mem_bytes() {
return Err(HoronError::InvalidOperation(format!(
"coords cover {} bytes but the file has {} semantic dims ({} bytes)",
coords.len(),
self.dims,
self.mem_bytes()
)));
}
coords.resize(self.mem_bytes(), 0);
if !self.gacl {
for d in 0..self.reserved_dims() {
if Self::dim_raw(coords, d) != 0 {
return Err(HoronError::InvalidOperation(format!(
"quantized file without GACL stores no reserved dims — dim {} must be zero",
d
)));
}
}
}
for u in 0..self.user_dims() {
let d = DIM_USER_DEFINED_START + u;
let raw = Self::dim_raw(coords, d);
let q = quantize_raw(raw).ok_or_else(|| {
HoronError::InvalidOperation(format!(
"semantic dim {} is outside the TQ1.9 range ±29524/19683 (≈±1.49987)",
d
))
})?;
coords[d * 16..d * 16 + 16].copy_from_slice(&dequantize_raw(q).to_le_bytes());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scale_matches_gmath() {
use g_math::fixed_point::domains::balanced_ternary::trit_q1_9::SCALE_TQ1_9;
assert_eq!(TQ19_SCALE, SCALE_TQ1_9 as i128);
}
#[test]
fn roundtrip_exhaustive() {
for q in -TQ19_MAX_RAW..=TQ19_MAX_RAW {
let raw = dequantize_raw(q);
assert_eq!(quantize_raw(raw), Some(q), "round-trip failed at q={}", q);
}
}
#[test]
fn matches_gmath_from_rational() {
use g_math::fixed_point::domains::balanced_ternary::TritQ1_9;
for &(n, d) in &[
(1i64, 3u64),
(-1, 3),
(1, 7),
(-5, 9),
(1, 1),
(-1, 1),
(29524, 19683),
(-29524, 19683),
(0, 1),
(4217, 10000),
] {
let expected = TritQ1_9::from_rational(n, d).unwrap().raw();
let mag = (n.unsigned_abs() as u128) << 64;
let dv = d as u128;
let (quot, rem) = (mag / dv, mag % dv);
let quot = if rem * 2 >= dv { quot + 1 } else { quot };
let raw = if n < 0 { -(quot as i128) } else { quot as i128 };
assert_eq!(quantize_raw(raw), Some(expected), "mismatch at {}/{}", n, d);
}
}
#[test]
fn out_of_range_is_none() {
let boundary = dequantize_raw(TQ19_MAX_RAW);
let half_step = (1i128 << 64) / (2 * TQ19_SCALE);
assert_eq!(quantize_raw(boundary), Some(TQ19_MAX_RAW));
assert_eq!(quantize_raw(boundary + 2 * half_step), None);
assert_eq!(quantize_raw(-(boundary + 2 * half_step)), None);
assert_eq!(quantize_raw(i128::MAX), None);
assert_eq!(quantize_raw(i128::MIN), None);
assert_eq!(quantize_raw(2i128 << 64), None);
}
#[test]
fn layout_sizes() {
let l = SemLayout { dims: 40, quantized: true, gacl: false };
assert_eq!(l.mem_bytes(), 640);
assert_eq!(l.disk_bytes(), 48);
let g = SemLayout { dims: 40, quantized: true, gacl: true };
assert_eq!(g.disk_bytes(), 16 * 16 + 24 * 2);
assert_eq!(SemLayout::plain(40).disk_bytes(), 640);
}
#[test]
fn encode_decode_roundtrip() {
let l = SemLayout { dims: 20, quantized: true, gacl: false };
let mut full = vec![0u8; l.mem_bytes()];
for (i, q) in [(16usize, 1234i16), (17, -29524), (18, 0), (19, 29524)] {
full[i * 16..i * 16 + 16].copy_from_slice(&dequantize_raw(q).to_le_bytes());
}
let disk = l.encode_tail(&full).unwrap();
assert_eq!(disk.len(), l.disk_bytes());
assert_eq!(l.decode_tail(&disk).unwrap(), full);
}
#[test]
fn encode_rejects_reserved_and_range() {
let l = SemLayout { dims: 20, quantized: true, gacl: false };
let mut full = vec![0u8; l.mem_bytes()];
full[0..16].copy_from_slice(&1i128.to_le_bytes());
assert!(l.encode_tail(&full).is_err());
let mut full = vec![0u8; l.mem_bytes()];
full[16 * 16..17 * 16].copy_from_slice(&(2i128 << 64).to_le_bytes());
assert!(l.encode_tail(&full).is_err());
let g = SemLayout { dims: 20, quantized: true, gacl: true };
let mut full = vec![0u8; g.mem_bytes()];
full[0..16].copy_from_slice(&12345i128.to_le_bytes());
let disk = g.encode_tail(&full).unwrap();
assert_eq!(g.decode_tail(&disk).unwrap(), full);
}
#[test]
fn canonicalize_writes_through() {
let l = SemLayout { dims: 20, quantized: true, gacl: false };
let mut coords = vec![0u8; l.mem_bytes()];
let off_grid = (1i128 << 64) / 7;
coords[16 * 16..17 * 16].copy_from_slice(&off_grid.to_le_bytes());
l.canonicalize(&mut coords).unwrap();
let snapped =
i128::from_le_bytes(coords[16 * 16..17 * 16].try_into().unwrap());
assert_ne!(snapped, off_grid);
assert_eq!(snapped, dequantize_raw(quantize_raw(off_grid).unwrap()));
let again = coords.clone();
let mut coords2 = coords;
l.canonicalize(&mut coords2).unwrap();
assert_eq!(coords2, again);
let mut short = Vec::new();
l.canonicalize(&mut short).unwrap();
assert_eq!(short.len(), l.mem_bytes());
let mut long = vec![0u8; l.mem_bytes() + 16];
assert!(l.canonicalize(&mut long).is_err());
}
}