#![allow(clippy::module_name_repetitions)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Trit {
MinusOne,
Zero,
One,
}
impl Trit {
#[must_use]
pub const fn to_weight(self, scale: i16) -> i16 {
match self {
Self::MinusOne => -scale,
Self::Zero => 0,
Self::One => scale,
}
}
#[must_use]
pub fn from_weight(weight: i16, scale: i16) -> Self {
if scale <= 0 {
return Self::Zero;
}
let threshold = (i32::from(scale) + 1) / 2;
let w = i32::from(weight);
if w >= threshold {
Self::One
} else if w <= -threshold {
Self::MinusOne
} else {
Self::Zero
}
}
}
#[must_use]
pub fn tensor_scale(weights: &[i16]) -> i16 {
if weights.is_empty() {
return 0;
}
let len = weights.len() as i64;
let sum_abs: i64 = weights.iter().map(|w| i64::from(*w).abs()).sum();
let floor = sum_abs / len;
let mean = if (sum_abs % len) * 2 >= len {
floor + 1
} else {
floor
};
mean.min(i64::from(i16::MAX)) as i16
}
#[must_use]
pub fn project_to_ternary(weight: i16, scale: i16) -> i16 {
Trit::from_weight(weight, scale).to_weight(scale)
}
#[must_use]
pub fn ternarize(weights: &mut [i16]) -> i16 {
let scale = tensor_scale(weights);
if scale == 0 {
return 0;
}
for w in weights.iter_mut() {
*w = project_to_ternary(*w, scale);
}
scale
}
pub const STOCHASTIC_FLIP_RATE: u32 = 3000;
#[must_use]
pub fn stochastic_ternary_flip(current_weight: i16, gamma: i16, residual: i16, draw: u16) -> i16 {
if gamma <= 0 || residual == 0 {
return project_to_ternary(current_weight, gamma);
}
let threshold = u32::from(residual.unsigned_abs())
.saturating_mul(STOCHASTIC_FLIP_RATE)
.min(0xFFFF) as u16;
if draw >= threshold {
return project_to_ternary(current_weight, gamma);
}
if residual > 0 {
current_weight.saturating_add(gamma).min(gamma)
} else {
current_weight.saturating_sub(gamma).max(-gamma)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::shadow_unrelated)]
use super::*;
use proptest::prelude::*;
#[test]
fn to_weight_round_trips_all_three() {
for scale in [1_i16, 50, 125, 1000] {
assert_eq!(Trit::MinusOne.to_weight(scale), -scale);
assert_eq!(Trit::Zero.to_weight(scale), 0);
assert_eq!(Trit::One.to_weight(scale), scale);
}
}
#[test]
fn from_weight_boundaries_at_half_gamma() {
let g = 125_i16; assert_eq!(Trit::from_weight(62, g), Trit::Zero);
assert_eq!(Trit::from_weight(63, g), Trit::One);
assert_eq!(Trit::from_weight(-62, g), Trit::Zero);
assert_eq!(Trit::from_weight(-63, g), Trit::MinusOne);
assert_eq!(Trit::from_weight(125, g), Trit::One);
assert_eq!(Trit::from_weight(-125, g), Trit::MinusOne);
assert_eq!(Trit::from_weight(0, g), Trit::Zero);
}
#[test]
fn from_weight_degenerate_scale_is_zero() {
assert_eq!(Trit::from_weight(100, 0), Trit::Zero);
assert_eq!(Trit::from_weight(100, -5), Trit::Zero);
}
#[test]
fn odd_gamma_boundary_rounds_up() {
assert_eq!(Trit::from_weight(2, 5), Trit::Zero);
assert_eq!(Trit::from_weight(3, 5), Trit::One);
}
#[test]
fn project_is_identity_on_grid() {
let g = 125_i16;
for w in [g, 0, -g] {
assert_eq!(project_to_ternary(w, g), w);
}
}
#[test]
fn project_snaps_off_grid_to_nearest() {
let g = 125_i16;
assert_eq!(project_to_ternary(130, g), 125);
assert_eq!(project_to_ternary(60, g), 0);
assert_eq!(project_to_ternary(-200, g), -125);
}
#[test]
fn ternarize_balanced_weights_to_three_levels() {
let mut w = [80_i16, 150, 200, 120, -200, -120];
let g = ternarize(&mut w);
assert!(g > 0);
for &x in &w {
assert!(
x == g || x == 0 || x == -g,
"ternarized weight {x} not in {{-{g}, 0, {g}}}"
);
}
assert!(w[0] > 0 && w[3] > 0);
assert!(w[4] < 0 && w[5] < 0);
}
#[test]
fn ternarize_then_project_at_same_gamma_is_stable() {
let mut w = [80_i16, 150, 200, -180, -120, 60];
let gamma = ternarize(&mut w);
for &x in &w {
assert_eq!(
project_to_ternary(x, gamma),
x,
"on-grid weight must be fixed by project at its own γ"
);
}
}
#[test]
fn ternarize_empty_returns_zero() {
let mut w: [i16; 0] = [];
assert_eq!(ternarize(&mut w), 0);
}
#[test]
fn tensor_scale_rounds_half_up() {
assert_eq!(tensor_scale(&[10, 15]), 13);
assert_eq!(tensor_scale(&[10, 14]), 12);
}
#[test]
fn stochastic_flip_zero_residual_is_no_op() {
let g = 125_i16;
for &w in &[g, 0, -g] {
assert_eq!(stochastic_ternary_flip(w, g, 0, 0), w);
assert_eq!(stochastic_ternary_flip(w, g, 0, 65535), w);
}
}
#[test]
fn stochastic_flip_zero_gamma_is_no_op() {
assert_eq!(stochastic_ternary_flip(100, 0, 5, 0), 0);
assert_eq!(stochastic_ternary_flip(100, -1, 5, 0), 0);
}
#[test]
fn stochastic_flip_draw_zero_always_flips() {
let g = 125_i16;
assert_eq!(stochastic_ternary_flip(-g, g, 1, 0), 0); assert_eq!(stochastic_ternary_flip(0, g, 1, 0), g); assert_eq!(stochastic_ternary_flip(g, g, 1, 0), g); assert_eq!(stochastic_ternary_flip(g, g, -1, 0), 0); assert_eq!(stochastic_ternary_flip(0, g, -1, 0), -g); assert_eq!(stochastic_ternary_flip(-g, g, -1, 0), -g); }
#[test]
fn stochastic_flip_draw_max_never_flips() {
let g = 125_i16;
for &w in &[g, 0, -g] {
assert_eq!(stochastic_ternary_flip(w, g, 5, 65535), w);
assert_eq!(stochastic_ternary_flip(w, g, -5, 65535), w);
}
}
#[test]
fn stochastic_flip_sign_correctness() {
let g = 125_i16;
assert!(stochastic_ternary_flip(0, g, 3, 0) >= 0);
assert_eq!(stochastic_ternary_flip(0, g, 3, 0), g);
assert!(stochastic_ternary_flip(0, g, -3, 0) <= 0);
assert_eq!(stochastic_ternary_flip(0, g, -3, 0), -g);
}
#[test]
fn stochastic_flip_saturates_at_extremes() {
let g = 125_i16;
assert_eq!(stochastic_ternary_flip(g, g, 5, 0), g);
assert_eq!(stochastic_ternary_flip(-g, g, -5, 0), -g);
}
#[test]
fn stochastic_flip_output_always_on_grid() {
let g = 125_i16;
for &w in &[g, 0, -g] {
for &res in &[0_i16, 1, 3, 5, -1, -3, -5] {
for &draw in &[0_u16, 1, 100, 1000, 10000, 50000, 65535] {
let result = stochastic_ternary_flip(w, g, res, draw);
assert!(
result == g || result == 0 || result == -g,
"off-grid result {result} for w={w}, res={res}, draw={draw}"
);
}
}
}
}
proptest! {
#[test]
fn prop_ternarize_output_on_grid(
n in 1_usize..=200,
seed_w in -500i16..=500,
) {
let mut w: Vec<i16> = (0..n).map(|i| seed_w.wrapping_add(i as i16)).collect();
let g = ternarize(&mut w);
prop_assert!(g >= 0);
for &x in &w {
prop_assert!(x == g || x == 0 || x == -g, "off-grid: {x}, γ={g}");
}
}
#[test]
fn prop_ternarize_preserves_sign(
weights in prop::collection::vec(-1000i16..=1000, 1..=50),
) {
let mut w = weights.clone();
let _g = ternarize(&mut w);
for (orig, &quantized) in weights.iter().zip(w.iter()) {
if *orig > 0 {
prop_assert!(quantized >= 0, "positive weight flipped negative");
} else if *orig < 0 {
prop_assert!(quantized <= 0, "negative weight flipped positive");
}
}
}
#[test]
fn prop_trit_round_trip(trit in prop_oneof![
Just(Trit::MinusOne), Just(Trit::Zero), Just(Trit::One),
], scale in 1_i16..=2000) {
let w = trit.to_weight(scale);
prop_assert_eq!(Trit::from_weight(w, scale), trit);
}
#[test]
fn prop_project_idempotent(w in -2000i16..=2000, scale in 1_i16..=2000) {
let once = project_to_ternary(w, scale);
let twice = project_to_ternary(once, scale);
prop_assert_eq!(once, twice);
}
#[test]
fn prop_scale_bounded(
weights in prop::collection::vec(-1000i16..=1000, 1..=50),
) {
let g = tensor_scale(&weights);
let max_abs = weights.iter().map(|w| w.abs()).max().unwrap_or(0);
prop_assert!(g >= 0);
prop_assert!(g <= max_abs, "γ {g} exceeds max|w| {max_abs}");
}
#[test]
fn prop_stochastic_flip_output_on_grid(
bucket in prop_oneof![Just(-1_i16), Just(0), Just(1)],
gamma in 1_i16..=2000,
residual in -5i16..=5,
draw in 0u16..=65535,
) {
let w = bucket * gamma;
let result = stochastic_ternary_flip(w, gamma, residual, draw);
prop_assert!(
result == gamma || result == 0 || result == -gamma,
"off-grid: {result}, γ={gamma}"
);
}
#[test]
fn prop_stochastic_flip_sign_correct(
bucket in prop_oneof![Just(-1_i16), Just(0), Just(1)],
gamma in 1_i16..=2000,
residual_abs in 1i16..=5,
draw in 0u16..=65535,
) {
let w = bucket * gamma;
let ltp = stochastic_ternary_flip(w, gamma, residual_abs, draw);
prop_assert!(ltp >= w, "LTP must not decrease weight: {ltp} < {w}");
let ltd = stochastic_ternary_flip(w, gamma, -residual_abs, draw);
prop_assert!(ltd <= w, "LTD must not increase weight: {ltd} > {w}");
}
#[test]
fn prop_stochastic_flip_zero_residual_noop(
bucket in prop_oneof![Just(-1_i16), Just(0), Just(1)],
gamma in 1_i16..=2000,
draw in 0u16..=65535,
) {
let w = bucket * gamma;
prop_assert_eq!(stochastic_ternary_flip(w, gamma, 0, draw), w);
}
#[test]
fn prop_stochastic_flip_p_range(
gamma in 1_i16..=2000,
residual_abs in 1i16..=5,
) {
for &w in &[gamma, 0, -gamma] {
prop_assert_eq!(
stochastic_ternary_flip(w, gamma, residual_abs, 65535),
w
);
}
}
}
}