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
38 changes: 30 additions & 8 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,27 @@ and `13` is not, every `n` from `24` is `4 a + 9 b` and `23` is not
| `exists a in ZZ* : exists b in ZZ* : 13 = 3 a + 8 b` | `UnhandledParseException` (left as written when quantifiers arrived) | `False` |
| `forall n in ZZ : n >= 14 implies (exists a in ZZ* : exists b in ZZ* : n = 3 a + 8 b)` | `UnhandledParseException` (left as written when quantifiers arrived) | `True` |

### An equation in a remainder is solved to its whole family

`f mod a = r` has one solution per period, and the solver had no way to write that. On 2.5.0
`(x mod 4) + 1 = 3` answered `{ }` -- no solution, for an equation with infinitely many. Where
the numeric fallback reached it, it answered with the few solutions it found in its window:
`(2x + 1) mod 4 = 3` was `{ -5, -3, -1, 1, 3, 5 }`, and 7 is a solution as well. The remainder
is now inverted the way the trigonometric functions are, with a whole parameter: `f = r + a n`
where `r` is a value the floored remainder takes -- `[0, a)` for a positive `a`, `(a, 0]` for a
negative one -- and no solution where it is not. A divisor or value that is not a number is left
unsolved.
[#1629](https://github.com/asc-community/AngouriMath/issues/1629), Algebrite's issue 87 from
[#180](https://github.com/asc-community/AngouriMath/issues/180). Both columns measured on a build,
`v2.5.0` against this change.

| `"….".ToEntity().Solve("x")` of | Was (2.5.0) | Is |
|---|---|---|
| `(x mod 4) + 1 = 3` | `{ }` | `{ 2 + 4 * n_1 }` |
| `(2x + 1) mod 4 = 3` | `{ -5, -3, -1, 1, 3, 5 }`, six of infinitely many | `{ (3 + 4 * n_1 - 1) / 2 }`, every odd number |
| `x mod (-4) = -1` | `{ }` | `{ -1 + 4 * n_1 }` |
| `x mod 4 = 5` | `{ }` | `{ }` (unchanged: modulo 4 the remainder never takes 5) |

### The modulus of a whole number is a whole number

With the facts in scope making `s` a whole number, `|s| in ZZ` and `|s| in ZZ*` are `True`, and
Expand Down Expand Up @@ -2411,18 +2432,19 @@ now has no value, as the interpreter's has none.
### An equation the solver cannot invert is left unsolved, not answered with no roots

`x! = 6` was answered `{ }`, a claim that it has no roots, and it has 3. The solver isolates `x`
by inverting the function around it, and for a factorial, a binomial coefficient, `mod`, `gcd`,
`lcm`, `min`, `max`, `phi`, `prime`, the valuation, a sum, a product, a limit, a set with `x`
inside it and a few more, the inversion had no way to write the preimage and returned none. Such
an equation is now left unsolved, as the set of `x` for which it holds, the way a statement
the solver has no arm for already was. Roots found beside it are kept. A value these functions
provably never take still has no roots: the factorial is the gamma function one along, which
has no zeros, so `x! = 0` is still `{ }`, and so is `arcsin(x) = 5`.
by inverting the function around it, and for a factorial, a binomial coefficient, `mod` by a
divisor that is not a number, `gcd`, `lcm`, `min`, `max`, `phi`, `prime`, the valuation, a sum,
a product, a limit, a set with `x` inside it and a few more, the inversion had no way to write
the preimage and returned none. Such an equation is now left unsolved, as the set of `x` for
which it holds, the way a statement the solver has no arm for already was. Roots found beside it
are kept. A value these functions provably never take still has no roots: the factorial is the
gamma function one along, which has no zeros, so `x! = 0` is still `{ }`, and so is
`arcsin(x) = 5`.

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"x! = 6".Solve("x")` | `{ }` | `{ x : x! = 6 }` |
| `"x mod 3 = 1".Solve("x")` | `{ }` | `{ x : x mod 3 = 1 }` |
| `"x mod a = 1".Solve("x")` | `{ }` | `{ x : x mod a = 1 }` |
| `"gcd(x, 4) = 2".Solve("x")` | `{ }` | `{ x : gcd(x, 4) = 2 }` |
| `"max(x, 1) = 3".Solve("x")` | `{ }` | `{ x : max(x, 1) = 3 }` |
| `"phi(x) = 4".Solve("x")` | `{ }` | `{ x : phi(x) = 4 }` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,31 @@ partial record Divf

partial record Modf
{
// x % a = value has one solution per period, so inverting it means introducing
// an integer parameter the way the trigonometric inversions do. Until that is
// written the equation is left unsolved: no solutions would claim it has none.
// x mod a = value has one solution per period, and is solved the way the
// trigonometric inversions are, with a whole parameter n. The remainder is the
// floored one, taking the sign of the divisor -- -1 mod 4 is 3, 5 mod (-4) is -3 and
// 5/2 mod 2 is 1/2 -- so it takes each value in [0, a) once per period for a positive
// a, and each in (a, 0] for a negative one. So x = value + |a| n for every whole n
// where value is in that range -- the same family as value + a n, since n runs over
// every whole number -- and there is no x where it is not: (x mod 4) + 1 = 3 is
// x = 2 + 4 n, the congruence x = 2 (mod 4) written out. A divisor with x in it, or a
// divisor or value that is not a number, leaves the equation unsolved, which is not
// the same as claiming it has no solution.
// https://github.com/asc-community/AngouriMath/issues/1629
private protected override IEnumerable<Entity>? InvertNode(Entity value, Entity x)
=> null;
{
if (Divisor.ContainsNode(x)
|| Divisor.Evaled is not Real { IsFinite: true, IsZero: false } period
|| value.Evaled is not Real { IsFinite: true } remainder)
return null;
var inRange = period.IsNegative
? (remainder.IsNegative || remainder.IsZero) && remainder > period
: !remainder.IsNegative && remainder < period;
if (!inRange)
return Enumerable.Empty<Entity>();
var step = period.IsNegative ? (-Divisor).InnerSimplified : Divisor;
return Dividend.Invert(value + step * Variable.CreateUnique(this + value, "n"), x);
}
}

partial record Powf
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ namespace AngouriMath.Tests.Algebra.SolveTest
{
/// <summary>
/// An equation in a function whose preimage the inverter cannot write -- a factorial, a
/// binomial coefficient, a residue, a gcd -- was answered with the empty set, a claim that
/// binomial coefficient, a gcd -- was answered with the empty set, a claim that
/// it has no roots. <c>x! = 6</c> has the root 3. Such an equation is now left unsolved, as
/// the set of <c>x</c> for which it holds.
/// </summary>
/// <remarks>
/// These assert what the answers mean -- which values are in the set -- rather than the
/// shape they are written in, so that solving one of them properly later does not fail them.
/// A solution written as a family in a whole parameter, as a remainder's is, is checked at
/// values of the parameter instead, since membership does not range over it.
/// </remarks>
[Trait("Area", "Algebra")]
public sealed class AnUnwrittenInverseIsNotTheEmptySetTest
Expand All @@ -33,7 +35,6 @@ public sealed class AnUnwrittenInverseIsNotTheEmptySetTest
[Theory]
[InlineData("x! = 6", "3", "2")]
[InlineData("binomial(x, 2) = 3", "-2", "2")]
[InlineData("x mod 3 = 1", "4", "3")]
[InlineData("gcd(x, 4) = 2", "6", "8")]
[InlineData("max(x, 1) = 3", "3", "1")]
[InlineData("prime(x) = 7", "4", "3")]
Expand Down
90 changes: 90 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/RemainderEquationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// 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.Linq;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;
using static AngouriMath.Entity;

namespace AngouriMath.Tests.Algebra
{
/// <summary>
/// An equation in a remainder, <c>f mod a = r</c>, solved as the congruence it is:
/// <c>f = r + a n</c> for every whole <c>n</c>, where <c>r</c> is a value the floored remainder
/// takes, and no solution where it is not. Algebrite's issue 87, from #180.
/// https://github.com/asc-community/AngouriMath/issues/1629
/// </summary>
[Trait("Area", "Algebra")]
public sealed class RemainderEquationTest
{
/// <summary>Every member of the family satisfies the equation, at several values of its parameter.</summary>
[Theory]
[InlineData("(x mod 4) + 1 = 3")]
[InlineData("x mod 4 = 0")]
[InlineData("x mod (-4) = -1")]
[InlineData("(2x + 1) mod 4 = 3")]
[InlineData("x mod 2 = 1/2")]
public void EverySolutionSatisfiesIt(string equation)
{
var solutions = Assert.IsType<Set.FiniteSet>(equation.ToEntity().Solve("x"));
Assert.NotEmpty(solutions.Elements);
foreach (var solution in solutions.Elements)
{
var parameter = Assert.Single(solution.Vars);
for (var n = -2; n <= 2; n++)
Assert.Equal(Boolean.True, equation.ToEntity().Substitute("x", solution.Substitute(parameter, n)).Evaled);
}
}

/// <summary>
/// And the family is all of them: <c>x mod 4 = 2</c> holds exactly when <c>x - 2</c> is a
/// multiple of 4, so at <c>n</c> from -2 to 2 the family is -6, -2, 2, 6 and 10.
/// </summary>
[Fact]
public void TheFamilyIsEverySolution()
{
var solution = Assert.Single(Assert.IsType<Set.FiniteSet>("(x mod 4) + 1 = 3".ToEntity().Solve("x")).Elements);
var parameter = Assert.Single(solution.Vars);
Assert.Equal(new Entity[] { -6, -2, 2, 6, 10 },
Enumerable.Range(-2, 5).Select(n => solution.Substitute(parameter, n).Evaled).ToArray());
}

/// <summary>
/// A value solves it exactly when the family reaches it at a whole parameter: for
/// <c>x mod 3 = 1</c>, 4 is reached at <c>n = 1</c>, and 3 only at <c>n = 2/3</c>.
/// </summary>
[Theory]
[InlineData("4", true)]
[InlineData("3", false)]
public void AValueSolvesItWhenTheFamilyReachesItAtAWholeParameter(string value, bool solves)
{
var solution = Assert.Single(Assert.IsType<Set.FiniteSet>("x mod 3 = 1".ToEntity().Solve("x")).Elements);
var parameter = Assert.Single(solution.Vars);
var at = Assert.Single(Assert.IsType<Set.FiniteSet>(solution.Equalizes(value.ToEntity()).Solve(parameter)).Elements);
Assert.Equal(solves, at.Evaled is Number.Integer);
}

/// <summary>
/// A value the remainder never takes has no solution: modulo 4 it lies in [0, 4), modulo
/// -4 in (-4, 0].
/// </summary>
[Theory]
[InlineData("x mod 4 = 5")]
[InlineData("x mod 4 = -1")]
[InlineData("x mod (-4) = 1")]
public void AValueTheRemainderNeverTakesHasNoSolution(string equation)
=> Assert.Empty(Assert.IsType<Set.FiniteSet>(equation.ToEntity().Solve("x")).Elements);

/// <summary>A divisor or a value that is not a number leaves it unsolved, which is not the same as no solution.</summary>
[Theory]
[InlineData("x mod a = 1")]
[InlineData("x mod 4 = y")]
public void WhatItCannotReadIsLeftUnsolved(string equation)
=> Assert.IsType<Set.ConditionalSet>(equation.ToEntity().Solve("x"));
}
}
16 changes: 9 additions & 7 deletions Sources/Tests/UnitTests/Convenience/ModulusTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,16 @@ public void TheLinqCompilerKeepsTheFractionalPart() =>
Assert.Equal(1.5, "x mod y".ToEntity().Compile<double, double, double>("x", "y")(7.5, 2), 9);

/// <summary>
/// x mod a = value has one solution per period, so answering it means introducing an
/// integer parameter the way the trigonometric inversions do. Until that is written the
/// equation is left unsolved, as the set of x for which it holds. This asserted the
/// empty set, which claims there is no such x, and 4 is one. Pinned so that whoever
/// writes the inversion sees this change.
/// x mod a = value has one solution per period, and is answered the way the
/// trigonometric inversions are, with a whole parameter: x mod 3 = 1 is x = 1 + 3 n,
/// which is 4 at n = 1. https://github.com/asc-community/AngouriMath/issues/1629
/// </summary>
[Fact]
public void SolvingIsNotClaimed() =>
Assert.IsType<Entity.Set.ConditionalSet>(("x mod 3".ToEntity() - 1).SolveEquation("x"));
public void SolvingGivesEverySolution()
{
var solution = Assert.Single(Assert.IsType<Entity.Set.FiniteSet>(("x mod 3".ToEntity() - 1).SolveEquation("x")).Elements);
var parameter = Assert.Single(solution.Vars);
Assert.Equal((Entity)4, solution.Substitute(parameter, 1).Evaled);
}
}
}
Loading