diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 0b1cc649e..c74e243ee 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -25,6 +25,7 @@ read first.
| **silent** | `exp(x)`, `log10(x)`, `log2(x)` | products of undeclared variables | the functions |
| loud | `arcsinh(x)` and its five relatives | a product | `UnrecognizedFunctionParseException` |
| loud | `floor(x)` and ten other names the library lacks | a product, silently | `UnrecognizedFunctionParseException` |
+| **silent** | `sqrt(x^2)`, `sqrt(-x)` and their kind | `x`, `i*sqrt(x)` — wrong for negative x | left as written |
| loud | `mod` as a variable name | a variable | a keyword, so a parse error |
| **silent** | `Stringize` of powers, lambdas, applications, piecewises | did not parse back | parses back |
| **silent** | numbers below `1e-16` | rounded to `0` | kept |
@@ -160,6 +161,47 @@ their expression to fix nobody's.
[#733](https://github.com/asc-community/AngouriMath/issues/733), PR
[#750](https://github.com/asc-community/AngouriMath/pull/750).
+### `sqrt(x^2)` is no longer `x`, and `sqrt(-x)` is no longer `i * sqrt(x)`
+
+| | |
+|---|---|
+| was | `x`, and `i * sqrt(x)` |
+| is | left as written |
+
+Both were wrong, and by a whole sign rather than a rounding:
+
+```csharp
+"sqrt(x ^ 2)".Simplify() // was x — at x = -0.63 that is -0.63 where it is 0.63
+"(x ^ 2) ^ (3/2)".Simplify() // was x ^ 3 — at x = -2 that is -8 where it is 8
+"sqrt(-x)".Simplify() // was i*sqrt(x) — at x = -0.63 that is -0.7937, not 0.7937
+```
+
+Two rules were being applied without the condition each needs. `(a^b)^c = a^(b*c)` holds for
+a positive `a`, and for any `a` when `c` is a whole number — `(a^b)^3` is `a^b` multiplied by
+itself three times however `a` is signed. `(a*x)^c = a^c * x^c` holds for a positive `a`, and
+again for a whole `c`. Both are now restricted to exactly that, and unchanged where they hold:
+`(x^(1/2))^2` is still `x`, `(-x)^2` is still `x^2`, `(2^2)^(1/2)` is still `2`.
+
+`sqrt(x^2)` is `abs(x)`, and the library does not write that for you — it leaves the
+expression alone rather than answering something false. Writing `abs` requires knowing the
+expression is real, which is what
+[#719](https://github.com/asc-community/AngouriMath/issues/719)'s codomain now makes sayable
+and is not yet read by the simplifier.
+
+**One thing got worse, and it is worth stating plainly.** The inequality solver was leaning
+on `sqrt(4a^2) = 2a`: `(x - a)(x + a) <= 0` answered `[a; -a]`, which is right for `a < 0`
+and empty for `a > 0`. It now answers `{ a, -a } \/ (sqrt(a^2); -sqrt(a^2))`, whose interval
+is empty for either sign. Neither form is right for both, because the solver has no case
+split on the sign of a symbolic coefficient — that is
+[#757](https://github.com/asc-community/AngouriMath/issues/757), and it was invisible while an
+unsound rewrite was making it look right half the time.
+
+Found by `work/simpsweep`, which generates the expressions it checks; its count of
+disagreements over 10463 expressions goes **30 to 0**.
+
+[#752](https://github.com/asc-community/AngouriMath/issues/752), PR
+[#758](https://github.com/asc-community/AngouriMath/pull/758).
+
### `mod` is now a keyword
| | |
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs
index 55ec3197d..c2d9c3825 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs
@@ -42,7 +42,18 @@ expr is Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2))
Divf(Powf(var any1, var any2), Powf(var any1a, var any3)) when any1 == any1a => new Powf(any1, any2 - any3),
// ({} ^ {}) ^ {} = {} ^ ({} * {})
- Powf(Powf(var any1, var any2), var any3) => new Powf(any1, any2 * any3),
+ //
+ // True for a positive base whatever the exponents, and for any base when the
+ // outer exponent is a whole number -- (a^b)^3 is a^b * a^b * a^b however a is
+ // signed. Outside those two it is false, and it was applied unconditionally:
+ //
+ // sqrt(x^2) came back as x, which at -0.63 is -0.63 where it is 0.63
+ // (x^2)^(3/2) came back as x^3, which at -2 is -8 where it is 8
+ //
+ // https://github.com/asc-community/AngouriMath/issues/752
+ Powf(Powf(var any1, var any2), var any3)
+ when any3 is Integer || any1.Evaled is Real { IsPositive: true }
+ => new Powf(any1, any2 * any3),
// {1} ^ n * {2} ^ n = ({1} * {2}) ^ n
Mulf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 * any2, any3),
@@ -82,7 +93,15 @@ expr is Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2))
Mulf(Mulf(var any2, var any1), Powf(var any1a, var any3)) when any1 == any1a => new Powf(any1, any3 + 1) * any2,
// (a * x) ^ c = a^c * x^c
- Powf(Mulf(Number const1, var any1), Number const2) =>
+ //
+ // Taking a factor out from under a root needs that factor to be positive, or the
+ // root to be a whole power. With a negative one it moves the branch: sqrt(-x)
+ // became sqrt(-1) * sqrt(x) = i * sqrt(x), and at x = -0.63 the first is 0.7937
+ // while the second is -0.7937 -- the negation, not the value. A positive constant
+ // is safe whatever the sign of x, which is why the rule is narrowed rather than
+ // removed. https://github.com/asc-community/AngouriMath/issues/752
+ Powf(Mulf(Number const1, var any1), Number const2)
+ when const2 is Integer || const1 is Real { IsPositive: true } =>
new Powf(const1, const2) * new Powf(any1, const2),
// {1} ^ (-1) = 1 / {1}
diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/SolveInequality.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/SolveInequality.cs
index 3c337b2f5..bd0524dba 100644
--- a/Sources/Tests/UnitTests/Algebra/SolveTest/SolveInequality.cs
+++ b/Sources/Tests/UnitTests/Algebra/SolveTest/SolveInequality.cs
@@ -22,7 +22,19 @@ public sealed class SolveInequality
[InlineData("(x - 2)(x + 2) > 0", @"(-oo; -2) \/ (2; +oo)")]
[InlineData("(x - 2)(x + 2) < 0", "(-2; 2)")]
[InlineData("(x - 2)(x + 2) <= 0", "[-2; 2]")]
- [InlineData("(x - a)(x + a) <= 0", "[a; -a]")]
+ // A symbolic coefficient has no known sign, and the solver builds (root1; root2)
+ // without asking which root is the smaller -- so one sign of `a` always comes out
+ // as an empty interval. This used to read as [a; -a], correct for a < 0 and empty
+ // for a > 0, and it read that way only because sqrt(4a^2) was simplifying to 2a.
+ // That is unsound for a < 0 and is fixed in
+ // https://github.com/asc-community/AngouriMath/issues/752; with it gone the interval
+ // is empty for either sign and the endpoints survive.
+ //
+ // Neither form is right for both signs. Pinned as it now stands rather than deleted,
+ // because the expectation is still evidence -- of
+ // https://github.com/asc-community/AngouriMath/issues/757, which wants the case split
+ // on the sign that would answer this properly.
+ [InlineData("(x - a)(x + a) <= 0", @"{ a, -a } \/ (sqrt(a ^ 2); -sqrt(a ^ 2))")]
public void Test(string initial, string expected)
{
Variable x = "x";
diff --git a/Sources/Tests/UnitTests/Common/PowerOfPowerBranchTest.cs b/Sources/Tests/UnitTests/Common/PowerOfPowerBranchTest.cs
new file mode 100644
index 000000000..0d7ed8c6a
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/PowerOfPowerBranchTest.cs
@@ -0,0 +1,121 @@
+//
+// Copyright (c) 2019-2022 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using System.Linq;
+using AngouriMath;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// Two rules rewrote a power in a way that is only true on part of the plane, and both
+ /// changed the value for a negative argument: (a^b)^c = a^(b*c), which needs a
+ /// positive a or a whole c, and (a*x)^c = a^c * x^c, which needs a
+ /// positive a or a whole c. Both were applied unconditionally, so
+ /// sqrt(x^2) came back as x and sqrt(-x) as i * sqrt(x) --
+ /// each the negation of the right answer where x is negative.
+ /// https://github.com/asc-community/AngouriMath/issues/752
+ ///
+ public sealed class PowerOfPowerBranchTest
+ {
+ ///
+ /// Value, not shape. The magnitude was right in every one of these and only the sign
+ /// was wrong, so a test comparing printed forms could have passed throughout.
+ ///
+ private static void AssertSameValueAt(string expr, string point)
+ {
+ var original = expr.ToEntity();
+ var simplified = original.Simplify();
+ double At(Entity e)
+ {
+ var substituted = e.Substitute("x", point.ToEntity());
+ while (substituted is Entity.Providedf(var inner, _)) substituted = inner;
+ var value = substituted.EvalNumerical();
+ Assert.True(System.Math.Abs(value.ImaginaryPart.EDecimal.ToDouble()) < 1e-9,
+ $"{expr} is not real at x = {point}");
+ return value.RealPart.EDecimal.ToDouble();
+ }
+ Assert.Equal(At(original), At(simplified), 9);
+ }
+
+ ///
+ /// The property, checked at a negative point, which is the only place any of this
+ /// goes wrong. Every one of these was off by a sign before.
+ ///
+ [Theory]
+ [InlineData("sqrt(x ^ 2)")]
+ [InlineData("(x ^ 2) ^ (1/2)")]
+ [InlineData("(x * x) ^ (1/2)")]
+ [InlineData("(x ^ 2) ^ (3/2)")]
+ [InlineData("(x ^ 2) ^ (1/2 - 1)")]
+ [InlineData("(x ^ 2) ^ (1/2 - 2)")]
+ [InlineData("sqrt(-x)")]
+ [InlineData("sqrt(-1 * x)")]
+ [InlineData("sqrt(x / -1)")]
+ [InlineData("(-x) ^ (1/2)")]
+ [InlineData("(-x) ^ (1/2 - 1)")]
+ public void ARewriteThatMovesTheBranchIsNotMade(string expr) =>
+ AssertSameValueAt(expr, "-63/100");
+
+ ///
+ /// What the rules still do, because there they are true. A whole outer exponent makes
+ /// (a^b)^c sound whatever the sign of a -- it is a^b multiplied by
+ /// itself c times -- and a positive constant may always be taken out from under a
+ /// root.
+ ///
+ [Theory]
+ [InlineData("(x ^ (1/2)) ^ 2", "x")]
+ [InlineData("(x ^ 2) ^ 2", "x ^ 4")]
+ [InlineData("(x ^ 2) ^ 3", "x ^ 6")]
+ [InlineData("(-x) ^ 2", "x ^ 2")]
+ [InlineData("(2 ^ 2) ^ (1/2)", "2")]
+ [InlineData("(4 ^ (1/2)) ^ (1/2)", "sqrt(2)")]
+ public void ARewriteThatHoldsIsStillMade(string expr, string expected) =>
+ Assert.Equal(Entity.Number.Integer.Create(0),
+ (expr.ToEntity().Simplify() - expected.ToEntity()).Simplify());
+
+ // A positive base is unaffected in either rule, which is where they came from.
+ [Theory]
+ [InlineData("(2 ^ x) ^ 3")]
+ [InlineData("(3 * x) ^ 2")]
+ [InlineData("sqrt(4 * x)")]
+ [InlineData("(x ^ 3) ^ 2")]
+ public void APositiveBaseKeepsItsValue(string expr) =>
+ AssertSameValueAt(expr, "17/10");
+
+ ///
+ /// The property that found all of this, stated over the shapes it found them in:
+ /// wherever the expression and its simplification are both real, they are the same
+ /// number. work/simpsweep asks it of ten thousand generated expressions and
+ /// went from 30 disagreements to 0 with these two rules narrowed.
+ ///
+ [Fact]
+ public void NoPowerRewriteChangesTheValueAtANegativePoint()
+ {
+ string[] shapes =
+ {
+ "sqrt({0})", "({0}) ^ (1/2)", "({0}) ^ (3/2)", "({0}) ^ 2", "({0}) ^ 3",
+ "1 / sqrt({0})", "sqrt(sqrt({0}))",
+ };
+ string[] insides = { "x ^ 2", "x * x", "-x", "-1 * x", "x / -1", "x ^ 4", "2 * x ^ 2" };
+ foreach (var shape in shapes)
+ foreach (var inside in insides)
+ {
+ var expr = string.Format(shape, inside);
+ var original = expr.ToEntity();
+ var simplified = original.Simplify();
+ var before = original.Substitute("x", "-63/100".ToEntity()).EvalNumerical();
+ var after = simplified.Substitute("x", "-63/100".ToEntity()).EvalNumerical();
+ Assert.Equal(before.RealPart.EDecimal.ToDouble(),
+ after.RealPart.EDecimal.ToDouble(), 9);
+ Assert.Equal(before.ImaginaryPart.EDecimal.ToDouble(),
+ after.ImaginaryPart.EDecimal.ToDouble(), 9);
+ }
+ }
+ }
+}
diff --git a/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs b/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs
index b4c2a84ca..66b085bc2 100644
--- a/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs
+++ b/Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs
@@ -127,14 +127,25 @@ public void TheSecondRemarkableLimitStillReadsWhatGatheringGivesIt(
}
///
- /// What is deliberately left. A fractional c would have to divide the exponent
- /// rather than move into the base, so (sqrt(x) + 1)^x / sqrt(x)^x would
- /// become ((sqrt(x) + 1)^2 / x)^(x/2) -- a squared numerator to buy a
- /// gathered form. That is a judgement about output rather than the gap this fixes,
- /// so it is pinned as it stands rather than forced.
+ /// This was the case deliberately left: a fractional c would have to divide the
+ /// exponent rather than move into the base, so it would have become
+ /// ((sqrt(x) + 1)^2 / x)^(x/2) -- a squared numerator to buy a gathered form.
+ ///
+ /// It gathers now, and better, without that trade. Narrowing the
+ /// (a^b)^c = a^(b*c) rule to where it is true
+ /// (https://github.com/asc-community/AngouriMath/issues/752) means
+ /// sqrt(x)^x no longer flattens to x^(x/2), so both exponents stay
+ /// x and the ordinary pairing rule reaches it. The gap this file is about was
+ /// a rewrite running ahead of the pairing, and one less rewrite is one less way to
+ /// run ahead of it.
///
[Fact]
- public void AFractionalExponentRatioIsStillNotGathered() =>
- Assert.False(Simplified("(sqrt(x) + 1) ^ x / sqrt(x) ^ x") is Entity.Powf);
+ public void AFractionalExponentRatioGathersOnceNothingFlattensItFirst()
+ {
+ var simplified = Simplified("(sqrt(x) + 1) ^ x / sqrt(x) ^ x");
+ Assert.True(simplified is Entity.Powf,
+ $"came back as {simplified.Stringize()}");
+ AssertSameValueAt("(sqrt(x) + 1) ^ x / sqrt(x) ^ x", ("x", "13/10"));
+ }
}
}