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
42 changes: 42 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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

| | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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}
Expand Down
14 changes: 13 additions & 1 deletion Sources/Tests/UnitTests/Algebra/SolveTest/SolveInequality.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
121 changes: 121 additions & 0 deletions Sources/Tests/UnitTests/Common/PowerOfPowerBranchTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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: <c>(a^b)^c = a^(b*c)</c>, which needs a
/// positive <c>a</c> or a whole <c>c</c>, and <c>(a*x)^c = a^c * x^c</c>, which needs a
/// positive <c>a</c> or a whole <c>c</c>. Both were applied unconditionally, so
/// <c>sqrt(x^2)</c> came back as <c>x</c> and <c>sqrt(-x)</c> as <c>i * sqrt(x)</c> --
/// each the negation of the right answer where x is negative.
/// https://github.com/asc-community/AngouriMath/issues/752
/// </summary>
public sealed class PowerOfPowerBranchTest
{
/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
[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");

/// <summary>
/// What the rules still do, because there they are true. A whole outer exponent makes
/// <c>(a^b)^c</c> sound whatever the sign of a -- it is <c>a^b</c> multiplied by
/// itself c times -- and a positive constant may always be taken out from under a
/// root.
/// </summary>
[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");

/// <summary>
/// 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. <c>work/simpsweep</c> asks it of ten thousand generated expressions and
/// went from 30 disagreements to 0 with these two rules narrowed.
/// </summary>
[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);
}
}
}
}
25 changes: 18 additions & 7 deletions Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,25 @@ public void TheSecondRemarkableLimitStillReadsWhatGatheringGivesIt(
}

/// <summary>
/// What is deliberately left. A fractional c would have to divide the exponent
/// rather than move into the base, so <c>(sqrt(x) + 1)^x / sqrt(x)^x</c> would
/// become <c>((sqrt(x) + 1)^2 / x)^(x/2)</c> -- 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
/// <c>((sqrt(x) + 1)^2 / x)^(x/2)</c> -- a squared numerator to buy a gathered form.
/// <para/>
/// It gathers now, and better, without that trade. Narrowing the
/// <c>(a^b)^c = a^(b*c)</c> rule to where it is true
/// (https://github.com/asc-community/AngouriMath/issues/752) means
/// <c>sqrt(x)^x</c> no longer flattens to <c>x^(x/2)</c>, so both exponents stay
/// <c>x</c> 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.
/// </summary>
[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"));
}
}
}
Loading