"""Generate exact-input logarithmic-family references using only the standard library. Run this file with --check to verify the checked-in C# fixtures. Component sums and 1+x are formed exactly at 2200 digits and checked with Fraction. Decimal.ln and base conversion are evaluated independently at 450 and 650 digits, requiring identical 120-digit output and range flags. Tests do not require Python. """ from decimal import Decimal, localcontext from fractions import Fraction from pathlib import Path import argparse import math import random import sys from generate_log import exact_sum, inputs OUTPUT = Path(__file__).with_name('LogarithmicFunctionsReferenceData.cs') def plus_one_inputs(): epsilon = math.ulp(0.0) yield -1.0, epsilon yield -1.0, 2.0 ** -1000 yield -1.0, 2.0 ** -54 for high in [-0.999, -0.75, -0.5, -0.125, 0.125, 0.5, 1.0, 3.0, 9.0, sys.float_info.min, 1e100, sys.float_info.max]: for low in [0.0, math.ulp(high) / 4, -math.ulp(high) / 4]: yield high, low for exponent in [-1074, -1073, -1022, -1000, -500, -100, -55, -54, -53]: for sign in [-1, 1]: high = sign * 2.0 ** exponent for low in [0.0, math.ulp(high) / 4, -math.ulp(high) / 4]: yield high, low # Sparse lows and both sides of the actual high-component dispatch boundaries. for high in [-2.0 ** -100, 2.0 ** -100]: for low in [-2.0 ** -180, 2.0 ** -180]: yield high, low for high in [-0.5, 0.5, -2.0 ** -54, 2.0 ** -54]: for neighbor in [math.nextafter(high, -math.inf), high, math.nextafter(high, math.inf)]: for low in [-math.ulp(neighbor) / 4, 0.0, math.ulp(neighbor) / 4]: yield neighbor, low rng = random.Random(271828) for _ in range(96): high = rng.uniform(-0.99, 1.0) yield high, rng.choice([-1, 1]) * math.ulp(high) / 4 def base_inputs(): values = [(0.5, 0.0), (3.0, 2.0 ** -53), (10.0, -2.0 ** -51), (math.ulp(0.0), 0.0), (sys.float_info.max, 0.0), (1.0, 2.0 ** -54), (1.0, -2.0 ** -54), (1.0, math.ulp(0.0)), (1.0, -math.ulp(0.0)), (1.0, 2.0 ** -500), (1.0, -2.0 ** -500)] for high, low in values: for base_high, base_low in values: yield high, low, base_high, base_low def cases(): for _, high, low in inputs(): for operation in ['Log2', 'Log10']: yield operation, high, low, 0.0, 0.0 for high, low in plus_one_inputs(): for operation in ['LogP1', 'Log2P1', 'Log10P1']: yield operation, high, low, 0.0, 0.0 for high, low, base_high, base_low in base_inputs(): yield 'LogBase', high, low, base_high, base_low def reference(operation, high, low, base_high, base_low, precision): value = exact_sum(high, low) if operation.endswith('P1'): with localcontext() as context: context.prec = 2200 value += 1 assert Fraction(value) == Fraction(high) + Fraction(low) + 1 assert value > 0 base = exact_sum(base_high, base_low) if operation == 'LogBase' else None with localcontext() as context: context.prec = precision result = value.ln() if base is not None: result /= base.ln() elif operation.startswith('Log2'): result /= Decimal(2).ln() elif operation.startswith('Log10'): result /= Decimal(10).ln() with localcontext() as output_context: output_context.prec = 120 rounded = +result assert abs(result - rounded) <= abs(rounded) * Decimal(2) ** -350 # Independently classify against binary64's exact range midpoints. magnitude = abs(Fraction(result)) overflow = magnitude >= Fraction((2 ** 54 - 1) * 2 ** 970) underflow = magnitude != 0 and magnitude <= Fraction(1, 2 ** 1075) return format(rounded, 'e') if rounded else '0e+0', overflow, underflow def generate(): rows = [] for case in dict.fromkeys(cases()): result = reference(*case, 450) assert result == reference(*case, 650), case operation, high, low, base_high, base_low = case text, overflow, underflow = result rows.append(f' yield return new("{operation}", {high!r}, {low!r}, ' f'{base_high!r}, {base_low!r}, "{text}", ' f'{str(overflow).lower()}, {str(underflow).lower()});') content = '''// Generated by generate_logarithmic_functions.py; do not hand-edit reference literals. // Exact component sums and 1+x; Decimal.ln at 450/650 digits, rounded to 120 digits. namespace Just.PreciseMath.Tests.ReferenceData; internal static class LogarithmicFunctionsReferenceData { internal static IEnumerable> Cases() { ''' + '\n'.join(rows) + '\n }\n}\n' return content, len(rows) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--check', action='store_true') args = parser.parse_args() content, count = generate() if args.check: assert OUTPUT.read_text() == content, 'Fixture is stale; regenerate it' print(f'Verified {count} logarithmic-family references at 450/650 digits.') else: OUTPUT.write_text(content) print(f'Generated {count} references in {OUTPUT.name}.') if __name__ == '__main__': main()