Beispiel #1
0
def extract_var_tuple_indices(indexed_var_tuple):
    '''
    Given an ExprTuple of only IndexedVar and ExprRange entries, returns
    an ExprTuple of just the corresponding indices (including ranges of 
    indices and nested ranges of indices).
    '''
    from proveit import IndexedVar, ExprRange
    from proveit._core_.expression.operation.indexed_var import extract_indices
    indices = []
    if not isinstance(indexed_var_tuple, ExprTuple):
        raise TypeError("'indexed_var_tuple' must be an ExprTuple")
    for entry in indexed_var_tuple:
        if isinstance(entry, IndexedVar):
            entry_indices = extract_indices(entry)
            if len(entry_indices) == 1:
                indices.append(entry_indices[0])
            else:
                indices.append(entry_indices)
        elif isinstance(entry, ExprRange):
            inner_indices = extract_var_tuple_indices(ExprTuple(entry.body))
            assert len(inner_indices) == 1
            body = inner_indices[0]
            indices.append(
                ExprRange(entry.parameter, body, entry.start_index,
                          entry.end_index))
        else:
            raise TypeError("'var_range' must be an ExprTuple only of "
                            "IndexedVar or (nested) ExprRange entries.")
    return ExprTuple(*indices)
Beispiel #2
0
 def entry_end(entry):
     if isinstance(entry, ExprRange):
         if isinstance(entry.body, ExprRange):
             # Return an ExprRange of lambda maps.
             return ExprRange(entry.parameter, entry.body.end_index,
                              entry.start_index, entry.end_index)
         else:
             return entry.end_index
     return one  # for individual elements, use start=end=1
Beispiel #3
0
def var_array(var,
              start_index_or_indices,
              end_index_or_indices,
              array_type=ExprArray):
    from proveit import (safe_dummy_vars, composite_expression, IndexedVar)
    from .vert_expr_array import VertExprArray
    start_indices = composite_expression(start_index_or_indices)
    end_indices = composite_expression(end_index_or_indices)
    if array_type not in (ExprArray, VertExprArray, ExprTuple):
        raise ValueError("Not a valid array type: %s" % array_type)
    if not (start_indices.num_entries() == end_indices.num_entries() == 2):
        raise ValueError("Expecting two start indices and two end indices "
                         "when creating a Variable array")
    _i, _j = safe_dummy_vars(2, var, start_indices, end_indices)
    return array_type(
        ExprRange(_i, [
            ExprRange(_j, IndexedVar(var, [_i, _j]), start_indices[1],
                      end_indices[1])
        ], start_indices[0], end_indices[0]))
Beispiel #4
0
 def entry_map(entry):
     if isinstance(entry, ExprRange):
         if isinstance(entry.body, ExprRange):
             # Return an ExprRange of lambda maps.
             return ExprRange(entry.parameter,
                              entry.body.lambda_map,
                              entry.start_index, entry.end_index)
         else:
             # Use the ExprRange entry's map.
             return entry.lambda_map
     # For individual elements, just map to the
     # elemental entry.
     return Lambda(_x, entry)
Beispiel #5
0
 def entry_map(entry):
     # Don't auto-simplify the entry.
     preserved_exprs.add(entry)
     if isinstance(entry, ExprRange):
         if isinstance(entry.body, ExprRange):
             # Return an ExprRange of lambda maps.
             return ExprRange(entry.parameter,
                              entry.body.lambda_map,
                              entry.true_start_index,
                              entry.true_end_index)
         else:
             return entry.lambda_map
     # For individual elements, just map to the
     # elemental entry.
     return Lambda(_x, entry)
Beispiel #6
0
 def deduceInBool(self, assumptions=USE_DEFAULTS):
     '''
     Attempt to deduce, then return, that this forall expression is in the set of BOOLEANS,
     as all forall expressions are (they are taken to be false when not true).
     '''
     from proveit.number import one
     from ._axioms_ import forall_in_bool
     _x = self.instanceParams
     P_op, _P_op = Operation(P, _x), self.instanceExpr
     _n = _x.length(assumptions)
     x_1_to_n = ExprTuple(ExprRange(k, IndexedVar(x, k), one, _n))
     return forall_in_bool.specialize({
         n: _n,
         P_op: _P_op,
         x_1_to_n: _x
     },
                                      assumptions=assumptions)
Beispiel #7
0
 def deduce_in_bool(self, assumptions=USE_DEFAULTS):
     '''
     Attempt to deduce, then return, that this forall expression
     is in the set of BOOLEANS, as all forall expressions are
     (they are taken to be false when not true).
     '''
     from proveit.numbers import one
     from . import forall_in_bool
     _x = self.instance_params
     P_op, _P_op = Function(P, _x), self.instance_expr
     _n = _x.num_elements(assumptions)
     x_1_to_n = ExprTuple(ExprRange(k, IndexedVar(x, k), one, _n))
     return forall_in_bool.instantiate({
         n: _n,
         P_op: _P_op,
         x_1_to_n: _x
     },
                                       assumptions=assumptions)
Beispiel #8
0
    def known_vec_spaces(vecs, *, field=None):
        '''
        Return the known vector spaces of the given vecs under the
        specified field (or the default field).
        '''
        # Effort to appropriately handle an ExprRange operand added
        # here by wdc and ww on 1/3/2022.
        vec_spaces = []
        for vec in vecs:
            if isinstance(vec, ExprRange):
                # create our expr range
                with defaults.temporary() as tmp_defaults:
                    assumption = InSet(vec.parameter,
                            Interval(vec.true_start_index, vec.true_end_index))
                    tmp_defaults.assumptions = (
                            defaults.assumptions + (assumption ,))
                    body = VecSpaces.known_vec_space(vec.body, field=field)
                vec_spaces.append(
                    ExprRange(vec.parameter, body,
                              vec.true_start_index, vec.true_end_index))
            else:
                vec_spaces.append(VecSpaces.known_vec_space(vec, field=field))

        return vec_spaces
Beispiel #9
0
    def _get_cart_exps_field_and_exponents(self):
        '''
        Helper for derive_cart_exp_membership.
        '''
        domain = self.domain

        def raise_invalid():
            raise ValueError("'derive_cart_exp_membership' only applicable on "
                             "a TensorProdMembership where the domain is a "
                             "tensor product of Cartesian exponentials on the "
                             "same field, not %s" % domain)

        assert isinstance(domain, TensorProd)
        _ns = []
        _K = None
        for domain_factor in domain.factors.entries:
            if isinstance(domain_factor, ExprRange):
                if not isinstance(domain_factor.body, CartExp):
                    raise_invalid()
                factor_K = domain_factor.body.base
                _ns.append(
                    ExprRange(domain_factor.parameter,
                              domain_factor.body.exponent,
                              domain_factor.start_index,
                              domain_factor.end_index,
                              styles=domain_factor.get_styles()))
            elif isinstance(domain_factor, CartExp):
                factor_K = domain_factor.base
                _ns.append(domain_factor.exponent)
            else:
                raise_invalid()
            if _K is None:
                _K = factor_K
            elif _K != factor_K:
                raise_invalid()
        return _K, _ns
Beispiel #10
0
def deduce_number_set(expr, **defaults_config):
    '''
    Prove that 'expr' is an Expression that represents a number
    in a standard number set that is as restrictive as we can
    readily know.
    '''
    from proveit.logic import And, InSet, Equals, NotEquals
    from proveit.numbers import Less, LessEq, zero

    # Find the first (most restrictive) number set that
    # contains 'expr' or something equal to it.

    for number_set in sorted_number_sets:
        membership = None
        for eq_expr in Equals.yield_known_equal_expressions(expr):
            if isinstance(eq_expr, ExprRange):
                membership = And(
                    ExprRange(eq_expr.parameter,
                              InSet(eq_expr.body, number_set),
                              eq_expr.true_start_index,
                              eq_expr.true_end_index,
                              styles=eq_expr.get_styles()))
            else:
                membership = InSet(eq_expr, number_set)
            if membership.proven():
                break  # found a known number set membership
            else:
                membership = None
        if membership is not None:
            membership = InSet(expr, number_set).prove()
            break

    if hasattr(expr, 'deduce_number_set'):
        # Use 'deduce_number_set' method.
        try:
            deduced_membership = expr.deduce_number_set()
        except (UnsatisfiedPrerequisites, ProofFailure):
            deduced_membership = None
        if deduced_membership is not None:
            assert isinstance(deduced_membership, Judgment)
            if not isinstance(deduced_membership.expr, InSet):
                raise TypeError("'deduce_number_set' expected to prove an "
                                "InSet type expression")
            if deduced_membership.expr.element != expr:
                raise TypeError("'deduce_number_set' was expected to prove "
                                "that %s is in some number set" % expr)
            # See if this deduced number set is more restrictive than
            # what we had surmised already.
            deduced_number_set = deduced_membership.domain
            if membership is None:
                membership = deduced_membership
                number_set = deduced_number_set
            elif (deduced_number_set != number_set
                  and number_set.includes(deduced_number_set)):
                number_set = deduced_number_set
                membership = deduced_membership

    if membership is None:
        from proveit import defaults
        raise UnsatisfiedPrerequisites(
            "Unable to prove any number membership for %s" % expr)

    # Already proven to be in some number set,
    # Let's see if we can restrict it further.
    if Less(zero, expr).proven():  # positive
        number_set = pos_number_set.get(number_set, None)
    elif Less(expr, zero).proven():  # negative
        number_set = neg_number_set.get(number_set, None)
    elif LessEq(zero, expr).proven():  # non-negative
        number_set = nonneg_number_set.get(number_set, None)
    elif LessEq(expr, zero).proven():  # non-positive
        number_set = nonpos_number_set.get(number_set, None)
    elif NotEquals(expr, zero).proven():
        number_set = nonzero_number_set.get(number_set, None)
    if number_set is None:
        # Just use what we have already proven.
        return membership.prove()
    return InSet(expr, number_set).prove()
Beispiel #11
0
    def deduce_in_vec_space(self, vec_space=None, *, field, **defaults_config):
        '''
        Prove that this linear combination of vectors is in a vector
        space.  The vector space may be specified or inferred via known
        memberships.  A field for the vector space must be specified.
        '''
        from proveit.linear_algebra import ScalarMult

        terms = self.terms
        if vec_space is None:
            vec_space = VecSpaces.common_known_vec_space(terms, field=field)
        field = VecSpaces.known_field(vec_space)
        all_scaled = all((isinstance(term, ScalarMult) or (
            isinstance(term, ExprRange) and isinstance(term.body, ScalarMult)))
                         for term in terms)
        if all_scaled:
            # Use a linear combination theorem since everything
            # is scaled.
            from proveit.linear_algebra.scalar_multiplication import (
                binary_lin_comb_closure, lin_comb_closure)
            if terms.is_double():
                # Special linear combination binary case
                _a, _b = terms[0].scalar, terms[1].scalar
                _x, _y = terms[0].scaled, terms[1].scaled
                return binary_lin_comb_closure.instantiate({
                    K: field,
                    V: vec_space,
                    a: _a,
                    b: _b,
                    x: _x,
                    y: _y
                })
            else:
                # General linear combination case
                _a = []
                _x = []
                for term in terms:
                    if isinstance(term, ExprRange):
                        _a.append(
                            ExprRange(term.parameter, term.body.scalar,
                                      term.true_start_index,
                                      term.true_end_index))
                        _x.append(
                            ExprRange(term.parameter, term.body.scaled,
                                      term.true_start_index,
                                      term.true_end_index))
                    else:
                        _a.append(term.scalar)
                        _x.append(term.scaled)
                _n = terms.num_elements()
                return lin_comb_closure.instantiate({
                    n: _n,
                    K: field,
                    V: vec_space,
                    a: _a,
                    x: _x
                })
        else:
            # Use a vector addition closure theorem.
            from . import binary_closure, closure
            if terms.is_double():
                # Special binary case
                return binary_closure.instantiate({
                    K: field,
                    V: vec_space,
                    x: terms[0],
                    y: terms[1]
                })
            else:
                # General case
                _n = terms.num_elements()
                return closure.instantiate({
                    n: _n,
                    K: field,
                    V: vec_space,
                    x: terms
                })
Beispiel #12
0
    def get_format_cell_entries(self, format_cell_entries=None):
        '''
        Returns a list of (for the most part) lists of entries in 
        correspondence with each format cell of this ExprArray.  It is
        possible to have an entry that represents an entire inner list
        (an entire row, or column in the case of a VertExprArray).
        Each entry is a pair or triple tuple with the first item 
        containing an Expression corresponding to the entry and the
        others indicating the role of the cell.  It will be composed
        of the role within the outer ExprTuple and the role within the 
        inner ExprTuple respectively with the following exception.
        For a ExprRange of ExprTuples of ExprRanges with explicit
        parameterization, the explicit parameterization will be
        shown compactly only in the center of the range of ranges
        but will be made implicit in the cells above/below/left/right.
                
        The 'role' information will be used to determine how to format
        the cell with respect to using horizontal/vertical ellipses,
        etc.

        If 'format_cell_entries' is provided, append to it rather
        than creating a new list.
        '''
        # Construct the list of (for the most part) lists of entries
        # first by simply composing outer and inner roles.
        # Remember coordinates of entries that are 'explicit' outside
        # and inside -- we'll edit their surrounding entries next.
        if format_cell_entries is None:
            format_cell_entries = []
        else:
            if not isinstance(format_cell_entries, list):
                raise TypeError("Expecting 'format_cell_entries' to be a "
                                "list")
        doubly_explicit_coordinates = []
        for _i, outer_cell_info in enumerate(
                ExprTuple.yield_format_cell_info(self)):
            (outer_expr, outer_role), assumptions = outer_cell_info
            orig_outer_expr = outer_expr
            if isinstance(outer_expr, ExprRange):
                outer_expr = outer_expr.innermost_body()
                # But then wrap the inner_expr's below with the
                # ExprRange's if the inner_expr is not an ExprRange.
            if isinstance(outer_expr, ExprTuple):
                # An explicit inner list.
                with defaults.temporary() as tmp_defaults:
                    tmp_defaults.automation = False
                    tmp_defaults.assumptions = assumptions
                    inner_format_cell_entries = []
                    for _j, inner_cell_info in enumerate(
                            outer_expr.yield_format_cell_info()):
                        (inner_expr, inner_role), _ = inner_cell_info
                        if (orig_outer_expr != outer_expr
                                and not isinstance(inner_expr, ExprRange)):
                            # The other_expr was originally an
                            # ExprRange but the inner_expr is not.  In
                            # this case, we want to show the entry expr
                            # as an ExprRange for the outer orientation
                            # with this inner expression.  That is,
                            # let's wrap the inner_expr with the
                            # ExprRange(s) in the same way as the
                            # original ExprRange.
                            _expr_ranges = [orig_outer_expr]
                            while isinstance(_expr_ranges[-1].body, ExprRange):
                                _expr_ranges.append(_expr_ranges[-1].body)
                            for _er in reversed(_expr_ranges):
                                inner_expr = ExprRange(
                                    _er.parameter,
                                    inner_expr,
                                    _er.true_start_index,
                                    _er.true_end_index,
                                    use_canonical_parameter=False)
                                inner_expr = inner_expr.with_mimicked_style(
                                    _er, ignore_inapplicable_styles=True)
                        # Compose outer and inner roles.
                        inner_format_cell_entries.append(
                            (inner_expr, outer_role, inner_role))
                        if outer_role == inner_role == 'explicit':
                            doubly_explicit_coordinates.append((_i, _j))
                format_cell_entries.append(inner_format_cell_entries)
            else:
                # Represent an entire inner list with an entry.
                format_cell_entries.append((orig_outer_expr, outer_role))

        # Where roles are 'explicit' outside and inside, we'll make
        # surrounding roles be implicit for a more compact
        # representation (avoiding redundant information).
        for (_i, _j) in doubly_explicit_coordinates:
            # Make implicit "above" (before w.r.t. outer level)
            _k = 1
            while True:
                expr, outer_role, inner_role = format_cell_entries[_i - _k][_j]
                format_cell_entries[_i - _k][_j] = (expr, outer_role,
                                                    'implicit')
                if format_cell_entries[_i - _k][_j][1] == 0:
                    break  # First of the ExprRange -- done.
                _k += 1
            # Make implicit "below" (after w.r.t. outer level)
            expr, outer_role, inner_role = format_cell_entries[_i + 1][_j]
            format_cell_entries[_i + 1][_j] = (expr, outer_role, 'implicit')
            # Make implicit to the "left" (before w.r.t. inner level).
            _k = 1
            while True:
                expr, outer_role, inner_role = format_cell_entries[_i][_j - _k]
                format_cell_entries[_i][_j - _k] = (expr, 'implicit',
                                                    inner_role)
                if format_cell_entries[_i][_j - _k][2] == 0:
                    break  # First of the ExprRange -- done.
                _k += 1
            # Make implicit to the "right" (after w.r.t. inner level).
            expr, outer_role, inner_role = format_cell_entries[_i][_j + 1]
            format_cell_entries[_i][_j + 1] = (expr, 'implicit', inner_role)

        return format_cell_entries
Beispiel #13
0
    def deduce_equal_or_not(self, other_tuple, **defaults_config):
        '''
        Prove and return that this ExprTuple is either equal or
        not equal to other_tuple or raises an UnsatisfiedPrerequisites
        or NotImplementedError if we cannot readily prove either of
        these.
        '''
        from proveit import (ExprRange, safe_dummy_var,
                             UnsatisfiedPrerequisites)
        from proveit.logic import (
                And, Or, Equals, NotEquals, deduce_equal_or_not)
        if self == other_tuple:
            return Equals(self, other_tuple).conclude_via_reflexivity
        if not isinstance(other_tuple, ExprTuple):
            raise TypeError("Expecting 'other_tuple' to be an ExprTuple "
                            "not a %s"%other_tuple.__class__)
        _i = self.num_elements()
        _j = other_tuple.num_elements()
        size_relation = deduce_equal_or_not(_i, _j)
        if isinstance(size_relation.expr, NotEquals):
            # Not equal because the lengths are different.
            return self.not_equal(other_tuple)
        
        def raise_non_corresponding():
            raise NotImplementedError(
                    "ExprTuple.deduce_equal_or_not is only "
                    "implemented for the case when ExprRanges "
                    "match up: %s vs %s"%self, other_tuple)
            
        if self.num_entries() == other_tuple.num_entries():
            if self.num_entries()==1 and self.contains_range():
                if not other_tuple.contains_range():
                    # One ExprTuple has a range but the other doesn't.
                    # That case isn't handled.
                    raise_non_corresponding()
                lhs_range = self.entries[0]
                rhs_range = other_tuple.entries[0]
                start_index = lhs_range.start_index
                end_index = lhs_range.end_index
                if ((start_index != rhs_range.start_index) or
                        (end_index != rhs_range.end_index)):
                    # Indices must match for a proper correspondence.
                    raise_non_corresponding()
                if lhs_range.parameter != rhs_range.parameter:
                    # Use a safe common parameter.
                    param = safe_dummy_var(lhs_range.body, rhs_range.body)
                    lhs_range_body = lhs_range.body.basic_replaced(
                            {lhs_range.parameter: param})
                    rhs_range_body = rhs_range.body.basic_replaced(
                            {rhs_range.parameter: param})
                else:
                    param = lhs_range.parameter
                    lhs_range_body = lhs_range.body
                    rhs_range_body = rhs_range.body
                inner_assumptions = defaults.assumptions + (
                        lhs_range.parameter_condition(),)
                try:
                    body_relation = deduce_equal_or_not(
                            lhs_range_body, rhs_range_body,
                            assumptions=inner_assumptions)
                    if isinstance(body_relation, Equals):
                        # Every element is equal, so the ExprTuples 
                        # are equal.
                        return self.deduce_equality(
                                Equals(self, other_tuple))
                    else:
                        # Every element is not equal, so the ExprTuples 
                        # are not equal.
                        # This will enable "any" from "all".
                        And(ExprRange(
                                param, NotEquals(lhs_range_body,
                                                 rhs_range_body),
                                start_index, end_index)).prove()
                        return self.not_equal(other_tuple)
                except (NotImplementedError, UnsatisfiedPrerequisites):
                    pass
                if And(ExprRange(param, Equals(lhs_range_body, 
                                               rhs_range_body),
                                 start_index, end_index)).proven():
                    # Every element is equal, so the ExprTuples 
                    # are equal.
                    return self.deduce_equality(
                            Equals(self, other_tuple))
                elif Or(ExprRange(param, NotEquals(lhs_range_body,
                                                   rhs_range_body),
                        start_index, end_index)).proven():
                    # Some element pair is not equal, so the ExprTuples 
                    # are not equal.
                    return self.not_equal(other_tuple)
                raise UnsatisfiedPrerequisites(
                        "Could not determine whether %s = %s"
                        %(self, other_tuple))
            
            # Loop through each entry pair in correspondence and
            # see if we can readily prove whether or not they are
            # all equal.
            for idx, (_x, _y) in enumerate(
                    zip(self.entries, other_tuple.entries)):
                if isinstance(_x, ExprRange) != isinstance(_y, ExprRange):
                    raise_non_corresponding()
                if _x == _y:
                    # The expressions are the same, so we know they
                    # are equal.
                    continue
                if isinstance(_x, ExprRange):
                    # Wrap ExprRanges in ExprTuples and compare as
                    # single entry tuples.
                    _x = ExprTuple(_x)
                    _y = ExprTuple(_y)
                    _k = _x.num_elements()
                    _l = _y.num_elements()
                    size_relation = deduce_equal_or_not(_k, _l)
                    if isinstance(size_relation.expr, NotEquals):
                        # Not implemented when the ExprRanges don't
                        # correspond in size.
                        raise_non_corresponding()
                    relation = deduce_equal_or_not(_x, _y)
                else:
                    # Compare singular entries.
                    relation = deduce_equal_or_not(_x, _y)
                if isinstance(relation.expr, NotEquals):
                    # Aha! They are not equal.
                    return self.not_equal(other_tuple)
            # They are equal!
            return self.deduce_equality(Equals(self, other_tuple))

        raise NotImplementedError(
                    "ExprTuple.deduce_equal_or_not is not implemented "
                    "for ExprTuples that have a different number of "
                    "elements.")
Beispiel #14
0
    def factorization_of_scalars(self, **defaults_config):
        '''
        Prove equality with a Qmult in which the complex
        numbers are pulled to the front andassociated together via Mult.
        For example,
            (a A b B c C d D e) = ((a*b*c*d*e) A B C D)
        where a, b, c, d, and e are complex numbers, '*' denotes
        number multiplication, and spaces, here, denote the Qmult
        operation.
        
        Also see scalar_mult_factorization.
        '''
        from . import (QmultCodomain, qmult_pulling_scalar_out_front,
                       qmult_pulling_scalars_out_front,
                       qmult_scalar_association)
        expr = self
        eq = TransRelUpdater(expr)

        # First, lets prove the Qmult is well-formed and, in the
        # process, ensure to know which operands are Complex.
        if not InClass(self, QmultCodomain).proven():
            QmultCodomain.membership_object(self).conclude()

        # Go through the operands in reverse order so the complex
        # factors will be in the original order out front in the end.
        n_complex_entries = 0
        for _k, operand in enumerate(reversed(self.operands.entries)):
            _idx = self.operands.num_entries() - _k - 1 + n_complex_entries
            if InSet(operand, Complex).proven():
                # We have a complex number to pull out in front.
                _A = expr.operands[:_idx]
                _b = operand
                _C = expr.operands[_idx + 1:]
                _l = _A.num_elements()
                _n = _C.num_elements()
                expr = eq.update(
                    qmult_pulling_scalar_out_front.instantiate(
                        {
                            l: _l,
                            n: _n,
                            b: _b,
                            A: _A,
                            C: _C
                        }, preserve_all=True))
                n_complex_entries += 1
            elif isinstance(operand, ExprRange):
                if ExprRange(operand.parameter, InSet(operand.body, Complex),
                             operand.true_start_index,
                             operand.true_end_index).proven():
                    # We have a range of complex numbers to pull out in
                    # front.
                    _A = expr.operands[:_idx]
                    _b = ExprTuple(operand)
                    _C = expr.operands[_idx + 1:]
                    _j = _b.num_elements()
                    _l = _A.num_elements()
                    _n = _C.num_elements()
                    thm = qmult_pulling_scalars_out_front
                    expr = eq.update(
                        thm.instantiate(
                            {
                                j: _j,
                                l: _l,
                                n: _n,
                                b: _b,
                                A: _A,
                                C: _C
                            },
                            preserve_all=True))
                    n_complex_entries += 1

        # Associate the complex numbers, now out in front.
        _b = expr.operands[:n_complex_entries]
        _A = expr.operands[n_complex_entries:]
        _j = _b.num_elements()
        _l = _A.num_elements()
        if (_b.num_entries() > 0 and not _b.is_single()
                and _A.num_entries() > 0):
            expr = eq.update(
                qmult_scalar_association.instantiate(
                    {
                        j: _j,
                        l: _l,
                        b: _b,
                        A: _A
                    }, preserve_expr=expr))
            # The multiplication of complex numbers is complex.
            expr.operands[0].deduce_in_number_set(Complex)

        return eq.relation