diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 415a76396..e00978a73 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -111,6 +111,61 @@ read first. | | `"sum(k, k, 1, n)".ToEntity().Simplify()`, and every summation whose body is a polynomial in the index | `sum(k, k, 1, n)` — carried | `piecewise((n + n ^ 2) / 2 provided n >= 0, 0)` | | | `"sum(k, k, 1, 100000)".ToEntity().Simplify()`, and every concrete range past a hundred terms | `sum(k, k, 1, 100000)` — carried | `5000050000` | | | `"product(k, k, 1, n)".ToEntity().Simplify()`, and every product whose body is a monomial in the index | `product(k, k, 1, n)` — carried | `piecewise(n! provided n >= 1, 1)` | +| | `"1/(x^2 + 1)^2".Integrate("x")`, and every proper rational function over a repeated irreducible quadratic | `integral(1 / (1 + x ^ 2) ^ 2, x)` — left unevaluated, after 2.9 s | `arctan(x) / 2 + C + 1/2 * x / (x ^ 2 + 1)`, in 0.11 s | +| | `"x^2/(x^2 + 2)^2".Integrate("x")`, and every polynomial numerator over one | `integral(x ^ 2 / (x ^ 2 + 2) ^ 2, x)` — left unevaluated, after 93 s | the antiderivative, in 0.57 s | +| | `"1/(x^2 - 1)^2".Integrate("x")`, and every repeated quadratic whose roots are real | `C - ln(x + -1) / 4 + ln(1 + 2 * x + x ^ 2) / 8 + -1/2 * x / (x ^ 2 + -1)` | `C - ln(1 + (-2) / (x + 1)) / 4 + -1/2 * x / (x ^ 2 + -1)` — the same value, one logarithm rather than two | + +### A repeated quadratic denominator is integrated + +`1/(x^2 + 1)^2` had no antiderivative, while `1/(x^2 - 1)^2` and `1/(x^2 + 2x + 1)^2` both did — a +denominator with real roots comes apart into linear factors and never reached the gap. What was +missing was the irreducible case +([#180](https://github.com/asc-community/AngouriMath/issues/180)). + +``` +"1/(x^2 + 1)^2".Integrate("x") was integral(1 / (1 + x ^ 2) ^ 2, x) + is arctan(x) / 2 + C + 1/2 * x / (x ^ 2 + 1) +"x^2/(x^2 + 2)^2".Integrate("x") was integral(x ^ 2 / (x ^ 2 + 2) ^ 2, x) + is (sqrt(2) * arctan(sqrt(2) * x / 2) * 2 + (-4) * x / (x ^ 2 + 2)) / 8 + + C provided not x ^ 2 + 2 = 0 +``` + +The condition on that second one is the library's own and it is true: the antiderivative has no +value where the denominator vanishes. It is attached to some of these answers and not others, and I +did not pin down what decides which — recorded as observed rather than explained. + +Writing `Q` for the quadratic, `u` for its derivative `2ax + b` and `D` for `4ac - b^2`, the +identity `u^2 = 4aQ - D` turns the derivative of `u/Q^(m-1)` into an equation for the integral of +`1/Q^m`, which is unrolled down to the single power the table already answered. It is an identity in +`a`, `b` and `c` rather than a fact about signs, so one line serves both signs of the discriminant; +`D = 0` is the sole exclusion, and the division by `D` is why. A numerator of higher degree is +divided by `Q` first. + +**A denominator with real roots now takes this route rather than partial fractions.** The value is +unchanged and the form is shorter — one logarithm where there were two — which is why the +node-count metric that ranks candidates selects it. No test pinned the old spelling. + +**The cost, measured rather than estimated.** Where the integrand is rational this is between 26 and +165 times faster, because `TryStandardIntegrals` runs before every search and a shape answered +outright never enters the candidate exploration. Where the integrand is a transcendental function +over a repeated quadratic — `sin(x)/(x^2 + 1)^2`, `e^x/(x^2 + 1)^2`, `ln(x)/(x^2 + 1)^2`, none of +which has an elementary antiderivative — the same search now takes **3.4 to 10.8 times longer to +conclude nothing**, because the sub-integral it used to fail on immediately now succeeds and +integration by parts carries on from a larger expression. Both arms measured on one machine, master +and branch, the same way: + +| | master | now | +|---|---|---| +| `1/(x^2 + 1)^2` | 2879 ms, unevaluated | **111 ms, answered** | +| `x^2/(x^2 + 2)^2` | 92 990 ms, unevaluated | **565 ms, answered** | +| `1/(x^4 + 1)^2` | 3811 ms | 3870 ms — the base is not quadratic, and nothing changes | +| `ln(x)/(x^2 + 1)^2` | 15 034 ms, unevaluated | 51 072 ms, unevaluated | +| `sin(x)/(x^2 + 1)^2` | 26 492 ms, unevaluated | 114 851 ms, unevaluated | +| `e^x/(x^2 + 1)^2` | 7944 ms, unevaluated | 85 683 ms, unevaluated | + +That last group is a growth in integration by parts rather than in this rule, which those integrands +never match; it is filed separately rather than fixed here, since bounding by-parts is a change to +machinery every integral goes through. ### A nested radical comes apart diff --git a/Sources/.editorconfig b/Sources/.editorconfig index febbf95b7..35cd5c5f6 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -189,3 +189,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/PatternsTest/DenestRadicalTest.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/RepeatedQuadraticIntegralTest.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/IntegralPatterns.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs index a5fd1dff5..a3ad55db2 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IntegralPatterns.cs @@ -223,6 +223,18 @@ _ when TryReadOverSquareTimesRoot(expr, x, out var overFactor, out var overRadic && a.Evaled is Entity.Number.Complex { IsZero: false } => IntegrateLinearOverQuadratic(p, q, a, b, c, denominator, x), + // ∫ N(x)/(ax^2 + bx + c)^n dx for a whole n of two or more and a proper fraction. + // After the single-power arms above, which already read a denominator like + // (x + 1)^2 -- expanded, it is a quadratic, so the perfect-square arm there answers + // it and nothing about that changes. + Entity.Divf(var numerator, Entity.Powf(var repeated, Entity.Number.Integer repetitions)) when + repetitions.EInteger.CanFitInInt32() + && repetitions.EInteger.ToInt32Checked() is var n and >= 2 + && TreeAnalyzer.TryGetPolyQuadratic(repeated, x, out var qa, out var qb, out var qc) + && (!numerator.ContainsNode(x) || qa.Evaled is Entity.Number.Complex { IsZero: false }) + && IntegrateRationalOverPowerOfQuadratic(numerator, qa, qb, qc, repeated, n, x) is { } repeatedAnswer + => repeatedAnswer, + // ∫ (px + q)/(bx + c) dx, the same rewrite one degree down: the quotient is the // constant p/b plus a remainder over the divisor. x/(x + 1) had no antiderivative. Entity.Divf(var numerator, var denominator) when @@ -529,5 +541,170 @@ private static Entity IntegrateRationalQuadratic(Entity numerator, Entity a, Ent new Entity.Providedf(lnCase, discriminant < 0) ]).InnerSimplified; } + + /// + /// ∫ k / (a x^2 + b x + c)^n dx for a whole n of two or more, by the reduction that + /// takes one power off the denominator at a time. + /// + /// + /// + /// Write Q for the quadratic, u for its derivative 2ax + b and + /// D for 4ac - b^2. Then u^2 is 4aQ - D, so differentiating + /// u / Q^(m-1) gives 2a(3 - 2m)/Q^(m-1) + (m-1)D/Q^m, and reading that as + /// an equation for the integral of 1/Q^m leaves + /// + /// J_m = (u / Q^(m-1) + 2a(2m - 3) J_(m-1)) / ((m - 1) D) + /// + /// which is unrolled down to J_1, the case the table above already answers. It is + /// an identity in a, b and c rather than a fact about signs, so one line serves a + /// positive and a negative discriminant alike; D = 0 is the only exclusion, and + /// the division by D is why. + /// + /// + /// The two shapes the reduction cannot speak for get their own arms, and each is + /// elementary: with a = 0 the denominator is a linear power, and with + /// D = 0 the quadratic is u^2/(4a) so the whole integrand is a power of + /// u. Both are already answered for a numeric quadratic — by the power rule and + /// by partial fractions — and the arms are here for a symbolic one, where the + /// discriminant's sign is not known and dropping an arm is not available. + /// + /// + /// What was missing was the irreducible case. 1/(x^2 - 1)^2 had an antiderivative + /// and 1/(x^2 + 2x + 1)^2 had one, because a denominator with real roots comes + /// apart into linear factors and never reaches here; 1/(x^2 + 1)^2 had none. + /// https://github.com/asc-community/AngouriMath/issues/180 + /// + /// + private static Entity IntegrateOverPowerOfQuadratic( + Entity numerator, Entity a, Entity b, Entity c, int power, Entity.Variable x) + { + var quadratic = a * x * x + b * x + c; + var derivative = 2 * a * x + b; + var discriminant = 4 * a * c - b * b; + + // a = 0: k/(bx + c)^n, an ordinary power -- and the constant k/c^n when b is zero + // too, where writing the power would divide by it. Same reason as the rule above. + var linearCase = TreeAnalyzer.IsZero(b) + ? numerator * x / MathS.Pow(c, power) + : numerator * MathS.Pow(b * x + c, 1 - power) / (b * (1 - power)); + + // D = 0: the quadratic is u^2/(4a), so the integrand is k(4a)^n / u^(2n). + var perfectSquareCase = + numerator * MathS.Pow(4 * a, power) * MathS.Pow(derivative, 1 - 2 * power) + / (2 * a * (1 - 2 * power)); + + var sqrtDiscriminant = MathS.Sqrt(discriminant); + var sqrtNegDiscriminant = MathS.Sqrt(-discriminant); + + return MathS.Piecewise([ + new Entity.Providedf(linearCase, a.EqualTo(0)), + new Entity.Providedf(perfectSquareCase, discriminant.EqualTo(0)), + new Entity.Providedf( + Reduce(2 * MathS.Arctan(derivative / sqrtDiscriminant) / sqrtDiscriminant), + discriminant > 0), + new Entity.Providedf( + Reduce(AntiderivativeLog((derivative - sqrtNegDiscriminant) / (derivative + sqrtNegDiscriminant)) + / sqrtNegDiscriminant), + discriminant < 0) + ]).InnerSimplified; + + Entity Reduce(Entity firstPower) + { + var accumulated = firstPower; + for (var m = 2; m <= power; m++) + accumulated = + (derivative / MathS.Pow(quadratic, m - 1) + 2 * a * (2 * m - 3) * accumulated) + / ((m - 1) * discriminant); + return numerator * accumulated; + } + } + + /// + /// ∫ (px + q)/(a x^2 + b x + c)^n dx, by the same rewrite of the numerator the single + /// power uses: px + q = (p/2a)(2ax + b) + (q - pb/2a). The first part is the + /// denominator's own derivative over a power of it, which integrates as a power; the + /// second is the constant-numerator case above. + /// + /// + /// A numerator that is already a multiple of that derivative leaves nothing of the + /// second part, and it is dropped rather than multiplied by zero. Multiplying is not + /// harmless: the reduction is a quotient by the quadratic, so 0 * (2x/(x^2 + 1)) + /// is 0 provided not 1 + x^2 = 0 and not 0 — correctly, since the factor + /// has no value there. Carrying that condition would attach it to the answer for + /// x/(x^2 + 1)^2, which had none before and is owed none. + /// + private static Entity IntegrateLinearOverPowerOfQuadratic( + Entity p, Entity q, Entity a, Entity b, Entity c, Entity quadratic, int power, Entity.Variable x) + { + var alongTheDerivative = p / (2 * a) * MathS.Pow(quadratic, 1 - power) / (1 - power); + var constantPart = q - p * b / (2 * a); + return TreeAnalyzer.IsZero(constantPart) + ? alongTheDerivative + : alongTheDerivative + IntegrateOverPowerOfQuadratic(constantPart, a, b, c, power, x); + } + + /// + /// ∫ N(x)/(a x^2 + b x + c)^n dx for a numerator of any degree, or + /// where the fraction is improper and this is not the rule for it. + /// + /// + /// + /// Dividing the numerator by the quadratic writes N = qQ + r with r linear + /// at worst, so N/Q^n is q/Q^(n-1) + r/Q^n — one power off the denominator + /// and two degrees off the numerator at each step, which ends. The remainder is answered + /// by the two helpers above and the quotient goes round again. + /// + /// + /// Improper fractions are declined rather than divided out here. The recursion + /// bottoms out at a single power, and a numerator still of degree two or more there is + /// exactly the improper case, which + /// already opens with + /// . Declining hands it there instead of + /// answering it twice, in two shapes, from two places. + /// + /// + /// This is not only reach. TryStandardIntegrals runs before every search, so a + /// shape answered here never reaches the candidate exploration that re-integrates + /// rewritten forms — which is where this family's cost has always been. Both + /// x^2/(x^2 + 2)^2 and (x^2 + 1)/(x^2 + 2)^2 were tens of seconds spent to + /// return the integral unevaluated. + /// + /// + private static Entity? IntegrateRationalOverPowerOfQuadratic( + Entity numerator, Entity a, Entity b, Entity c, Entity quadratic, int power, Entity.Variable x) + { + if (!numerator.ContainsNode(x)) + return power == 1 + ? IntegrateRationalQuadratic(numerator, a, b, c, x) + : IntegrateOverPowerOfQuadratic(numerator, a, b, c, power, x); + + if (TreeAnalyzer.TryGetPolyLinear(numerator, x, out var p, out var q)) + return power == 1 + ? IntegrateLinearOverQuadratic(p, q, a, b, c, quadratic, x) + : IntegrateLinearOverPowerOfQuadratic(p, q, a, b, c, quadratic, power, x); + + if (power < 2) + return null; + + var division = TreeAnalyzer.PolynomialLongDivision(numerator, quadratic); + if (division is null) + return null; + + // The quotient is what is wanted; the remainder that comes back with it is already + // divided by the divisor -- x^2 over x^2 + 2 answers (1, -2/(x^2 + 2)) -- so taking + // it as a polynomial and dividing it again is a numerator that is not one, and the + // next division declines. Subtracting gives the polynomial remainder whatever shape + // the division chose to report. + // Reduced before either is used: the division reports its quotient built up term by + // term, so x^2 over x^2 + 2 comes back as `0 + 1` rather than `1`, and the arms below + // ask whether a numerator is constant. + var quotient = division.Value.Divided.InnerSimplified; + var remainder = (numerator - quotient * quadratic).InnerSimplified; + + return IntegrateRationalOverPowerOfQuadratic(remainder, a, b, c, quadratic, power, x) is { } head + && IntegrateRationalOverPowerOfQuadratic(quotient, a, b, c, quadratic, power - 1, x) is { } rest + ? head + rest + : null; + } } } diff --git a/Sources/Tests/UnitTests/Calculus/RepeatedQuadraticIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RepeatedQuadraticIntegralTest.cs new file mode 100644 index 000000000..31c443c45 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/RepeatedQuadraticIntegralTest.cs @@ -0,0 +1,155 @@ +// +// 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 AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A quadratic denominator raised to a power. 1/(x^2 + 1)^2 had no antiderivative, + /// while 1/(x^2 - 1)^2 and 1/(x^2 + 2x + 1)^2 both did — a denominator with + /// real roots comes apart into linear factors and never reached the gap. + /// https://github.com/asc-community/AngouriMath/issues/180 + /// + [Trait("Area", "Calculus")] + public sealed class RepeatedQuadraticIntegralTest + { + /// + /// Differentiates the answer back and compares it with the integrand at points. The + /// guard on integral( is the whole point of the helper: differentiating an + /// unevaluated integral(f, x) gives back f, so without it every case + /// the rule declines would pass. + /// + private static Entity AssertIsAntiderivative(string integrand, params double[] points) + { + var f = integrand.ToEntity(); + var antiderivative = f.Integrate("x"); + Assert.DoesNotContain("integral(", antiderivative.Stringize()); + var derivative = antiderivative.Substitute("C", 0).Differentiate("x"); + foreach (var point in points) + { + var expected = f.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + var actual = derivative.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.Equal(expected, actual, 8); + } + return antiderivative; + } + + // k/(ax^2 + bx + c)^n with no real roots -- the shape that had no antiderivative at all. + [Theory] + [InlineData("1 / (x ^ 2 + 1) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("1 / (x ^ 2 + 4) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("1 / (3 * x ^ 2 + 5) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("1 / (2 * x ^ 2 + 3 * x + 7) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("5 / (x ^ 2 + 2 * x + 5) ^ 2", new[] { 0.37, 1.4, -0.83 })] + public void AConstantOverARepeatedIrreducibleQuadratic(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + // The reduction takes one power off at a time, so a third and a fourth power are the + // same line applied again rather than new cases. + [Theory] + [InlineData("1 / (x ^ 2 + 1) ^ 3", new[] { 0.37, 1.4, -0.83 })] + [InlineData("1 / (x ^ 2 + 1) ^ 4", new[] { 0.37, 1.4, -0.83 })] + [InlineData("1 / (x ^ 2 + 2) ^ 5", new[] { 0.37, 1.4, -0.83 })] + public void AHigherPowerIsTheSameReductionAppliedAgain(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + // (px + q)/(ax^2 + bx + c)^n, by writing the numerator as a multiple of the + // denominator's derivative plus a constant. + [Theory] + [InlineData("(x + 3) / (x ^ 2 + 2 * x + 5) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("x / (x ^ 2 + 1) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("x / (x ^ 2 + 1) ^ 3", new[] { 0.37, 1.4, -0.83 })] + [InlineData("(2 * x + 1) / (x ^ 2 + x + 1) ^ 2", new[] { 0.37, 1.4, -0.83 })] + public void ALinearNumeratorOverARepeatedQuadratic(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + // A numerator of higher degree is divided by the quadratic: N = qQ + r takes one power + // off the denominator and two degrees off the numerator, and ends. + [Theory] + [InlineData("x ^ 2 / (x ^ 2 + 2) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("(x ^ 2 + 1) / (x ^ 2 + 2) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("x ^ 3 / (x ^ 2 + 1) ^ 2", new[] { 0.37, 1.4, -0.83 })] + [InlineData("(x ^ 3 + x + 1) / (x ^ 2 + 1) ^ 3", new[] { 0.37, 1.4, -0.83 })] + [InlineData("(x ^ 2 + x) / (x ^ 2 + 3) ^ 2", new[] { 0.37, 1.4, -0.83 })] + public void APolynomialNumeratorIsDividedByTheQuadratic(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + /// + /// The one case checked exactly rather than at points: the derivative of the answer is + /// the integrand, with a residual of zero, which is the identity itself rather than a + /// consequence of it holding at the places sampled. + /// + /// + /// The zero comes back conditional, and that is right rather than a shortfall. + /// Both the integrand and its antiderivative are undefined where 1 + x^2 = 0 — + /// off the real line, at +-i — and "these two agree everywhere" would be a + /// claim about points where neither has a value. Asserted on the node rather than on + /// the printed form, so that a change of spelling does not read as a change of answer. + /// + [Fact] + public void TheAnswerDifferentiatesBackToAConditionalZero() + { + var antiderivative = "1 / (x ^ 2 + 1) ^ 2".ToEntity().Integrate("x").Substitute("C", 0); + var residual = (antiderivative.Differentiate("x") - "1 / (x ^ 2 + 1) ^ 2".ToEntity()).Simplify(); + + var zero = residual is Entity.Providedf(var expression, _) ? expression : residual; + Assert.Equal(Entity.Number.Integer.Create(0), zero); + } + + /// + /// A numerator that is already a multiple of the denominator's derivative leaves no + /// second integral, and the answer carries no condition. Multiplying the reduction by + /// zero instead of dropping it would attach one, since 0 * (2x/(x^2 + 1)) is + /// 0 provided not 1 + x^2 = 0 and not 0. + /// + [Fact] + public void ANumeratorAlongTheDerivativeIsOwedNoCondition() + { + var answer = "x / (x ^ 2 + 1) ^ 2".ToEntity().Integrate("x").Stringize(); + Assert.DoesNotContain("provided", answer); + } + + // A symbolic quadratic cannot have its discriminant's sign decided, so every arm + // survives and the answer is the piecewise. Without the two arms the reduction cannot + // speak for -- a zero leading coefficient and a zero discriminant -- those inputs would + // carry a division by zero into the answer. + [Theory] + [InlineData("1 / (a * x ^ 2 + c) ^ 2")] + [InlineData("1 / (a * x ^ 2 + b * x + c) ^ 2")] + public void ASymbolicQuadraticIsAnsweredAsAPiecewise(string integrand) + { + var answer = integrand.ToEntity().Integrate("x").Stringize(); + Assert.DoesNotContain("integral(", answer); + Assert.Contains("piecewise", answer); + Assert.Contains("arctan", answer); + } + + // Already answered before this rule, by the power rule and by partial fractions, and + // still answered: a denominator with real roots comes apart into linear factors, and a + // perfect square is a linear factor to twice the power. + [Theory] + [InlineData("1 / (x ^ 2 - 1) ^ 2", new[] { 1.4, 2.6, -3.1 })] + [InlineData("1 / (x + 1) ^ 2", new[] { 0.37, 1.4, 2.6 })] + [InlineData("1 / (x ^ 2 + 2 * x + 1) ^ 2", new[] { 0.37, 1.4, 2.6 })] + [InlineData("1 / (x ^ 2 - 2) ^ 2", new[] { 0.37, 2.6, -3.1 })] + public void ADenominatorThatFactorsIsStillAnswered(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + /// + /// An improper fraction is declined by this rule rather than divided out in it — the + /// recursion bottoms out at a single power with a numerator still of degree two, which + /// is the case SolveByPartialFractions already opens with a long division. It is + /// answered; the point is that it is answered there. + /// + [Fact] + public void AnImproperFractionIsAnsweredByTheDivisionThatAlreadyExisted() => + AssertIsAntiderivative("x ^ 4 / (x ^ 2 + 1) ^ 2", 0.37, 1.4, -0.83); + } +}