Skip to content

Commit 8f67bb2

Browse files
committed
WIP #1427 Reduce[4*x^3 - 4*x > 0, x, Reals]
1 parent 7290254 commit 8f67bb2

2 files changed

Lines changed: 231 additions & 4 deletions

File tree

  • symja_android_library/matheclipse-core/src

symja_android_library/matheclipse-core/src/main/java/org/matheclipse/core/reflection/system/Reduce.java

Lines changed: 198 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,13 +1042,22 @@ public IExpr evaluate(final IAST ast, EvalEngine engine) {
10421042
setInequalityDomainsRecursive(expr, domainMap);
10431043

10441044
if (domain == S.Reals || domain == S.Complexes) {
1045+
// Inequalities are inherently real-valued, so the following two reductions are applied
1046+
// independent of the requested domain.
1047+
10451048
// try to decide a single univariate (polynomial) inequality globally with the help of the
1046-
// symbolic optimizers Minimize/Maximize (e.g. x^2 + 1 > 0 is always True). Inequalities are
1047-
// inherently real-valued, so this is applied independent of the requested domain.
1049+
// symbolic optimizers Minimize/Maximize (e.g. x^2 + 1 > 0 is always True).
10481050
IExpr decided = decideInequalityByExtrema(arg1, variable, engine);
10491051
if (decided.isPresent()) {
10501052
return decided;
10511053
}
1054+
1055+
// solve a single univariate polynomial inequality by a sign analysis of the real roots,
1056+
// e.g. 4*x^3-4*x>0 reduces to the interval set (-1<x<0)||x>1
1057+
IExpr solved = reducePolynomialInequalityReals(arg1, variable, engine);
1058+
if (solved.isPresent()) {
1059+
return solved;
1060+
}
10521061
}
10531062

10541063
IExpr logicalExpand = S.LogicalExpand.of(engine, expr);
@@ -1741,6 +1750,193 @@ private static IExpr decideInequalityByExtrema(IExpr arg1, IExpr variable, EvalE
17411750
return F.NIL;
17421751
}
17431752

1753+
/**
1754+
* Reduce a single univariate polynomial inequality <code>lhs OP rhs</code> (with
1755+
* <code>OP</code> one of {@link S#Less}, {@link S#LessEqual}, {@link S#Greater},
1756+
* {@link S#GreaterEqual}) over the {@link S#Reals} by a sign analysis of the polynomial
1757+
* <code>f = lhs - rhs</code>. The sign of a real polynomial can only change at its real roots, so
1758+
* the real line is split at the distinct real roots and the sign of <code>f</code> is sampled in
1759+
* each open region. The satisfied regions (and, for the non-strict relations, the roots
1760+
* themselves) are merged into a maximal {@link S#IntervalData} set and returned as the
1761+
* corresponding {@link S#Or} of comparators. For example <code>4*x^3-4*x&gt;0</code> reduces to
1762+
* <code>(-1&lt;x&lt;0)||x&gt;1</code>.
1763+
*
1764+
* <p>
1765+
* Inequalities are inherently real-valued, so this reduction is applied independent of the
1766+
* requested domain ({@link S#Reals} as well as the default {@link S#Complexes}).
1767+
*
1768+
* @param arg1 the first argument of {@code Reduce}
1769+
* @param variable the (single) variable
1770+
* @param engine the evaluation engine
1771+
* @return the reduced solution set, {@link S#False} if it is empty,
1772+
* {@code Element(variable, Reals)} if it is the whole real line, or {@link F#NIL} if the
1773+
* real roots of <code>f</code> could not be determined numerically
1774+
*/
1775+
private static IExpr reducePolynomialInequalityReals(IExpr arg1, IExpr variable,
1776+
EvalEngine engine) {
1777+
int headID = arg1.headID();
1778+
if (headID != ID.Less && headID != ID.LessEqual && headID != ID.Greater
1779+
&& headID != ID.GreaterEqual) {
1780+
return F.NIL;
1781+
}
1782+
IAST comparator = (IAST) arg1;
1783+
if (comparator.argSize() != 2) {
1784+
return F.NIL;
1785+
}
1786+
// f(root) == 0 satisfies only the non-strict relations `<=` / `>=`
1787+
final boolean rootInSolution = headID == ID.LessEqual || headID == ID.GreaterEqual;
1788+
final boolean greaterDirection = headID == ID.Greater || headID == ID.GreaterEqual;
1789+
1790+
// rewrite `lhs OP rhs` as `f OP 0` with f = lhs - rhs
1791+
IExpr f = engine.evaluate(F.ExpandAll(F.Subtract(comparator.arg1(), comparator.arg2())));
1792+
if (f.isFree(variable) || !f.isPolynomial(variable)) {
1793+
return F.NIL;
1794+
}
1795+
1796+
// the sign of f can only change at its distinct real roots
1797+
IExpr solved = S.Solve.of(engine, F.Equal(f, F.C0), variable, S.Reals);
1798+
if (!solved.isList() || !solved.isFree(S.Solve)) {
1799+
return F.NIL;
1800+
}
1801+
IAST solutions = (IAST) solved;
1802+
List<IExpr> rootExacts = new ArrayList<IExpr>(solutions.argSize());
1803+
List<Double> rootValues = new ArrayList<Double>(solutions.argSize());
1804+
for (int i = 1; i < solutions.size(); i++) {
1805+
IExpr solution = solutions.get(i);
1806+
if (!solution.isList1() || !solution.first().isRule()
1807+
|| !solution.first().first().equals(variable)) {
1808+
return F.NIL;
1809+
}
1810+
IExpr rootValue = engine.evaluate(solution.first().second());
1811+
double d;
1812+
try {
1813+
d = rootValue.evalDouble();
1814+
} catch (ArgumentTypeException aex) {
1815+
return F.NIL;
1816+
}
1817+
if (Double.isNaN(d) || Double.isInfinite(d)) {
1818+
return F.NIL;
1819+
}
1820+
insertSortedDistinct(rootExacts, rootValues, rootValue, d);
1821+
}
1822+
1823+
final int n = rootExacts.size();
1824+
// sign of f on each of the n+1 open regions between/around the roots
1825+
boolean[] regionInSolution = new boolean[n + 1];
1826+
for (int k = 0; k <= n; k++) {
1827+
final double sample;
1828+
if (n == 0) {
1829+
sample = 0.0;
1830+
} else if (k == 0) {
1831+
sample = rootValues.get(0) - 1.0;
1832+
} else if (k == n) {
1833+
sample = rootValues.get(n - 1) + 1.0;
1834+
} else {
1835+
sample = (rootValues.get(k - 1) + rootValues.get(k)) / 2.0;
1836+
}
1837+
int sign = polynomialSignAt(f, variable, sample, engine);
1838+
if (sign == 0) {
1839+
return F.NIL;
1840+
}
1841+
regionInSolution[k] = greaterDirection ? sign > 0 : sign < 0;
1842+
}
1843+
1844+
// merge the satisfied regions (and included roots) into maximal intervals. The atoms are
1845+
// traversed left to right: even index t == region t/2, odd index t == root (t-1)/2.
1846+
IASTAppendable intervalData = F.IntervalDataAlloc(n + 2);
1847+
boolean open = false;
1848+
IExpr min = F.NIL;
1849+
IExpr minType = S.Less;
1850+
IExpr max = F.NIL;
1851+
IExpr maxType = S.Less;
1852+
for (int t = 0; t <= 2 * n; t++) {
1853+
final boolean isRegion = (t & 1) == 0;
1854+
final int index = t / 2;
1855+
final boolean inSolution = isRegion ? regionInSolution[index] : rootInSolution;
1856+
final IExpr leftValue;
1857+
final IExpr rightValue;
1858+
final IBuiltInSymbol edgeType;
1859+
if (isRegion) {
1860+
leftValue = index == 0 ? F.CNInfinity : rootExacts.get(index - 1);
1861+
rightValue = index == n ? F.CInfinity : rootExacts.get(index);
1862+
edgeType = S.Less;
1863+
} else {
1864+
leftValue = rootExacts.get(index);
1865+
rightValue = leftValue;
1866+
edgeType = S.LessEqual;
1867+
}
1868+
if (inSolution) {
1869+
if (!open) {
1870+
open = true;
1871+
min = leftValue;
1872+
minType = edgeType;
1873+
}
1874+
max = rightValue;
1875+
maxType = edgeType;
1876+
} else if (open) {
1877+
intervalData.append(F.List(min, minType, maxType, max));
1878+
open = false;
1879+
}
1880+
}
1881+
if (open) {
1882+
intervalData.append(F.List(min, minType, maxType, max));
1883+
}
1884+
1885+
if (intervalData.isAST0()) {
1886+
return S.False;
1887+
}
1888+
if (intervalData.isAST1()) {
1889+
IAST only = (IAST) intervalData.arg1();
1890+
if (only.arg1().isNegativeInfinity() && only.arg4().isInfinity()) {
1891+
return F.Element(variable, S.Reals);
1892+
}
1893+
}
1894+
return engine.evaluate(IntervalDataSym.intervalToOr(intervalData, variable));
1895+
}
1896+
1897+
/**
1898+
* Insert <code>value</code>/<code>numericValue</code> into the parallel lists
1899+
* <code>exacts</code>/<code>values</code> keeping them sorted ascending by numeric value and
1900+
* skipping numeric duplicates.
1901+
*/
1902+
private static void insertSortedDistinct(List<IExpr> exacts, List<Double> values, IExpr value,
1903+
double numericValue) {
1904+
int pos = 0;
1905+
while (pos < values.size() && values.get(pos).doubleValue() < numericValue) {
1906+
pos++;
1907+
}
1908+
if (pos < values.size() && values.get(pos).doubleValue() == numericValue) {
1909+
// duplicate real root - keep the first representation
1910+
return;
1911+
}
1912+
exacts.add(pos, value);
1913+
values.add(pos, Double.valueOf(numericValue));
1914+
}
1915+
1916+
/**
1917+
* Numerically evaluate the sign of the polynomial <code>f</code> at <code>variable == sample</code>
1918+
* .
1919+
*
1920+
* @return <code>1</code> / <code>-1</code> for a positive / negative value, <code>0</code> if the
1921+
* value is zero or cannot be evaluated to a real number
1922+
*/
1923+
private static int polynomialSignAt(IExpr f, IExpr variable, double sample, EvalEngine engine) {
1924+
IExpr value = engine.evaluate(F.subst(f, variable, F.num(sample)));
1925+
double d;
1926+
try {
1927+
d = value.evalDouble();
1928+
} catch (ArgumentTypeException aex) {
1929+
return 0;
1930+
}
1931+
if (d > 0.0) {
1932+
return 1;
1933+
}
1934+
if (d < 0.0) {
1935+
return -1;
1936+
}
1937+
return 0;
1938+
}
1939+
17441940
/**
17451941
* Expands chained comparator functions (e.g., Less(a, b, c)) and Inequality ASTs into binary And
17461942
* expressions (e.g., And(Less(a, b), Less(b, c))) so they can be processed as AST2 elements by

symja_android_library/matheclipse-core/src/test/java/org/matheclipse/core/system/ReduceTest.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,41 @@ public void testReduceIssue1413() {
262262
"x>1&&x<2");
263263
check("Reduce({0 < x < 2, 1 < x < 4}, x)", //
264264
"x>1&&x<2");
265-
// TODO
265+
// cubic inequality reduces over the reals even in the default domain
266266
check("Reduce(x^3-2*x+1<0,x)", //
267-
"-2*x+x^3<-1");
267+
"x<1/2*(-1-Sqrt(5))||(x>1/2*(-1+Sqrt(5))&&x<1)");
268268
}
269269

270+
@Test
271+
public void testReduceIssue1427() {
272+
// Implement Reduce for cubic polynomial inequality.
273+
// `(x>-1&&x<0)||x>1` is Symja's compact form of `-1 < x < 0 || x > 1`
274+
check("Reduce(4*x^3-4*x>0,x,Reals)", //
275+
"(x>-1&&x<0)||x>1");
276+
// inequalities are real-valued, so the same reduction happens in the default domain
277+
check("Reduce(4*x^3-4*x>0,x)", //
278+
"(x>-1&&x<0)||x>1");
279+
}
270280

281+
@Test
282+
public void testReducePolynomialInequalityReals() {
283+
// non-strict relation includes the roots
284+
check("Reduce(4*x^3-4*x>=0,x,Reals)", //
285+
"(x>=-1&&x<=0)||x>=1");
286+
// opposite direction
287+
check("Reduce(4*x^3-4*x<0,x,Reals)", //
288+
"x<-1||(x>0&&x<1)");
289+
// even multiplicity root: sign doesn't change, strict `>` excludes the touching point x==1
290+
check("Reduce((x-1)^2*(x+2)>0,x,Reals)", //
291+
"(x>-2&&x<1)||x>1");
292+
// ... and the non-strict `>=` collapses to a single half-line
293+
check("Reduce((x-1)^2*(x+2)>=0,x,Reals)", //
294+
"x>=-2");
295+
// irrational (exact) roots
296+
check("Reduce(x^2-2>0,x,Reals)", //
297+
"x<-Sqrt(2)||x>Sqrt(2)");
298+
// higher degree (quintic) with five real roots
299+
check("Reduce(x^5-5*x^3+4*x>0,x,Reals)", //
300+
"(x>-2&&x<-1)||(x>0&&x<1)||x>2");
301+
}
271302
}

0 commit comments

Comments
 (0)