Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ read first.
| | `MathS.Vector()` and `new Entity[0].ToVector()` | `IndexOutOfRangeException` — outside the documented hierarchy | `InvalidMatrixOperationException` |
| | `"x3 - 1".ToEntity().Factorize()`, and every polynomial no rewrite rule has a rule for | `x ^ 3 - 1` — handed back whole | `(x - 1) * (x ^ 2 + x + 1)` |
| | `"x^2/(x^4 + 1)".Integrate("x")`, and every quotient whose denominator is a biquadratic irreducible over `Q` | `integral(x ^ 2 / (x ^ 4 + 1), x)` — left unevaluated | the antiderivative, over the real quadratic factors |
| | `"sqrt(x)/(1 + x^2)".Integrate("x")`, and every integrand a fractional power of the variable makes rational | `integral(sqrt(x) / (1 + x ^ 2), x)` — left unevaluated | the antiderivative |
| | `"sqrt(tan(x))".Integrate("x")`, and every integrand that is a function of `tan(x)` alone and rational in it | `integral(sqrt(tan(x)), x)` — left unevaluated | the antiderivative |
| | `Entity.DomainConditionIn(Domain)` | did not exist | the domain of definition for a **stated** reading, through the whole tree |
| | `MathS.Polynomials.Factor("(y * x3 + 1) * (x4 - y3)", "x")`, and bivariate polynomials whose leading coefficient in the main variable is a polynomial | `null` — a refusal | `(x ^ 4 - y ^ 3) * (x ^ 3 * y + 1)` |
| | `MathS.Polynomials.Factor("x7 - y7", "x")`, and bivariate polynomials whose substituted image over-factors | `null` — a refusal | `(x - y) * (x ^ 6 + x ^ 5 * y + … + y ^ 6)` |
Expand Down Expand Up @@ -2401,7 +2403,72 @@ because every guard here is rational arithmetic on coefficients already read.

`sqrt(tan(x))`, the one remaining entry on #233's list, is **not** answered by this. It reduces
under `u = sqrt(tan x)` to `2 * integral(u^2/(u^4 + 1), u)`, which is now integrable — but the
substitution that gets there is a separate capability and is not built here.
substitution that gets there is a separate capability and is not built here. *It is built in the
entry below, and `sqrt(tan(x))` is now answered.*

[#233](https://github.com/asc-community/AngouriMath/issues/233).

### A fractional power, and the tangent, are substitutions

Two substitutions, in one entry because neither reaches `sqrt(tan(x))` without the other and
without the entry above. That integral is the last of the five
[#233](https://github.com/asc-community/AngouriMath/issues/233) lists, and the one it calls "very
painful, requires different solvers" — which it is. Three capabilities in a row:

```
int sqrt(tan(x)) dx
--- u = tan(x), dx = du/(1 + u^2) -------> int sqrt(u)/(1 + u^2) du
--- t = sqrt(u), a fractional power -------> int 2t^2/(1 + t^4) dt
--- 1 + t^4 factored over the reals -------> logarithms and arctangents
```

Take any one away and it comes back unevaluated.

```
"sqrt(tan(x))".Integrate("x")

was integral(sqrt(tan(x)), x)
is a sum of two logarithms and two arctangents in sqrt(tan(x)) — the shape the
entry above produces, with sqrt(tan(x)) where it had x
```

**The fractional power.** A power substitution rewrites the other powers of the variable into
powers of itself: for `u = x^r` the identity is `x^n = u^(n/r)`, applied wherever `n/r` is a whole
number. A whole `r` reaches only the powers it divides, which is what this did before. An `r` of
`1/2` reaches every one of them — including the bare `x`, which a whole `r` never can — so
`int sqrt(x)/(1 + x^2)` becomes `int 2u^2/(1 + u^4) du`. `1/(1 + sqrt(x))`,
`1/(sqrt(x) * (1 + x))`, `1/(sqrt(x) * (1 + x^2))` and `1/(sqrt(x) + x)` come with it.

The rewrite is made in two passes, powers first and a leftover bare `x` second, because the tree
is rewritten from the leaves up: in one pass the `x` inside `sqrt(x)` is reached before the
`sqrt(x)` node is, and `u = sqrt(x)` turns it into `sqrt(u^2)` rather than `u` — an integrand free
of `x` and no more integrable than it started.

**The tangent.** An integrand that is a function of `tan(x)` and of nothing else becomes a
rational function under `u = tan(x)`, with `dx` as `du/(1 + u^2)`. The test is the rewrite itself:
replace every `tan(x)` and see whether an `x` survives. `tan(x) + x` keeps one and is declined,
which is right — it is answered, but by linearity over the sum.

This is a step of its own rather than a candidate for the general substitution, and the reason is
worth recording. The general one divides the integrand by `du/dx` and asks what is left, which
works while the substitution survives the division. Here it does not: `sqrt(tan(x))` over the
derivative of `sqrt(tan(x))` is `2 tan(x) cos(x)^2`, which is `sin(2x)` and is simplified to it —
a correct answer to a question that has stopped being about the tangent.

**What is still declined**, each by what the rewrite hands on rather than by the rewrite:
`cotan` is its own node rather than a reciprocal of the tangent, so `sqrt(cotan(x))` never starts;
`tan(x)^2` and `tan(x)^3` become improper fractions, and dividing an improper fraction out is not
something the rational integrator does; `1/(1 + tan(x)^2)` becomes `1/(1 + u^2)^2`, a repeated
irreducible quadratic. On the other side, `sqrt(x)/(1 + x^4)` becomes `2u^2/(1 + u^8)`, whose
denominator is neither factorable over the rationals nor a biquadratic.

**The rule for the tangent itself still wins**, being reached first: `int tan(x)` is
`-ln(cos(x)) + C` and not the longer thing this would produce.

**The corpus gate now reads 40 solved of 40.** Its one unsolved problem was `int:hard`, and
`int:hard` is `sqrt(tan(x))` — chosen for that list as the standing example of an integral out of
reach. The gate reached the same verdict independently, by differentiating the answer back rather
than by comparing it with anything.

[#233](https://github.com/asc-community/AngouriMath/issues/233).

Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed

[Tests/UnitTests/Common/ExponentialOfALogarithmTest.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/TangentSubstitutionIntegralTest.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
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,56 @@ arg is Entity.Powf(var y, var pow) ? // log(b, y^p) = ln(y^p) / ln(b) = ln(p) /
_ => null
};

/// <summary>
/// An integrand that is a function of <c>tan(x)</c> and of nothing else, integrated by
/// the substitution <c>u = tan(x)</c>, under which <c>dx</c> is <c>du/(1 + u^2)</c>.
/// </summary>
/// <remarks>
/// <para>
/// The substitution that turns every rational function of the tangent into a rational
/// function, and <c>int sqrt(tan(x))</c> — the last of the five integrals
/// <a href="https://github.com/asc-community/AngouriMath/issues/233">#233</a> lists — into
/// <c>int sqrt(u)/(1 + u^2) du</c>.
/// </para>
/// <para>
/// <b>Why this is not a candidate in <see cref="SolveBySubstitution"/>.</b> That one
/// divides the integrand by <c>du/dx</c> and asks whether any <c>x</c> is left, which
/// works when the substitution survives the division. Here it does not:
/// <c>sqrt(tan(x))</c> over the derivative of <c>sqrt(tan(x))</c> is
/// <c>2 tan(x) cos(x)^2</c>, which is <c>sin(2x)</c> and is simplified to it — a correct
/// answer to a question that has stopped being about the tangent. Rewriting the integrand
/// asks a different question and does not lose the shape.
/// </para>
/// <para>
/// The test is the rewrite itself: replace every <c>tan(x)</c> and see whether an
/// <c>x</c> survives. <c>tan(x) + x</c> keeps one and is declined, which is right — it is
/// not a function of the tangent alone. <c>cotan</c> is <b>not</b> covered, since it is
/// its own node rather than a reciprocal of this one, so <c>sqrt(cotan(x))</c> is still
/// declined.
/// </para>
/// <para>
/// <b>No condition is owed by the substitution</b>, but the answer inherits the
/// tangent's: <c>u = tan(x)</c> is undefined exactly where the integrand is, since the
/// integrand is a function of it, and <c>1 + u^2</c> is never zero for real <c>u</c>.
/// </para>
/// </remarks>
internal static Entity? SolveByTangentSubstitution(Entity expr, Entity.Variable x, bool integrateByParts)
{
var tangent = MathS.Tan(x);
if (!expr.ContainsNode(tangent))
return null;

var uSub = Variable.CreateUnique(expr, "u_tan");
var inU = expr.Substitute(tangent, uSub);
if (inU.ContainsNode(x))
return null;

var integrand = (inU / (1 + MathS.Sqr(uSub))).InnerSimplified;
return Integration.ComputeIndefiniteIntegral(integrand, uSub, integrateByParts) is { } result
? result.Substitute(uSub, tangent)
: null;
}

/// <summary>
/// Attempts to solve an integral using u-substitution.
/// Looks for patterns where f(g(x)) * g'(x) can be integrated as F(g(x)).
Expand Down Expand Up @@ -276,31 +326,69 @@ arg is Entity.Powf(var y, var pow) ? // log(b, y^p) = ln(y^p) / ln(b) = ln(p) /
/// </para>
/// <para>
/// So a power substitution rewrites the other powers of the variable into powers of
/// itself, which it can do exactly when its exponent divides theirs. Nothing is assumed
/// by it: the caller still checks that no <c>x</c> survives, so a rewrite that does not
/// clear the variable leaves the candidate rejected as before — which is what happens to
/// <c>x^2 / (x^4 + 1)</c>, where a bare <c>x</c> remains and the integral genuinely needs
/// a factorisation this does not have.
/// itself. For <c>u = x^r</c> the identity is <c>x^n = u^(n/r)</c>, and the rewrite is
/// made wherever <c>n/r</c> is a whole number — which for a whole <c>r</c> means
/// <c>r</c> divides <c>n</c>, and is the only case this covered at first.
/// </para>
/// <para>
/// <b>A fractional <c>r</c> is the case that reaches the other way</b>, and it is what
/// <c>int sqrt(x)/(1 + x^2)</c> needs. There <c>u = sqrt(x)</c>, so <c>r</c> is
/// <c>1/2</c> and <c>n/r</c> is <c>2n</c> — a whole number for every <c>n</c>, including
/// the bare <c>x</c> that a whole <c>r</c> can never rewrite. The integrand becomes
/// <c>2u^2/(1 + u^4)</c>, which is answered.
/// </para>
/// <para>
/// Nothing is assumed by it: the caller still checks that no <c>x</c> survives, so a
/// rewrite that does not clear the variable leaves the candidate rejected as before.
/// </para>
/// </remarks>
private static Entity InTermsOf(Entity expr, Entity u, Entity.Variable uSub, Entity.Variable x)
{
if (u is not Powf(var powerBase, Number.Integer exponent)
|| powerBase != x
|| !exponent.EInteger.CanFitInInt32())
if (u is not Powf(var powerBase, Number.Rational exponent) || powerBase != x)
return expr.Substitute(u, uSub);
var k = exponent.EInteger.ToInt32Checked();
if (k < 2)
var r = exponent.ERational;
if (r.IsZero)
return expr.Substitute(u, uSub);
return expr.Replace(node =>
node is Powf(var otherBase, Number.Integer otherExponent)
// The written powers of x first, and only then a bare x that is left over. Both in
// one pass does not work, because Replace rewrites from the leaves up: the x inside
// sqrt(x) is reached before the sqrt(x) node is, so with u = sqrt(x) it becomes
// sqrt(u^2) rather than u, and the integrand ends up free of x and no more
// integrable than it started.
//
// The exponent is read as a rational rather than a whole number because the
// candidate is itself one of these nodes: with u = sqrt(x) the integrand still holds
// a sqrt(x), and leaving it alone leaves an x that rejects the candidate. Anything
// else -- x inside a function, or a base that is not x -- is left for the caller's
// check on whether x survives.
var written = expr.Replace(node =>
node is Powf(var otherBase, Number.Rational otherExponent)
&& otherBase == x
&& otherExponent.EInteger.CanFitInInt32()
&& otherExponent.EInteger.ToInt32Checked() is var n
&& n >= k
&& n % k == 0
? (n == k ? uSub : MathS.Pow(uSub, n / k))
&& Rewritten(otherExponent.ERational) is { } power
? power
: node);
return written.Replace(node =>
node == x && Rewritten(PeterO.Numbers.ERational.One) is { } bare ? bare : node);

// u^(n/r), where that is a whole power, and null where it is not. Done on the four
// integers rather than by dividing the rationals and asking whether the result is
// whole: an ERational quotient is not reduced, so 4/2 answers that question with a
// denominator of two and every whole r would be refused.
//
// The ceiling is against a tiny r turning a modest power of x into an enormous one,
// rather than against anything mathematical.
Entity? Rewritten(PeterO.Numbers.ERational n)
{
var scaled = n.Numerator.Multiply(r.Denominator);
var by = n.Denominator.Multiply(r.Numerator);
if (by.IsZero || !scaled.Remainder(by).IsZero)
return null;
var whole = scaled.Divide(by);
if (whole.IsZero || whole.Abs().CompareTo(PeterO.Numbers.EInteger.FromInt32(64)) > 0)
return null;
return whole.Equals(PeterO.Numbers.EInteger.One)
? uSub
: MathS.Pow(uSub, Number.Integer.Create(whole));
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ private static Entity Normalized(Entity expr) =>
if ((answer = IndefiniteIntegralSolver.SolveAsPolynomialTerm(expr, x, integrateByParts)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveLogarithmic(expr, x, integrateByParts)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveBySubstitution(expr, x, integrateByParts)) is { }) return answer;
// After the general substitution rather than inside it, because the general one
// divides by du/dx and asks what is left, and that question loses the shape here:
// sqrt(tan(x)) over the derivative of sqrt(tan(x)) simplifies to sin(2x), in which
// the substitution is no longer visible. This one rewrites rather than divides.
if ((answer = IndefiniteIntegralSolver.SolveByTangentSubstitution(expr, x, integrateByParts)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveByPartialFractions(expr, x, integrateByParts)) is { }) return answer;
// Linearity comes before integration by parts, because it decomposes the problem
// into strictly simpler ones where by parts searches. It cannot cost an answer:
Expand Down
35 changes: 35 additions & 0 deletions Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,41 @@ private static void DifferentiatesBack(string integrand)
+ "so this asserts almost nothing");
}

/// <summary>
/// A <em>fractional</em> power of the variable as the substitution, which is the same
/// rewrite read the other way: <c>u = x^r</c> means <c>x^n = u^(n/r)</c>, and where a
/// whole <c>r</c> can only rewrite the powers it divides, an <c>r</c> of <c>1/2</c>
/// rewrites every one of them — the bare <c>x</c> included, which a whole <c>r</c> never
/// can.
/// </summary>
/// <remarks>
/// <c>int sqrt(x)/(1 + x^2)</c> becomes <c>int 2u^2/(1 + u^4) du</c> under
/// <c>u = sqrt(x)</c>, which is answered — so this reaches what it does partly because
/// the denominator now factors over the reals.
/// <a href="https://github.com/asc-community/AngouriMath/issues/233">#233</a>
/// </remarks>
[Theory]
[InlineData("sqrt(x)/(1 + x^2)")]
[InlineData("1/(1 + sqrt(x))")]
[InlineData("1/(sqrt(x) * (1 + x))")]
[InlineData("1/(sqrt(x) * (1 + x^2))")]
[InlineData("1/(sqrt(x) + x)")]
public void AFractionalPowerIsAlsoASubstitution(string integrand)
=> DifferentiatesBack(integrand);

/// <summary>
/// Where a fractional substitution reaches and the rest of the chain does not, recorded
/// so the boundary is visible. <c>sqrt(x)/(1 + x^4)</c> becomes <c>2u^2/(1 + u^8)</c>,
/// whose denominator is neither factorable over the rationals nor a biquadratic; and
/// <c>sqrt(x)/(x + 1)</c> becomes <c>2u^2/(1 + u^2)</c>, which is improper, and dividing
/// an improper fraction out is not something the rational integrator does.
/// </summary>
[Theory]
[InlineData("sqrt(x)/(1 + x^4)")]
[InlineData("sqrt(x)/(x + 1)")]
public void WhereTheChainStops(string integrand)
=> Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize());

/// <summary>
/// The shape the issue is about: an odd power over an even one, where the substitution is
/// the root of the denominator's power.
Expand Down
Loading
Loading