diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index ced53ccae..4262161f5 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -656,6 +656,23 @@ with the multiplier gathered from the whole product in the exponent
| `"e^(2*acoth(2*x))*(3 - 12*x^2)^2".Integrate("x")` | `integral(…)` — left unevaluated | `(144 * x ^ 5 / 5 + 144 * x ^ 4 / 4 + (-36) * x ^ 2 / 2 + (-9) * x provided not 2 * x + -1 = 0) + C` |
| `"e^(1/3*acoth(x))*x^2".Integrate("x")` | `integral(…)` — left unevaluated | an antiderivative in `((x + 1)/(x - 1))^(1/6)` |
+### `Solve` no longer answers the empty set for a statement it cannot read
+
+**Wrong answer fixed.** `"x^2 in (0; 1)".Solve("x")` was `{}` — and so was every `f(x) in S` with
+anything but a bare `x` on the left, and every statement the solver had no arm for: the arm at the
+end of the dispatch answered the empty set, which claims there is no such `x`. A membership is
+solved now — the members of a listed set each as an equation, an interval as its bounds, each
+strict or not as the end is — and a statement that is not read is the set of `x` with the
+property, left as written. A quantifier that put the negated body to the solver read that
+emptiness as a proof: `forall x in RR : x^2 in ZZ` was `True`
+([#1409](https://github.com/asc-community/AngouriMath/issues/1409), the reference's pre-images).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"x^2 in (0; 1)".Solve("x")` | `{}` — wrong | `((-oo; 0) \/ (0; +oo)) /\ (-1; 1)` |
+| `"x^2 in {1, 4}".Solve("x")` | `{}` — wrong | `{ 1, -1, 2, -2 }` |
+| `"x^2 in ZZ".Solve("x")` | `{}` — wrong | `{ x : x ^ 2 in ZZ }`, left as written |
+| `forall x in RR : x^2 in ZZ` | `UnhandledParseException` (quantifiers are new since) | `False` |
### `...` is the pattern operator
`{1, 2, ..., n}`, `{2, 4, ..., 2 n}`, `{5, 10, 15, ...}`, `{..., -1, 0}`, `1 + 2 + ... + n` and
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 4d1cc500b..d7cf87fac 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -291,6 +291,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[AngouriMath/Functions/NumberTheory/ResidueClasses.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/MembershipSolvingTest.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
+
[AngouriMath/Core/PatternOperator.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
diff --git a/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Subset.cs b/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Subset.cs
index 623d7412b..475b4098a 100644
--- a/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Subset.cs
+++ b/Sources/AngouriMath/Core/Entity/Omni/Sets/SetOperators.Subset.cs
@@ -223,6 +223,13 @@ internal static partial class SetOperators
var over = sub is ConditionalSet { DeclaredMembership: (var declared, _) } ? declared as Set : sub;
switch (super)
{
+ // The operators are the connectives: x in A \/ B is x in A or x in B.
+ case Unionf(Set a, Set b):
+ return MembershipAsComparison(x, a, sub) is { } inA && MembershipAsComparison(x, b, sub) is { } inB ? inA | inB : null;
+ case Intersectionf(Set a, Set b):
+ return MembershipAsComparison(x, a, sub) is { } inBoth && MembershipAsComparison(x, b, sub) is { } andIn ? inBoth & andIn : null;
+ case SetMinusf(Set a, Set b):
+ return MembershipAsComparison(x, a, sub) is { } inLeft && MembershipAsComparison(x, b, sub) is { } notIn ? inLeft & !notIn : null;
case ConditionalSet { DeclaredMembership: (var declaredOfSuper, var rest), Var: Variable y } builder
when declaredOfSuper is Set declaredSet:
// x in { y in S : p } is x in S and p(x); the first conjunct is read the
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs
index 77af2804d..2c73f591d 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs
@@ -12,6 +12,7 @@
using PeterO.Numbers;
using AngouriMath.Functions.Continuous.Solvers.SetSolver;
using static AngouriMath.Entity;
+using static AngouriMath.Entity.Number;
using static AngouriMath.Entity.Set;
namespace AngouriMath.Functions.Algebra.AnalyticalSolving
@@ -319,6 +320,11 @@ internal static Set Solve(Entity expr, Variable x)
Variable when expr == x => new FiniteSet(true),
Inf(var var, Set set) when var == x => set,
+ // f(x) in S: the members of a listed S each as an equation, an interval as its
+ // two bounds, and anything else left as the set of x with the property, since
+ // a solver that answered "no x" here was answering wrongly -- x^2 in (0; 1) was
+ // the empty set. https://github.com/asc-community/AngouriMath/issues/1409
+ Inf(var member, Set set) when member.ContainsNode(x) => Membership(member, set, x) ?? new ConditionalSet(x, expr),
// a x + b = c (mod n): one residue class, or none, by the gcd; a congruence of
// higher degree by the residues that satisfy it, where the modulus is small
@@ -328,8 +334,43 @@ internal static Set Solve(Entity expr, Variable x)
Providedf(var e, var predicate) => Solve(e, x).Filter(predicate, x),
Piecewise p => EquationSolver.SolvePiecewise(p, x, Solve),
- // TODO: Although piecewise needed?
- _ => Set.Empty
+ // A statement the solver has no arm for is the set of x with the property, left
+ // as written -- not the empty set, which claims there is no such x. A statement
+ // that does not mention x is one of everything or of nothing, and the set
+ // builder over it evaluates to that.
+ _ => new ConditionalSet(x, expr)
};
+
+ ///
+ /// The x with f(x) in a set: for a listed set, the union of the equations
+ /// f(x) = s; for an interval, the conjunction of its bounds, each strict or not as
+ /// the end is; for a set that is neither.
+ ///
+ private static Set? Membership(Entity member, Set set, Variable x)
+ {
+ switch (set)
+ {
+ case FiniteSet listed:
+ Set? union = null;
+ foreach (var element in listed)
+ {
+ var solutions = Solve(member.Equalizes(element), x);
+ union = union is null ? solutions : MathS.Union(union, solutions);
+ }
+ return union ?? Set.Empty;
+ case Interval interval:
+ Entity? condition = null;
+ if (interval.Left.Evaled != Real.NegativeInfinity)
+ condition = interval.LeftClosed ? member >= interval.Left : member > interval.Left;
+ if (interval.Right.Evaled != Real.PositiveInfinity)
+ {
+ var upper = interval.RightClosed ? member <= interval.Right : member < interval.Right;
+ condition = condition is null ? upper : condition & upper;
+ }
+ return condition is null ? MathS.Sets.R : Solve(condition, x);
+ default:
+ return null;
+ }
+ }
}
}
diff --git a/Sources/Tests/UnitTests/Algebra/MembershipSolvingTest.cs b/Sources/Tests/UnitTests/Algebra/MembershipSolvingTest.cs
new file mode 100644
index 000000000..93a66654d
--- /dev/null
+++ b/Sources/Tests/UnitTests/Algebra/MembershipSolvingTest.cs
@@ -0,0 +1,69 @@
+//
+// 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 Xunit;
+using static AngouriMath.Entity;
+
+namespace AngouriMath.Tests.Algebra
+{
+ ///
+ /// Solve of f(x) in S: the members of a listed S each as an equation,
+ /// an interval as its bounds. It answered the empty set for every one of these, and
+ /// for every statement it had no arm for -- a claim that no x exists, made about
+ /// x^2 in (0; 1). A statement the solver cannot read is the set of x with the
+ /// property now, left as written. The reference's pre-images (Sullivan and Mackey,
+ /// Ex 7.3.10, §7.3.5 Try 1) are this.
+ ///
+ ///
+ [Trait("Area", "Algebra")]
+ public sealed class MembershipSolvingTest
+ {
+ private static Set Solved(string statement) => statement.ToEntity().Solve("x");
+
+ /// Ex 7.3.10: the pre-images of x^2; the book prints (-1, 1) for the last, which is wrong at 0.
+ [Theory]
+ [InlineData("x^2 in {1}", "{-1, 1}")]
+ [InlineData("x^2 in {1, 4}", "{-2, -1, 1, 2}")]
+ [InlineData("x^2 in (-oo; 0)", "{}")]
+ [InlineData("x^2 in [0; +oo)", "RR")]
+ [InlineData("x^2 in (0; 1)", "(-1; 0) \\/ (0; 1)")]
+ [InlineData("x + 1 in [2; 3]", "[1; 2]")]
+ [InlineData("2 x in (0; 4]", "(0; 2]")]
+ public void APreImageIsSolvedThroughTheMembersOrTheBounds(string statement, string expected)
+ {
+ // Compared at points rather than as printed forms: the solver writes a union of
+ // intervals in its own order, and the same set two ways is the same set.
+ var solved = Solved(statement);
+ var wanted = (Set)expected.ToEntity().Evaled;
+ foreach (var at in new[] { -3.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 3.0 })
+ {
+ var point = (Entity)at;
+ Assert.True(solved.TryContains(point, out var got), $"{solved} does not decide {at}");
+ Assert.True(wanted.TryContains(point, out var want), $"{wanted} does not decide {at}");
+ Assert.True(got == want, $"{statement} solved as {solved}: {at} is {(got ? "in" : "out")}, and should be {(want ? "in" : "out")}");
+ }
+ }
+
+ /// A statement with no arm is the set of x with the property, not the empty set.
+ [Fact]
+ public void WhatIsNotReadIsLeftAsWritten()
+ {
+ Assert.Equal("{ x : x^2 in ZZ }".ToEntity(), Solved("x^2 in ZZ"));
+ Assert.Equal("{ x : x^2 in QQ }".ToEntity(), Solved("x^2 in QQ"));
+ // And a quantifier that asked the solver no longer reads that emptiness as a proof:
+ // forall x in RR : x^2 in ZZ was True.
+ Assert.Equal(Boolean.False, "forall x in RR : x^2 in ZZ".ToEntity().Evaled);
+ }
+
+ /// The trigonometric equation's family is the pre-image of {0} under the sine.
+ [Fact]
+ public void AMemberEquationKeepsItsFamily()
+ => Assert.Contains("n_1", Solved("sin(x) in {0}").Stringize());
+ }
+}