using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Tools.Mathematics;
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.Mathematics;
using AngouriMath.Core;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.TrimSmokeTest;
///
/// Lets the Mathematics tools compute what their tests compute, because AngouriMath is marked as trimmable.
///
///
/// The trimmer therefore removes every part of AngouriMath which nothing but reflection reaches, and
/// Trimming.props suppresses the warnings which would name such a part. Each check takes another part of it:
/// the parser, exact and precise arithmetic, LaTeX, Compile for the numerical integration, and the symbolic
/// operations together with their numerical check. The expected values are those of the tests in
/// app/Tests/Tools/Mathematics, which run untrimmed.
///
public static class MathematicsChecks
{
private static readonly MathComputationService SERVICE = new(NullLogger.Instance);
///
/// Returns a description of every computation which fails or gives another result than untrimmed.
///
/// One line per failure, naming the computation and what went wrong.
public static IReadOnlyList FindFailures()
{
var failures = new List();
//
// The parser, exact arithmetic, the decimal expansion, and LaTeX:
//
Expect(failures, "calculate 547 / 57576", Calculation("547 / 57576", 20), new()
{
["exact"] = "547/57576",
["exact_latex"] = @"\frac{547}{57576}",
["decimal"] = "0.0095004863137418368765",
});
//
// Arithmetic with many digits, which takes the guard digits:
//
Expect(failures, "calculate pi^4/15 to 60 digits", Calculation("pi^4/15", 60), new()
{
["decimal"] = "6.49393940226682914909602217924700741664850571151236144609786",
["digits"] = "60",
});
//
// Divisors, multiples, and a sum in closed form:
//
Expect(failures, "calculate gcd, lcm, and a sum", Calculation("gcd(12, 18) + lcm(4, 6) + sum(k^2, k, 1, 100)", 20), new()
{
["exact"] = "338368",
});
//
// A tiny value, which AngouriMath's downcasting would turn into 0, and one evaluated without it:
//
Expect(failures, "calculate the energy of a photon of 500 nm", Calculation("6.62607015e-34 * 299792458 / 500e-9", 20), new()
{
["decimal"] = "3.9728917142978574e-19",
["digits"] = "20",
});
Expect(failures, "calculate e^(-100) * pi", Calculation("e^(-100) * pi", 20), new()
{
["decimal"] = "1.1686963357062938093e-43",
});
//
// The numerical integration, whose integrand AngouriMath compiles:
//
Expect(failures, "check the integral of x^3/(e^x-1) from 0 to infinity", Calculation("integral(x^3/(e^x-1), x, 0, +oo) = pi^4/15", 20), new()
{
["numerically_equal"] = "true",
["left/approximate"] = "true",
["right/expression"] = "pi ^ 4 / 15",
});
//
// The symbolic operations, each with its numerical check:
//
Expect(failures, "simplify (x^2 - 1)/(x - 1)", Symbolic(SymbolicOperation.SIMPLIFY, "(x^2 - 1)/(x - 1)"), new()
{
["result"] = "x + 1",
["condition"] = "not x - 1 = 0",
["verification"] = "passed",
});
Expect(failures, "expand (x + 1)^3", Symbolic(SymbolicOperation.EXPAND, "(x + 1)^3"), new()
{
["result"] = "1 + 3 * x + 3 * x ^ 2 + x ^ 3",
["verification"] = "passed",
});
Expect(failures, "factor x^4 - 1", Symbolic(SymbolicOperation.FACTOR, "x^4 - 1"), new()
{
["result"] = "(x + 1) * (x - 1) * (x ^ 2 + 1)",
["verification"] = "passed",
});
Expect(failures, "differentiate x^2*sin(x)", Symbolic(SymbolicOperation.DIFFERENTIATE, "x^2*sin(x)"), new()
{
["result"] = "x ^ 2 * cos(x) + 2 * x * sin(x)",
["verification"] = "passed",
});
Expect(failures, "integrate x^2", Symbolic(SymbolicOperation.INTEGRATE, "x^2"), new()
{
["result"] = "x ^ 3 / 3 + C",
["verification"] = "passed",
});
Expect(failures, "integrate 1/(1 + x^2) from 0 to infinity", Symbolic(SymbolicOperation.INTEGRATE, "1/(1 + x^2)", lowerBound: "0", upperBound: "+oo"), new()
{
["result"] = "pi / 2",
["verification"] = "passed",
});
Expect(failures, "take the limit of sin(x)/x at 0", Symbolic(SymbolicOperation.LIMIT, "sin(x)/x", approach: "0"), new()
{
["result"] = "1",
});
Expect(failures, "solve x^2 - 5*x + 6 = 0", Symbolic(SymbolicOperation.SOLVE, "x^2 - 5*x + 6 = 0"), new()
{
["solutions/0/value"] = "2",
["solutions/1/value"] = "3",
["verification"] = "passed",
});
return failures;
}
private static Func Calculation(string expression, int digits) => () => SERVICE.ComputeAsync(token => CalculateTool.Calculate(expression, digits, token)).GetAwaiter().GetResult();
private static Func Symbolic(SymbolicOperation operation, string expression, string? lowerBound = null, string? upperBound = null, string? approach = null) => () => SERVICE.ComputeAsync(_ => ComputeSymbolicallyTool.Compute(new SymbolicRequest(operation, expression, null, 1, lowerBound, upperBound, approach, ApproachFrom.BothSides))).GetAwaiter().GetResult();
///
/// Runs a computation and compares the values of its result with the expected ones.
///
/// Where to add a failure.
/// What the computation does, completing "The Mathematics tools could not ...".
/// The computation.
/// The expected values, by their path in the result, e.g., solutions/0/value.
private static void Expect(List failures, string task, Func compute, Dictionary expected)
{
JsonObject result;
try
{
result = compute();
}
catch (Exception exception)
{
//
// A member the trimmer removed surfaces as the innermost exception, e.g., below a TypeInitializationException:
//
var cause = exception.GetBaseException();
failures.Add($"The Mathematics tools could not {task}: {cause.GetType().Name}: {cause.Message}");
return;
}
foreach (var (path, value) in expected)
{
var actual = TextAt(result, path);
if (actual != value)
failures.Add($"The Mathematics tools gave '{actual ?? "nothing"}' instead of '{value}' for {path} when asked to {task}.");
}
}
private static string? TextAt(JsonNode? node, string path)
{
foreach (var step in path.Split('/'))
{
if (int.TryParse(step, NumberStyles.None, CultureInfo.InvariantCulture, out var index))
node = node is JsonArray array && index < array.Count ? array[index] : null;
else
node = node is JsonObject json ? json[step] : null;
}
if (node is null)
return null;
return node.GetValueKind() switch
{
JsonValueKind.String => node.GetValue(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
_ => node.ToString(),
};
}
}