diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 02dedd57f..30e4a8cca 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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)` |
@@ -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).
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 5cf7aca58..0158e73df 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -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
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 594b32a4a..51a95a875 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -205,6 +205,56 @@ arg is Entity.Powf(var y, var pow) ? // log(b, y^p) = ln(y^p) / ln(b) = ln(p) /
_ => null
};
+ ///
+ /// An integrand that is a function of tan(x) and of nothing else, integrated by
+ /// the substitution u = tan(x), under which dx is du/(1 + u^2).
+ ///
+ ///
+ ///
+ /// The substitution that turns every rational function of the tangent into a rational
+ /// function, and int sqrt(tan(x)) — the last of the five integrals
+ /// #233 lists — into
+ /// int sqrt(u)/(1 + u^2) du.
+ ///
+ ///
+ /// Why this is not a candidate in . That one
+ /// divides the integrand by du/dx and asks whether any x is left, which
+ /// works when 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. Rewriting the integrand
+ /// asks a different question and does not lose the shape.
+ ///
+ ///
+ /// 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
+ /// not a function of the tangent alone. cotan is not covered, since it is
+ /// its own node rather than a reciprocal of this one, so sqrt(cotan(x)) is still
+ /// declined.
+ ///
+ ///
+ /// No condition is owed by the substitution, but the answer inherits the
+ /// tangent's: u = tan(x) is undefined exactly where the integrand is, since the
+ /// integrand is a function of it, and 1 + u^2 is never zero for real u.
+ ///
+ ///
+ 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;
+ }
+
///
/// Attempts to solve an integral using u-substitution.
/// Looks for patterns where f(g(x)) * g'(x) can be integrated as F(g(x)).
@@ -276,31 +326,69 @@ arg is Entity.Powf(var y, var pow) ? // log(b, y^p) = ln(y^p) / ln(b) = ln(p) /
///
///
/// 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 x survives, so a rewrite that does not
- /// clear the variable leaves the candidate rejected as before — which is what happens to
- /// x^2 / (x^4 + 1), where a bare x remains and the integral genuinely needs
- /// a factorisation this does not have.
+ /// itself. For u = x^r the identity is x^n = u^(n/r), and the rewrite is
+ /// made wherever n/r is a whole number — which for a whole r means
+ /// r divides n, and is the only case this covered at first.
+ ///
+ ///
+ /// A fractional r is the case that reaches the other way, and it is what
+ /// int sqrt(x)/(1 + x^2) needs. There u = sqrt(x), so r is
+ /// 1/2 and n/r is 2n — a whole number for every n, including
+ /// the bare x that a whole r can never rewrite. The integrand becomes
+ /// 2u^2/(1 + u^4), which is answered.
+ ///
+ ///
+ /// Nothing is assumed by it: the caller still checks that no x survives, so a
+ /// rewrite that does not clear the variable leaves the candidate rejected as before.
///
///
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));
+ }
}
///
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index db4fbb1f7..5aaef26c5 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -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:
diff --git a/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs
index 965c7e8a0..0f2ed99a6 100644
--- a/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs
@@ -71,6 +71,41 @@ private static void DifferentiatesBack(string integrand)
+ "so this asserts almost nothing");
}
+ ///
+ /// A fractional power of the variable as the substitution, which is the same
+ /// rewrite read the other way: u = x^r means x^n = u^(n/r), and where a
+ /// whole r can only rewrite the powers it divides, an r of 1/2
+ /// rewrites every one of them — the bare x included, which a whole r never
+ /// can.
+ ///
+ ///
+ /// int sqrt(x)/(1 + x^2) becomes int 2u^2/(1 + u^4) du under
+ /// u = sqrt(x), which is answered — so this reaches what it does partly because
+ /// the denominator now factors over the reals.
+ /// #233
+ ///
+ [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);
+
+ ///
+ /// Where a fractional substitution reaches and the rest of the chain does not, recorded
+ /// so the boundary is visible. sqrt(x)/(1 + x^4) becomes 2u^2/(1 + u^8),
+ /// whose denominator is neither factorable over the rationals nor a biquadratic; and
+ /// sqrt(x)/(x + 1) becomes 2u^2/(1 + u^2), which is improper, and dividing
+ /// an improper fraction out is not something the rational integrator does.
+ ///
+ [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());
+
///
/// The shape the issue is about: an odd power over an even one, where the substitution is
/// the root of the denominator's power.
diff --git a/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs
new file mode 100644
index 000000000..ef177d239
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/TangentSubstitutionIntegralTest.cs
@@ -0,0 +1,129 @@
+//
+// 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 AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Calculus
+{
+ ///
+ /// Integrands that are a function of tan(x) and of nothing else, which the
+ /// substitution u = tan(x) turns into a rational function — dx being
+ /// du/(1 + u^2).
+ /// #233
+ ///
+ ///
+ ///
+ /// int sqrt(tan(x)) is the last of the five integrals that issue lists, and the one it
+ /// calls "very painful, requires different solvers", which it is: the substitution here makes
+ /// it int sqrt(u)/(1 + u^2) du, a fractional power substitution makes that
+ /// int 2t^2/(1 + t^4) dt, and answering that needs the denominator factored
+ /// over the reals. Three capabilities in a row, and it comes out unevaluated if any of them
+ /// is missing.
+ ///
+ ///
+ /// Checked by differentiating the answer back and comparing at points, never by comparing
+ /// printed forms — the answers here are sums of logarithms and arctangents of radicals in
+ /// tan(x), and their shape says nothing about whether they differentiate back.
+ ///
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class TangentSubstitutionIntegralTest
+ {
+ ///
+ /// Sample points inside a single branch of the tangent, well away from the pole at
+ /// pi/2. sqrt(tan(x)) is real only where the tangent is non-negative, so
+ /// they stay on (0, pi/2) rather than straddling zero.
+ ///
+ private static readonly double[] Points = { 0.2, 0.45, 0.8, 1.1, 1.35 };
+
+ private static void DifferentiatesBack(string integrand)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ Assert.DoesNotContain("integral(", integral.Stringize());
+
+ var derivative = integral.Substitute("C", 0).Differentiate("x");
+ var original = integrand.ToEntity();
+ var compared = 0;
+ foreach (var at in Points)
+ {
+ var got = derivative.Substitute("x", at).EvalNumerical();
+ var want = original.Substitute("x", at).EvalNumerical();
+ if (got.IsNaN || want.IsNaN)
+ continue;
+ compared++;
+ var difference = Math.Abs((double)(got - want).RealPart);
+ 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}");
+ }
+ Assert.True(compared >= 4,
+ $"only {compared} of {Points.Length} points were comparable for {integrand}, "
+ + "so this asserts almost nothing");
+ }
+
+ /// The integral the issue names, and the reason this substitution exists.
+ [Theory]
+ [InlineData("sqrt(tan(x))")]
+ public void TheIntegralTheIssueNames(string integrand) => DifferentiatesBack(integrand);
+
+ ///
+ /// A function of the tangent alone, whatever it is made of. Each of these is answered
+ /// through the rewrite rather than by a rule of its own.
+ ///
+ [Theory]
+ [InlineData("1/sqrt(tan(x))")]
+ [InlineData("tan(x)/(1 + tan(x)^2)")]
+ public void AFunctionOfTheTangentAlone(string integrand) => DifferentiatesBack(integrand);
+
+ ///
+ /// What the neighbours still answer, and must go on answering the same way: the tangent
+ /// itself has a rule, which is reached before this and gives the shorter form.
+ ///
+ [Theory]
+ [InlineData("tan(x)", "-ln(cos(x)) + C")]
+ [InlineData("tan(2 * x)", "-ln(cos(2 * x)) / 2 + C")]
+ public void TheRuleForTheTangentItselfStillWins(string integrand, string expected)
+ => Assert.Equal(expected.ToEntity(), integrand.ToEntity().Integrate("x"));
+
+ ///
+ /// An integrand that mentions x outside the tangent is not a function of the
+ /// tangent alone, and the rewrite leaves that x behind rather than pretending
+ /// otherwise. tan(x) + x is answered, but by linearity over the sum, not by this.
+ ///
+ [Theory]
+ [InlineData("tan(x) + x", "-ln(cos(x)) + x ^ 2 / 2 + C")]
+ public void AnXOutsideTheTangentIsNotRewritten(string integrand, string expected)
+ => Assert.Equal(expected.ToEntity(), integrand.ToEntity().Integrate("x"));
+
+ ///
+ /// What is still declined, recorded so the boundary is visible rather than inferred from
+ /// an absence. Each is declined by what the rewrite hands on, not by the rewrite itself:
+ ///
+ ///
+ ///
+ /// - sqrt(cotan(x)) — cotan is its own node rather than a reciprocal of
+ /// the tangent, so the rewrite finds nothing to replace and this never starts.
+ /// - tan(x)^2 and tan(x)^3 become u^2/(1 + u^2) and
+ /// u^3/(1 + u^2), which are improper — a polynomial plus a proper fraction
+ /// — and dividing that out is not something the rational integrator does.
+ /// - 1/(1 + tan(x)^2) becomes 1/(1 + u^2)^2, a repeated irreducible
+ /// quadratic, which the partial fraction step declines because there is no rule for a
+ /// numerator over one.
+ ///
+ ///
+ [Theory]
+ [InlineData("sqrt(cotan(x))")]
+ [InlineData("tan(x) ^ 2")]
+ [InlineData("tan(x) ^ 3")]
+ [InlineData("1/(1 + tan(x)^2)")]
+ public void WhatIsStillDeclined(string integrand)
+ => Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize());
+ }
+}
diff --git a/Sources/Tests/UnitTests/Corpus/Corpus.cs b/Sources/Tests/UnitTests/Corpus/Corpus.cs
index ad71d8718..2beacf7a6 100644
--- a/Sources/Tests/UnitTests/Corpus/Corpus.cs
+++ b/Sources/Tests/UnitTests/Corpus/Corpus.cs
@@ -138,7 +138,11 @@ public static class Corpus
new("int:by-parts-cyclic", Op.Integrate, "sin(x) * e ^ x"),
new("int:arctan-form", Op.Integrate, "1 / (1 + x ^ 2)"),
new("int:partial-fractions", Op.Integrate, "1 / (x ^ 4 + 3 * x ^ 2 + 2)"),
- new("int:hard", Op.Integrate, "sqrt(tan(x))", Verdict.Unsolved),
+ // Solved since the tangent substitution: u = tan(x) makes it sqrt(u)/(1 + u^2), a
+ // fractional power substitution makes that 2t^2/(1 + t^4), and the biquadratic
+ // denominator factors over the reals. It was the corpus's standing example of an
+ // integral that is out of reach. https://github.com/asc-community/AngouriMath/issues/233
+ new("int:hard", Op.Integrate, "sqrt(tan(x))"),
// --- Limits: compared against the stated value ---
new("lim:0/0", Op.Limit, "sin(x) / x", Approach: "0", Expected: "1"),
diff --git a/Sources/Tests/UnitTests/Corpus/corpus-baseline.tsv b/Sources/Tests/UnitTests/Corpus/corpus-baseline.tsv
index 3603a8cfa..b2667d374 100644
--- a/Sources/Tests/UnitTests/Corpus/corpus-baseline.tsv
+++ b/Sources/Tests/UnitTests/Corpus/corpus-baseline.tsv
@@ -3,12 +3,12 @@
# in Corpus.cs, re-run the suite with AM_UPDATE_CORPUS_BASELINE=1, and commit both.
# Its git history is the per-commit history of the corpus -- item 36 of
# https://github.com/asc-community/AngouriMath/issues/746.
-# totals Solved=39 Unsolved=1 Wrong=0 Error=0 Timeout=0 Total=40
+# totals Solved=40 Unsolved=0 Wrong=0 Error=0 Timeout=0 Total=40
name op verdict
int:arctan-form Integrate Solved
int:by-parts-cyclic Integrate Solved
int:exponential Integrate Solved
-int:hard Integrate Unsolved
+int:hard Integrate Solved
int:partial-fractions Integrate Solved
int:power Integrate Solved
int:reciprocal Integrate Solved