@@ -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>0</code> reduces to
1762+ * <code>(-1<x<0)||x>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
0 commit comments