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
25 changes: 25 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ read first.
| | `MathS.Polynomials.Factor("x * y + y", "x")`, and any polynomial whose coefficients in the named variable share a common divisor | `null` — a refusal | `y * (x + 1)` |
| | `MathS.Polynomials.SquareFreePart("(x - y) ^ 2 * (x + y)", "x")`, and any polynomial in more than one variable | `null` — a refusal | `x ^ 2 - y ^ 2` |
| | `MathS.Polynomials.Factor("x ^ 2 - y ^ 2", "x")`, and polynomials in two variables of small enough bidegree | `null` — a refusal | `(x + y) * (x - y)` |
| **Silent** | `RewriteRules.Power.ApplyOnce("ln(1 / x)")`, and every `log(_, 1/_)` and `log(1/_, _)` whose argument is not decidably a positive real | `-ln(x)`, which is wrong on the negative reals | `ln(1 / x)`, left alone |

### An equation nothing settled is no longer answered with the empty set

Expand Down Expand Up @@ -305,6 +306,30 @@ bracketing for. The change only ever adds `\left(`/`\right)` groups, which CShar
parses, so nothing downstream needs a matching change
([#822](https://github.com/asc-community/AngouriMath/issues/822)).

### A reciprocal inside a logarithm is no longer moved out unconditionally

`ln(1/b) = -ln(b)` is false on the negative reals, because the principal argument does not negate
with its logarithm. At `b = -0.63`, `ln(1/b)` is `0.462 + πi` and `-ln(b)` is `0.462 − πi`.

Three arms of `PowerRules` applied it for every `b`, and they now carry the guard their neighbours
ten lines below already had — `ln(1/b)` is `ln(1) − ln(b)`, so a reciprocal is the *difference* case
of the logarithm gathering and the same helper answers it. Both ways of earning the rewrite carry
over: the argument is decidably a positive real, or the limit machinery is reading towards a
destination and has established the sign on the way.

| | before | now |
|---|---|---|
| `RewriteRules.Power.ApplyOnce("ln(1 / x)")` | `-ln(x)` | `ln(1 / x)` |
| `RewriteRules.Power.ApplyOnce("log(2, 1 / x)")` | `-log(2, x)` | `log(2, 1 / x)` |
| `RewriteRules.Power.ApplyOnce("log(1 / x, 1 / y)")` | `log(x, y)` | `log(1 / x, 1 / y)` |
| `RewriteRules.Power.ApplyOnce("ln(1 / 2.5)")` | `-ln(5/2)` | `-ln(5/2)` — the argument is positive |

**`Simplify` is unchanged**, and that is why this went unnoticed: `"ln(1 / x)".Simplify()` was
`ln(1 / x)` before and after, because the candidate search never picked that branch. So no answer
from the public simplifier moves. What moves is `RewriteRules.Power` applied on its own, which is
what [#746](https://github.com/asc-community/AngouriMath/issues/746) tier 2 makes a caller able to
do ([#1062](https://github.com/asc-community/AngouriMath/issues/1062)).

### `Factor` factors a polynomial in more than one variable

After the content is taken out, what remains may still have polynomial coefficients — and it can be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,27 @@ when MayTakeLogOfPower(any2, any3) => any3 * MathS.Log(any1, any2),
// https://github.com/asc-community/AngouriMath/issues/721
Logf(var any1, var any1a) logarithm when any1 == any1a
=> new Providedf(1, logarithm.DomainCondition),
Logf(Divf(Integer(1), var any1), Divf(Integer(1), var any2)) => MathS.Log(any1, any2),
Logf(var any1, Divf(Integer(1), var any2)) => -MathS.Log(any1, any2),
Logf(Divf(Integer(1), var any1), var any2) => -MathS.Log(any1, any2),
// ln(1/b) = -ln(b) is false on the negative reals, for the reason the pair of rules
// below is guarded for: the principal argument does not negate with its logarithm.
// At b = -0.63, ln(1/b) is 0.462 + pi*i and -ln(b) is 0.462 - pi*i. These three were
// applied unconditionally while their neighbours ten lines down carried a guard, and
// `work/rulecheck` reports them once its corpus is wide enough to build the shape.
//
// The condition is the same one, and is asked through the same helper: ln(1/b) is
// ln(1) - ln(b), so a reciprocal is the difference case with a numerator of 1. Both
// ways of earning it carry over -- the operand is decidably a positive real, or the
// limit machinery is reading towards a destination and has established the sign.
// https://github.com/asc-community/AngouriMath/issues/721
Logf(Divf(Integer(1), var any1), Divf(Integer(1), var any2))
when MayGatherLogarithms(Integer.One, any1, isDifference: true)
&& MayGatherLogarithms(Integer.One, any2, isDifference: true)
=> MathS.Log(any1, any2),
Logf(var any1, Divf(Integer(1), var any2))
when MayGatherLogarithms(Integer.One, any2, isDifference: true)
=> -MathS.Log(any1, any2),
Logf(Divf(Integer(1), var any1), var any2)
when MayGatherLogarithms(Integer.One, any1, isDifference: true)
=> -MathS.Log(any1, any2),


// ln(a) + ln(b) = ln(a*b), and the difference likewise, are false off the positive
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// 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;
using AngouriMath.Core.Transformations;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.PatternsTest
{
/// <summary>
/// <c>ln(1/b) = -ln(b)</c> was applied for every <c>b</c>, and it is false on the negative
/// reals: the principal argument does not negate with its logarithm. At <c>b = -0.63</c>,
/// <c>ln(1/b)</c> is <c>0.462 + pi*i</c> and <c>-ln(b)</c> is <c>0.462 - pi*i</c>.
/// https://github.com/asc-community/AngouriMath/issues/1062
/// </summary>
/// <remarks>
/// <para>
/// The same mistake as the logarithm gathering ten lines below it in <c>PowerRules</c>,
/// which was guarded while these three arms were not — the third time in this file's history
/// that a branch-cut fix has stopped at the rule it was reported against and left its
/// neighbours alone, after
/// <a href="https://github.com/asc-community/AngouriMath/issues/752">#752</a> and
/// <a href="https://github.com/asc-community/AngouriMath/issues/801">#801</a>.
/// </para>
/// <para>
/// Asserted against <see cref="RewriteRules.Power"/> rather than against
/// <see cref="Entity.Simplify"/>, because <c>Simplify</c> never picked this branch and so
/// would pass whatever the rule did. That is what made the defect invisible to
/// <c>boundcheck</c>, which measures <c>Simplify</c>.
/// </para>
/// </remarks>
[Trait("Area", "PatternsTest")]
public sealed class ReciprocalLogarithmBranchTest
{
/// <summary>
/// The rule set must not change the value at a negative argument. Compared against the
/// expression it came from rather than against an expected form: what matters is that
/// rewriting did not move the point.
/// </summary>
[Theory]
[InlineData("ln(1 / x)")]
[InlineData("log(2, 1 / x)")]
[InlineData("log(1 / x, 2)")]
[InlineData("log(1 / x, 1 / y)")]
public void RewritingKeepsTheValueAtANegativeArgument(string expr)
{
var rewritten = RewriteRules.Power.ApplyOnce(expr.ToEntity());
foreach (var (x, y) in new[] { (-0.63, -1.7), (-1.0, -2.0), (-2.5, -0.4), (-0.25, 3.0) })
{
var before = expr.ToEntity().Substitute("x", x).Substitute("y", y).EvalNumerical();
var after = rewritten.Substitute("x", x).Substitute("y", y).EvalNumerical();
var difference =
Math.Abs(before.RealPart.EDecimal.ToDouble() - after.RealPart.EDecimal.ToDouble())
+ Math.Abs(before.ImaginaryPart.EDecimal.ToDouble() - after.ImaginaryPart.EDecimal.ToDouble());
Assert.True(difference < 1e-9,
$"{expr} was rewritten to {rewritten.Stringize()}, which at x = {x}, y = {y} "
+ $"is {after.Stringize()} rather than {before.Stringize()}");
}
}

/// <summary>
/// The exact point the issue names, stated as itself rather than as a tolerance: the
/// imaginary parts must not be each other's negation.
/// </summary>
[Fact]
public void TheArgumentDoesNotNegateWithItsLogarithm()
{
var at = Entity.Number.Real.Create(PeterO.Numbers.EDecimal.FromString("-0.63"));
var direct = "ln(1 / x)".ToEntity().Substitute("x", at).EvalNumerical();
var negated = "-ln(x)".ToEntity().Substitute("x", at).EvalNumerical();
Assert.True(
Math.Abs(direct.ImaginaryPart.EDecimal.ToDouble()
+ negated.ImaginaryPart.EDecimal.ToDouble()) < 1e-9,
"the premise of this test has changed: the two are supposed to differ by the "
+ $"sign of the imaginary part, and they are {direct.Stringize()} and {negated.Stringize()}");
Assert.NotEqual(direct.Stringize(), negated.Stringize());
}

/// <summary>
/// The rule must keep firing where it is sound, or this would be a fix that removes an
/// answer. A positive real argument is safe, and that is the condition the guard asks.
/// </summary>
[Theory]
[InlineData("ln(1 / 2.5)", "-ln(5/2)")]
[InlineData("log(3, 1 / 2.5)", "-log(3, 5/2)")]
public void TheSoundCasesStillRewrite(string expr, string expected)
=> Assert.Equal(expected.ToEntity(), RewriteRules.Power.ApplyOnce(expr.ToEntity()));
}
}
Loading