mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-11 04:47:21 +00:00
Added the Mathematics tools as a prototype (#1162)
This commit is contained in:
1 parent
ce846e540e
commit
9c98bdae86
49 files changed
+4373
-11
No files matched your search
@@ -0,0 +1,426 @@
|
||||
using AngouriMath;
|
||||
using AngouriMath.Core;
|
||||
using AngouriMath.Core.Exceptions;
|
||||
|
||||
using PeterO.Numbers;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the behavior of AngouriMath which the Mathematics tools build on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AngouriMath puts correct answers ahead of backward compatibility, and says so: the release notes of
|
||||
/// 2.6.0 list 516 changed answers, fourteen of them silent, where the call still succeeds and returns
|
||||
/// something else. An update which changes anything below fails here first, not in a chat. Each test
|
||||
/// names what AngouriMath does; where the tools have to guard against it, the test says so.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class AngouriMathBehaviorTests
|
||||
{
|
||||
private static readonly Entity.Variable X = MathS.Var("x");
|
||||
|
||||
private static Entity Parse(string text)
|
||||
{
|
||||
using var _ = MathS.Settings.ExplicitParsingOnly.Set(true);
|
||||
return MathS.FromString(text);
|
||||
}
|
||||
|
||||
private static string DecimalOf(string text, int workingPrecision, int digits)
|
||||
{
|
||||
using var _ = MathS.Settings.DecimalPrecisionContext.Set(EContext.ForPrecision(workingPrecision));
|
||||
return Parse(text).EvalNumerical().RealPart.EDecimal.RoundToPrecision(EContext.ForPrecision(digits)).ToString();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RationalsStayExact()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("547 / 57576").Evaled, Is.EqualTo(Entity.Number.Rational.Create(547, 57576)));
|
||||
Assert.That(Parse("0.1 + 0.2").Evaled, Is.EqualTo(Entity.Number.Rational.Create(3, 10)), "A decimal literal is read as the rational it writes.");
|
||||
Assert.That(Parse("1.5e-3").Evaled, Is.EqualTo(Entity.Number.Rational.Create(3, 2000)));
|
||||
Assert.That(Parse("2^200 / 3^100").Evaled, Is.TypeOf<Entity.Number.Rational>());
|
||||
Assert.That(Parse("(-8)^(1/3)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(-2)), "The cube root of a negative integer is the real one.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IrrationalValuesAreDecimalsOfTheCurrentPrecision()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathS.Settings.DecimalPrecisionContext.Value.Precision.ToInt32Checked(), Is.EqualTo(100), "The default precision is 100 significant digits.");
|
||||
Assert.That(Parse("sqrt(2)").Evaled, Is.TypeOf<Entity.Number.Real>());
|
||||
Assert.That(Parse("sqrt(2)").InnerSimplified.ToString(), Is.EqualTo("sqrt(2)"), "Simplifying keeps an irrational value exact.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ElementaryFunctionsAreRightToTheLastDigit()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(DecimalOf("sin(1)", 60, 60), Is.EqualTo("0.841470984807896506652502321630298999622563060798371065672752"));
|
||||
Assert.That(DecimalOf("e", 60, 60), Is.EqualTo("2.71828182845904523536028747135266249775724709369995957496697"));
|
||||
Assert.That(DecimalOf("pi", 60, 60), Is.EqualTo("3.14159265358979323846264338327950288419716939937510582097494"));
|
||||
Assert.That(DecimalOf("ln(2)", 60, 60), Is.EqualTo("0.693147180559945309417232121458176568075500134360255254120680"));
|
||||
Assert.That(DecimalOf("sqrt(2)", 60, 60), Is.EqualTo("1.41421356237309504880168872420969807856967187537694807317668"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ACompoundExpressionNeedsGuardDigits()
|
||||
{
|
||||
// pi^4/15 is 6.4939394022668291490960221792470074166485057115123614460978572926..., see Python's decimal:
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(DecimalOf("pi^4/15", 60, 60), Is.Not.EqualTo("6.49393940226682914909602217924700741664850571151236144609786"), "At the precision asked for, the last digits are off: every step rounds.");
|
||||
Assert.That(DecimalOf("pi^4/15", 70, 60), Is.EqualTo("6.49393940226682914909602217924700741664850571151236144609786"), "Ten guard digits make up for it.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DowncastingTurnsTinyValuesIntoZero()
|
||||
{
|
||||
// The Boltzmann constant at 300 K, the energy of a photon of 500 nm, and e^-100:
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathS.Settings.DowncastingEnabled.Value, Is.True, "AngouriMath downcasts by default.");
|
||||
Assert.That(DecimalOf("1.380649e-23 * 300", 30, 20), Is.EqualTo("0"), "Downcasting takes a tiny value for the integer 0, so NumericEvaluator.EvaluateAt does without it.");
|
||||
Assert.That(DecimalOf("6.62607015e-34 * 299792458 / 500e-9", 40, 20), Is.EqualTo("0"));
|
||||
Assert.That(DecimalOf("e^(-100)", 60, 20), Is.EqualTo("0"), "More digits only lower the bound below which a value vanishes.");
|
||||
|
||||
using var withoutDowncasting = MathS.Settings.DowncastingEnabled.Set(false);
|
||||
Assert.That(DecimalOf("1.380649e-23 * 300", 30, 20), Is.EqualTo("4.14194700E-21"));
|
||||
Assert.That(DecimalOf("6.62607015e-34 * 299792458 / 500e-9", 30, 20), Is.EqualTo("3.97289171430E-19"), "500e-9 is read as the fraction 1/2000000, and dividing by it rounds to 30 decimal places, see below.");
|
||||
Assert.That(DecimalOf("6.62607015e-34 * 299792458 / 500e-9", 60, 20), Is.EqualTo("3.9728917142978574000E-19"));
|
||||
Assert.That(DecimalOf("e^(-100)", 30, 20), Is.EqualTo("3.7200759760208359630E-44"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DowncastingWhileParsingLosesTinyLiterals()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("1e-60"), Is.EqualTo(Entity.Number.Integer.Create(0)), "A literal below about 1e-50 is read as 0, so MathExpressionReader parses without downcasting.");
|
||||
Assert.That(Parse("6.62607015e-34"), Is.TypeOf<Entity.Number.Real>(), "Unlike 0.1, a literal with a long denominator stays an inexact decimal.");
|
||||
Assert.That(Parse("1 + 1e-40").InnerSimplified, Is.EqualTo(Entity.Number.Integer.Create(1)), "Simplifying drops an inexact decimal from a sum, so MathExpressionReader turns every literal into its fraction.");
|
||||
Assert.That(Entity.Number.Rational.Create(EInteger.One, EInteger.FromInt32(10).Pow(200)).EDecimal.IsZero, Is.True, "A fraction converts to a fixed number of decimal places, so NumericEvaluator converts an exact value itself.");
|
||||
Assert.That(Parse("pi/10^1500").InnerSimplified, Is.EqualTo(Entity.Number.Integer.Create(0)), "Simplifying takes a value below about 1e-1000 for 0, so NumericEvaluator checks an exact 0 numerically.");
|
||||
Assert.That(Parse("pi/10^1000").InnerSimplified, Is.TypeOf<Entity.Divf>());
|
||||
|
||||
// Where it evaluates a function numerically, the bound lies near the precision instead:
|
||||
using var lowerPrecision = MathS.Settings.DecimalPrecisionContext.Set(EContext.ForPrecision(50));
|
||||
Assert.That(MathS.FromString("132521403/200000000000000000000000000000000000000000 * sin(61/100)", useCache: false).InnerSimplified, Is.EqualTo(Entity.Number.Integer.Create(0)), "So NumericCheck evaluates an exact 0 numerically.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WithoutDowncastingAQuotientOfIntegersIsRoundedToDecimalPlaces()
|
||||
{
|
||||
using var withoutDowncasting = MathS.Settings.DowncastingEnabled.Set(false);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(DecimalOf("3/10^50 * 7", 30, 20), Is.EqualTo("0E-30"), "At 30 digits, the quotient keeps 30 decimal places, so NumericEvaluator.EvaluateAt adds the leading zeros of the smallest fraction to the precision.");
|
||||
Assert.That(DecimalOf("3/10^50 * 7", 80, 20), Is.EqualTo("2.1000000000000000000E-49"));
|
||||
Assert.That(DecimalOf("2^(-200)", 30, 20), Is.EqualTo("6.2230152778611417071E-61"), "A power keeps its significant digits.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SettingsBelongToTheFlowWhichSetsThem()
|
||||
{
|
||||
var inner = Task.Run(() =>
|
||||
{
|
||||
using var _ = MathS.Settings.DecimalPrecisionContext.Set(EContext.ForPrecision(30));
|
||||
return MathS.Settings.DecimalPrecisionContext.Value.Precision.ToInt32Checked();
|
||||
}).Result;
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(inner, Is.EqualTo(30));
|
||||
Assert.That(MathS.Settings.DecimalPrecisionContext.Value.Precision.ToInt32Checked(), Is.EqualTo(100), "A setting made in another flow does not leak into this one.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExplicitParsingRefusesImplicitMultiplication()
|
||||
{
|
||||
// Without explicit parsing, x2 is x^2, sin x is the variable sin times x, and foo(3) is foo times 3:
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathS.FromString("x2").ToString(), Is.EqualTo("x ^ 2"));
|
||||
Assert.That(MathS.FromString("sin x").ToString(), Is.EqualTo("sin * x"));
|
||||
Assert.That(MathS.FromString("foo(3)").ToString(), Is.EqualTo("foo * 3"));
|
||||
|
||||
foreach (var text in new[] { "2x", "x y", "x2", "sin x", "foo(3)", "2 pi", "x(x+1)", "(x+1)(x-1)" })
|
||||
Assert.Throws<MissingOperatorParseException>(() => Parse(text), text);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExplicitParsingReadsTheSyntaxTheToolsDocument()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("2*(x+1)").ToString(), Is.EqualTo("2 * (x + 1)"));
|
||||
Assert.That(Parse("-x^2").ToString(), Is.EqualTo("-x ^ 2"));
|
||||
Assert.That(Parse("3!").Evaled, Is.EqualTo(Entity.Number.Integer.Create(6)));
|
||||
Assert.That(Parse("log(100)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(2)), "log without a base is the decimal logarithm.");
|
||||
Assert.That(Parse("log(2, 8)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(3)), "The base comes first.");
|
||||
Assert.That(Parse("ln(e)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(1)));
|
||||
Assert.That(Parse("exp(x)").ToString(), Is.EqualTo("e ^ x"));
|
||||
Assert.That(Parse("e^(i*pi)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(-1)));
|
||||
Assert.That(Parse("π"), Is.TypeOf<Entity.Constant>());
|
||||
Assert.That(Parse("gamma(5)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(24)));
|
||||
Assert.That(Parse("binomial(5, 2)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(10)));
|
||||
Assert.That(Parse("abs(-2)").Evaled, Is.EqualTo(Entity.Number.Integer.Create(2)));
|
||||
Assert.That(Parse("a <> b").ToString(), Is.EqualTo("not a = b"));
|
||||
Assert.That(Parse("x_1 + x_2").ToString(), Is.EqualTo("x_1 + x_2"));
|
||||
Assert.That(Parse("integral(x^3/(e^x-1), x, 0, +oo)"), Is.TypeOf<Entity.Integralf>());
|
||||
Assert.That(Parse("limit(sin(x)/x, x, 0)"), Is.TypeOf<Entity.Limitf>());
|
||||
Assert.That(Parse("+oo"), Is.TypeOf<Entity.Number.Real>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SomeSpellingsReadAsSomethingElse()
|
||||
{
|
||||
// The tools refuse these before parsing:
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("a != b").ToString(), Is.EqualTo("a! = b"), "!= is the factorial of a, equated to b.");
|
||||
Assert.That(Parse("oo"), Is.TypeOf<Entity.Variable>(), "Only +oo and -oo are infinite; oo is a variable.");
|
||||
Assert.That(Parse("x | y"), Is.TypeOf<Entity.Dividesf>(), "| is divisibility, since 2.6.0.");
|
||||
Assert.That(Parse("2 = 2 = 2").ToString(), Is.EqualTo("2 = 2 and 2 = 2"), "A chain of equations is a conjunction.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnreadableInputThrowsAParseException()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
// Under explicit parsing, a character the lexer drops can leave two operands side by side, as in 2·3:
|
||||
foreach (var text in new[] { "2 + * 3", "(2 + 3", "", "√2", "x²", "2·3", "1,5", "2**3", "|x|", "\\frac{1}{2}" })
|
||||
Assert.That(() => Parse(text), Throws.InstanceOf<ParseException>(), text);
|
||||
|
||||
Assert.Throws<FunctionArgumentCountException>(() => Parse("sin(1, 2)"));
|
||||
Assert.Throws<FunctionArgumentCountException>(() => Parse("integral(x^2, x, 2)"), "An integral has two or four arguments.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AParseExceptionNamesLineAndColumn()
|
||||
{
|
||||
var exception = Assert.Throws<UnhandledParseException>(() => Parse("2 + * 3"))!;
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(exception.Message, Does.StartWith("line 1:4 extraneous input '*' expecting {"), "The message lists every token the parser expected, over a thousand characters.");
|
||||
Assert.That(exception.Message, Does.EndWith(", while parsing `2 + * 3`"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FreeVariablesLeaveOutBoundOnes()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("integral(x^2, x, 0, 1)").FreeVariables, Is.Empty);
|
||||
Assert.That(Parse("integral(x*a, x, 0, 1)").FreeVariables, Is.EquivalentTo(new[] { MathS.Var("a") }));
|
||||
Assert.That(Parse("pi + x").FreeVariables, Has.Some.InstanceOf<Entity.Constant>(), "The constants count among the free variables, see MathVariables.");
|
||||
Assert.That(Parse("ln(0)").FreeVariables.Select(variable => variable.Name), Is.EqualTo(new[] { "e" }), "ln is the logarithm to the base e.");
|
||||
Assert.That(Parse("x + 1").EvaluableNumerical, Is.False);
|
||||
Assert.Throws<CannotEvalException>(() => Parse("x + 1").EvalNumerical());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValuesWithoutAMeaningAreNaN()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("1/0").Evaled.ToString(), Is.EqualTo("NaN"));
|
||||
Assert.That(Parse("0/0").Evaled.ToString(), Is.EqualTo("NaN"));
|
||||
Assert.That(Parse("(-1)!").Evaled.ToString(), Is.EqualTo("NaN"));
|
||||
Assert.That(Parse("tan(pi/2)").Evaled.ToString(), Is.EqualTo("NaN"));
|
||||
Assert.That(Parse("ln(0)").Evaled.ToString(), Is.EqualTo("-oo"));
|
||||
Assert.That(Parse("sqrt(-1)").Evaled.ToString(), Is.EqualTo("i"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LatexComesFromLatexize()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("547 / 57576").Evaled.Latexize(), Is.EqualTo(@"\frac{547}{57576}"));
|
||||
Assert.That(Parse("x^3/(e^x-1)").Latexize(), Is.EqualTo(@"\frac{{x}^{3}}{{\mathrm{e}}^{x}-1}"));
|
||||
Assert.That(Parse("integral(x^3/(e^x-1), x, 0, +oo)").Latexize(), Is.EqualTo(@"\int_{0}^{\infty } \frac{{x}^{3}}{{\mathrm{e}}^{x}-1}\,\mathrm{d}x"));
|
||||
Assert.That(Parse("(x-1)/(x-1)").Simplify().Latexize(), Is.EqualTo(@"1 \quad \text{for} \quad x-1 \neq 0"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CompiledFunctionsReturnNaNWhereTheyHaveNoValue()
|
||||
{
|
||||
var bose = Parse("x^3/(e^x-1)").Compile<double, double>(X);
|
||||
var root = Parse("sqrt(x)").Compile<double, double>(X);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(bose(2.0), Is.EqualTo(8.0 / (Math.Exp(2.0) - 1.0)).Within(1e-15));
|
||||
Assert.That(bose(0.0), Is.NaN, "0/0 at the lower bound of the integral; a quadrature must not evaluate there.");
|
||||
Assert.That(root(-1.0), Is.NaN, "A compiled function stays real.");
|
||||
Assert.That(Parse("erf(x)").Compile<double, double>(X)(1.0), Is.EqualTo(0.8427007929497149).Within(1e-15));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimplifyStatesWhereItsAnswerHolds()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("sin(x)^2 + cos(x)^2").Simplify(), Is.EqualTo(Entity.Number.Integer.Create(1)));
|
||||
Assert.That(Parse("(x^2-1)/(x-1)").Simplify(), Is.TypeOf<Entity.Providedf>());
|
||||
Assert.That(Parse("(x^2-1)/(x-1)").Simplify().ToString(), Is.EqualTo("x + 1 provided not x - 1 = 0"));
|
||||
Assert.That(Parse("sqrt(x^2)").Simplify().ToString(), Is.EqualTo("sqrt(x ^ 2)"), "Over the complex numbers, sqrt(x^2) is not x.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExpandAndFactorizeAreInverse()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("(x+1)^3").Expand().ToString(), Is.EqualTo("1 + 3 * x + 3 * x ^ 2 + x ^ 3"));
|
||||
Assert.That(Parse("x^4 - 1").Factorize().ToString(), Is.EqualTo("(x + 1) * (x - 1) * (x ^ 2 + 1)"));
|
||||
Assert.That(Parse("x^2 - 5*x + 6").Factorize().ToString(), Is.EqualTo("(x - 3) * (x - 2)"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExpandLeavesAnExpressionWithTooManyTermsAsItIs()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathS.Settings.MaxExpansionTermCount.Value, Is.EqualTo(2000));
|
||||
Assert.That(Parse("(x+y+z+w)^60").Expand().ToString(), Is.EqualTo("(x + y + z + w) ^ 60"), "39,711 terms are above the limit.");
|
||||
Assert.That(Parse("(x+y)^100").Expand(), Is.TypeOf<Entity.Sumf>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DifferentiateNeedsASimplifyAfterwards()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("x^5").Differentiate(X, 3).ToString(), Is.EqualTo("3 * x ^ 2 * 4 * 5"));
|
||||
Assert.That(Parse("x^5").Differentiate(X, 3).Simplify().ToString(), Is.EqualTo("60 * x ^ 2"));
|
||||
Assert.That(Parse("x^2*sin(x)").Differentiate(X).Simplify().ToString(), Is.EqualTo("x ^ 2 * cos(x) + 2 * x * sin(x)"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnIndefiniteIntegralEndsInAConstantOrStaysAnIntegral()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("x^2").Integrate(X).ToString(), Is.EqualTo("x ^ 3 / 3 + C"));
|
||||
Assert.That(Parse("C*x").Integrate(X).ToString(), Is.EqualTo("C * x ^ 2 / 2 + C_1"), "The constant takes a name the expression does not use.");
|
||||
Assert.That(Parse("e^(-x^2)").Integrate(X).ToString(), Is.EqualTo("sqrt(pi) / 2 * erf(x) + C"));
|
||||
Assert.That(Parse("sin(sin(x))").Integrate(X), Is.TypeOf<Entity.Integralf>(), "Without a closed form, the integral comes back.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ADefiniteIntegralIsExactThroughItsNode()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("integral(x^2, x, 0, 1)").InnerSimplified, Is.EqualTo(Entity.Number.Rational.Create(1, 3)));
|
||||
Assert.That(Parse("integral(sin(x), x, 0, pi)").InnerSimplified, Is.EqualTo(Entity.Number.Integer.Create(2)));
|
||||
Assert.That(Parse("integral(ln(x), x, 0, 1)").InnerSimplified, Is.EqualTo(Entity.Number.Integer.Create(-1)), "An integrable singularity at a bound.");
|
||||
Assert.That(Parse("integral(1/(1+x^2), x, 0, +oo)").InnerSimplified.Simplify().ToString(), Is.EqualTo("pi / 2"));
|
||||
Assert.That(Parse("integral(1/x, x, -1, 1)").InnerSimplified.ToString(), Is.EqualTo("NaN"), "It diverges across the pole.");
|
||||
Assert.That(Parse("integral(x^3/(e^x-1), x, 0, +oo)").InnerSimplified, Is.TypeOf<Entity.Integralf>(), "No closed form is found for pi^4/15.");
|
||||
Assert.That(Parse("integral(x^3/(e^x-1), x, 0, +oo)").Evaled.ToString(), Does.Contain("2.718281828459045"), "Evaled replaces the constants with decimals, even inside an integral it cannot take.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ALimitIsNaNWhereItDoesNotExist()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("sin(x)/x").Limit(X, 0), Is.EqualTo(Entity.Number.Integer.Create(1)));
|
||||
Assert.That(Parse("1/x").Limit(X, 0, ApproachFrom.Right).ToString(), Is.EqualTo("+oo"));
|
||||
Assert.That(Parse("1/x").Limit(X, 0, ApproachFrom.Left).ToString(), Is.EqualTo("-oo"));
|
||||
Assert.That(Parse("1/x").Limit(X, 0, ApproachFrom.BothSides).ToString(), Is.EqualTo("NaN"));
|
||||
Assert.That(Parse("(1 + 1/x)^x").Limit(X, Parse("+oo")), Is.TypeOf<Entity.Constant>());
|
||||
Assert.That(Parse("gamma(x)*sin(x)^x").Limit(X, 0), Is.TypeOf<Entity.Limitf>(), "A limit it cannot take comes back as a limit.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SolveWorksOverTheComplexNumbers()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Parse("x^2 - 5*x + 6 = 0").Solve(X).ToString(), Is.EqualTo("{ 2, 3 }"));
|
||||
Assert.That(Parse("x^2 + 1 = 0").Solve(X).ToString(), Is.EqualTo("{ i, -i }"));
|
||||
using (MathS.Settings.Codomain.Set(Domain.Real))
|
||||
Assert.That(Parse("x^2 + 1 = 0").Solve(X).ToString(), Is.EqualTo("{ i, -i }"), "The codomain does not restrict the solutions.");
|
||||
|
||||
Assert.That(Parse("x^2 - 2 = 0").Solve(X).ToString(), Is.EqualTo("{ sqrt(2), -sqrt(2) }"), "The solutions stay exact.");
|
||||
Assert.That(Parse("sin(x) = 1/2").Solve(X).ToString(), Is.EqualTo("{ pi / 6 + 2 * pi * n_1, pi - pi / 6 + 2 * pi * n_1 }"), "n_1 stands for any integer.");
|
||||
Assert.That(Parse("x^2 < 4").Solve(X), Is.TypeOf<Entity.Set.Interval>());
|
||||
Assert.That(Parse("x + 1 = x").Solve(X).ToString(), Is.EqualTo("{ }"));
|
||||
Assert.Throws<SolveRequiresStatementException>(() => Parse("x^2 - 4").Solve(X));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WithoutNewtonSolveAnswersOnlyInClosedForm()
|
||||
{
|
||||
var withNewton = (Entity.Set.FiniteSet)Parse("x^5 + 3*x + 1 = 0").Solve(X);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(withNewton.Count, Is.EqualTo(5));
|
||||
Assert.That(withNewton.All(root => root is Entity.Number.Complex { IsExact: false }), "Newton's roots are doubles, printed with every binary digit.");
|
||||
|
||||
using (MathS.Settings.AllowNewton.Set(false))
|
||||
Assert.That(Parse("x^5 + 3*x + 1 = 0").Solve(X), Is.TypeOf<Entity.Set.ConditionalSet>());
|
||||
|
||||
Assert.That(Parse("x^200 + x + 1 = 0").Solve(X), Is.TypeOf<Entity.Set.ConditionalSet>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SolveHonorsTheCancellationTokenOfItsThread()
|
||||
{
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
using var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
||||
MathS.Multithreading.SetLocalCancellationToken(source.Token);
|
||||
return Parse("x^20 + x + 1 = 0").Solve(X);
|
||||
});
|
||||
|
||||
var exception = Assert.Throws<AggregateException>(() => task.Wait(TimeSpan.FromSeconds(10)))!;
|
||||
Assert.That(exception.InnerException, Is.InstanceOf<OperationCanceledException>(), "Without the token, this takes about 1.6 seconds.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeepNestingNeedsALargeStack()
|
||||
{
|
||||
// About 500 nested parentheses overflow the stack of a thread pool thread, which ends the process.
|
||||
// A thread of its own with 64 MB reads the deepest nesting a 2,000-character expression can hold:
|
||||
Entity? result = null;
|
||||
var thread = new Thread(() => result = Parse(new string('(', 1000) + "1" + new string(')', 1000)).Evaled, 64 * 1024 * 1024);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
|
||||
Assert.That(result, Is.EqualTo(Entity.Number.Integer.Create(1)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using AIStudio.Tools.Mathematics;
|
||||
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.Mathematics;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
using PeterO.Numbers;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class CalculateToolTests
|
||||
{
|
||||
private static JsonObject Calculate(string expression, int digits = 20) => CalculateTool.Calculate(expression, digits, CancellationToken.None);
|
||||
|
||||
private static string Refusal(string expression) => Assert.Throws<MathComputationException>(() => Calculate(expression))!.Message;
|
||||
|
||||
private static string Text(JsonObject result, string property) => result[property]!.GetValue<string>();
|
||||
|
||||
private static double RelativeError(JsonObject result, string expected)
|
||||
{
|
||||
var reference = EDecimal.FromString(expected);
|
||||
return EDecimal.FromString(Text(result, "decimal")).Subtract(reference).Abs().Divide(reference.Abs(), EContext.ForPrecision(20)).ToDouble();
|
||||
}
|
||||
|
||||
private static JsonElement Arguments(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void KeepsAFractionExact()
|
||||
{
|
||||
var result = Calculate("547 / 57576");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result["expression"]!.GetValue<string>(), Is.EqualTo("547 / 57576"), "The expression reads as written; its value is the fraction.");
|
||||
Assert.That(result["exact"]!.GetValue<string>(), Is.EqualTo("547/57576"));
|
||||
Assert.That(result["exact_latex"]!.GetValue<string>(), Is.EqualTo(@"\frac{547}{57576}"));
|
||||
Assert.That(result["decimal"]!.GetValue<string>(), Is.EqualTo("0.0095004863137418368765"));
|
||||
Assert.That(result["digits"]!.GetValue<int>(), Is.EqualTo(20));
|
||||
Assert.That(result["approximate"]!.GetValue<bool>(), Is.False);
|
||||
Assert.That(result.ContainsKey("estimated_error"), Is.False);
|
||||
Assert.That(result.ContainsKey("issues"), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivesAnIrrationalValueWithAllDigitsAskedFor()
|
||||
{
|
||||
var root = Calculate("sqrt(2)", 30);
|
||||
var power = Calculate("pi^4/15", 60);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(root["decimal"]!.GetValue<string>(), Is.EqualTo("1.41421356237309504880168872421"));
|
||||
Assert.That(root.ContainsKey("exact"), Is.False);
|
||||
Assert.That(power["decimal"]!.GetValue<string>(), Is.EqualTo("6.49393940226682914909602217924700741664850571151236144609786"), "Without guard digits, the last digits would be off.");
|
||||
Assert.That(power["digits"]!.GetValue<int>(), Is.EqualTo(60));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoesNotTakeANumberCloseToAnIntegerForIt()
|
||||
{
|
||||
// e^(pi*sqrt(163)) is 262537412640768743.99999999999925...
|
||||
var result = Calculate("e^(pi*sqrt(163))", 30);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.ContainsKey("exact"), Is.False);
|
||||
Assert.That(result["decimal"]!.GetValue<string>(), Is.EqualTo("262537412640768743.999999999999"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivesComplexAndInfiniteValues()
|
||||
{
|
||||
var root = Calculate("sqrt(-4)");
|
||||
var logarithm = Calculate("ln(0)");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(root["decimal"]!.GetValue<string>(), Is.EqualTo("2i"));
|
||||
Assert.That(root.ContainsKey("exact"), Is.True);
|
||||
Assert.That(logarithm["decimal"]!.GetValue<string>(), Is.EqualTo("-oo"));
|
||||
Assert.That(logarithm["digits"]!.GetValue<int>(), Is.Zero);
|
||||
Assert.That(logarithm["issues"]![0]!.GetValue<string>(), Is.EqualTo("The value is infinite."));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadsEveryDecimalLiteralAsItsFraction()
|
||||
{
|
||||
// AngouriMath alone reads 6.62607015e-34 as an inexact decimal and 1e-60 as 0, see AngouriMathBehaviorTests.DowncastingWhileParsingLosesTinyLiterals:
|
||||
var planck = Calculate("6.62607015e-34");
|
||||
var tiny = Calculate("1e-60");
|
||||
var equation = Calculate("1 + 1e-40 = 1");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(planck, "exact"), Is.EqualTo("132521403/200000000000000000000000000000000000000000"));
|
||||
Assert.That(Text(planck, "decimal"), Is.EqualTo("6.62607015e-34"));
|
||||
Assert.That(Text(tiny, "exact"), Is.EqualTo("1/1" + new string('0', 60)));
|
||||
Assert.That(equation["exactly_equal"]!.GetValue<bool>(), Is.False, "Simplifying would drop an inexact 1e-40 from the sum.");
|
||||
Assert.That(equation["numerically_equal"]!.GetValue<bool>(), Is.True, "Both sides agree in the 20 digits compared.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void KeepsTheDigitsOfTinyValues()
|
||||
{
|
||||
// Each of these came out as 0 while AngouriMath downcast, see AngouriMathBehaviorTests.DowncastingTurnsTinyValuesIntoZero:
|
||||
var photon = Calculate("6.62607015e-34 * 299792458 / 500e-9");
|
||||
var thermal = Calculate("1.380649e-23 * 300");
|
||||
var exponential = Calculate("e^(-100)");
|
||||
var tinier = Calculate("exp(-1000)");
|
||||
var fraction = Calculate("21/10^50 * pi");
|
||||
var sum = Calculate("21/10^50 + 1e-60");
|
||||
var exactFraction = Calculate("1/10^200");
|
||||
var integral = Calculate("integral(1e-30*x, x, 0, 1)");
|
||||
var quotient = Calculate("pi/10^200");
|
||||
var simplifiedToZero = Calculate("1e-2000 * pi");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(RelativeError(quotient, "3.14159265358979323846264e-200"), Is.LessThan(1e-19), "AngouriMath rounds a quotient to decimal places, so the precision grows until the value shows.");
|
||||
Assert.That(simplifiedToZero.ContainsKey("exact"), Is.False, "AngouriMath simplifies the value to 0, see AngouriMathBehaviorTests.DowncastingWhileParsingLosesTinyLiterals.");
|
||||
Assert.That(RelativeError(simplifiedToZero, "3.14159265358979323846264e-2000"), Is.LessThan(1e-19));
|
||||
Assert.That(Text(photon, "decimal"), Is.EqualTo("3.9728917142978574e-19"));
|
||||
Assert.That(Text(thermal, "decimal"), Is.EqualTo("4.141947e-21"));
|
||||
Assert.That(RelativeError(exponential, "3.72007597602083596295969580386e-44"), Is.LessThan(1e-19));
|
||||
Assert.That(RelativeError(tinier, "5.07595889754945676529180947957e-435"), Is.LessThan(1e-19));
|
||||
Assert.That(RelativeError(fraction, "6.5973445725385658007715511049e-49"), Is.LessThan(1e-19), "The fraction is rounded to decimal places unless the precision grows by its leading zeros.");
|
||||
Assert.That(Text(sum, "decimal"), Is.EqualTo("2.10000000001e-49"));
|
||||
Assert.That(Text(exactFraction, "exact"), Is.EqualTo("1/1" + new string('0', 200)));
|
||||
Assert.That(Text(exactFraction, "decimal"), Is.EqualTo("1e-200"));
|
||||
Assert.That(RelativeError(integral, "5e-31"), Is.LessThan(1e-13));
|
||||
foreach (var result in new[] { photon, thermal, exponential, tinier, fraction, sum, exactFraction })
|
||||
Assert.That(result["digits"]!.GetValue<int>(), Is.EqualTo(20));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaysWhenAZeroMayBeATinyValue()
|
||||
{
|
||||
// Simplifying gives 0, but the fraction 1e-2000 needs more digits than NumericEvaluator.MAX_PRECISION for a numerical check:
|
||||
var result = Calculate("1e-2000 * pi - 1e-2000 * pi");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(result, "decimal"), Is.EqualTo("0"));
|
||||
Assert.That(result["digits"]!.GetValue<int>(), Is.Zero);
|
||||
Assert.That(result["issues"]!.AsArray().Select(issue => issue!.GetValue<string>()), Has.Some.StartsWith("The value evaluates to 0 with ").And.Some.Contains("cannot show that it is exactly 0"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IntegratesADefiniteIntegralNumerically()
|
||||
{
|
||||
var result = Calculate("integral(x^3/(e^x-1), x, 0, +oo)");
|
||||
var digits = result["digits"]!.GetValue<int>();
|
||||
var exact = Math.Pow(Math.PI, 4) / 15;
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(digits, Is.InRange(12, 15), "A double carries no more, and the estimated error of the quadrature is conservative.");
|
||||
Assert.That(double.Parse(result["decimal"]!.GetValue<string>(), System.Globalization.CultureInfo.InvariantCulture), Is.EqualTo(exact).Within(Math.Pow(10, 1 - digits) * exact), "Every digit reported is right.");
|
||||
Assert.That(result["approximate"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(result.ContainsKey("estimated_error"), Is.True);
|
||||
Assert.That(result.ContainsKey("exact"), Is.False);
|
||||
Assert.That(result["issues"]!.AsArray().Select(issue => issue!.GetValue<string>()), Has.Some.Contains("was integrated numerically"));
|
||||
Assert.That(result["issues"]!.AsArray().Select(issue => issue!.GetValue<string>()), Has.Some.StartsWith("Only "), "Fewer digits than asked for are certain.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComputesAroundAnIntegral()
|
||||
{
|
||||
var result = Calculate("1 + integral(e^(-x^2), x, -oo, +oo)");
|
||||
Assert.That(double.Parse(result["decimal"]!.GetValue<string>(), System.Globalization.CultureInfo.InvariantCulture), Is.EqualTo(1 + Math.Sqrt(Math.PI)).Within(1e-13));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChecksTheExampleEquation()
|
||||
{
|
||||
var result = Calculate("integral(x^3/(e^x-1), x, 0, +oo) = pi^4/15");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result["numerically_equal"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(result["matching_digits"]!.GetValue<int>(), Is.InRange(12, 15));
|
||||
Assert.That(result["left"]!["approximate"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(result["right"]!["expression"]!.GetValue<string>(), Is.EqualTo("pi ^ 4 / 15"));
|
||||
Assert.That(result.ContainsKey("exactly_equal"), Is.False, "Only two exact sides settle an equation.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SettlesAnEquationOfExactValues()
|
||||
{
|
||||
var equal = Calculate("2 + 2 = 4");
|
||||
var close = Calculate("1/3 = 0.333");
|
||||
var beyondTheDigits = Calculate("1/3 = 333333333333333333333333333333/1000000000000000000000000000000");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(equal["exactly_equal"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(equal["numerically_equal"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(close["exactly_equal"]!.GetValue<bool>(), Is.False);
|
||||
Assert.That(close["numerically_equal"]!.GetValue<bool>(), Is.False);
|
||||
Assert.That(close["matching_digits"]!.GetValue<int>(), Is.EqualTo(3));
|
||||
Assert.That(beyondTheDigits["numerically_equal"]!.GetValue<bool>(), Is.True, "Twenty digits agree.");
|
||||
Assert.That(beyondTheDigits["exactly_equal"]!.GetValue<bool>(), Is.False, "The exact values differ all the same.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesWhatHasNoNumericValue()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Refusal("1/0"), Does.Contain("has no value: it evaluates to NaN"));
|
||||
Assert.That(Refusal("x + 1"), Does.Contain("contains the variable x, so it has no numeric value. compute_symbolically works with variables."));
|
||||
Assert.That(Refusal("a*b"), Does.Contain("contains the variables a, b,"));
|
||||
Assert.That(Refusal("1 < 2"), Does.Contain("is a statement, such as a comparison"));
|
||||
Assert.That(Refusal("(10^9)!"), Does.Contain("digits as an exact number"));
|
||||
Assert.That(() => Calculate("integral(x^2, x)"), Throws.InstanceOf<MathComputationException>());
|
||||
Assert.That(Refusal("sum(1/k, k, 1, 1000)"), Is.EqualTo("'sum(1 / k, k, 1, 1000)' has no value the tool can compute as a single number, as for a sum without a closed form, an infinite sum, or a matrix."), "AngouriMath sums only in closed form; its own message tells a developer to call EvaluableNumerical.");
|
||||
Assert.That(Refusal("sum(1/k^2, k, 1, +oo)"), Does.Contain("has no value the tool can compute as a single number"));
|
||||
Assert.That(Refusal("[1, 2] + [3, 4]"), Does.Contain("has no value the tool can compute as a single number"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComputesDivisorsMultiplesAndSumsInClosedForm()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(Calculate("gcd(12, 18)"), "exact"), Is.EqualTo("6"));
|
||||
Assert.That(Text(Calculate("lcm(4, 6, 10)"), "exact"), Is.EqualTo("60"), "Three arguments nest into two calls.");
|
||||
Assert.That(Text(Calculate("gcd(2^100, 6^50)"), "exact"), Is.EqualTo("1125899906842624"));
|
||||
Assert.That(Text(Calculate("sum(k^2, k, 1, 100)"), "exact"), Is.EqualTo("338350"));
|
||||
Assert.That(Text(Calculate("sum(k, k, 1, 10^7)"), "exact"), Is.EqualTo("50000005000000"), "AngouriMath sums in closed form, never term by term.");
|
||||
Assert.That(Text(Calculate("sum(k, k, 10, 1)"), "exact"), Is.EqualTo("0"), "An empty sum is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesAnIntegralItCannotIntegrate()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Refusal("integral(1/x, x, -1, 1)"), Does.StartWith("The integrand of 'integral(1 / x, x, -1, 1)' has no finite value at x = 0, inside the interval of integration."));
|
||||
Assert.That(Refusal("integral(sin(x)/x, x, 0, +oo)"), Does.StartWith("The numerical integration of 'integral(sin(x) / x, x, 0, +oo)' did not settle."));
|
||||
Assert.That(Refusal("integral(integral(x*y, y, 0, 1), x, 0, 1)"), Does.Contain("depends on the variable of an outer integral"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecutesThroughTheComputationService()
|
||||
{
|
||||
var tool = new CalculateTool(null!, new MathComputationService(NullLogger<MathComputationService>.Instance));
|
||||
var result = await tool.ExecuteAsync(Arguments("""{"expression": "547 / 57576", "digits": 5}"""), null!);
|
||||
|
||||
Assert.That(result.JsonContent!["decimal"]!.GetValue<string>(), Is.EqualTo("0.0095005"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesDigitsOutsideTheRange()
|
||||
{
|
||||
var tool = new CalculateTool(null!, new MathComputationService(NullLogger<MathComputationService>.Instance));
|
||||
var tooMany = Arguments("""{"expression": "1/3", "digits": 101}""");
|
||||
var none = Arguments("""{"expression": "1/3", "digits": 0}""");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(() => tool.ExecuteAsync(tooMany, null!), Throws.ArgumentException.With.Message.EqualTo("Argument 'digits' must be at most 100, but was 101. Leave it out to get 20 significant digits."));
|
||||
Assert.That(() => tool.ExecuteAsync(none, null!), Throws.ArgumentException);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TellsTheModelNothingAboutTheLibrary()
|
||||
{
|
||||
var definition = new CalculateTool(null!, new MathComputationService(NullLogger<MathComputationService>.Instance)).GetDefinition();
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(definition.SystemPromptInstructions, Does.Not.Contain("AngouriMath"), "A model would take the name for something it may call or look up.");
|
||||
Assert.That(definition.Function.DescriptionForLLM, Does.Not.Contain("AngouriMath"));
|
||||
Assert.That(definition.Function.Parameters.GetRawText(), Does.Not.Contain("AngouriMath"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
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.Tests.Tools.Mathematics;
|
||||
|
||||
/// <summary>
|
||||
/// Checks compute_symbolically through the computation service, so it runs with the settings of the tools: explicit
|
||||
/// parsing, and solving without Newton's method.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class ComputeSymbolicallyToolTests
|
||||
{
|
||||
private static readonly MathComputationService SERVICE = new(NullLogger<MathComputationService>.Instance);
|
||||
|
||||
private static Task<JsonObject> Compute(SymbolicOperation operation, string expression, string? variable = null, int order = 1, string? lowerBound = null, string? upperBound = null, string? approach = null, ApproachFrom direction = ApproachFrom.BothSides) =>
|
||||
SERVICE.ComputeAsync(_ => ComputeSymbolicallyTool.Compute(new SymbolicRequest(operation, expression, variable, order, lowerBound, upperBound, approach, direction)));
|
||||
|
||||
private static string Text(JsonObject json, string property) => json[property]!.GetValue<string>();
|
||||
|
||||
private static IEnumerable<string> Issues(JsonObject json) => json["issues"]?.AsArray().Select(issue => issue!.GetValue<string>()) ?? [];
|
||||
|
||||
private static JsonElement Arguments(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
private static string RequestRefusal(string json) => Assert.Throws<ArgumentException>(() => ComputeSymbolicallyTool.ReadRequest(Arguments(json)))!.Message;
|
||||
|
||||
[Test]
|
||||
public async Task SimplifiesAndStatesTheCondition()
|
||||
{
|
||||
var identity = await Compute(SymbolicOperation.SIMPLIFY, "sin(x)^2 + cos(x)^2");
|
||||
var quotient = await Compute(SymbolicOperation.SIMPLIFY, "(x^2 - 1)/(x - 1)");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(identity, "result"), Is.EqualTo("1"));
|
||||
Assert.That(Text(identity, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(identity["complete"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(Text(quotient, "result"), Is.EqualTo("x + 1"));
|
||||
Assert.That(Text(quotient, "condition"), Is.EqualTo("not x - 1 = 0"));
|
||||
Assert.That(Text(quotient, "verification"), Is.EqualTo("passed"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExpandsAndFactors()
|
||||
{
|
||||
var expanded = await Compute(SymbolicOperation.EXPAND, "(x + 1)^3");
|
||||
var unchanged = await Compute(SymbolicOperation.EXPAND, "(x + y + z + w)^60");
|
||||
var factored = await Compute(SymbolicOperation.FACTOR, "x^4 - 1");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(expanded, "result"), Is.EqualTo("1 + 3 * x + 3 * x ^ 2 + x ^ 3"));
|
||||
Assert.That(Text(expanded, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Issues(unchanged), Has.Some.StartsWith("The tool left the expression as it is"));
|
||||
Assert.That(Text(factored, "result"), Is.EqualTo("(x + 1) * (x - 1) * (x ^ 2 + 1)"));
|
||||
Assert.That(Text(factored, "verification"), Is.EqualTo("passed"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DifferentiatesAndChecksEachOrder()
|
||||
{
|
||||
var product = await Compute(SymbolicOperation.DIFFERENTIATE, "x^2*sin(x)");
|
||||
var third = await Compute(SymbolicOperation.DIFFERENTIATE, "x^5", order: 3);
|
||||
var parameter = await Compute(SymbolicOperation.DIFFERENTIATE, "a*x^2", "x");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(product, "result"), Is.EqualTo("x ^ 2 * cos(x) + 2 * x * sin(x)"));
|
||||
Assert.That(Text(product, "variable"), Is.EqualTo("x"));
|
||||
Assert.That(Text(product, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Text(third, "result"), Is.EqualTo("60 * x ^ 2"));
|
||||
Assert.That(third["order"]!.GetValue<int>(), Is.EqualTo(3));
|
||||
Assert.That(Text(third, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Text(parameter, "verification"), Is.EqualTo("passed"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IntegratesWithoutBounds()
|
||||
{
|
||||
var power = await Compute(SymbolicOperation.INTEGRATE, "x^2");
|
||||
var withoutClosedForm = await Compute(SymbolicOperation.INTEGRATE, "sin(sin(x))");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(power, "result"), Is.EqualTo("x ^ 3 / 3 + C"));
|
||||
Assert.That(Text(power, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Issues(power), Has.Member("C stands for any constant."));
|
||||
Assert.That(withoutClosedForm["complete"]!.GetValue<bool>(), Is.False);
|
||||
Assert.That(Text(withoutClosedForm, "verification"), Is.EqualTo("not_possible"));
|
||||
Assert.That(Issues(withoutClosedForm), Has.Member("The tool found no antiderivative in closed form."));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IntegratesWithBoundsAndChecksByQuadrature()
|
||||
{
|
||||
var polynomial = await Compute(SymbolicOperation.INTEGRATE, "x^2", lowerBound: "0", upperBound: "1");
|
||||
var arctangent = await Compute(SymbolicOperation.INTEGRATE, "1/(1 + x^2)", lowerBound: "0", upperBound: "+oo");
|
||||
var bose = await Compute(SymbolicOperation.INTEGRATE, "x^3/(e^x - 1)", lowerBound: "0", upperBound: "+oo");
|
||||
var pole = await Compute(SymbolicOperation.INTEGRATE, "1/x", lowerBound: "-1", upperBound: "1");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(polynomial, "result"), Is.EqualTo("1/3"));
|
||||
Assert.That(Text(polynomial, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Text(arctangent, "result"), Is.EqualTo("pi / 2"));
|
||||
Assert.That(Text(arctangent, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(bose["complete"]!.GetValue<bool>(), Is.False, "AngouriMath finds no closed form for pi^4/15.");
|
||||
Assert.That(Issues(bose), Has.Member("The tool found no closed form for this integral."));
|
||||
Assert.That(Text(pole, "result"), Is.EqualTo("NaN"));
|
||||
Assert.That(Issues(pole), Has.Some.StartsWith("The result is NaN: the integral has no value"));
|
||||
Assert.That(Text(pole, "verification"), Is.EqualTo("not_possible"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TakesLimits()
|
||||
{
|
||||
var sinc = await Compute(SymbolicOperation.LIMIT, "sin(x)/x", approach: "0");
|
||||
var fromTheRight = await Compute(SymbolicOperation.LIMIT, "1/x", approach: "0", direction: ApproachFrom.Right);
|
||||
var fromBothSides = await Compute(SymbolicOperation.LIMIT, "1/x", approach: "0");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Text(sinc, "result"), Is.EqualTo("1"));
|
||||
Assert.That(Text(sinc, "direction"), Is.EqualTo("both"));
|
||||
Assert.That(Text(sinc, "verification"), Is.EqualTo("not_possible"));
|
||||
Assert.That(Text(fromTheRight, "result"), Is.EqualTo("+oo"));
|
||||
Assert.That(Text(fromTheRight, "direction"), Is.EqualTo("right"));
|
||||
Assert.That(Text(fromBothSides, "result"), Is.EqualTo("NaN"));
|
||||
Assert.That(Issues(fromBothSides), Has.Some.StartsWith("The result is NaN, which the tool gives where the limit does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SolvesAndChecksEverySolution()
|
||||
{
|
||||
var quadratic = await Compute(SymbolicOperation.SOLVE, "x^2 - 5*x + 6 = 0");
|
||||
var complex = await Compute(SymbolicOperation.SOLVE, "x^2 + 1 = 0");
|
||||
var periodic = await Compute(SymbolicOperation.SOLVE, "sin(x) = 1/2");
|
||||
var withoutEquals = await Compute(SymbolicOperation.SOLVE, "x^2 - 4");
|
||||
var tiny = await Compute(SymbolicOperation.SOLVE, "1.5e30*x = 3");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(quadratic["solutions"]!.AsArray().Select(solution => solution!["value"]!.GetValue<string>()), Is.EqualTo(new[] { "2", "3" }));
|
||||
Assert.That(tiny["solutions"]![0]!["decimal"]!.GetValue<string>(), Is.EqualTo("2e-30"), "The decimal of a tiny solution keeps its digits, see NumericEvaluator.EvaluateAt.");
|
||||
Assert.That(quadratic["solutions"]![0]!["real"]!.GetValue<bool>(), Is.True);
|
||||
Assert.That(Text(quadratic, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(complex["solutions"]![0]!["real"]!.GetValue<bool>(), Is.False);
|
||||
Assert.That(Text(complex, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Text(periodic, "verification"), Is.EqualTo("passed"), "n_1 takes integer values in the check.");
|
||||
Assert.That(Issues(periodic), Has.Member("n_1 stands for any integer."));
|
||||
Assert.That(Issues(withoutEquals), Has.Member("The expression has no '=', so it was solved for expression = 0."));
|
||||
Assert.That(withoutEquals["solutions"]!.AsArray(), Has.Count.EqualTo(2));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WorksWithSumsAndDivisors()
|
||||
{
|
||||
var gauss = await Compute(SymbolicOperation.SOLVE, "sum(k, k, 1, n) = 55", "n");
|
||||
var derivative = await Compute(SymbolicOperation.DIFFERENTIATE, "sum(k*x^k, k, 1, 3)", "x");
|
||||
var divisor = await Compute(SymbolicOperation.SIMPLIFY, "gcd(12, 18) * x");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(gauss["solutions"]!.AsArray().Select(solution => solution!["value"]!.GetValue<string>()), Is.EqualTo(new[] { "10" }), "The closed form holds for n >= 0 only, so -11 is no solution.");
|
||||
Assert.That(Text(gauss, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Text(derivative, "result"), Is.EqualTo("1 + 4 * x + 9 * x ^ 2"));
|
||||
Assert.That(Text(derivative, "verification"), Is.EqualTo("passed"));
|
||||
Assert.That(Text(divisor, "result"), Is.EqualTo("6 * x"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaysWhenThereIsNoSolutionInClosedForm()
|
||||
{
|
||||
var quintic = await Compute(SymbolicOperation.SOLVE, "x^5 + 3*x + 1 = 0");
|
||||
var none = await Compute(SymbolicOperation.SOLVE, "x + 1 = x");
|
||||
var inequality = await Compute(SymbolicOperation.SOLVE, "x^2 < 4");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(quintic["complete"]!.GetValue<bool>(), Is.False, "Without Newton's method, there are no approximate roots.");
|
||||
Assert.That(Text(quintic, "verification"), Is.EqualTo("not_possible"));
|
||||
Assert.That(none["solutions"]!.AsArray(), Is.Empty);
|
||||
Assert.That(Issues(none), Has.Member("There is no solution."));
|
||||
Assert.That(Text(inequality, "result"), Is.EqualTo("(-2; 2)"));
|
||||
Assert.That(inequality.ContainsKey("solutions"), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesWhatTheOperationCannotTake()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(async () => await Compute(SymbolicOperation.SIMPLIFY, "x = 2"), Throws.InstanceOf<MathComputationException>().With.Message.EqualTo("Argument 'expression' is a statement, such as an equation or a comparison, which only the operation 'solve' takes."));
|
||||
Assert.That(async () => await Compute(SymbolicOperation.DIFFERENTIATE, "a*x^2"), Throws.InstanceOf<MathComputationException>().With.Message.EqualTo("Argument 'expression' has the variables a, x, so argument 'variable' has to say which one to differentiate by."));
|
||||
Assert.That(async () => await Compute(SymbolicOperation.INTEGRATE, "2"), Throws.InstanceOf<MathComputationException>().With.Message.EqualTo("Argument 'expression' has no variable to integrate over."));
|
||||
Assert.That(async () => await Compute(SymbolicOperation.SOLVE, "x^2 = 2", "pi"), Throws.InstanceOf<MathComputationException>().With.Message.EqualTo("Argument 'variable' must name a variable, such as x, but was 'pi'."));
|
||||
Assert.That(async () => await Compute(SymbolicOperation.EXPAND, "(x + y)^500"), Throws.InstanceOf<MathComputationException>().With.Message.Contains("raises a sum to the power of 500"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesArgumentsTheOperationDoesNotUse()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(RequestRefusal("""{"operation": "simplify", "expression": "x", "variable": "x"}"""), Is.EqualTo("Argument 'variable' is not used by the operation 'simplify'. Leave it out."));
|
||||
Assert.That(RequestRefusal("""{"operation": "integrate", "expression": "x", "order": 2}"""), Is.EqualTo("Argument 'order' is not used by the operation 'integrate'. Leave it out."));
|
||||
Assert.That(RequestRefusal("""{"operation": "integrate", "expression": "x", "lower_bound": "0"}"""), Is.EqualTo("Argument 'lower_bound' needs argument 'upper_bound' as well, since a definite integral has two bounds. Leave both out for an antiderivative."));
|
||||
Assert.That(RequestRefusal("""{"operation": "limit", "expression": "1/x"}"""), Does.StartWith("Argument 'approach' is required for the operation 'limit'"));
|
||||
Assert.That(RequestRefusal("""{"operation": "solve", "expression": "x = 1", "direction": "left"}"""), Is.EqualTo("Argument 'direction' is not used by the operation 'solve'. Leave it out."));
|
||||
Assert.That(RequestRefusal("""{"operation": "differentiate", "expression": "x", "order": 11}"""), Is.EqualTo("Argument 'order' must be at most 10, but was 11. Leave it out for the first derivative."));
|
||||
Assert.That(RequestRefusal("""{"operation": "derive", "expression": "x"}"""), Is.EqualTo("Argument 'operation' must be one of simplify, expand, factor, differentiate, integrate, limit, solve, but was 'derive'."));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadsTheArgumentsOfALimit()
|
||||
{
|
||||
var request = ComputeSymbolicallyTool.ReadRequest(Arguments("""{"operation": "limit", "expression": "1/x", "variable": null, "approach": "0", "direction": "left"}"""));
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(request.Operation, Is.EqualTo(SymbolicOperation.LIMIT));
|
||||
Assert.That(request.Variable, Is.Null, "In strict mode, a model passes null for an argument it leaves out.");
|
||||
Assert.That(request.Approach, Is.EqualTo("0"));
|
||||
Assert.That(request.Direction, Is.EqualTo(ApproachFrom.Left));
|
||||
Assert.That(request.Order, Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecutesThroughTheComputationService()
|
||||
{
|
||||
var tool = new ComputeSymbolicallyTool(null!, SERVICE);
|
||||
var result = await tool.ExecuteAsync(Arguments("""{"operation": "factor", "expression": "x^2 - 5*x + 6"}"""), null!);
|
||||
Assert.That(result.JsonContent!["result"]!.GetValue<string>(), Is.EqualTo("(x - 3) * (x - 2)"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TellsTheModelNothingAboutTheLibrary()
|
||||
{
|
||||
var definition = new ComputeSymbolicallyTool(null!, SERVICE).GetDefinition();
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(definition.SystemPromptInstructions, Does.Not.Contain("AngouriMath"), "A model would take the name for something it may call or look up.");
|
||||
Assert.That(definition.Function.DescriptionForLLM, Does.Not.Contain("AngouriMath"));
|
||||
Assert.That(definition.Function.Parameters.GetRawText(), Does.Not.Contain("AngouriMath"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using AIStudio.Tools.Mathematics;
|
||||
|
||||
using AngouriMath;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class DoubleExponentialQuadratureTests
|
||||
{
|
||||
private static Func<double, double> Compiled(string expression) => MathExpressionReader.Read(expression, "expression").Compile<double, double>(MathS.Var("x"));
|
||||
|
||||
/// <summary>
|
||||
/// Checks a converged result against the exact value, and that its estimated error is honest.
|
||||
/// </summary>
|
||||
private static void AssertIntegral(Func<double, double> integrand, double lower, double upper, double exact, double relativeTolerance = 1e-13)
|
||||
{
|
||||
var result = DoubleExponentialQuadrature.Integrate(integrand, lower, upper);
|
||||
var actualError = Math.Abs(result.Value - exact);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Status, Is.EqualTo(QuadratureStatus.CONVERGED));
|
||||
Assert.That(result.Value, Is.EqualTo(exact).Within(relativeTolerance * Math.Abs(exact)));
|
||||
Assert.That(actualError, Is.LessThanOrEqualTo(Math.Max(result.EstimatedError, 1e-15 * Math.Abs(exact))), "The estimated error must not claim more digits than the value has.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IntegratesOverAFiniteInterval()
|
||||
{
|
||||
AssertIntegral(x => x * x, 0, 1, 1.0 / 3);
|
||||
AssertIntegral(Math.Sin, 0, Math.PI, 2);
|
||||
AssertIntegral(Math.Exp, -1, 2, Math.Exp(2) - Math.Exp(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IntegratesASingularityAtTheLowerBoundZero()
|
||||
{
|
||||
AssertIntegral(Math.Log, 0, 1, -1);
|
||||
AssertIntegral(x => 1 / Math.Sqrt(x), 0, 1, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReachesASingularityAtAnotherBoundWithFewerDigits()
|
||||
{
|
||||
var result = DoubleExponentialQuadrature.Integrate(x => 1 / Math.Sqrt(1 - x), 0, 1);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Status, Is.EqualTo(QuadratureStatus.CONVERGED));
|
||||
Assert.That(result.Value, Is.EqualTo(2).Within(1e-6));
|
||||
Assert.That(Math.Abs(result.Value - 2), Is.LessThanOrEqualTo(result.EstimatedError), "Near 1, a double carries few digits of the distance to 1; the estimate has to say so.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IntegratesOverInfiniteIntervals()
|
||||
{
|
||||
AssertIntegral(x => Math.Exp(-x), 0, double.PositiveInfinity, 1);
|
||||
AssertIntegral(Math.Exp, double.NegativeInfinity, 0, 1);
|
||||
AssertIntegral(x => 1 / (1 + x * x), 0, double.PositiveInfinity, Math.PI / 2);
|
||||
AssertIntegral(x => Math.Exp(-x * x), double.NegativeInfinity, double.PositiveInfinity, Math.Sqrt(Math.PI));
|
||||
AssertIntegral(x => Math.Exp(-x), 2, double.PositiveInfinity, Math.Exp(-2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IntegratesTheExampleOfTheBoseIntegral()
|
||||
{
|
||||
// The compiled integrand is 0/0 at 0, and has no finite value for x below about 1e-16, where e^x - 1 is 0 in doubles:
|
||||
var bose = Compiled("x^3/(e^x-1)");
|
||||
Assert.That(double.IsFinite(bose(1e-17)), Is.False);
|
||||
AssertIntegral(bose, 0, double.PositiveInfinity, Math.Pow(Math.PI, 4) / 15);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReportsAnIntegralWhichDoesNotSettle()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(DoubleExponentialQuadrature.Integrate(x => Math.Sin(x) / x, 0, double.PositiveInfinity).Status, Is.EqualTo(QuadratureStatus.NOT_CONVERGED), "sin(x)/x converges only conditionally, which the substitution cannot settle.");
|
||||
Assert.That(DoubleExponentialQuadrature.Integrate(x => 1 / x, 0, 1).Status, Is.EqualTo(QuadratureStatus.NOT_CONVERGED), "1/x diverges at 0.");
|
||||
Assert.That(DoubleExponentialQuadrature.Integrate(x => 1 / x, 1, double.PositiveInfinity).Status, Is.EqualTo(QuadratureStatus.NOT_CONVERGED), "1/x diverges at infinity.");
|
||||
Assert.That(DoubleExponentialQuadrature.Integrate(Math.Sin, 0, double.PositiveInfinity).Status, Is.EqualTo(QuadratureStatus.NOT_CONVERGED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReportsWhereTheIntegrandHasNoValue()
|
||||
{
|
||||
var pole = DoubleExponentialQuadrature.Integrate(x => 1 / x, -1, 1);
|
||||
var root = DoubleExponentialQuadrature.Integrate(Compiled("sqrt(x - 1)"), 0, 2);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(pole.Status, Is.EqualTo(QuadratureStatus.NO_VALUE));
|
||||
Assert.That(pole.FailurePoint, Is.Zero, "The pole lies in the middle of the interval.");
|
||||
Assert.That(root.Status, Is.EqualTo(QuadratureStatus.NO_VALUE));
|
||||
Assert.That(root.FailurePoint, Is.LessThan(1), "A compiled function stays real, so sqrt has no value below 1.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChangesTheSignForReversedBounds()
|
||||
{
|
||||
var reversed = DoubleExponentialQuadrature.Integrate(x => x * x, 1, 0);
|
||||
var empty = DoubleExponentialQuadrature.Integrate(x => x * x, 2, 2);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reversed.Value, Is.EqualTo(-1.0 / 3).Within(1e-15));
|
||||
Assert.That(empty.Value, Is.Zero);
|
||||
Assert.That(empty.Status, Is.EqualTo(QuadratureStatus.CONVERGED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SettlesAnIntegralOfZero()
|
||||
{
|
||||
var result = DoubleExponentialQuadrature.Integrate(Math.Sin, -1, 1);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Status, Is.EqualTo(QuadratureStatus.CONVERGED));
|
||||
Assert.That(result.Value, Is.EqualTo(0).Within(1e-15));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StopsAtTheCancellationToken()
|
||||
{
|
||||
var canceled = new CancellationToken(canceled: true);
|
||||
Assert.Throws<OperationCanceledException>(() => DoubleExponentialQuadrature.Integrate(x => Math.Sin(x) / x, 0, double.PositiveInfinity, canceled));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using AIStudio.Tools.Mathematics;
|
||||
|
||||
using AngouriMath;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class MathComputationServiceTests
|
||||
{
|
||||
private static readonly TimeSpan SHORT_BUDGET = TimeSpan.FromMilliseconds(200);
|
||||
|
||||
private static MathComputationService CreateService(TimeSpan? budget = null) => new(NullLogger<MathComputationService>.Instance)
|
||||
{
|
||||
TimeBudget = budget ?? MathComputationService.DEFAULT_TIME_BUDGET,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A computation which checks its token, and nothing else.
|
||||
/// </summary>
|
||||
private static int SpinUntilCanceled(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
Thread.SpinWait(100);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return 0;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AComputationRunsWithTheSettingsOfTheTools()
|
||||
{
|
||||
var service = CreateService();
|
||||
var settings = await service.ComputeAsync(_ => new[]
|
||||
{
|
||||
Thread.CurrentThread.Name,
|
||||
MathS.Settings.ExplicitParsingOnly.Value.ToString(),
|
||||
MathS.Settings.AllowNewton.Value.ToString(),
|
||||
});
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(settings, Is.EqualTo(new[] { "Mathematics", "True", "False" }));
|
||||
Assert.That(MathS.Settings.ExplicitParsingOnly.Value, Is.False, "The settings stay with the computation.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TheLongestExpressionsDoNotOverflowTheStack()
|
||||
{
|
||||
// Each of these holds as deep a tree as 2,000 characters allow; on a thread pool thread,
|
||||
// the first one would end the test run:
|
||||
var service = CreateService();
|
||||
var parentheses = new string('(', 999) + "1" + new string(')', 999);
|
||||
var powerTower = string.Join("^", Enumerable.Repeat("x", 1000));
|
||||
var sineChain = string.Concat(Enumerable.Repeat("sin(", 399)) + "1" + new string(')', 399);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(await service.ComputeAsync(_ => MathExpressionReader.Read(parentheses, "expression").Evaled), Is.EqualTo(Entity.Number.Integer.Create(1)));
|
||||
Assert.That(await service.ComputeAsync(_ => MathExpressionReader.Read(powerTower, "expression").Simplify().ToString().Length), Is.GreaterThan(1000));
|
||||
Assert.That(await service.ComputeAsync(_ => MathExpressionReader.Read(sineChain, "expression").EvalNumerical().ToString()), Does.StartWith("0.08"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AComputationWhichChecksItsTokenStopsAtTheBudget()
|
||||
{
|
||||
var service = CreateService(SHORT_BUDGET);
|
||||
var exception = (await Assert.ThrowsAsync<MathComputationException>(() => service.ComputeAsync(SpinUntilCanceled)))!;
|
||||
|
||||
Assert.That(exception.Message, Is.EqualTo("The computation did not finish within 0.2 seconds."));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AComputationWhichIgnoresItsTokenKeepsTheSlotUntilItEnds()
|
||||
{
|
||||
// Not disposed: the stuck computation may still be waiting for it when the test ends.
|
||||
var service = CreateService(SHORT_BUDGET);
|
||||
var release = new ManualResetEventSlim();
|
||||
var stuck = (await Assert.ThrowsAsync<MathComputationException>(() => service.ComputeAsync(_ =>
|
||||
{
|
||||
release.Wait(CancellationToken.None);
|
||||
return 1;
|
||||
})))!;
|
||||
|
||||
var waiting = (await Assert.ThrowsAsync<MathComputationException>(() => service.ComputeAsync(_ => 2)))!;
|
||||
release.Set();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(stuck.Message, Does.StartWith("The computation did not finish within 0.2 seconds and could not be stopped."));
|
||||
Assert.That(waiting.Message, Is.EqualTo("Another computation is still running, so this one could not start within 0.2 seconds."));
|
||||
Assert.That(await service.ComputeAsync(_ => 3), Is.EqualTo(3), "Once the stuck computation ends, the slot is free again.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheRequestCancelsAComputation()
|
||||
{
|
||||
var service = CreateService();
|
||||
var request = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
|
||||
Assert.That(() => service.ComputeAsync(SpinUntilCanceled, request.Token), Throws.InstanceOf<OperationCanceledException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnExceptionOfAngouriMathBecomesAMessageAndFreesTheSlot()
|
||||
{
|
||||
var service = CreateService();
|
||||
var exception = (await Assert.ThrowsAsync<MathComputationException>(() => service.ComputeAsync(_ => MathExpressionReader.Read("x + 1", "expression").EvalNumerical())))!;
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(exception.Message, Does.StartWith("The tool could not compute this:`x + 1` evaluates to `x + 1`"));
|
||||
Assert.That(await service.ComputeAsync(_ => 4), Is.EqualTo(4));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using AIStudio.Tools.Mathematics;
|
||||
|
||||
using AngouriMath;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class MathExpressionReaderTests
|
||||
{
|
||||
private static string Refusal(string text, string argumentName = "expression") => Assert.Throws<MathComputationException>(() => MathExpressionReader.Read(text, argumentName))!.Message;
|
||||
|
||||
[Test]
|
||||
public void ReadsTheSyntaxOfTheTools()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathExpressionReader.Read("integral(x^3/(e^x-1), x, 0, +oo)", "expression"), Is.TypeOf<Entity.Integralf>());
|
||||
Assert.That(MathExpressionReader.Read("x^2 = 4", "expression"), Is.TypeOf<Entity.Equalsf>());
|
||||
Assert.That(MathExpressionReader.Read("x <= 2", "expression"), Is.TypeOf<Entity.LessOrEqualf>(), "The equals sign of <= is no equation.");
|
||||
Assert.That(MathExpressionReader.Read("x >= 2", "expression"), Is.TypeOf<Entity.GreaterOrEqualf>());
|
||||
Assert.That(MathExpressionReader.Read("a <> b", "expression").ToString(), Is.EqualTo("not a = b"));
|
||||
Assert.That(MathExpressionReader.Read(new string('1', MathLimits.MAX_EXPRESSION_LENGTH), "expression"), Is.InstanceOf<Entity.Number.Integer>());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesWhatItCannotReadAsMeant()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Refusal(" "), Is.EqualTo("Argument 'expression' is empty."));
|
||||
Assert.That(Refusal(new string('1', MathLimits.MAX_EXPRESSION_LENGTH + 1)), Is.EqualTo("Argument 'expression' has 2,001 characters, but at most 2,000 are read."));
|
||||
Assert.That(Refusal("a != b"), Is.EqualTo("Argument 'expression' contains '!=', which the tool reads as a factorial followed by '='. Not-equal is written '<>'."));
|
||||
Assert.That(Refusal("|x - 1|"), Does.StartWith("Argument 'expression' contains '|', which the tool reads as divisibility."));
|
||||
Assert.That(Refusal("2 = 2 = 2"), Does.StartWith("Argument 'expression' contains 2 equals signs, but an expression holds one equation at most."));
|
||||
Assert.That(Refusal("oo", "approach"), Is.EqualTo("Argument 'approach' uses 'oo', which the tool reads as a variable. Infinity is written +oo or -oo."));
|
||||
Assert.That(Refusal("1/oo"), Does.Contain("uses 'oo'"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NamesTheMissingOperator()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Refusal("2x"), Is.EqualTo("Argument 'expression' has no operator between '2' and 'x' at column 2. Multiplication is written '*', as in 2*x, and a function takes its argument in parentheses, as in sin(x)."));
|
||||
Assert.That(Refusal("sin x"), Does.StartWith("Argument 'expression' has no operator between 'sin' and 'x' at column 5."));
|
||||
Assert.That(Refusal("foo(3)"), Is.EqualTo("Argument 'expression' has no operator between 'foo' and '(' at column 4. The tool knows no function named 'foo', so it would read the name as a variable times the parentheses. Multiplication is written '*'."));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShortensAParseErrorToItsColumnAndCause()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Refusal("2 + * 3"), Is.EqualTo("Argument 'expression' cannot be read at column 5: extraneous input '*'."));
|
||||
Assert.That(Refusal("(2 + 3"), Is.EqualTo("Argument 'expression' cannot be read at column 7: no viable alternative at input '(2+3'."));
|
||||
Assert.That(Refusal(@"\frac{1}{2}"), Does.StartWith("Argument 'expression' cannot be read at column 1:"));
|
||||
Assert.That(Refusal("sin(1, 2)"), Is.EqualTo("Argument 'expression' cannot be read: sin should have exactly 1 argument but 2 arguments are provided."));
|
||||
Assert.That(Refusal("integral(x^2, x, 2)", "lower_bound"), Does.StartWith("Argument 'lower_bound' cannot be read: integral should have exactly 4 arguments or 2 arguments"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using AIStudio.Tools.Mathematics;
|
||||
|
||||
using AngouriMath;
|
||||
|
||||
using PeterO.Numbers;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class MathFormattingTests
|
||||
{
|
||||
private static string Format(string value, int digits) => MathFormatting.FormatDecimal(EDecimal.FromString(value), digits);
|
||||
|
||||
[Test]
|
||||
public void WritesDecimalsWithTheirSignificantDigits()
|
||||
{
|
||||
var quotient = Entity.Number.Rational.Create(547, 57576).EDecimal;
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathFormatting.FormatDecimal(quotient, 20), Is.EqualTo("0.0095004863137418368765"));
|
||||
Assert.That(MathFormatting.FormatDecimal(quotient, 3), Is.EqualTo("0.0095"), "0.00950 at three digits; trailing zeros are left out.");
|
||||
Assert.That(Format("0.5", 20), Is.EqualTo("0.5"));
|
||||
Assert.That(Format("120", 20), Is.EqualTo("120"));
|
||||
Assert.That(Format("1234567.891", 4), Is.EqualTo("1235000"));
|
||||
Assert.That(Format("-0.000001234", 20), Is.EqualTo("-0.000001234"));
|
||||
Assert.That(Format("0", 20), Is.EqualTo("0"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WritesVeryLargeAndVerySmallDecimalsInScientificNotation()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Format("602214076000000000000000", 20), Is.EqualTo("6.02214076e+23"));
|
||||
Assert.That(Format("100000000000000000000", 20), Is.EqualTo("100000000000000000000"), "1e20 is still written out.");
|
||||
Assert.That(Format("1E-60", 20), Is.EqualTo("1e-60"));
|
||||
Assert.That(Format("-1.5E-7", 20), Is.EqualTo("-1.5e-7"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WritesValuesWithoutAMeaningAsTheToolsReadThem()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathFormatting.FormatDecimal(double.NaN, 15), Is.EqualTo("NaN"));
|
||||
Assert.That(MathFormatting.FormatDecimal(double.PositiveInfinity, 15), Is.EqualTo("+oo"));
|
||||
Assert.That(MathFormatting.FormatDecimal(EDecimal.NegativeInfinity, 15), Is.EqualTo("-oo"));
|
||||
Assert.That(MathFormatting.FormatDecimal(Math.PI, 15), Is.EqualTo("3.14159265358979"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CountsTheDigitsTwoValuesShare()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathFormatting.CountMatchingDigits(EDecimal.FromString("1.0000001"), EDecimal.FromString("1.0000002"), 20), Is.EqualTo(7));
|
||||
Assert.That(MathFormatting.CountMatchingDigits(EDecimal.FromString("0.99999"), EDecimal.FromString("1.00001"), 20), Is.EqualTo(4), "No digit is the same, but the values are close.");
|
||||
Assert.That(MathFormatting.CountMatchingDigits(EDecimal.FromString("2.5"), EDecimal.FromString("2.5"), 20), Is.EqualTo(20));
|
||||
Assert.That(MathFormatting.CountMatchingDigits(EDecimal.FromString("1"), EDecimal.FromString("-1"), 20), Is.Zero);
|
||||
Assert.That(MathFormatting.CountMatchingDigits(EDecimal.NaN, EDecimal.NaN, 20), Is.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesAResultBeyondTheLimit()
|
||||
{
|
||||
var text = new string('1', MathLimits.MAX_RESULT_LENGTH + 1);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MathFormatting.EnsureLength(new string('1', MathLimits.MAX_RESULT_LENGTH), "The result"), Has.Length.EqualTo(MathLimits.MAX_RESULT_LENGTH));
|
||||
Assert.That(() => MathFormatting.EnsureLength(text, "The result"), Throws.InstanceOf<MathComputationException>().With.Message.EqualTo("The result has 20,001 characters, but at most 20,000 are returned."));
|
||||
Assert.That(MathFormatting.ToLatex(MathExpressionReader.Read("547 / 57576", "expression"), "The LaTeX"), Is.EqualTo(@"\frac{547}{57576}"));
|
||||
Assert.That(MathFormatting.ToText(MathExpressionReader.Read("x^3/(e^x-1)", "expression"), "The result"), Is.EqualTo("x ^ 3 / (e ^ x - 1)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using AIStudio.Tools.Mathematics;
|
||||
|
||||
using AngouriMath;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the guards refuse what AngouriMath could not stop, and nothing it computes quickly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// None of the refused expressions is computed here: each of them would run on long past the test.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class MathGuardsTests
|
||||
{
|
||||
private static Entity Read(string text) => MathExpressionReader.Read(text, "expression");
|
||||
|
||||
private static string ExactSizeRefusal(string text) => Assert.Throws<MathComputationException>(() => MathGuards.CheckExactSize(Read(text)))!.Message;
|
||||
|
||||
[Test]
|
||||
public void RefusesAPowerWithTooManyDigits()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ExactSizeRefusal("9^9^9"), Is.EqualTo("'9 ^ 9 ^ 9' would have about 369,693,100 digits as an exact number; its decimal logarithm is about 369693099.632. The tool computes such a number in full and cannot stop midway, so exact numbers are limited to 20,000 digits."));
|
||||
Assert.That(ExactSizeRefusal("9^9^9^9"), Does.StartWith("'9 ^ 9 ^ 9' would have"), "The inner power is judged first, before its value is needed.");
|
||||
Assert.That(ExactSizeRefusal("1 + 10^20001"), Does.StartWith("'10 ^ 20001' would have about 20,001 digits"));
|
||||
Assert.That(ExactSizeRefusal("(1/2)^100000"), Does.Contain("would have about 30,103 digits"), "Numerator and denominator count.");
|
||||
Assert.That(ExactSizeRefusal("sqrt(2)^1000000"), Does.Contain("would have about"), "sqrt(2)^1000000 is the exact number 2^500000.");
|
||||
Assert.That(ExactSizeRefusal("x^(9^9^9)"), Does.StartWith("'9 ^ 9 ^ 9' would have"), "An exponent is a number even when the base is a variable.");
|
||||
Assert.That(ExactSizeRefusal("(e^0 + 1)^(10^9)"), Does.Contain("would have about 301,029,996 digits"), "A constant is no variable: the base is the number 2.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesAFactorialWithTooManyDigits()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ExactSizeRefusal("(10^9)!"), Does.StartWith("'(10 ^ 9)!' would have about 8,565,705,"));
|
||||
Assert.That(ExactSizeRefusal("10000!"), Does.StartWith("'10000!' would have about 35,660 digits"));
|
||||
Assert.That(ExactSizeRefusal("(10!)!"), Does.StartWith("'(10!)!' would have about"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LetsThroughWhatIsComputedQuickly()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
foreach (var text in new[] { "2^10000", "10^19999", "5000!", "0.5!", "(3!)!", "x^1000000", "2^x", "pi^1000000", "e^(10^6)", "1^(10^100)", "0^(10^100)", "547 / 57576", "integral(x^3/(e^x-1), x, 0, +oo)" })
|
||||
Assert.DoesNotThrow(() => MathGuards.CheckExactSize(Read(text)), text);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RefusesTheExpansionOfAHighPowerOfASum()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
var refusal = Assert.Throws<MathComputationException>(() => MathGuards.CheckExpansion(Read("1 + (x + y)^500")))!;
|
||||
Assert.That(refusal.Message, Is.EqualTo("'(x + y) ^ 500' raises a sum to the power of 500. Where the tool may expand, a sum is raised to exponents up to 200 only, since it cannot stop a larger expansion midway."));
|
||||
Assert.Throws<MathComputationException>(() => MathGuards.CheckExpansion(Read("(x - 1)^(-300)")));
|
||||
|
||||
foreach (var text in new[] { "(x + y)^200", "x^500", "(x * y)^500", "(x + y)^n" })
|
||||
Assert.DoesNotThrow(() => MathGuards.CheckExpansion(Read(text)), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using AIStudio.Tools.Mathematics;
|
||||
|
||||
using AngouriMath;
|
||||
|
||||
namespace AIStudio.Tests.Tools.Mathematics;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class NumericCheckTests
|
||||
{
|
||||
private static NumericCheckOutcome Check(string left, string right) => NumericCheck.AreEqual(MathExpressionReader.Read(left, "left"), MathExpressionReader.Read(right, "right"));
|
||||
|
||||
[Test]
|
||||
public void PassesWhatIsEqual()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Check("sin(x)^2 + cos(x)^2", "1"), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
Assert.That(Check("(x + y)^2", "x^2 + 2*x*y + y^2"), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
Assert.That(Check("x^4 - 1", "(x + 1)*(x - 1)*(x^2 + 1)"), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
Assert.That(Check("547 / 57576", "1094 / 115152"), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PassesAnExpansionWhoseTermsCancel()
|
||||
{
|
||||
// At the sample point x = 1.47, the terms of the expanded form reach about 10^92 and add up to
|
||||
// about 10^-66; in doubles, or at 50 digits, nothing of the value would be left:
|
||||
var power = MathExpressionReader.Read("(x - 1)^200", "expression");
|
||||
Assert.That(NumericCheck.AreEqual(power, power.Expand()), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FailsWhatDiffers()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Check("x^2", "x^3"), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
Assert.That(Check("(x + y)^2", "x^2 + y^2"), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
Assert.That(Check("pi^4/15", "6.4939394022668"), Is.EqualTo(NumericCheckOutcome.FAILED), "Fourteen digits are not equality.");
|
||||
Assert.That(Check("sin(x)", "sin(x + 2*pi)"), Is.EqualTo(NumericCheckOutcome.PASSED), "pi is a constant, not a variable which takes the values of the sample points.");
|
||||
Assert.That(Check("e^x", "x + 1"), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TellsTinyValuesApart()
|
||||
{
|
||||
// With an absolute tolerance of 1e-30, any two values below it would pass for equal:
|
||||
var x = MathS.Var("x");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Check("6.62607015e-34 * x", "6.62607015e-35 * x"), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
Assert.That(Check("6.62607015e-34 * sin(x)", "6.62607015e-34 * sin(x) * (1 + 1e-20)"), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
Assert.That(Check("6.62607015e-34 * x^2", "x * 6.62607015e-34 * x"), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
Assert.That(Check("sin(2*x) - 2*sin(x)*cos(x)", "0"), Is.EqualTo(NumericCheckOutcome.PASSED), "Against 0, the tolerance stays absolute, since terms which cancel leave their rounding noise.");
|
||||
Assert.That(NumericCheck.IsDerivativeOf(MathExpressionReader.Read("1.32521403e-33 * x", "derivative"), MathExpressionReader.Read("6.62607015e-34 * x^2", "function"), x), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
Assert.That(NumericCheck.IsDerivativeOf(MathExpressionReader.Read("6.62607015e-34 * x", "derivative"), MathExpressionReader.Read("6.62607015e-34 * x^2", "function"), x), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ComparesWhereAConditionHolds()
|
||||
{
|
||||
var simplified = MathExpressionReader.Read("(x^2 - 1)/(x - 1)", "expression").Simplify();
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(simplified, Is.TypeOf<Entity.Providedf>());
|
||||
Assert.That(NumericCheck.AreEqual(simplified, MathExpressionReader.Read("x + 1", "expression")), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsNotPossibleWithoutValues()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Check("integral(sin(sin(x)), x)", "x"), Is.EqualTo(NumericCheckOutcome.NOT_POSSIBLE), "An integral without a closed form has no value.");
|
||||
Assert.That(Check("1/(x - x)", "1/(y - y)"), Is.EqualTo(NumericCheckOutcome.NOT_POSSIBLE), "No sample point has a value.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChecksADerivativeByItsCentralDifference()
|
||||
{
|
||||
var x = MathS.Var("x");
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(NumericCheck.IsDerivativeOf(MathExpressionReader.Read("2*x*sin(x) + x^2*cos(x)", "derivative"), MathExpressionReader.Read("x^2*sin(x)", "function"), x), Is.EqualTo(NumericCheckOutcome.PASSED));
|
||||
Assert.That(NumericCheck.IsDerivativeOf(MathExpressionReader.Read("2*x + 1", "derivative"), MathExpressionReader.Read("x^2", "function"), x), Is.EqualTo(NumericCheckOutcome.FAILED));
|
||||
Assert.That(NumericCheck.IsDerivativeOf(MathExpressionReader.Read("a", "derivative"), MathExpressionReader.Read("a*x + C", "function"), x), Is.EqualTo(NumericCheckOutcome.PASSED), "Other variables, the constant of integration among them, take sample values.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivesAnIntegerParameterIntegerValues()
|
||||
{
|
||||
var parameter = MathS.Var("n_1");
|
||||
var values = Enumerable.Range(0, 6).Select(point => NumericCheck.SampleAssignment(point, [parameter])[parameter]).Distinct().ToList();
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(values, Is.EquivalentTo(new Entity[] { -1, 0, 1 }));
|
||||
Assert.That(NumericCheck.IsIntegerParameter(MathS.Var("n_12")), Is.True);
|
||||
Assert.That(NumericCheck.IsIntegerParameter(MathS.Var("n")), Is.False);
|
||||
Assert.That(NumericCheck.IsIntegerParameter(MathS.Var("n_x")), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheSamplePointsAreFixedAndDifferForEachVariable()
|
||||
{
|
||||
var variables = new[] { MathS.Var("x"), MathS.Var("y"), MathS.Var("z") };
|
||||
var first = NumericCheck.SampleAssignment(0, variables);
|
||||
var again = NumericCheck.SampleAssignment(0, variables);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(again, Is.EqualTo(first));
|
||||
Assert.That(first.Values.Distinct().Count(), Is.EqualTo(3));
|
||||
Assert.That(first[variables[0]], Is.EqualTo(Entity.Number.Rational.Create(61, 100)));
|
||||
Assert.That(NumericCheck.SampleAssignment(1, variables)[variables[0]], Is.Not.EqualTo(first[variables[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Mathematics;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.Mailboxes;
|
||||
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.Mathematics;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Tools.ToolCalling;
|
||||
|
||||
@@ -188,6 +193,39 @@ public sealed class ToolCollectionTests : ToolRegistryTestBase
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheMathematicsCollectionGathersTheToolsWhichCompute()
|
||||
{
|
||||
var definition = new MathematicsToolCollection().GetDefinition();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(definition.Id, Is.EqualTo(ToolSelectionRules.MATHEMATICS_COLLECTION_ID));
|
||||
Assert.That(definition.ToolIds, Is.EqualTo(new[] { ToolSelectionRules.CALCULATE_TOOL_ID, ToolSelectionRules.COMPUTE_SYMBOLICALLY_TOOL_ID }));
|
||||
Assert.That(definition.MinimumProviderConfidence, Is.EqualTo(ConfidenceLevel.NONE), "The tools see nothing but what the model writes into their arguments.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TheMathematicsCollectionAppearsOnlyWithItsPreview()
|
||||
{
|
||||
var computationService = new MathComputationService(NullLogger<MathComputationService>.Instance);
|
||||
IToolImplementation[] tools = [new CalculateTool(this.SettingsManager, computationService), new ComputeSymbolicallyTool(this.SettingsManager, computationService)];
|
||||
var registry = new ToolRegistry(tools, [new CodeToolDefinitionSource(tools)], [new MathematicsToolCollection()], this.SettingsManager, this.CreateToolSettingsService(), NullLogger<ToolRegistry>.Instance);
|
||||
|
||||
var withoutPreview = await registry.GetCatalogAsync(AIStudio.Tools.Components.CHAT);
|
||||
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Add(PreviewFeatures.PRE_MATHEMATICS_2026);
|
||||
var withPreview = await registry.GetCatalogAsync(AIStudio.Tools.Components.CHAT);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(withoutPreview, Is.Empty);
|
||||
Assert.That(withPreview.Select(item => item.Id), Is.EqualTo(new[] { ToolSelectionRules.MATHEMATICS_COLLECTION_ID }), "Both tools appear as one collection.");
|
||||
Assert.That(registry.GetCollectionId(ToolSelectionRules.COMPUTE_SYMBOLICALLY_TOOL_ID), Is.EqualTo(ToolSelectionRules.MATHEMATICS_COLLECTION_ID));
|
||||
Assert.That(registry.ExpandSelection([ToolSelectionRules.MATHEMATICS_COLLECTION_ID]), Is.EquivalentTo(new[] { ToolSelectionRules.CALCULATE_TOOL_ID, ToolSelectionRules.COMPUTE_SYMBOLICALLY_TOOL_ID }));
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> StoredLevels => this.SettingsManager.ConfigurationData.Tools.MinimumProviderConfidenceByToolId;
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in new issue
Block a user