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: 24 additions & 1 deletion BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ the square root the solver takes after solving for `x^2`, where an equation has
| `"x^4 - (a^2 + b^2)*x^2 + a^2*b^2 = 0".Solve("x")` | `{ sqrt((a ^ 2 + b ^ 2 - sqrt((-(a ^ 2 + b ^ 2)) ^ 2 - 4 * a ^ 2 * b ^ 2)) / 2), … }` | `{ -b, b, -a, a }` |
| `"sin(x)^2 = (a + 1)^2".Solve("x")` | `{ arcsin(sqrt((a + 1) ^ 2)) + 2 * pi * n_1, … }` | `{ arcsin(-a - 1) + 2 * pi * n_1, … }` |

### A fractional power is inverted where its values lie

**Silent: a root gains the condition under which it is one.** The principal power `z^p` has argument `p arg(z)`, so
for `0 < p < 1` its values lie in the sector `-p pi < arg <= p pi`, with `0`, and for `-1 < p < 0` in
`p pi <= arg < -p pi`, without `0`. `x^p = a` was solved to `a^(1/p)` with no condition, which is no root where `a` is
outside the sector: `sqrt(x) = a` was `{ a ^ 2 }`, and at `a = -1` that is `1`, whose square root is `1` and not `-1`. The root now carries
the sector, written with `arg`, as `abs`'s inversion carries its own condition. A number on the other side is decided
as before, and `|p| > 1` needs no condition, since every value is then a power. What the equation is provided is known
while it is solved, as a provided expression is differentiated: a value known to be real, or declared real, has the
half-line for its sector, one known to lie on it needs no condition, and one known to lie off it has no root. A condition that is already part of
another is no longer stated twice: a system's rows carried the same condition once a substitution
([#212](https://github.com/asc-community/AngouriMath/issues/212)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"sqrt(x) = a".ToEntity().Solve("x")` | `{ a ^ 2 }` | `{ a ^ 2 provided a = 0 or -1/2 * pi < arg(a) and arg(a) <= 1/2 * pi }` |
| `"x^(1/3) = a".ToEntity().Solve("x")` | `{ a ^ 3 }` | `{ a ^ 3 provided a = 0 or -1/3 * pi < arg(a) and arg(a) <= 1/3 * pi }` |
| `"x^(-1/2) = a".ToEntity().Solve("x")` | `{ a ^ (-2) }` | `{ a ^ (-2) provided not a = 0 and -1/2 * pi <= arg(a) and arg(a) < 1/2 * pi }` |
| `"(sqrt(x) = a) provided a in RR".ToEntity().Solve("x")` | `{ a ^ 2 provided a in RR }` | `{ a ^ 2 provided a in RR and a >= 0 }` |
| `"(sqrt(x) = a) provided a > 0".ToEntity().Solve("x")` | `{ a ^ 2 provided a > 0 }` | `{ a ^ 2 provided a > 0 }` |
| `"(sqrt(x) = a) provided a < 0".ToEntity().Solve("x")` | `{ a ^ 2 provided a < 0 }` | `{ }` |
| `MathS.Equations("sqrt(y) - x", "y - a").Solve("x", "y")` | `[[sqrt(a), (sqrt(a)) ^ 2], [-sqrt(a), (-sqrt(a)) ^ 2]]` | `[[sqrt(a) provided C(sqrt(a)), (sqrt(a)) ^ 2 provided C(sqrt(a))], [-sqrt(a) provided C(-sqrt(a)), (-sqrt(a)) ^ 2 provided C(-sqrt(a))]]`, `C(v)` being `v = 0 or -1/2 * pi < arg(v) and arg(v) <= 1/2 * pi` |

### A system with a product equation is solved branch by branch

**Silent: solutions that were missing are answered.** `EquationSystem.Solve` eliminated `x * y = 0` for `y` as `0`,
Expand Down Expand Up @@ -229,7 +252,7 @@ is `null`. A row with symbols in it is tried at one value of them, and where it
| `MathS.Equations("sqrt(y) - x", "x + 1").Solve("x", "y")` | `[[-1, 1]]` | `null`: `sqrt(1) - (-1)` is 2 |
| `MathS.Equations("sqrt(y) - x", "y - 4").Solve("x", "y")` | `[[2, 4], [-2, 4]]` | `[[2, 4]]` |
| `MathS.Equations("ln(x) + ln(y)", "x - y").Solve("x", "y")` | `[[-1, -1], [1, 1]]` | `[[1, 1]]`: `ln(-1) + ln(-1)` is `2 pi i` |
| `MathS.Equations("sqrt(y) - x", "y - a").Solve("x", "y")` | `[[sqrt(a), (sqrt(a)) ^ 2], [-sqrt(a), (-sqrt(a)) ^ 2]]` | `null`: the second row is a solution only at `a = 0` |
| `MathS.Equations("sqrt(y) - x", "y - a").Solve("x", "y")` | `[[sqrt(a), (sqrt(a)) ^ 2], [-sqrt(a), (-sqrt(a)) ^ 2]]` | both rows, each with where it holds: see *A fractional power is inverted where its values lie* |

### The power reduction of a squared cosine is proved, as the squared sine's is

Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Convenience/ConjugateAndArgumentTest.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/Algebra/SolveTest/FractionalPowerRangeTest.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/Convenience/IversonTest.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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ when @base.ContainsNode(x) && !power.ContainsNode(x)
// reached at all. https://github.com/asc-community/AngouriMath/issues/744
case Function when expr.Nodes.Count(node => node == x) == 1:
return expr.Invert(0, x)?.Select(ent => TryDowncast(expr, x, ent)).ToSet();
// Solved knowing the condition, as a provided expression is differentiated: the
// roots are filtered by it anyway, and what holds under it can be stated by it.
// https://github.com/asc-community/AngouriMath/issues/212
case Providedf(var expression, var predicate):
return Solve(expression, x, compensateSolving).Filter(predicate, x);
return TreeAnalyzer.Assuming(predicate, () => Solve(expression, x, compensateSolving)).Filter(predicate, x);
case Piecewise p:
return EquationSolver.SolvePiecewise(p, x, (e, x) => Solve(e, x, compensateSolving));
// A product is zero exactly where one of its factors is. Without this the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,46 @@ partial record Powf
return Together(Number.GetAllRootsOf1(pow.Abs()).Select(root => Base.Invert(root * MathS.Pow(value, 1 / Exponent), x)));
}
else
return Base.Invert(MathS.Pow(value, 1 / Exponent), x);
return Base.Invert(MathS.Pow(value, 1 / Exponent), x)
?.Select(root => root.Provided(InTheRange(value)));
}
// a ^ x = value => x = log(a, value)
// TODO: determine how we should return periodic roots as
// (e ^ x) ^ a != (e ^ a) ^ x for logs
return Exponent.Invert(MathS.Log(Base, value), x);
}

// The principal power z^p is e^(p ln z), so its argument is p arg(z): in (-p pi, p pi]
// for 0 < p < 1, and in [p pi, -p pi) for -1 < p < 0, where it is never 0. A value
// outside that sector is no power of anything, and v^(1/p) is then a number whose
// power is not v: sqrt(y) = x was answered { x ^ 2 }, and sqrt((-1) ^ 2) is 1. For
// |p| > 1 the sector is the whole plane, and v^(1/p) is a preimage of every v. A
// symbolic exponent has no sector to state.
// https://github.com/asc-community/AngouriMath/issues/212
private Entity InTheRange(Entity value)
{
if (Exponent.Evaled is not Real { EDecimal.IsFinite: true } p
|| p.EDecimal.Abs().CompareTo(PeterO.Numbers.EDecimal.One) >= 0)
return Boolean.True;
// Where the value is known to be real -- by its codomain, or by what the equation
// it came from was provided -- arg(value) is 0 or pi, so the sector is the half-line
// value >= 0 for p > 0 and value > 0 for p < 0, and nothing is stated where that is
// known as well: sqrt(x) = a provided a > 0 is a ^ 2 provided a > 0.
Entity halfLine = p.IsPositive ? new GreaterOrEqualf(value, Integer.Zero) : new Greaterf(value, Integer.Zero);
Entity outside = p.IsPositive ? new Lessf(value, Integer.Zero) : new LessOrEqualf(value, Integer.Zero);
if (TreeAnalyzer.IsKnown(halfLine))
return Boolean.True;
if (TreeAnalyzer.IsKnown(outside))
return Boolean.False;
if (Conjugatef.IsKnownReal(value) || TreeAnalyzer.IsKnownToBeReal(value))
return halfLine;
var bound = (Exponent * MathS.pi).InnerSimplified;
var opposite = ((-Exponent).InnerSimplified * MathS.pi).InnerSimplified;
var angle = MathS.Arg(value);
return p.IsPositive
? value.EqualTo(Integer.Zero) | (new Lessf(opposite, angle) & new LessOrEqualf(angle, bound))
: !value.EqualTo(Integer.Zero) & new LessOrEqualf(bound, angle) & new Lessf(angle, opposite);
}
}

// TODO: Consider case when sin(sin(x)) where double-mention of n occures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ private static Entity Equivalent(Entity left, Entity right)
// enough to list them. https://github.com/asc-community/AngouriMath/issues/1409
Congruentf(var left, var right, Integer modulus) when Congruence(left - right, modulus, x) is { } classes => classes,

Providedf(var e, var predicate) => Solve(e, x)?.Filter(predicate, x),
Providedf(var e, var predicate) => TreeAnalyzer.Assuming(predicate, () => Solve(e, x))?.Filter(predicate, x),
Piecewise p => EquationSolver.SolvePiecewiseOrNull(p, x, Solve),

// A statement the solver has no arm for is the set of x with the property, left
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ public partial record Conjugatef
// Real where the node's own codomain says so: a modulus, or a symbol declared with
// domain(x, RR). A bare Variable is Domain.Any, so conjugate(x) stays. Not the codomain
// setting, since what this returns is cached on the node and the setting is not.
private static bool IsKnownReal(Entity entity)
internal static bool IsKnownReal(Entity entity)
=> entity.Codomain is Domain.Prime or Domain.PositiveInteger or Domain.NonNegativeInteger
or Domain.Integer or Domain.Rational or Domain.Real;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,38 @@ protected override Entity InnerSimplify(bool isExact) =>
(a, b) => (a, b) switch
{
(Providedf exprProvided, Providedf predProvided) =>
Decide(exprProvided.Expression, exprProvided.Predicate & predProvided.Predicate & predProvided.Expression),
(var expr, Providedf predProvided) => Decide(expr, predProvided.Predicate & predProvided.Expression),
(Providedf exprProvided, var pred) => Decide(exprProvided.Expression, pred & exprProvided.Predicate),
Decide(exprProvided.Expression, Both(Both(exprProvided.Predicate, predProvided.Predicate), predProvided.Expression)),
(var expr, Providedf predProvided) => Decide(expr, Both(predProvided.Predicate, predProvided.Expression)),
(Providedf exprProvided, var pred) => Decide(exprProvided.Expression, Both(pred, exprProvided.Predicate)),
(var expr, var pred) => Decide(expr, pred),
},
(@this, a, b) => ((Providedf)@this).New(a, b), isExact);

// Two conditions joined, with a conjunct already stated not stated again. A row of a
// system substituted into the expressions that carry its condition had it conjoined
// once a substitution, five times over for sqrt(y) - x, y - a. The conjunction is
// rebuilt only where something repeats, so every other condition keeps its shape.
// https://github.com/asc-community/AngouriMath/issues/212
private static Entity Both(Entity first, Entity second)
{
var conjuncts = new List<Entity>();
var repeated = false;
void Gather(Entity condition)
{
if (condition is Andf(var left, var right))
{
Gather(left);
Gather(right);
}
else if (conjuncts.Contains(condition))
repeated = true;
else
conjuncts.Add(condition);
}
Gather(first);
Gather(second);
return repeated ? conjuncts.Aggregate((joined, next) => joined & next) : first & second;
}
}

partial record Piecewise
Expand Down
26 changes: 26 additions & 0 deletions Sources/AngouriMath/Functions/TreeAnalyzer/RealValued.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,32 @@ or Arctanf or Arccotanf or Signumf or Erff or Erfcf or Erfif or Eif or Sif or Sh
/// </summary>
[ThreadStatic] private static Entity? assumed;

/// <summary>
/// Whether what <see cref="Assuming"/> says is known forces <paramref name="predicate"/>.
/// </summary>
internal static bool IsKnown(Entity predicate)
=> assumed is { } known && PredicateEntailment.Entails(known, predicate);

/// <summary>
/// Whether what <see cref="Assuming"/> says is known makes <paramref name="expr"/> real: it is
/// in <c>RR</c>, or it is ordered against something, which an order comparison has a truth
/// value only for.
/// </summary>
internal static bool IsKnownToBeReal(Entity expr)
{
if (assumed is not { } known)
return false;
if (PredicateEntailment.Entails(known, expr.In(AngouriMath.Core.Domain.Real)))
return true;
static bool Orders(Entity condition, Entity expr) => condition switch
{
Andf(var left, var right) => Orders(left, expr) || Orders(right, expr),
ComparisonSign sign => sign.DirectChildren[0] == expr || sign.DirectChildren[1] == expr,
_ => false
};
return Orders(known, expr);
}

/// <summary>
/// Runs <paramref name="work"/> with <paramref name="condition"/> known to hold, as well
/// as whatever was known already.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// 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 AngouriMath;
using AngouriMath.Extensions;
using System.Linq;
using Xunit;
using static AngouriMath.Entity;

namespace AngouriMath.Tests.Algebra
{
/// <summary>
/// The principal power <c>z^p</c> has argument <c>p arg(z)</c>, so for <c>|p| &lt; 1</c> its
/// values lie in a sector, and an equation with its other side outside the sector has no
/// solution. <c>sqrt(x) = a</c> was answered <c>{ a ^ 2 }</c>, which is wrong at
/// <c>a = -1</c>: <c>sqrt((-1)^2)</c> is <c>1</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/212">#212</a>
/// </summary>
[Trait("Area", "Algebra")]
public sealed class FractionalPowerRangeTest
{
private static Entity OnlyRoot(string equation)
{
var roots = Assert.IsType<Set.FiniteSet>(equation.ToEntity().Solve("x"));
return Assert.Single(roots);
}

/// <summary>
/// The root holds exactly where its condition does, at a value inside the sector, one on
/// its included edge, and one outside.
/// </summary>
[Theory]
[InlineData("sqrt(x) = a", "2", true)]
[InlineData("sqrt(x) = a", "0", true)]
[InlineData("sqrt(x) = a", "i", true)]
[InlineData("sqrt(x) = a", "-i", false)]
[InlineData("sqrt(x) = a", "-1", false)]
[InlineData("x^(1/3) = a", "1 + i", true)]
[InlineData("x^(1/3) = a", "-1", false)]
[InlineData("x^(-1/2) = a", "-i", true)]
[InlineData("x^(-1/2) = a", "i", false)]
[InlineData("x^(-1/2) = a", "0", false)]
public void TheRootHoldsWhereItsConditionDoes(string equation, string value, bool holds)
{
var root = Assert.IsType<Providedf>(OnlyRoot(equation));
var at = value.ToEntity();
Assert.Equal(Boolean.Create(holds), root.Predicate.Substitute("a", at).EvalBoolean());
if (!holds)
return;
var lhs = equation.Split('=')[0].ToEntity();
var residual = (lhs.Substitute("x", root.Expression.Substitute("a", at)) - at).EvalNumerical();
Assert.True(Number.Abs(residual).EDecimal.CompareTo(PeterO.Numbers.EDecimal.FromString("1e-30")) < 0,
$"{equation} at a = {value}: the residual is {residual}");
}

/// <summary>A number on the other side is decided, as it was.</summary>
[Theory]
[InlineData("sqrt(x) = 2", "{ 4 }")]
[InlineData("sqrt(x) = -1", "{ }")]
[InlineData("sqrt(x) = i", "{ -1 }")]
[InlineData("sqrt(x) = -i", "{ }")]
public void ANumberIsDecided(string equation, string expected)
=> Assert.Equal(expected.ToEntity(), equation.ToEntity().Solve("x"));

/// <summary>
/// What the equation is provided is known while it is solved. A real value has argument 0
/// or <c>pi</c>, so the sector is the half-line; a value known to lie on it needs nothing,
/// and one known to lie off it has no root.
/// </summary>
[Theory]
[InlineData("(sqrt(x) = a) provided a in RR", "{ a ^ 2 provided a in RR and a >= 0 }")]
[InlineData("(sqrt(x) = a) provided a > 0", "{ a ^ 2 provided a > 0 }")]
[InlineData("(sqrt(x) = a) provided a >= 1", "{ a ^ 2 provided a >= 1 }")]
[InlineData("(sqrt(x) = a) provided a > -5", "{ a ^ 2 provided a > -5 and a >= 0 }")]
[InlineData("(x^(-1/2) = a) provided a in RR", "{ a ^ (-2) provided a in RR and a > 0 }")]
[InlineData("(sqrt(x) = a) provided a < 0", "{ }")]
[InlineData("(x^(-1/2) = a) provided a <= 0", "{ }")]
public void WhatTheEquationIsProvidedIsKnown(string equation, string expected)
=> Assert.Equal(expected.ToEntity(), equation.ToEntity().Solve("x"));

/// <summary>A symbol declared real is the same case: <c>domain(a, RR)</c>.</summary>
[Fact]
public void ASymbolDeclaredRealHasTheHalfLine()
=> Assert.Equal("{ domain(a, RR) ^ 2 provided domain(a, RR) >= 0 }".ToEntity(),
"sqrt(x) = domain(a, RR)".ToEntity().Solve("x"));

/// <summary>
/// For <c>|p| &gt; 1</c> the sector is the whole plane, and <c>v^(1/p)</c> is a preimage
/// of every <c>v</c>, so there is nothing to state.
/// </summary>
[Fact]
public void APowerAboveOneNeedsNoCondition()
=> Assert.IsNotType<Providedf>(OnlyRoot("x^(3/2) = a"));
}
}
Loading
Loading