t27.aiРусский

GF20 and GF24

You will learn

The 20- and 24-bit GoldenFloats, 1 + 7 + 12 and 1 + 9 + 14, and the tolerance each spec allows.

gf20.t27 holds 1 sign, 7 exponent and 12 mantissa bits with bias 63; gf24.t27 holds 1 sign, 9 exponent and 14 mantissa bits with bias 255. The lesson opens gf24.t27, whose PHI_DISTANCE is 0.0248, and its test allows anything under 0.03; MEMORY_RATIO_VS_FP32 is 0.75. GF20 writes its distance as 0.03463, under a test bound of 0.04, while 7 / 12 sits 0.03470 from 1 / phi: the stored constant and the arithmetic differ in the fourth decimal. Between them, both widths keep the ratio within 0.035 of 1 / phi.

Try it

Find PHI_DISTANCE and EXP_BIAS in gf24.t27 and check the bias against 2^(9 - 1) - 1. Then compute 7 / 12 against 0.618034 and compare it with the constant gf20.t27 stores.

Open the interactive lesson →

GoldenFloat 15: GF20 and GF24
GoldenFloat 15: GF20 and GF24 ↗

GF24: 24 bits as the spec lays them out, beside GF20, read from gf24.t27. Lesson 15 of the GoldenFloat course.

specs/numeric/gf24.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/numeric/gf24.t27
// GoldenFloat24 — 24-bit φ-structured floating point
// NUMERIC-STANDARD-001 — Agent 7 (P1)

module GF24 {
    // Import base format family
    use numeric::goldenfloat_family;
    use numeric::phi_ratio;

    // ═════════════════════════════════════════════════════════════════
    // 1. Format Definition
    // ═════════════════════════════════════════════════════════════════════════

    // GF24 bit layout: [S|EEEE EEEE|MMM MMMM MMMM MMMM MM]
    //   S: 1 bit  (sign)
    //   E: 9 bits (exponent)
    //   M: 14 bits (mantissa)

    const BITS : u8 = 24;
    const SIGN_BITS : u8 = 1;
    const EXP_BITS : u8 = 9;
    const MANT_BITS : u8 = 14;

    // Bias for exponent (2^(9-1) - 1 = 255)
    const EXP_BIAS : u16 = 255;

    // φ-ratio: exp/mant = 9/14 ≈ 0.643 (phi_distance = 0.025)
    const PHI_DISTANCE : f64 = 0.02482317991669112;

    // ═════════════════════════════════════════════════════════════════
    // 2. GoldenFloat24 Type
    // ═════════════════════════════════════════════════════════════════════════

    struct GF24 {
        raw : u32,  // 24-bit value stored in u32
    }

    // ═════════════════════════════════════════════════════════════════
    // 3. Encoding/Decoding
    // ═════════════════════════════════════════════════════════════════════════

    // Encode f32 to GF24
    fn encode(value: f32) -> GF24 {
        if (value == 0.0) {
            return GF24{ raw = 0 };
        }

        const sign = if (value < 0.0) { 1 } else { 0 };
        const abs_val = if (value < 0.0) { -value } else { value };

        // Extract exponent (unbiased)
        const exp_unbiased = floor_log2(abs_val) as i16;
        const exp_biased = (exp_unbiased + EXP_BIAS as i16) as u16;

        // Clamp exponent
        const exp_clamped = clamp_u16(exp_biased, 0, (1u16 << EXP_BITS) - 1);

        // Extract mantissa (14 bits)
        const mant = extract_mantissa(abs_val, exp_unbiased, MANT_BITS);

        return GF24{
            raw = ((sign as u32) << 23) |
                  ((exp_clamped as u32) << MANT_BITS) |
                  (mant as u32)
        };
    }

    // Decode GF24 to f32
    fn decode(gf: GF24) -> f32 {
        const sign = (gf.raw >> 23) as u8;
        const exp_biased = ((gf.raw >> MANT_BITS) & 0x1FF) as u16;
        const mant = (gf.raw & 0x3FFF) as u16;

        // Zero
        if (exp_biased == 0 && mant == 0) {
            return 0.0;
        }

        // Exponent
        const exp_unbiased = if (exp_biased == 0) {
            -(EXP_BIAS as i16) + 1
        } else {
            (exp_biased as i16) - EXP_BIAS as i16
        };

        // Mantissa
        const mant_normalized = if (exp_biased == 0) {
            (mant as f32) / 16384.0
        } else {
            1.0 + (mant as f32) / 16384.0
        };

        const value = mant_normalized * pow(2.0, exp_unbiased as f32);

        if (sign != 0) {
            return -value;
        }
        return value;
    }

    // ═════════════════════════════════════════════════════════════════
    // 4. Format Properties
    // ═════════════════════════════════════════════════════════════════════════

    fn max_value() -> f32 {
        const mant_max = 1.0 + 16383.0 / 16384.0;
        const exp_max = (1i16 << EXP_BITS) - 1 - EXP_BIAS as i16;
        return mant_max * pow(2.0, exp_max as f32);
    }

    fn min_positive() -> f32 {
        const mant_min = 1.0 / 16384.0;
        const exp_min = -(EXP_BIAS as i16) + 1;
        return mant_min * pow(2.0, exp_min as f32);
    }

    fn epsilon() -> f32 {
        return 1.0 / 16384.0;  // 0.000061035
    }

    // ═════════════════════════════════════════════════════════════════
    // 5. Validation
    // ═════════════════════════════════════════════════════════════════════════

    fn validate_format() -> bool {
        const fmt = goldenfloat_family::get_format_by_name("GF24");
        return (fmt != null) &&
               (fmt.?.bits == BITS) &&
               (fmt.?.exp_bits == EXP_BITS) &&
               (fmt.?.mant_bits == MANT_BITS);
    }

    // ═════════════════════════════════════════════════════════════════
    // 6. Use Cases
    // ═════════════════════════════════════════════════════════════════════════

    // GF24 is optimal for:
    // - Very high precision quantization
    // - Critical numerical stability
    // - Financial calculations
    // - 25% memory savings vs FP32

    // Memory: 24 bits = 3 bytes (~1.33x FP32 in same space)
    const MEMORY_RATIO_VS_FP32 : f32 = 24.0 / 32.0;  // 0.75

    // ═════════════════════════════════════════════════════════════════
    // 7. Helper Functions
    // ═════════════════════════════════════════════════════════════════════════

    fn floor_log2(x: f32) -> i16 {
        if (x <= 0.0) { return -32768; }
        let mut exp : i16 = 0;
        while (x >= 2.0) {
            x = x / 2.0;
            exp = exp + 1;
        }
        while (x < 1.0) {
            x = x * 2.0;
            exp = exp - 1;
        }
        return exp;
    }

    fn extract_mantissa(value: f32, exp: i16, mant_bits: u8) -> u16 {
        const normalized = value / pow(2.0, exp as f32);
        const frac = normalized - 1.0;
        const max_mant = (1u16 << mant_bits) - 1;
        return (frac * (max_mant as f32 + 1.0)) as u16;
    }

    fn clamp_u16(x: u16, min: u16, max: u16) -> u16 {
        if (x < min) { return min; }
        if (x > max) { return max; }
        return x;
    }

    fn pow(base: f32, exp: f32) -> f32 {
        // Efficient power function for GF24
        // Integer exponent: binary exponentiation
        // Fractional exponent: use logarithm approximation

        if (base <= 0.0 || exp == 0.0) {
            if (exp == 0.0) {
                return 1.0;
            }
            if (base == 0.0 && exp > 0.0) {
                return 0.0;
            }
            return 0.0 / 0.0;  // NaN for negative base with non-integer exp
        }

        // Check if exponent is (approximately) integer
        const is_integer = exp == floor(exp);

        if (is_integer) {
            // Binary exponentiation for integer exponents
            let exp_int = exp as i32;
            let mut result = 1.0;
            let mut base_acc = base;
            let mut e = exp_int;

            if (e < 0) {
                e = -e;
                base_acc = 1.0 / base_acc;
            }

            while (e > 0) {
                if (e % 2 == 1) {
                    result = result * base_acc;
                }
                base_acc = base_acc * base_acc;
                e = e / 2;
            }

            return result;
        }

        // Fractional exponent: x^y = exp(y * ln(x))
        const ln_val = ln_approx(base);
        return exp_approx(exp * ln_val);
    }

    // Natural logarithm approximation
    fn ln_approx(x: f32) -> f32 {
        if (x <= 0.0) {
            return 0.0 / 0.0;  // NaN
        }
        if (x == 1.0) {
            return 0.0;
        }

        // Series: ln(x) = 2 * ((x-1)/(x+1) + 1/3*((x-1)/(x+1))^3 + ...)
        const t = (x - 1.0) / (x + 1.0);
        const t2 = t * t;
        const t3 = t2 * t;
        const t5 = t3 * t2;
        const t7 = t5 * t2;

        return 2.0 * (t + t3 / 3.0 + t5 / 5.0 + t7 / 7.0);
    }

    // Exponential approximation
    fn exp_approx(x: f32) -> f32 {
        if (x == 0.0) {
            return 1.0;
        }

        // Taylor series: e^x = 1 + x + x^2/2! + x^3/3! + ...
        let mut result = 1.0;
        let mut term = 1.0;
        let mut exp_x = x;

        // Scale down for large inputs
        if (exp_x > 5.0 || exp_x < -5.0) {
            const k = floor(exp_x / 5.0) as i32;
            exp_x = exp_x - (k as f32) * 5.0;
        }

        for (i in 1..=8) {
            term = term * exp_x / (i as f32);
            result = result + term;
        }

        // Scale back if needed
        if (x > 5.0 || x < -5.0) {
            const k = floor(x / 5.0) as i32;
            if (k > 0) {
                for (i in 0..k) {
                    result = result * exp_approx(5.0);
                }
            } else if (k < 0) {
                for (i in k..0) {
                    result = result / exp_approx(5.0);
                }
            }
        }

        return result;
    }

    // Floor function
    fn floor(x: f32) -> f32 {
        let xi = x as i32;
        if (x >= 0.0 || x == xi as f32) {
            return xi as f32;
        }
        return (xi - 1) as f32;
    }

    // ═══════════════════════════════════════════════════════════════════════════════════════════════════════
    // TDD-Inside-Spec: Tests and Invariants for GF24
    // ═══════════════════════════════════════════════════════════════════════════════════════════════════════

    test gf24_decode_zero
        given gf = GF24{ raw = 0 }
        when value = decode(gf)
        then value == 0.0

    test gf24_encode_zero_roundtrip
        given original = 0.0
        and   encoded = encode(original)
        and   decoded = decode(encoded)
        then decoded == original

    test gf24_bits_sum_correct
        given total = SIGN_BITS + EXP_BITS + MANT_BITS
        then total == BITS

    test gf24_max_value_positive
        given max_val = max_value()
        then max_val > 0.0

    test gf24_min_positive_greater_than_zero
        given min_pos = min_positive()
        then min_pos > 0.0

    test gf24_epsilon_positive
        given eps = epsilon()
        then eps > 0.0

    test gf24_phi_distance_within_tolerance
        given phi_dist = PHI_DISTANCE
        then phi_dist < 0.03

    test gf24_memory_ratio_vs_fp32
        given ratio = MEMORY_RATIO_VS_FP32
        then abs(ratio - 0.75) < 0.01

    test gf24_validate_format_success
        given valid = validate_format()
        then valid == true

    invariant gf24_bits_constant
        assert BITS == 24

    invariant gf24_sign_bits_is_one
        assert SIGN_BITS == 1

    invariant gf24_exp_bits_is_nine
        assert EXP_BITS == 9

    invariant gf24_mant_bits_is_fourteen
        assert MANT_BITS == 14

    invariant gf24_max_ge_min_positive
        assert max_value() >= min_positive()

    invariant gf24_phi_distance_below_threshold
        assert PHI_DISTANCE < 0.03

    invariant gf24_exp_bias_positive
        assert EXP_BIAS > 0

    test gf24_pow_zero_exponent_returns_one
        given result = pow(2.0, 0.0)
        then abs(result - 1.0) < 1e-6

    test gf24_pow_one_exponent_returns_base
        given result = pow(5.0, 1.0)
        then abs(result - 5.0) < 1e-6

    test gf24_pow_positive_integer_exponent
        given result = pow(2.0, 5.0)
        and expected = 32.0
        then abs(result - expected) < 1e-5

    test gf24_pow_negative_integer_exponent
        given result = pow(2.0, -3.0)
        and expected = 0.125
        then abs(result - expected) < 1e-5

    test gf24_pow_fractional_exponent
        given result = pow(4.0, 0.5)
        and expected = 2.0
        then abs(result - expected) < 1e-4

    test gf24_pow_zero_base_positive_exponent
        given result = pow(0.0, 5.0)
        then result == 0.0

    test gf24_pow_one_base_any_exponent
        given result1 = pow(1.0, 10.0)
        and result2 = pow(1.0, -5.0)
        then abs(result1 - 1.0) < 1e-6 and abs(result2 - 1.0) < 1e-6

    test gf24_ln_approx_of_one
        given result = ln_approx(1.0)
        then abs(result) < 1e-6

    test gf24_ln_approx_of_e
        given e = 2.718281828459045 as f32
        and result = ln_approx(e)
        then abs(result - 1.0) < 0.01

    test gf24_ln_approx_negative_returns_nan
        given result = ln_approx(-1.0)
        then result != result  // NaN check

    test gf24_exp_approx_zero
        given result = exp_approx(0.0)
        then abs(result - 1.0) < 1e-6

    test gf24_exp_approx_one
        given e = 2.718281828459045 as f32
        and result = exp_approx(1.0)
        then abs(result - e) < 0.01

    test gf24_exp_approx_negative
        given result = exp_approx(-1.0)
        and expected = 1.0 / 2.718281828459045 as f32
        then abs(result - expected) < 0.01

    test gf24_floor_positive
        given result = floor(3.7)
        then abs(result - 3.0) < 1e-6

    test gf24_floor_negative
        given result = floor(-3.2)
        then abs(result - (-4.0)) < 1e-6

    test gf24_floor_integer
        given result = floor(5.0)
        then abs(result - 5.0) < 1e-6

    invariant gf24_pow_zero_exponent_identity
        // For every positive x; checked at three points, one on each side of 1.
        given x1 = 0.5
        and x2 = 2.5
        and x3 = 7.0
        assert abs(pow(x1, 0.0) - 1.0) < 1e-6 and abs(pow(x2, 0.0) - 1.0) < 1e-6 and abs(pow(x3, 0.0) - 1.0) < 1e-6

    invariant gf24_pow_one_exponent_identity
        // For every valid x; checked at three points.
        given x1 = 0.5
        and x2 = 2.5
        and x3 = 7.0
        assert abs(pow(x1, 1.0) - x1) < 1e-5 and abs(pow(x2, 1.0) - x2) < 1e-5 and abs(pow(x3, 1.0) - x3) < 1e-5

    invariant gf24_ln_exp_inversion
        given x = 2.0
        and y = ln_approx(x)
        then abs(exp_approx(y) - x) < 0.01

    invariant gf24_floor_returns_integer
        // floor returns a whole number, and the floor of a whole number is itself.
        given r1 = floor(3.7)
        and r2 = floor(-3.2)
        assert abs(floor(r1) - r1) < 1e-6 and abs(floor(r2) - r2) < 1e-6

    invariant gf24_floor_monotonic
        given x1 = 2.5
        and x2 = 3.5
        assert floor(x1) <= floor(x2)

    bench gf24_pow_integer_exponent
        measure: nanoseconds to compute pow(2.0, 10.0)
        target: < 500ns

    bench gf24_ln_latency
        measure: nanoseconds to compute ln_approx(2.0)
        target: < 300ns

    bench gf24_exp_latency
        measure: nanoseconds to compute exp_approx(1.0)
        target: < 500ns

    bench gf24_floor_latency
        measure: nanoseconds to compute floor(3.7)
        target: < 50ns

    bench gf24_encode_latency
        measure: nanoseconds to encode(1.0)
        target: < 250ns

    bench gf24_decode_latency
        measure: nanoseconds to decode(GF24{raw = 8388608})
        target: < 200ns
}

Open the lesson's spec in the player ↗

All lessons