From 95361819ceee978c1aec61fa1d198a6a6fb871b8 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 27 Sep 2026 21:44:39 +0000 Subject: [PATCH] Exponentials of quadratics beside sines, cosines and hyperbolic functions of quadratics are integrated onto the Gaussian A product of exponentials of polynomials of degree at most two, whole powers of sines and cosines of such polynomials, whole powers of sums of those exponentials (the hyperbolic functions) and a polynomial, where some exponent is a quadratic: with each sine or cosine written as exponentials of i times its argument and the product multiplied out, every term is k P(x) e^(Q(x)) with Q of degree at most two, the Gaussian's moments where Q is a quadratic and elementary where it is not. A term's exponent is kept as whole multiples of the exponents the integrand writes, so that e^(i q) e^(-i q) is decidably e^0. Rubi's 4.7.6, 6.1.5 and 6.2.5, 6.1.4's x^2 sinh(a + b x + c x^2)^2, and 2.3's f^(q1) g^(q2). Part of #1501. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 20 + Sources/.editorconfig | 3 + .../Integration/IndefiniteIntegralSolver.cs | 368 ++++++++++++++++++ .../Integration/IntegralPatterns.cs | 9 + .../Integration/Integration.Definition.cs | 3 + .../Calculus/ExponentialsOfQuadraticsTest.cs | 111 ++++++ 6 files changed, 514 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ExponentialsOfQuadraticsTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 65cda0823..31c96b616 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1710,6 +1710,26 @@ since they are these exponentials | `"(p + q*x)^2*f^(a + b*x + c*x^2)".Integrate("x")` | `integral((p + q * x) ^ 2 * f ^ (a + b * x + c * x ^ 2), x)` | the antiderivative | | `"x^2*sinh(a + b*x + c*x^2)".Integrate("x")` | `integral(x ^ 2 * (e ^ (a + b * x + c * x ^ 2) - e ^ (-(a + b * x + c * x ^ 2))) / 2, x)` | the antiderivative | +### Exponentials of quadratics beside sines, cosines and hyperbolic functions of quadratics are integrated + +`e^(x^2) sin(x)` was left unintegrated. A product of exponentials of polynomials of degree at most +two, whole powers of sines, cosines and hyperbolic functions of such polynomials, and a polynomial +is now integrated wherever some exponent is a quadratic. Each sine or cosine is written as +exponentials of `i` times its argument, and every term of the product multiplied out is the +Gaussian's. So the answer for a real integrand with a sine or cosine in it is written with the +imaginary unit, as Rubi's is: `e^(x^2) sin(x)` integrates to +`-i/2 e^(1/4) sqrt(pi)/2 erfi(x + i/2) + i/2 e^(1/4) sqrt(pi)/2 erfi(x - i/2)`. A sine or cosine of a +quadratic alone, or beside a polynomial only, is left to the rules that answered it before +([#1501](https://github.com/asc-community/AngouriMath/issues/1501)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"e^(x^2)*sin(x)".Integrate("x")` | `integral(e ^ x ^ 2 * sin(x), x)` | the antiderivative, with `erfi` | +| `"f^(a + b*x + c*x^2)*cos(d + p*x + q*x^2)".Integrate("x")` | `integral(f ^ (a + b * x + c * x ^ 2) * cos(d + p * x + q * x ^ 2), x)` | the antiderivative, with `erf` | +| `"f^(a + c*x^2)*cosh(d + p*x)^2".Integrate("x")` | `integral(f ^ (a + c * x ^ 2) * ((e ^ (d + p * x) + e ^ (-(d + p * x))) / 2) ^ 2, x)` | the antiderivative | +| `"x^2*sinh(a + b*x + c*x^2)^2".Integrate("x")` | `integral(x ^ 2 * ((e ^ (a + b * x + c * x ^ 2) - e ^ (-(a + b * x + c * x ^ 2))) / 2) ^ 2, x)` | the antiderivative | +| `"2^(x^2)*3^(x + 1)".Integrate("x")` | `integral(2 ^ x ^ 2 * 3 ^ (x + 1), x)` | the antiderivative, with `erfi` | + ### `binomial(n, k)` is a function **Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 3614f5364..47bf3f25e 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -639,3 +639,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/HalfOddPowerOfALogarithmTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + +[Tests/UnitTests/Calculus/ExponentialsOfQuadraticsTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index a3c203e66..4495c922b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -3225,6 +3225,374 @@ private static bool TryReadAMonomial(Entity term, Entity.Variable x, out int deg } } + /// + /// A product of exponentials of polynomials of degree at most two, whole powers of sines + /// and cosines of such polynomials, whole powers of sums of those exponentials -- which is + /// how a hyperbolic function is written -- and a polynomial, where some exponent is left a + /// quadratic. A sine or cosine is written as exponentials, sin(q) = (e^(i q) - e^(-i q))/(2i), + /// and the product multiplied out is a sum of terms k P(x) e^(Q(x)), with Q of + /// degree at most two: the Gaussian's moments where Q is a quadratic, and elementary + /// where it is not. Rubi's 4.7.6, 6.1.5 and 6.2.5, f^(a + b x + c x^2) sin(d + e x + f x^2)^n + /// and f^(a + b x + c x^2) cosh(d + e x + f x^2)^n, 6.1.4's x^2 sinh(a + b x + c x^2)^2, + /// and 2.3's f^(a + b x + c x^2) g^(d + e x + f x^2). + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// + /// + /// + /// A sine or cosine is taken only beside an exponential. Alone, or beside a polynomial, + /// the sine of a quadratic is a Fresnel integral, and x sin(x^2) is -cos(x^2)/2, + /// which the substitution answers with no imaginary unit in it. A sum of exponentials is + /// taken beside another exponential, or in a power: x^2 sinh(q) alone is a sum the + /// integrator splits, and each of its terms is the Gaussian's. + /// + /// + /// A term's exponent is kept as whole multiples of the exponents the integrand writes, not + /// as their sum: e^(i q) times e^(-i q) is then the multiple 0, decidably, + /// where the sum i q + (-i) q is not read as zero, and was taken for a Gaussian + /// whose square's coefficient is zero. + /// + /// + internal static Entity? SolveAProductOfExponentialsOfQuadratics(Entity expr, Entity.Variable x) + { + if (expr is not (Mulf or Divf or Powf)) + return null; + // By the written shape first, which reads and allocates nothing: most products asked + // here have one exponential in them, or none, and no sine. + int exponentials = 0, trigonometrics = 0; + var aPowerOfASum = false; + CountFactorsByShape(expr, x, ref exponentials, ref trigonometrics, ref aPowerOfASum); + if (trigonometrics > 0 ? exponentials == 0 : exponentials < 2 && !aPowerOfASum) + return null; + (exponentials, trigonometrics, aPowerOfASum) = (0, 0, false); + Entity constant = Number.Integer.One; + Entity? polynomial = null; + var exponents = new List(); + var terms = new List<(Entity Coefficient, int[] Multiples)> { (Number.Integer.One, new int[MostExponents]) }; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + if (AsExponentialsOfQuadratics(factor, x, exponents) is { } read) + { + // Below the bar, a single exponential only, whose reciprocal is one too. + if (underneath) + { + if (read.Count != 1) + return null; + var (coefficient, multiples) = read[0]; + for (var i = 0; i < multiples.Length; i++) + multiples[i] = -multiples[i]; + read[0] = (1 / coefficient, multiples); + } + if (factor.Nodes.Any(node => node is Sinf or Cosf)) + trigonometrics++; + else + exponentials++; + aPowerOfASum |= read.Count > 1 && factor is Powf(_, Number.Integer { EInteger: var power }) && power.CompareTo(EInteger.One) > 0; + if (Multiplied(terms, read) is not { } product) + return null; + terms = product; + continue; + } + if (!underneath && TreeAnalyzer.TryGetPolynomial(factor, x, out var inX) + && inX.Keys.All(degree => degree.Sign >= 0 && degree.CompareTo(EInteger.FromInt32(MostPowerOfAPolynomialBesideExponentials)) <= 0)) + { + polynomial = polynomial is null ? factor : polynomial * factor; + continue; + } + return null; + } + if (trigonometrics > 0 ? exponentials == 0 : exponentials < 2 && !aPowerOfASum) + return null; + var quadratics = new List<(Entity Coefficient, Entity A, Entity B, Entity C)>(terms.Count); + foreach (var (coefficient, multiples) in terms) + { + var (a, b, c) = TheExponent(multiples, exponents); + quadratics.Add((coefficient, a, b, c)); + } + // Some exponent left a quadratic: with none, the product is the elementary + // exponential's, and the rules for that answer it without an imaginary unit. + if (!quadratics.Any(term => !TreeAnalyzer.IsZero(term.A))) + return null; + Dictionary? monomials = null; + if (polynomial is not null && !TreeAnalyzer.TryGetPolynomial(polynomial, x, out monomials)) + return null; + Entity total = Number.Integer.Zero; + foreach (var (coefficient, a, b, c) in quadratics) + { + Entity? integral; + if (!TreeAnalyzer.IsZero(a)) + integral = IntegralPatterns.APolynomialTimesTheGaussian(polynomial, MathS.e, a, b, c, x); + else if (!TreeAnalyzer.IsZero(b)) + integral = MathS.Pow(MathS.e, b * x + c) * APolynomialAgainstAnExponential(monomials, b, x); + else + integral = MathS.Pow(MathS.e, c) * APolynomialIntegrated(monomials, x); + if (integral is null) + return null; + total += coefficient * integral; + } + return (constant * total).InnerSimplified; + } + + /// + /// An exponent the integrand writes, A x^2 + B x + C once read, of the exponential + /// Base^Written, or of e^(i Written) for a sine or cosine of it. + /// + private sealed record WrittenExponent(Entity Base, Entity Written, bool Imaginary, Entity A, Entity B, Entity C); + + private const int MostExponents = 6; + private const int MostPowerOfAPolynomialBesideExponentials = 32; + private const int MostPowerOfASumOfExponentials = 8; + private const int MostExponentialTerms = 64; + + /// + /// Counts the factors of by their written shape alone: a sine or + /// cosine, or a power of one; an exponential of ; and a sum, which may + /// be one of exponentials, or a whole power of a sum. An over-count, which the reading + /// that follows corrects: (1 + x) is counted as a sum. + /// + private static void CountFactorsByShape(Entity expr, Entity.Variable x, ref int exponentials, ref int trigonometrics, ref bool aPowerOfASum) + { + switch (expr) + { + case Mulf(var left, var right): + CountFactorsByShape(left, x, ref exponentials, ref trigonometrics, ref aPowerOfASum); + CountFactorsByShape(right, x, ref exponentials, ref trigonometrics, ref aPowerOfASum); + break; + case Divf(var numerator, var denominator): + CountFactorsByShape(numerator, x, ref exponentials, ref trigonometrics, ref aPowerOfASum); + CountFactorsByShape(denominator, x, ref exponentials, ref trigonometrics, ref aPowerOfASum); + break; + case Sinf or Cosf or Powf(Sinf or Cosf, Number.Integer): + trigonometrics++; + break; + case Powf(var @base, var exponent) when !@base.ContainsNode(x) && exponent.ContainsNode(x): + case Sumf or Minusf: + exponentials++; + break; + case Powf(Sumf or Minusf or Mulf or Divf, Number.Integer { EInteger: var power }) when power.CompareTo(EInteger.One) > 0: + exponentials++; + aPowerOfASum = true; + break; + } + } + + /// + /// as a sum of terms k e^(m_1 Q_1 + m_2 Q_2 + ...), where the + /// Q_j are and the m_j whole, or + /// : an exponential of a polynomial of degree at most two, a sine or + /// cosine of one, and sums, products, quotients by a constant and small whole powers of those. + /// + private static List<(Entity Coefficient, int[] Multiples)>? AsExponentialsOfQuadratics(Entity expr, Entity.Variable x, List exponents) + { + switch (expr) + { + case var constant when !constant.ContainsNode(x): + return new() { (constant, new int[MostExponents]) }; + case Powf(var @base, var exponent) when !@base.ContainsNode(x): + { + // e^(-q), as sinh(q) writes it, is -1 times the exponent of its e^q. + var (written, multiple) = exponent switch + { + Mulf(Number.Integer k, var inner) when k.EInteger.Abs().CompareTo(EInteger.FromInt32(MostPowerOfASumOfExponentials)) <= 0 => (inner, k.EInteger.ToInt32Checked()), + Mulf(var inner, Number.Integer k) when k.EInteger.Abs().CompareTo(EInteger.FromInt32(MostPowerOfASumOfExponentials)) <= 0 => (inner, k.EInteger.ToInt32Checked()), + _ => (exponent, 1) + }; + if (TheWrittenExponent(@base, written, imaginary: false, x, exponents) is not { } index) + return null; + var multiples = new int[MostExponents]; + multiples[index] = multiple; + return new() { (Number.Integer.One, multiples) }; + } + // sin(q) = (e^(i q) - e^(-i q))/(2i) and cos(q) = (e^(i q) + e^(-i q))/2. + case Sinf(var argument): + return AnImaginaryPair(-MathS.i / 2, MathS.i / 2, argument, x, exponents); + case Cosf(var argument): + return AnImaginaryPair(Number.Rational.Create(1, 2), Number.Rational.Create(1, 2), argument, x, exponents); + case Sumf(var left, var right): + { + if (AsExponentialsOfQuadratics(left, x, exponents) is not { } l || AsExponentialsOfQuadratics(right, x, exponents) is not { } r) + return null; + l.AddRange(r); + return l.Count > MostExponentialTerms ? null : l; + } + case Minusf(var left, var right): + { + if (AsExponentialsOfQuadratics(left, x, exponents) is not { } l || AsExponentialsOfQuadratics(right, x, exponents) is not { } r) + return null; + foreach (var (coefficient, multiples) in r) + l.Add((-coefficient, multiples)); + return l.Count > MostExponentialTerms ? null : l; + } + case Mulf(var left, var right): + { + if (AsExponentialsOfQuadratics(left, x, exponents) is not { } l || AsExponentialsOfQuadratics(right, x, exponents) is not { } r) + return null; + return Multiplied(l, r); + } + case Divf(var numerator, var denominator) when !denominator.ContainsNode(x): + { + if (AsExponentialsOfQuadratics(numerator, x, exponents) is not { } n) + return null; + for (var i = 0; i < n.Count; i++) + n[i] = (n[i].Coefficient / denominator, n[i].Multiples); + return n; + } + case Powf(var inner, Number.Integer power) when power.EInteger.Sign > 0 + && power.EInteger.CompareTo(EInteger.FromInt32(MostPowerOfASumOfExponentials)) <= 0: + { + if (AsExponentialsOfQuadratics(inner, x, exponents) is not { } once) + return null; + var result = once; + for (var k = power.EInteger.ToInt32Checked(); k > 1 && result is not null; k--) + result = Multiplied(result, once); + return result; + } + default: + return null; + } + } + + /// + /// The index of Base^Written, or of e^(i Written), among + /// , read and added where it is not there yet, or + /// where it is not a polynomial of degree at most two. + /// + private static int? TheWrittenExponent(Entity @base, Entity written, bool imaginary, Entity.Variable x, List exponents) + { + for (var i = 0; i < exponents.Count; i++) + if (exponents[i].Imaginary == imaginary && exponents[i].Base == @base && exponents[i].Written == written) + return i; + if (exponents.Count == MostExponents || !written.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyQuadratic(written, x, out var a, out var b, out var c)) + return null; + // A zero coefficient is left the integer zero: 0 ln(f), simplified, is 0 provided f is + // not 0, which is not read as zero, and a Gaussian was taken of 0 x^2. + Entity scale = imaginary ? MathS.i : @base == MathS.e ? Number.Integer.One : MathS.Ln(@base); + exponents.Add(new(@base, written, imaginary, Scaled(a, scale), Scaled(b, scale), Scaled(c, scale))); + return exponents.Count - 1; + + static Entity Scaled(Entity coefficient, Entity scale) + => TreeAnalyzer.IsZero(coefficient) ? Number.Integer.Zero + : scale == Number.Integer.One ? coefficient : (scale * coefficient).InnerSimplified; + } + + /// + /// k_+ e^(i q) + k_- e^(-i q) for the q, or + /// where it is not a polynomial of degree at most two. + /// + private static List<(Entity Coefficient, int[] Multiples)>? AnImaginaryPair(Entity plus, Entity minus, Entity argument, Entity.Variable x, List exponents) + { + if (TheWrittenExponent(MathS.e, argument, imaginary: true, x, exponents) is not { } index) + return null; + var (up, down) = (new int[MostExponents], new int[MostExponents]); + (up[index], down[index]) = (1, -1); + return new() { (plus, up), (minus, down) }; + } + + /// + /// The product of two sums of exponentials, with the terms of one exponent gathered, and + /// past . + /// + private static List<(Entity Coefficient, int[] Multiples)>? Multiplied(List<(Entity Coefficient, int[] Multiples)> left, List<(Entity Coefficient, int[] Multiples)> right) + { + if (left.Count * right.Count > MostExponentialTerms) + return null; + var gathered = new List<(Entity Coefficient, int[] Multiples)>(); + foreach (var l in left) + foreach (var r in right) + { + var multiples = new int[MostExponents]; + for (var i = 0; i < multiples.Length; i++) + multiples[i] = l.Multiples[i] + r.Multiples[i]; + var coefficient = l.Coefficient * r.Coefficient; + var at = gathered.FindIndex(term => term.Multiples.SequenceEqual(multiples)); + if (at < 0) + gathered.Add((coefficient, multiples)); + else + gathered[at] = (gathered[at].Coefficient + coefficient, multiples); + } + var product = new List<(Entity Coefficient, int[] Multiples)>(gathered.Count); + foreach (var (coefficient, multiples) in gathered) + { + var simplified = coefficient.InnerSimplified; + if (!TreeAnalyzer.IsZero(simplified)) + product.Add((simplified, multiples)); + } + return product; + } + + /// + /// (A, B, C) of the exponent sum_j m_j Q_j, with the zero multiples left out, + /// so that a term with none is decidably e^0. + /// + private static (Entity A, Entity B, Entity C) TheExponent(int[] multiples, List exponents) + { + Entity a = Number.Integer.Zero, b = Number.Integer.Zero, c = Number.Integer.Zero; + for (var i = 0; i < exponents.Count; i++) + { + if (multiples[i] == 0) + continue; + var (m, exponent) = (multiples[i], exponents[i]); + a = Plus(a, m, exponent.A); + b = Plus(b, m, exponent.B); + c = Plus(c, m, exponent.C); + } + return (a.InnerSimplified, b.InnerSimplified, c.InnerSimplified); + + static Entity Plus(Entity sum, int multiple, Entity coefficient) + { + if (coefficient == Number.Integer.Zero) + return sum; + var term = multiple == 1 ? coefficient : multiple * coefficient; + return sum == Number.Integer.Zero ? term : sum + term; + } + } + + /// + /// int P(x) e^(b x) dx over e^(b x): sum_j (-1)^j P^(j)(x)/b^(j + 1), by + /// parts, for P read into , or 1 where they are absent. + /// + private static Entity APolynomialAgainstAnExponential(Dictionary? monomials, Entity rate, Entity.Variable x) + { + if (monomials is null) + return 1 / rate; + Entity sum = Number.Integer.Zero; + foreach (var monomial in monomials) + { + var power = monomial.Key.ToInt32Checked(); + // The j-th derivative of x^k is k!/(k - j)! x^(k - j). + Entity falling = Number.Integer.One; + for (var j = 0; j <= power; j++) + { + var term = monomial.Value * falling * MathS.Pow(x, power - j) / MathS.Pow(rate, j + 1); + sum = j % 2 == 0 ? sum + term : sum - term; + falling *= power - j; + } + } + return sum; + } + + /// + /// int P(x) dx for P read into , or x where + /// they are absent. + /// + private static Entity APolynomialIntegrated(Dictionary? monomials, Entity.Variable x) + { + if (monomials is null) + return x; + Entity sum = Number.Integer.Zero; + foreach (var monomial in monomials) + { + var power = monomial.Key.ToInt32Checked() + 1; + sum += monomial.Value * MathS.Pow(x, power) / power; + } + return sum; + } + /// /// A polynomial times an exponential times a sine or a cosine — /// P(x) e^(a x) cos(b x) and its kin — integrated by the repeated by-parts that diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs index 19acdee79..4c9e9d394 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs @@ -85,6 +85,15 @@ private static Entity Gaussian(Entity @base, Entity a, Entity b, Entity c, Entit return MathS.Pow(@base, c - b * b / (4 * a)) * UnitGaussian(a * MathS.Ln(@base), x + b / (2 * a)); } + /// + /// int P(x) F^(a x^2 + b x + c) dx for a non-zero a and a polynomial P, + /// which may be absent: the Gaussian, or the sum of its moments at the square's centre. + /// + internal static Entity? APolynomialTimesTheGaussian(Entity? polynomial, Entity @base, Entity a, Entity b, Entity c, Entity.Variable x) + => polynomial is null + ? Gaussian(@base, a, b, c, x) + : ShiftedMoments(Entity.Number.Integer.One, 0, polynomial, @base, a, b, c, x); + /// /// int e^(A u^2) du for a non-zero A: sqrt(pi)/(2 sqrt(-A)) erf(sqrt(-A) u), /// or with erfi and the real root where A is decidably positive. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 715a162fd..af3cc8db1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -580,6 +580,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => if ((answer = IndefiniteIntegralSolver.SolveByWritingAnImaginaryTangentAsAnExponential(expr, x, integrateByParts)) is { }) return answer; // And `A cos(z) + i A sin(z)`, which is `A e^(i z)`, where no rotation is real. if ((answer = IndefiniteIntegralSolver.SolveByWritingAnImaginarySumOfACosineAndASineAsAnExponential(expr, x, integrateByParts)) is { }) return answer; + // Exponentials of quadratics, with sines, cosines and sums of them multiplied out: + // each term is the Gaussian's, where the search would take the product by parts. + if ((answer = IndefiniteIntegralSolver.SolveAProductOfExponentialsOfQuadratics(expr, x)) is { }) return answer; // A fractional power of a perfect square is the power of the modulus, sgn(P) P^(2r). if ((answer = IndefiniteIntegralSolver.SolveByTakingARootOfAPerfectSquare(expr, x, integrateByParts)) is { }) return answer; // x^(n - 1) g(x^n) with a symbolic n is g(u)/n under u = x^n. diff --git a/Sources/Tests/UnitTests/Calculus/ExponentialsOfQuadraticsTest.cs b/Sources/Tests/UnitTests/Calculus/ExponentialsOfQuadraticsTest.cs new file mode 100644 index 000000000..4f37a3e28 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ExponentialsOfQuadraticsTest.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using System.Linq; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A product of exponentials of quadratics, with sines, cosines and hyperbolic functions of + /// quadratics beside them, multiplied out into exponentials: each term is the Gaussian's. + /// https://github.com/asc-community/AngouriMath/issues/1501 + /// + [Trait("Area", "Calculus")] + public sealed class ExponentialsOfQuadraticsTest + { + private static readonly double[] Points = { -1.3, -0.6, 0.35, 0.9, 1.45 }; + + /// + /// Integrates, pins the parameters, and compares the derivative of the answer with the + /// integrand at . The parameters are pinned after integrating, so the + /// rule is asked the symbolic question. + /// + private static Entity DifferentiatesBack(string integrand, params (string Name, string Value)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => pins.Aggregate(e, (current, pin) => current.Substitute(pin.Name, pin.Value.ToEntity())); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in Points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart)); + Assert.True(difference / scale < 1e-9, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + return integral; + } + + private static readonly (string, string)[] Pins = + { ("a", "1/3"), ("b", "1/2"), ("c", "-1"), ("d", "2/3"), ("f", "2"), ("g", "3"), ("p", "3/4"), ("q", "1/5") }; + + /// + /// Rubi's 4.7.6: a sine or cosine written as exponentials of the imaginary unit times its + /// argument, so the answer holds an error function of a complex argument. + /// + [Theory] + [InlineData("e^(x^2)*sin(x)")] + [InlineData("e^(x^2)*cos(2*x + 1)")] + [InlineData("e^x*sin(x^2)")] + [InlineData("e^x*cos(1 + x + x^2)")] + [InlineData("2^(x^2 + x)*cos(x^2)")] + [InlineData("e^(-x^2)*sin(x)^2")] + [InlineData("e^x*cos(x^2 + x)^3")] + [InlineData("x*e^(x^2)*sin(x)")] + [InlineData("f^(a + b*x + c*x^2)*sin(d + p*x + q*x^2)")] + [InlineData("f^(a + b*x)*cos(d + q*x^2)^2")] + [InlineData("f^(a + c*x^2)*sin(d + p*x)^3")] + public void ASineOrCosineBesideAnExponential(string integrand) + => DifferentiatesBack(integrand, Pins); + + /// Rubi's 6.1.5 and 6.2.5, where every exponent is real. + [Theory] + [InlineData("e^x*sinh(x^2)")] + [InlineData("2^(x^2)*cosh(x + 1)")] + [InlineData("e^(x^2)*sinh(x)^2")] + [InlineData("f^(a + b*x + c*x^2)*sinh(d + p*x + q*x^2)")] + [InlineData("f^(a + c*x^2)*cosh(d + p*x)^2")] + [InlineData("f^(a + b*x)*sinh(d + q*x^2)^3")] + public void AHyperbolicBesideAnExponential(string integrand) + => DifferentiatesBack(integrand, Pins); + + /// + /// Rubi's 6.1.4 and 6.2.4: a power of a hyperbolic function of a quadratic beside a + /// polynomial, which was taken by parts past the budget. + /// + [Theory] + [InlineData("x^2*sinh(1 + x + x^2)^2")] + [InlineData("(1 + 2*x)^2*cosh(x - x^2)^2")] + [InlineData("x^2*cosh(a + b*x + c*x^2)^2")] + public void APowerOfAHyperbolicBesideAPolynomial(string integrand) + => DifferentiatesBack(integrand, Pins); + + /// Rubi's 2.3: two exponentials of quadratics, of different bases. + [Theory] + [InlineData("2^(x^2)*3^(x + 1)")] + [InlineData("f^(a + b*x + c*x^2)*g^(d + p*x + q*x^2)")] + public void TwoExponentials(string integrand) + => DifferentiatesBack(integrand, Pins); + + /// + /// A sine or cosine beside a polynomial only, or of a linear argument beside an + /// exponential of one, is left to the rules that answer it without an imaginary unit. + /// + [Theory] + [InlineData("x*sin(x^2)")] + [InlineData("e^x*sin(2*x)")] + [InlineData("e^(2*x)*cos(x)^2")] + public void ARealAnswerStaysReal(string integrand) + => Assert.DoesNotContain(DifferentiatesBack(integrand).Nodes, node => node == MathS.i); + } +}