t27.aiРусский

GF32: a single

You will learn

The 32-bit GoldenFloat, 1 + 12 + 19, bias 2047, and the two invariants that compare it with IEEE single.

gf32.t27 holds 32 bits: 1 sign, 12 exponent, 19 mantissa bits, and bias 2047. IEEE single spends 8 bits on the exponent and 23 on the mantissa; the spec keeps two invariants that say so, EXP_BITS > 8 and MANT_BITS < 23, so GF32 trades precision for range at the same width. Its PHI_DISTANCE is 0.0135, under the test bound of 0.015, and MEMORY_RATIO_VS_FP32 is 1. A comment calls GF32 second after GF12; by the distances in this course it is closer than GF12, and GF14 is closer still.

Try it

Find the two invariants of gf32.t27 that compare it with IEEE single. Then check EXP_BIAS against 2^(12 - 1) - 1 and compute 12 / 19.

Open the interactive lesson →

GoldenFloat 16: GF32, a single
GoldenFloat 16: GF32, a single ↗

GF32: 32 bits as the spec lays them out, beside IEEE single, read from gf32.t27. Lesson 16 of the GoldenFloat course.

specs/numeric/gf32.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/numeric/gf32.t27
// GoldenFloat32 — 32-bit φ-structured floating point
// NUMERIC-STANDARD-001 — Agent 8 (P1)

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

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

    // GF32 bit layout: [S|EEEE EEEE EEEE|MMM MMMM MMMM MMMM MMMM MMM]
    //   S: 1 bit  (sign)
    //   E: 12 bits (exponent)
    //   M: 19 bits (mantissa)

    const BITS : u8 = 32;
    const SIGN_BITS : u8 = 1;
    const EXP_BITS : u8 = 12;
    const MANT_BITS : u8 = 19;

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

    // φ-ratio: exp/mant = 12/19 ≈ 0.632 (phi_distance = 0.014)
    // This is the second-best φ-approximation after GF12
    const PHI_DISTANCE : f64 = 0.01354495894042812;

    // ═════════════════════════════════════════════════════════════════
    // 2. GoldenFloat32 Type
    // ═════════════════════════════════════════════════════════════════════════

    struct GF32 {
        raw : u32,  // 32-bit raw value
    }

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

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

        const sign = if (value < 0.0) { 1u32 } else { 0u32 };
        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 (19 bits)
        const mant = extract_mantissa(abs_val, exp_unbiased, MANT_BITS);

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

    // Decode GF32 to f32
    fn decode(gf: GF32) -> f32 {
        const sign = (gf.raw >> 31) as u8;
        const exp_biased = ((gf.raw >> MANT_BITS) & 0xFFF) as u16;
        const mant = (gf.raw & 0x7FFFF) as u32;

        // 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) / 524288.0
        } else {
            1.0 + (mant as f32) / 524288.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 + 524287.0 / 524288.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 / 524288.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 / 524288.0;  // 0.000001907
    }

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

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

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

    // GF32 is optimal for:
    // - Near-IEEE 754 precision with φ-optimized layout
    // - 12-bit exponent (vs IEEE's 8-bit) for wider dynamic range
    // - 19-bit mantissa (vs IEEE's 23-bit) - still good precision
    // - Same memory footprint as FP32, better φ-ratio

    // Comparison with IEEE FP32:
    // - IEEE: 1 sign, 8 exp, 23 mant → exp/mant = 0.348 (phi_distance = 0.270)
    // - GF32: 1 sign, 12 exp, 19 mant → exp/mant = 0.632 (phi_distance = 0.014)

    // Memory: 32 bits = 4 bytes (same as FP32)
    const MEMORY_RATIO_VS_FP32 : f32 = 1.0;

    // ═════════════════════════════════════════════════════════════════
    // 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) -> u32 {
        const normalized = value / pow(2.0, exp as f32);
        const frac = normalized - 1.0;
        const max_mant = (1u32 << mant_bits) - 1;
        return (frac * (max_mant as f32 + 1.0)) as u32;
    }

    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 GF32
        // 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 GF32
    // ═══════════════════════════════════════════════════════════════════════════════════════════════════════

    test gf32_decode_zero
        given gf = GF32{ raw = 0 }
        when value = decode(gf)
        then value == 0.0

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

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

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

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

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

    test gf32_phi_distance_near_optimal
        given phi_dist = PHI_DISTANCE
        then phi_dist < 0.015

    test gf32_memory_ratio_equals_one
        given ratio = MEMORY_RATIO_VS_FP32
        then ratio == 1.0

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

    invariant gf32_bits_constant
        assert BITS == 32

    invariant gf32_sign_bits_is_one
        assert SIGN_BITS == 1

    invariant gf32_exp_bits_is_twelve
        assert EXP_BITS == 12

    invariant gf32_mant_bits_is_nineteen
        assert MANT_BITS == 19

    invariant gf32_max_ge_min_positive
        assert max_value() >= min_positive()

    invariant gf32_phi_distance_near_optimal
        assert PHI_DISTANCE < 0.015

    invariant gf32_exp_bias_positive
        assert EXP_BIAS > 0

    invariant gf32_exp_wider_than_ieee
        assert EXP_BITS > 8  // IEEE FP32 has 8-bit exponent

    invariant gf32_mant_narrower_than_ieee
        assert MANT_BITS < 23  // IEEE FP32 has 23-bit mantissa

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

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

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

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

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

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

    test gf32_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 gf32_ln_approx_of_one
        given result = ln_approx(1.0)
        then abs(result) < 1e-6

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

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

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

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

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

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

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

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

    invariant gf32_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 gf32_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 gf32_ln_exp_inversion
        given x = 2.0
        and y = ln_approx(x)
        then abs(exp_approx(y) - x) < 0.01

    invariant gf32_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 gf32_floor_monotonic
        given x1 = 2.5
        and x2 = 3.5
        assert floor(x1) <= floor(x2)

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

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

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

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

    bench gf32_encode_latency
        measure: nanoseconds to encode(1.0)
        target: < 300ns

    bench gf32_decode_latency
        measure: nanoseconds to decode(GF32{raw = 1065353216})
        target: < 250ns
}

Open the lesson's spec in the player ↗

All lessons