1r"""Module that defines indexed objects
2
3The classes ``IndexedBase``, ``Indexed``, and ``Idx`` represent a
4matrix element ``M[i, j]`` as in the following diagram::
5
6       1) The Indexed class represents the entire indexed object.
7                  |
8               ___|___
9              '       '
10               M[i, j]
11              /   \__\______
12              |             |
13              |             |
14              |     2) The Idx class represents indices; each Idx can
15              |        optionally contain information about its range.
16              |
17        3) IndexedBase represents the 'stem' of an indexed object, here `M`.
18           The stem used by itself is usually taken to represent the entire
19           array.
20
21There can be any number of indices on an Indexed object.  No
22transformation properties are implemented in these Base objects, but
23implicit contraction of repeated indices is supported.
24
25Note that the support for complicated (i.e. non-atomic) integer
26expressions as indices is limited.  (This should be improved in
27future releases.)
28
29Examples
30========
31
32To express the above matrix element example you would write:
33
34>>> from sympy import symbols, IndexedBase, Idx
35>>> M = IndexedBase('M')
36>>> i, j = symbols('i j', cls=Idx)
37>>> M[i, j]
38M[i, j]
39
40Repeated indices in a product implies a summation, so to express a
41matrix-vector product in terms of Indexed objects:
42
43>>> x = IndexedBase('x')
44>>> M[i, j]*x[j]
45M[i, j]*x[j]
46
47If the indexed objects will be converted to component based arrays, e.g.
48with the code printers or the autowrap framework, you also need to provide
49(symbolic or numerical) dimensions.  This can be done by passing an
50optional shape parameter to IndexedBase upon construction:
51
52>>> dim1, dim2 = symbols('dim1 dim2', integer=True)
53>>> A = IndexedBase('A', shape=(dim1, 2*dim1, dim2))
54>>> A.shape
55(dim1, 2*dim1, dim2)
56>>> A[i, j, 3].shape
57(dim1, 2*dim1, dim2)
58
59If an IndexedBase object has no shape information, it is assumed that the
60array is as large as the ranges of its indices:
61
62>>> n, m = symbols('n m', integer=True)
63>>> i = Idx('i', m)
64>>> j = Idx('j', n)
65>>> M[i, j].shape
66(m, n)
67>>> M[i, j].ranges
68[(0, m - 1), (0, n - 1)]
69
70The above can be compared with the following:
71
72>>> A[i, 2, j].shape
73(dim1, 2*dim1, dim2)
74>>> A[i, 2, j].ranges
75[(0, m - 1), None, (0, n - 1)]
76
77To analyze the structure of indexed expressions, you can use the methods
78get_indices() and get_contraction_structure():
79
80>>> from sympy.tensor import get_indices, get_contraction_structure
81>>> get_indices(A[i, j, j])
82({i}, {})
83>>> get_contraction_structure(A[i, j, j])
84{(j,): {A[i, j, j]}}
85
86See the appropriate docstrings for a detailed explanation of the output.
87"""
88
89#   TODO:  (some ideas for improvement)
90#
91#   o test and guarantee numpy compatibility
92#      - implement full support for broadcasting
93#      - strided arrays
94#
95#   o more functions to analyze indexed expressions
96#      - identify standard constructs, e.g matrix-vector product in a subexpression
97#
98#   o functions to generate component based arrays (numpy and sympy.Matrix)
99#      - generate a single array directly from Indexed
100#      - convert simple sub-expressions
101#
102#   o sophisticated indexing (possibly in subclasses to preserve simplicity)
103#      - Idx with range smaller than dimension of Indexed
104#      - Idx with stepsize != 1
105#      - Idx with step determined by function call
106from collections.abc import Iterable
107
108from sympy import Number
109from sympy.core.assumptions import StdFactKB
110from sympy.core import Expr, Tuple, sympify, S
111from sympy.core.symbol import _filter_assumptions, Symbol
112from sympy.core.compatibility import (is_sequence, NotIterable)
113from sympy.core.logic import fuzzy_bool, fuzzy_not
114from sympy.core.sympify import _sympify
115from sympy.functions.special.tensor_functions import KroneckerDelta
116from sympy.multipledispatch import dispatch
117
118
119class IndexException(Exception):
120    pass
121
122
123class Indexed(Expr):
124    """Represents a mathematical object with indices.
125
126    >>> from sympy import Indexed, IndexedBase, Idx, symbols
127    >>> i, j = symbols('i j', cls=Idx)
128    >>> Indexed('A', i, j)
129    A[i, j]
130
131    It is recommended that ``Indexed`` objects be created by indexing ``IndexedBase``:
132    ``IndexedBase('A')[i, j]`` instead of ``Indexed(IndexedBase('A'), i, j)``.
133
134    >>> A = IndexedBase('A')
135    >>> a_ij = A[i, j]           # Prefer this,
136    >>> b_ij = Indexed(A, i, j)  # over this.
137    >>> a_ij == b_ij
138    True
139
140    """
141    is_commutative = True
142    is_Indexed = True
143    is_symbol = True
144    is_Atom = True
145
146    def __new__(cls, base, *args, **kw_args):
147        from sympy.utilities.misc import filldedent
148        from sympy.tensor.array.ndim_array import NDimArray
149        from sympy.matrices.matrices import MatrixBase
150
151        if not args:
152            raise IndexException("Indexed needs at least one index.")
153        if isinstance(base, (str, Symbol)):
154            base = IndexedBase(base)
155        elif not hasattr(base, '__getitem__') and not isinstance(base, IndexedBase):
156            raise TypeError(filldedent("""
157                The base can only be replaced with a string, Symbol,
158                IndexedBase or an object with a method for getting
159                items (i.e. an object with a `__getitem__` method).
160                """))
161        args = list(map(sympify, args))
162        if isinstance(base, (NDimArray, Iterable, Tuple, MatrixBase)) and all([i.is_number for i in args]):
163            if len(args) == 1:
164                return base[args[0]]
165            else:
166                return base[args]
167
168        obj = Expr.__new__(cls, base, *args, **kw_args)
169
170        try:
171            IndexedBase._set_assumptions(obj, base.assumptions0)
172        except AttributeError:
173            IndexedBase._set_assumptions(obj, {})
174        return obj
175
176    def _hashable_content(self):
177        return super()._hashable_content() + tuple(sorted(self.assumptions0.items()))
178
179    @property
180    def name(self):
181        return str(self)
182
183    @property
184    def _diff_wrt(self):
185        """Allow derivatives with respect to an ``Indexed`` object."""
186        return True
187
188    def _eval_derivative(self, wrt):
189        from sympy.tensor.array.ndim_array import NDimArray
190
191        if isinstance(wrt, Indexed) and wrt.base == self.base:
192            if len(self.indices) != len(wrt.indices):
193                msg = "Different # of indices: d({!s})/d({!s})".format(self,
194                                                                       wrt)
195                raise IndexException(msg)
196            result = S.One
197            for index1, index2 in zip(self.indices, wrt.indices):
198                result *= KroneckerDelta(index1, index2)
199            return result
200        elif isinstance(self.base, NDimArray):
201            from sympy.tensor.array import derive_by_array
202            return Indexed(derive_by_array(self.base, wrt), *self.args[1:])
203        else:
204            if Tuple(self.indices).has(wrt):
205                return S.NaN
206            return S.Zero
207
208    @property
209    def assumptions0(self):
210        return {k: v for k, v in self._assumptions.items() if v is not None}
211
212    @property
213    def base(self):
214        """Returns the ``IndexedBase`` of the ``Indexed`` object.
215
216        Examples
217        ========
218
219        >>> from sympy import Indexed, IndexedBase, Idx, symbols
220        >>> i, j = symbols('i j', cls=Idx)
221        >>> Indexed('A', i, j).base
222        A
223        >>> B = IndexedBase('B')
224        >>> B == B[i, j].base
225        True
226
227        """
228        return self.args[0]
229
230    @property
231    def indices(self):
232        """
233        Returns the indices of the ``Indexed`` object.
234
235        Examples
236        ========
237
238        >>> from sympy import Indexed, Idx, symbols
239        >>> i, j = symbols('i j', cls=Idx)
240        >>> Indexed('A', i, j).indices
241        (i, j)
242
243        """
244        return self.args[1:]
245
246    @property
247    def rank(self):
248        """
249        Returns the rank of the ``Indexed`` object.
250
251        Examples
252        ========
253
254        >>> from sympy import Indexed, Idx, symbols
255        >>> i, j, k, l, m = symbols('i:m', cls=Idx)
256        >>> Indexed('A', i, j).rank
257        2
258        >>> q = Indexed('A', i, j, k, l, m)
259        >>> q.rank
260        5
261        >>> q.rank == len(q.indices)
262        True
263
264        """
265        return len(self.args) - 1
266
267    @property
268    def shape(self):
269        """Returns a list with dimensions of each index.
270
271        Dimensions is a property of the array, not of the indices.  Still, if
272        the ``IndexedBase`` does not define a shape attribute, it is assumed
273        that the ranges of the indices correspond to the shape of the array.
274
275        >>> from sympy import IndexedBase, Idx, symbols
276        >>> n, m = symbols('n m', integer=True)
277        >>> i = Idx('i', m)
278        >>> j = Idx('j', m)
279        >>> A = IndexedBase('A', shape=(n, n))
280        >>> B = IndexedBase('B')
281        >>> A[i, j].shape
282        (n, n)
283        >>> B[i, j].shape
284        (m, m)
285        """
286        from sympy.utilities.misc import filldedent
287
288        if self.base.shape:
289            return self.base.shape
290        sizes = []
291        for i in self.indices:
292            upper = getattr(i, 'upper', None)
293            lower = getattr(i, 'lower', None)
294            if None in (upper, lower):
295                raise IndexException(filldedent("""
296                    Range is not defined for all indices in: %s""" % self))
297            try:
298                size = upper - lower + 1
299            except TypeError:
300                raise IndexException(filldedent("""
301                    Shape cannot be inferred from Idx with
302                    undefined range: %s""" % self))
303            sizes.append(size)
304        return Tuple(*sizes)
305
306    @property
307    def ranges(self):
308        """Returns a list of tuples with lower and upper range of each index.
309
310        If an index does not define the data members upper and lower, the
311        corresponding slot in the list contains ``None`` instead of a tuple.
312
313        Examples
314        ========
315
316        >>> from sympy import Indexed,Idx, symbols
317        >>> Indexed('A', Idx('i', 2), Idx('j', 4), Idx('k', 8)).ranges
318        [(0, 1), (0, 3), (0, 7)]
319        >>> Indexed('A', Idx('i', 3), Idx('j', 3), Idx('k', 3)).ranges
320        [(0, 2), (0, 2), (0, 2)]
321        >>> x, y, z = symbols('x y z', integer=True)
322        >>> Indexed('A', x, y, z).ranges
323        [None, None, None]
324
325        """
326        ranges = []
327        for i in self.indices:
328            sentinel = object()
329            upper = getattr(i, 'upper', sentinel)
330            lower = getattr(i, 'lower', sentinel)
331            if sentinel not in (upper, lower):
332                ranges.append(Tuple(lower, upper))
333            else:
334                ranges.append(None)
335        return ranges
336
337    def _sympystr(self, p):
338        indices = list(map(p.doprint, self.indices))
339        return "%s[%s]" % (p.doprint(self.base), ", ".join(indices))
340
341    @property
342    def free_symbols(self):
343        base_free_symbols = self.base.free_symbols
344        indices_free_symbols = {
345            fs for i in self.indices for fs in i.free_symbols}
346        if base_free_symbols:
347            return {self} | base_free_symbols | indices_free_symbols
348        else:
349            return indices_free_symbols
350
351    @property
352    def expr_free_symbols(self):
353        from sympy.utilities.exceptions import SymPyDeprecationWarning
354        SymPyDeprecationWarning(feature="expr_free_symbols method",
355                                issue=21494,
356                                deprecated_since_version="1.9").warn()
357        return {self}
358
359
360class IndexedBase(Expr, NotIterable):
361    """Represent the base or stem of an indexed object
362
363    The IndexedBase class represent an array that contains elements. The main purpose
364    of this class is to allow the convenient creation of objects of the Indexed
365    class.  The __getitem__ method of IndexedBase returns an instance of
366    Indexed.  Alone, without indices, the IndexedBase class can be used as a
367    notation for e.g. matrix equations, resembling what you could do with the
368    Symbol class.  But, the IndexedBase class adds functionality that is not
369    available for Symbol instances:
370
371      -  An IndexedBase object can optionally store shape information.  This can
372         be used in to check array conformance and conditions for numpy
373         broadcasting.  (TODO)
374      -  An IndexedBase object implements syntactic sugar that allows easy symbolic
375         representation of array operations, using implicit summation of
376         repeated indices.
377      -  The IndexedBase object symbolizes a mathematical structure equivalent
378         to arrays, and is recognized as such for code generation and automatic
379         compilation and wrapping.
380
381    >>> from sympy.tensor import IndexedBase, Idx
382    >>> from sympy import symbols
383    >>> A = IndexedBase('A'); A
384    A
385    >>> type(A)
386    <class 'sympy.tensor.indexed.IndexedBase'>
387
388    When an IndexedBase object receives indices, it returns an array with named
389    axes, represented by an Indexed object:
390
391    >>> i, j = symbols('i j', integer=True)
392    >>> A[i, j, 2]
393    A[i, j, 2]
394    >>> type(A[i, j, 2])
395    <class 'sympy.tensor.indexed.Indexed'>
396
397    The IndexedBase constructor takes an optional shape argument.  If given,
398    it overrides any shape information in the indices. (But not the index
399    ranges!)
400
401    >>> m, n, o, p = symbols('m n o p', integer=True)
402    >>> i = Idx('i', m)
403    >>> j = Idx('j', n)
404    >>> A[i, j].shape
405    (m, n)
406    >>> B = IndexedBase('B', shape=(o, p))
407    >>> B[i, j].shape
408    (o, p)
409
410    Assumptions can be specified with keyword arguments the same way as for Symbol:
411
412    >>> A_real = IndexedBase('A', real=True)
413    >>> A_real.is_real
414    True
415    >>> A != A_real
416    True
417
418    Assumptions can also be inherited if a Symbol is used to initialize the IndexedBase:
419
420    >>> I = symbols('I', integer=True)
421    >>> C_inherit = IndexedBase(I)
422    >>> C_explicit = IndexedBase('I', integer=True)
423    >>> C_inherit == C_explicit
424    True
425    """
426    is_commutative = True
427    is_symbol = True
428    is_Atom = True
429
430    @staticmethod
431    def _set_assumptions(obj, assumptions):
432        """Set assumptions on obj, making sure to apply consistent values."""
433        tmp_asm_copy = assumptions.copy()
434        is_commutative = fuzzy_bool(assumptions.get('commutative', True))
435        assumptions['commutative'] = is_commutative
436        obj._assumptions = StdFactKB(assumptions)
437        obj._assumptions._generator = tmp_asm_copy  # Issue #8873
438
439    def __new__(cls, label, shape=None, *, offset=S.Zero, strides=None, **kw_args):
440        from sympy import MatrixBase, NDimArray
441
442        assumptions, kw_args = _filter_assumptions(kw_args)
443        if isinstance(label, str):
444            label = Symbol(label, **assumptions)
445        elif isinstance(label, Symbol):
446            assumptions = label._merge(assumptions)
447        elif isinstance(label, (MatrixBase, NDimArray)):
448            return label
449        elif isinstance(label, Iterable):
450            return _sympify(label)
451        else:
452            label = _sympify(label)
453
454        if is_sequence(shape):
455            shape = Tuple(*shape)
456        elif shape is not None:
457            shape = Tuple(shape)
458
459        if shape is not None:
460            obj = Expr.__new__(cls, label, shape)
461        else:
462            obj = Expr.__new__(cls, label)
463        obj._shape = shape
464        obj._offset = offset
465        obj._strides = strides
466        obj._name = str(label)
467
468        IndexedBase._set_assumptions(obj, assumptions)
469        return obj
470
471    @property
472    def name(self):
473        return self._name
474
475    def _hashable_content(self):
476        return super()._hashable_content() + tuple(sorted(self.assumptions0.items()))
477
478    @property
479    def assumptions0(self):
480        return {k: v for k, v in self._assumptions.items() if v is not None}
481
482    def __getitem__(self, indices, **kw_args):
483        if is_sequence(indices):
484            # Special case needed because M[*my_tuple] is a syntax error.
485            if self.shape and len(self.shape) != len(indices):
486                raise IndexException("Rank mismatch.")
487            return Indexed(self, *indices, **kw_args)
488        else:
489            if self.shape and len(self.shape) != 1:
490                raise IndexException("Rank mismatch.")
491            return Indexed(self, indices, **kw_args)
492
493    @property
494    def shape(self):
495        """Returns the shape of the ``IndexedBase`` object.
496
497        Examples
498        ========
499
500        >>> from sympy import IndexedBase, Idx
501        >>> from sympy.abc import x, y
502        >>> IndexedBase('A', shape=(x, y)).shape
503        (x, y)
504
505        Note: If the shape of the ``IndexedBase`` is specified, it will override
506        any shape information given by the indices.
507
508        >>> A = IndexedBase('A', shape=(x, y))
509        >>> B = IndexedBase('B')
510        >>> i = Idx('i', 2)
511        >>> j = Idx('j', 1)
512        >>> A[i, j].shape
513        (x, y)
514        >>> B[i, j].shape
515        (2, 1)
516
517        """
518        return self._shape
519
520    @property
521    def strides(self):
522        """Returns the strided scheme for the ``IndexedBase`` object.
523
524        Normally this is a tuple denoting the number of
525        steps to take in the respective dimension when traversing
526        an array. For code generation purposes strides='C' and
527        strides='F' can also be used.
528
529        strides='C' would mean that code printer would unroll
530        in row-major order and 'F' means unroll in column major
531        order.
532
533        """
534
535        return self._strides
536
537    @property
538    def offset(self):
539        """Returns the offset for the ``IndexedBase`` object.
540
541        This is the value added to the resulting index when the
542        2D Indexed object is unrolled to a 1D form. Used in code
543        generation.
544
545        Examples
546        ==========
547        >>> from sympy.printing import ccode
548        >>> from sympy.tensor import IndexedBase, Idx
549        >>> from sympy import symbols
550        >>> l, m, n, o = symbols('l m n o', integer=True)
551        >>> A = IndexedBase('A', strides=(l, m, n), offset=o)
552        >>> i, j, k = map(Idx, 'ijk')
553        >>> ccode(A[i, j, k])
554        'A[l*i + m*j + n*k + o]'
555
556        """
557        return self._offset
558
559    @property
560    def label(self):
561        """Returns the label of the ``IndexedBase`` object.
562
563        Examples
564        ========
565
566        >>> from sympy import IndexedBase
567        >>> from sympy.abc import x, y
568        >>> IndexedBase('A', shape=(x, y)).label
569        A
570
571        """
572        return self.args[0]
573
574    def _sympystr(self, p):
575        return p.doprint(self.label)
576
577
578class Idx(Expr):
579    """Represents an integer index as an ``Integer`` or integer expression.
580
581    There are a number of ways to create an ``Idx`` object.  The constructor
582    takes two arguments:
583
584    ``label``
585        An integer or a symbol that labels the index.
586    ``range``
587        Optionally you can specify a range as either
588
589        * ``Symbol`` or integer: This is interpreted as a dimension. Lower and
590          upper bounds are set to ``0`` and ``range - 1``, respectively.
591        * ``tuple``: The two elements are interpreted as the lower and upper
592          bounds of the range, respectively.
593
594    Note: bounds of the range are assumed to be either integer or infinite (oo
595    and -oo are allowed to specify an unbounded range). If ``n`` is given as a
596    bound, then ``n.is_integer`` must not return false.
597
598    For convenience, if the label is given as a string it is automatically
599    converted to an integer symbol.  (Note: this conversion is not done for
600    range or dimension arguments.)
601
602    Examples
603    ========
604
605    >>> from sympy import Idx, symbols, oo
606    >>> n, i, L, U = symbols('n i L U', integer=True)
607
608    If a string is given for the label an integer ``Symbol`` is created and the
609    bounds are both ``None``:
610
611    >>> idx = Idx('qwerty'); idx
612    qwerty
613    >>> idx.lower, idx.upper
614    (None, None)
615
616    Both upper and lower bounds can be specified:
617
618    >>> idx = Idx(i, (L, U)); idx
619    i
620    >>> idx.lower, idx.upper
621    (L, U)
622
623    When only a single bound is given it is interpreted as the dimension
624    and the lower bound defaults to 0:
625
626    >>> idx = Idx(i, n); idx.lower, idx.upper
627    (0, n - 1)
628    >>> idx = Idx(i, 4); idx.lower, idx.upper
629    (0, 3)
630    >>> idx = Idx(i, oo); idx.lower, idx.upper
631    (0, oo)
632
633    """
634
635    is_integer = True
636    is_finite = True
637    is_real = True
638    is_symbol = True
639    is_Atom = True
640    _diff_wrt = True
641
642    def __new__(cls, label, range=None, **kw_args):
643        from sympy.utilities.misc import filldedent
644
645        if isinstance(label, str):
646            label = Symbol(label, integer=True)
647        label, range = list(map(sympify, (label, range)))
648
649        if label.is_Number:
650            if not label.is_integer:
651                raise TypeError("Index is not an integer number.")
652            return label
653
654        if not label.is_integer:
655            raise TypeError("Idx object requires an integer label.")
656
657        elif is_sequence(range):
658            if len(range) != 2:
659                raise ValueError(filldedent("""
660                    Idx range tuple must have length 2, but got %s""" % len(range)))
661            for bound in range:
662                if (bound.is_integer is False and bound is not S.Infinity
663                        and bound is not S.NegativeInfinity):
664                    raise TypeError("Idx object requires integer bounds.")
665            args = label, Tuple(*range)
666        elif isinstance(range, Expr):
667            if range is not S.Infinity and fuzzy_not(range.is_integer):
668                raise TypeError("Idx object requires an integer dimension.")
669            args = label, Tuple(0, range - 1)
670        elif range:
671            raise TypeError(filldedent("""
672                The range must be an ordered iterable or
673                integer SymPy expression."""))
674        else:
675            args = label,
676
677        obj = Expr.__new__(cls, *args, **kw_args)
678        obj._assumptions["finite"] = True
679        obj._assumptions["real"] = True
680        return obj
681
682    @property
683    def label(self):
684        """Returns the label (Integer or integer expression) of the Idx object.
685
686        Examples
687        ========
688
689        >>> from sympy import Idx, Symbol
690        >>> x = Symbol('x', integer=True)
691        >>> Idx(x).label
692        x
693        >>> j = Symbol('j', integer=True)
694        >>> Idx(j).label
695        j
696        >>> Idx(j + 1).label
697        j + 1
698
699        """
700        return self.args[0]
701
702    @property
703    def lower(self):
704        """Returns the lower bound of the ``Idx``.
705
706        Examples
707        ========
708
709        >>> from sympy import Idx
710        >>> Idx('j', 2).lower
711        0
712        >>> Idx('j', 5).lower
713        0
714        >>> Idx('j').lower is None
715        True
716
717        """
718        try:
719            return self.args[1][0]
720        except IndexError:
721            return
722
723    @property
724    def upper(self):
725        """Returns the upper bound of the ``Idx``.
726
727        Examples
728        ========
729
730        >>> from sympy import Idx
731        >>> Idx('j', 2).upper
732        1
733        >>> Idx('j', 5).upper
734        4
735        >>> Idx('j').upper is None
736        True
737
738        """
739        try:
740            return self.args[1][1]
741        except IndexError:
742            return
743
744    def _sympystr(self, p):
745        return p.doprint(self.label)
746
747    @property
748    def name(self):
749        return self.label.name if self.label.is_Symbol else str(self.label)
750
751    @property
752    def free_symbols(self):
753        return {self}
754
755
756@dispatch(Idx, Idx)
757def _eval_is_ge(lhs, rhs): # noqa:F811
758
759    other_upper = rhs if rhs.upper is None else rhs.upper
760    other_lower = rhs if rhs.lower is None else rhs.lower
761
762    if lhs.lower is not None and (lhs.lower >= other_upper) == True:
763        return True
764    if lhs.upper is not None and (lhs.upper < other_lower) == True:
765        return False
766    return None
767
768
769@dispatch(Idx, Number)  # type:ignore
770def _eval_is_ge(lhs, rhs): # noqa:F811
771
772    other_upper = rhs
773    other_lower = rhs
774
775    if lhs.lower is not None and (lhs.lower >= other_upper) == True:
776        return True
777    if lhs.upper is not None and (lhs.upper < other_lower) == True:
778        return False
779    return None
780
781
782@dispatch(Number, Idx)  # type:ignore
783def _eval_is_ge(lhs, rhs): # noqa:F811
784
785    other_upper = lhs
786    other_lower = lhs
787
788    if rhs.upper is not None and (rhs.upper <= other_lower) == True:
789        return True
790    if rhs.lower is not None and (rhs.lower > other_upper) == True:
791        return False
792    return None
793