|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Golden Fraction oracle for gf{N} -> FP32 decode (independent of the RTL). |
| 3 | +
|
| 4 | +Emits iverilog test vectors: one line "GFHEX FP32HEX" per case. The RTL |
| 5 | +testbench (tb_gf_decode_param_pipe.v) reads these and compares bit-for-bit |
| 6 | +against gf_decode_param_pipe.v (latency=2). This oracle is a SECOND, |
| 7 | +structurally independent decode path (exact rational -> RNE to binary32), |
| 8 | +NOT a transcription of the Verilog -- satisfies the 2-witness honesty rule. |
| 9 | +
|
| 10 | +Trinity Catalog-100 horizon-B routing prep, 2026-07-24. |
| 11 | +Author: Vasilev (gHashTag), ORCID 0009-0008-4294-6159. |
| 12 | +Status: [verified SW на iverilog] once the tb passes. |
| 13 | +""" |
| 14 | +import sys, struct, argparse |
| 15 | +from fractions import Fraction |
| 16 | + |
| 17 | + |
| 18 | +def pow2(e): |
| 19 | + """Exact Fraction 2^e for any integer e.""" |
| 20 | + return Fraction(1 << e) if e >= 0 else Fraction(1, 1 << (-e)) |
| 21 | + |
| 22 | + |
| 23 | +def fp32_bits_from_fraction(sign, frac): |
| 24 | + """Round an exact non-negative Fraction to IEEE binary32, RNE. Return u32.""" |
| 25 | + if frac == 0: |
| 26 | + return (sign << 31) |
| 27 | + # Find integer e with 2^e <= frac < 2^(e+1) using exact rational compare. |
| 28 | + num, den = frac.numerator, frac.denominator |
| 29 | + e = num.bit_length() - den.bit_length() # within +/-1 of the true value |
| 30 | + while frac < pow2(e): |
| 31 | + e -= 1 |
| 32 | + while frac >= pow2(e + 1): |
| 33 | + e += 1 |
| 34 | + |
| 35 | + if e >= -126: |
| 36 | + # normal candidate: mantissa = frac/2^e in [1,2), scaled by 2^23 |
| 37 | + mant_scaled = (frac / pow2(e)) * (1 << 23) # in [2^23, 2^24) |
| 38 | + q = round_half_even(mant_scaled) |
| 39 | + if q == (1 << 24): # rounded up to 2.0 |
| 40 | + q = 1 << 23 |
| 41 | + e += 1 |
| 42 | + if e > 127: |
| 43 | + return (sign << 31) | 0x7F800000 # overflow -> inf |
| 44 | + mant = q - (1 << 23) |
| 45 | + biased = e + 127 |
| 46 | + return (sign << 31) | (biased << 23) | mant |
| 47 | + else: |
| 48 | + # subnormal (or underflow): value in units of 2^-149 |
| 49 | + units = frac / pow2(-149) |
| 50 | + q = round_half_even(units) |
| 51 | + if q >= (1 << 23): # rounded up to smallest normal |
| 52 | + return (sign << 31) | (1 << 23) |
| 53 | + return (sign << 31) | q |
| 54 | + |
| 55 | + |
| 56 | +def round_half_even(fr): |
| 57 | + """Round a Fraction to nearest integer, ties to even.""" |
| 58 | + fl = fr.numerator // fr.denominator |
| 59 | + rem = fr - fl |
| 60 | + if rem < Fraction(1, 2): |
| 61 | + return fl |
| 62 | + if rem > Fraction(1, 2): |
| 63 | + return fl + 1 |
| 64 | + return fl if (fl % 2 == 0) else fl + 1 |
| 65 | + |
| 66 | + |
| 67 | +def gf_decode_exact(word, N, E, M, BIAS): |
| 68 | + """Independent exact decode: return ('nan'|'inf'|('val',sign,Fraction)).""" |
| 69 | + sign = (word >> (N - 1)) & 1 |
| 70 | + exp = (word >> M) & ((1 << E) - 1) |
| 71 | + mant = word & ((1 << M) - 1) |
| 72 | + exp_max = (1 << E) - 1 |
| 73 | + if exp == exp_max and mant == 0: |
| 74 | + return ('inf', sign, None) |
| 75 | + if exp == exp_max and mant != 0: |
| 76 | + return ('nan', sign, None) |
| 77 | + if exp == 0 and mant == 0: |
| 78 | + return ('val', sign, Fraction(0)) |
| 79 | + if exp == 0: # subnormal |
| 80 | + val = Fraction(mant, 1 << M) * (Fraction(2) ** (1 - BIAS)) |
| 81 | + return ('val', sign, val) |
| 82 | + # normal |
| 83 | + val = (Fraction(1) + Fraction(mant, 1 << M)) * (Fraction(2) ** (exp - BIAS)) |
| 84 | + return ('val', sign, val) |
| 85 | + |
| 86 | + |
| 87 | +def expected_fp32(word, N, E, M, BIAS): |
| 88 | + kind, sign, val = gf_decode_exact(word, N, E, M, BIAS) |
| 89 | + if kind == 'nan': |
| 90 | + return 0x7FC00001 |
| 91 | + if kind == 'inf': |
| 92 | + return (sign << 31) | 0x7F800000 |
| 93 | + return fp32_bits_from_fraction(sign, val) |
| 94 | + |
| 95 | + |
| 96 | +def main(): |
| 97 | + ap = argparse.ArgumentParser() |
| 98 | + ap.add_argument("--N", type=int, required=True) |
| 99 | + ap.add_argument("--E", type=int, required=True) |
| 100 | + ap.add_argument("--M", type=int, required=True) |
| 101 | + ap.add_argument("--BIAS", type=int, required=True) |
| 102 | + ap.add_argument("--mode", choices=["exhaustive", "representative"], default="representative") |
| 103 | + ap.add_argument("--seed", type=int, default=20260724) |
| 104 | + ap.add_argument("--count", type=int, default=20000) |
| 105 | + ap.add_argument("--out", required=True) |
| 106 | + a = ap.parse_args() |
| 107 | + |
| 108 | + words = [] |
| 109 | + total = 1 << a.N |
| 110 | + if a.mode == "exhaustive" or total <= a.count: |
| 111 | + words = list(range(total)) |
| 112 | + else: |
| 113 | + import random |
| 114 | + r = random.Random(a.seed) |
| 115 | + s = set() |
| 116 | + # always include the 5-class corners + all-exp boundaries |
| 117 | + for exp in [0, 1, (1 << a.E) - 2, (1 << a.E) - 1]: |
| 118 | + for mant in [0, 1, (1 << a.M) - 1, (1 << a.M) >> 1]: |
| 119 | + for sgn in [0, 1]: |
| 120 | + s.add((sgn << (a.N - 1)) | (exp << a.M) | (mant & ((1 << a.M) - 1))) |
| 121 | + while len(s) < a.count: |
| 122 | + s.add(r.getrandbits(a.N)) |
| 123 | + words = sorted(s) |
| 124 | + |
| 125 | + with open(a.out, "w") as f: |
| 126 | + for w in words: |
| 127 | + fp = expected_fp32(w, a.N, a.E, a.M, a.BIAS) |
| 128 | + f.write(f"{w:0{(a.N + 3) // 4}x} {fp:08x}\n") |
| 129 | + print(f"wrote {len(words)} vectors to {a.out} (N={a.N} E={a.E} M={a.M} BIAS={a.BIAS})") |
| 130 | + |
| 131 | + |
| 132 | +if __name__ == "__main__": |
| 133 | + main() |
0 commit comments