def conversionToAddition(self, assumptions=USE_DEFAULTS): ''' From multiplication by an integer as the first factor, derive and return the equivalence of this multiplication to a repeated addition; for example, 3*c = c + c + c. ''' from ._axioms_ import multDef if hasattr(self.operands[0], 'asInt'): reps = self.operands[0].asInt() else: raise ValueError( "Cannot 'expandAsAddition' unless the first operand is a literal integer: %s" % str(self)) expr = self eq = TransRelUpdater( self, assumptions) # for convenience updating our equation # Group together the remaining factors if necessary: if len(self.operands) > 2: expr = eq.update( expr.association(1, len(self.operands) - 1, assumptions)) _x = self.operands[1] _n = num(reps) eq.update( multDef.specialize({ n: _n, a: [_x] * reps, x: _x }, assumptions=assumptions)) return eq.relation
def groupCommutation(expr, initIdx, finalIdx, length, disassociate=True, assumptions=USE_DEFAULTS): ''' Derive a commutation equivalence on a group of multiple operands by associating them together first. If 'dissassociate' is true, the group will be disassociated at end. ''' from proveit import TransRelUpdater if initIdx < 0: initIdx = len(expr.operands) + initIdx # use wrap-around indexing if finalIdx < 0: finalIdx = len(expr.operands) + finalIdx # use wrap-around indexing if length == 1: return expr.commutation(initIdx, finalIdx, assumptions=assumptions) eq = TransRelUpdater( expr, assumptions) # for convenience while updating our equation expr = eq.update(expr.association(initIdx, length, assumptions=assumptions)) expr = eq.update( expr.commutation(initIdx, finalIdx, assumptions=assumptions)) if disassociate: expr = eq.update(expr.disassociation(finalIdx, assumptions=assumptions)) return eq.relation
def negSimplifications(self, assumptions=USE_DEFAULTS): ''' Equivalence method that derives a simplification in which negated factors are factored out. For example: (-w)*(-x)*y*(-z) = -(w*x*y*z) ''' from proveit.number import Neg expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self, assumptions) # Work in reverse order so indices don't need to be updated. for rev_idx, operand in enumerate(reversed(self.operands)): if isinstance(operand, Neg): idx = len(self.operands) - rev_idx - 1 if isinstance(expr, Mult): expr = eq.update(expr.negSimplification(idx, assumptions)) elif isinstance(expr, Neg): expr = eq.update( expr.innerNegMultSimplification(idx, assumptions)) return eq.relation
def pairwise_evaluation(expr, **defaults_config): ''' Evaluation routine applicable to associative operations in which operands at the beginning are paired and evaluated sequentially. ''' from proveit import TransRelUpdater from proveit.logic import is_irreducible_value # successively evaluate and replace the operation performed on # the first two operands # for convenience while updating our equation: eq = TransRelUpdater(expr) if is_irreducible_value(expr): # The expression is already irreducible, so we are done. return eq.relation if expr.operands.num_entries() == 2: raise ValueError("pairwise_evaluation may only be used when there " "are more than 2 operands.") # While there are more than 2 operands, associate the first 2 # and auto-simplify. while expr.operands.num_entries() > 2: expr = eq.update(expr.association(0, length=2, auto_simplify=True)) if is_irreducible_value(expr): # The new expression is irreducible, so we are done. return eq.relation elif not is_irreducible_value(expr.operands[0]): # Auto-simplication failed to convert the first operand # to an irreducible value, so break out of this loop # and generate an appropriate error by trying to # evaluate it directly. expr = eq.update(expr.inner_expr().operands[0].evaluate()) return eq.relation
def shallow_simplification(self, *, must_evaluate=False, **defaults_config): ''' Returns a proven simplification equation for this TensorProd expression assuming the operands have been simplified. Currently deals only with: (1) simplifying a TensorProd(x) (i.e. a TensorProd with a single operand x) to x itself. For example, TensorProd(x) = x. (2) Ungrouping nested tensor products. (3) Factoring out scalars. ''' if self.operands.is_single(): from . import unary_tensor_prod_def _V = VecSpaces.known_vec_space(self.operand) _K = VecSpaces.known_field(_V) return unary_tensor_prod_def.instantiate( {K:_K, V:_V, A:self.operands[0]}, preserve_all=True) # for convenience updating our equation: expr = self eq = TransRelUpdater(expr) if TensorProd._simplification_directives_.ungroup: # ungroup the expression (disassociate nested additions). _n = 0 length = expr.operands.num_entries() - 1 # loop through all operands while _n < length: operand = expr.operands[_n] if isinstance(operand, TensorProd): # if it is grouped, ungroup it expr = eq.update(expr.disassociation( _n, preserve_all=True)) length = expr.operands.num_entries() _n += 1 if TensorProd._simplification_directives_.factor_scalars: # Next, pull out scalar factors try: VecSpaces.known_vec_space(self) except ValueError: raise UnsatisfiedPrerequisites( "No known vector space for %s"%self) for _k, operand in enumerate(expr.operands): if isinstance(operand, ScalarMult): # Just pull out the first one we see and let # recursive simplifications take care of any more. # To make sure this happens we turn auto_simplify # on, and those simpiflications should all be fair # game as part of shallow_simplification. expr = eq.update(expr.scalar_factorization( _k, auto_simplify=True)) break # Future processing possible here. return eq.relation
def cancelations(self, **defaults_config): ''' Deduce and return an equality between self and a form in which all simple division cancellations are performed. ''' from proveit.numbers import Mult expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self) numer_factors = (self.numerator.operands if isinstance( self.numerator, Mult) else [self.numerator]) denom_factors = (self.denominator.operands if isinstance( self.denominator, Mult) else [self.denominator]) denom_factors_set = set(denom_factors) for numer_factor in numer_factors: if numer_factor in denom_factors_set: expr = eq.update( expr.cancelation(numer_factor, preserve_all=True)) denom_factors_set.remove(numer_factor) return eq.relation
def factorization(self, theFactor, pull="left", groupFactor=True, groupRemainder=False, assumptions=USE_DEFAULTS): ''' Factor out "theFactor" from this product, pulling it either to the "left" or "right". If "theFactor" is a product, this may factor out a subset of the operands as long as they are next to each other (use commute to make this happen). If there are multiple occurrences, the first occurrence is used. If groupFactor is True and theFactor is a product, these operands are grouped together as a sub-product. If groupRemainder is True and there are multiple remaining operands (those not in "theFactor"), then these remaining operands are grouped together as a sub-product. Returns the equality that equates self to this new version. Give any assumptions necessary to prove that the operands are in Complexes so that the associative and commutation theorems are applicable. ''' expr = self eq = TransRelUpdater(expr, assumptions) idx, num = self.index(theFactor, alsoReturnNum=True) expr = eq.update( self.groupCommutation(idx, 0 if pull == 'left' else -num, length=num, assumptions=assumptions)) if groupFactor and num > 1: if pull == 'left': # use 0:num type of convention like standard pythong expr = eq.update( expr.association(0, num, assumptions=assumptions)) elif pull == 'right': expr = eq.update( expr.association(-num, num, assumptions=assumptions)) if groupRemainder and len(self.operands) - num > 1: # if the factor has been group, effectively there is just 1 factor operand now num_factor_operands = 1 if groupFactor else num if pull == 'left': expr = eq.update( expr.association(num_factor_operands, len(self.operands) - num_factor_operands, assumptions=assumptions)) elif pull == 'right': expr = eq.update( expr.association(0, num_factor_operands, assumptions=assumptions)) return eq.relation
def group_commutation(expr, init_idx, final_idx, length, disassociate=True, assumptions=USE_DEFAULTS): ''' Derive a commutation equivalence on a group of multiple operands by associating them together first. If 'dissassociate' is true, the group will be disassociated at end. For example, the following call: Or(A,B,C,D).group_commutation(0, 1, length=2, assumptions=in_bool(A,B,C,D)) essentially goes through the following steps: (1) associates 2 elements (i.e. length = 2) starting at index 0 to obtain (A V B) V C V D (2) removes the element to be commuted to obtain C V D (2) inserts the element to be commuted at the desire index to obtain C V (A V B) V D (3) then disassociates to obtain C V A V B V D (4) eventually producing the output: {A in Bool, ..., D in Bool} |- (A V B V C V D) = (C V A V B V D) ''' from proveit import TransRelUpdater # use the following to allow/acknowledge wrap-around indexing if init_idx < 0: init_idx = expr.operands.num_entries() + init_idx # wrap if final_idx < 0: final_idx = expr.operands.num_entries() + final_idx # wrap if length == 1: return expr.commutation(init_idx, final_idx, assumptions=assumptions) # for convenience while updating our equation: eq = TransRelUpdater(expr, assumptions) expr = eq.update( expr.association(init_idx, length, assumptions=assumptions)) expr = eq.update( expr.commutation(init_idx, final_idx, assumptions=assumptions)) if disassociate: expr = eq.update( expr.disassociation(final_idx, assumptions=assumptions)) return eq.relation
def deep_one_eliminations(self, **defaults_config): ''' Eliminate ones from the numerator, the denominator, and as a division by one. ''' from proveit.numbers import one expr = self # A convenience to allow successive update to the equation # via transitivities (starting with self=self). eq = TransRelUpdater(self) for _i, operand in enumerate(self.operands): if hasattr(operand, 'deep_one_eliminations'): expr = eq.update( expr.inner_expr().operands[_i].deep_one_eliminations()) if expr.denominator == one: expr = eq.update(expr.divide_by_one_elimination()) return eq.relation
def elem_substitution(self, elem=None, sub_elem=None, assumptions=USE_DEFAULTS): ''' Deduce that this enum Set expression is equal to the Set obtained when every instance of elem in self is replaced by the sub_elem. The deduction depends on the sub_elem being equal to the elem it is replacing. Examples: Let S = Set(a, b, a, b, a, c). Then S.elem_substitution() gives ERROR; S.elem_substitution(elem=d, sub_elem=two, assumptions=[Equals(d, two)] gives ERROR; S.elem_substitution(elem=b, sub_elem=four, assumptions=[Equals(b, four)]) gives |- S = {a, 4, a, 4, a, c}; S.single_elem_substitution(elem=a, sub_elem=two, assumptions=[Equals(a, two)]) gives |- S = {2, b, 2, b, 2, c}; S.single_elem_substitution(elem=c, sub_elem=d, assumptions=[Equals(c, d)]) gives |- S = {a, b, a, b, a, d}; ''' # First, a quick check on elem and sub_elem arguments if elem == None or sub_elem == None: raise ValueError("Set.elem_substitution() method requires " "the specification of the element to be replaced " "and the requested substitution value, but " "found elem={0} and sub_elem={1}.".format( elem, sub_elem)) if elem not in self.operands: raise ValueError("In calling the Set.elem_substitution() method, " "the element '{0}' to be replaced does not " "appear to exist in the original set {1}.".format( elem, self)) # count the number of elems to replace with the sub_elem self_list = list(self.operands) num_elems_to_replace = self_list.count(elem) from proveit import TransRelUpdater eq = TransRelUpdater(self, assumptions) expr = self while num_elems_to_replace > 0: expr = eq.update( expr.single_elem_substitution(elem=elem, sub_elem=sub_elem, assumptions=assumptions)) num_elems_to_replace -= 1 return eq.relation
def pairwise_evaluation(expr, assumptions): ''' Evaluation routine applicable to associative operations in which operands at the beginning are paired and evaluated sequentially. ''' from proveit import TransRelUpdater # successively evaluate and replace the operation performed on # the first two operands # for convenience while updating our equation: eq = TransRelUpdater(expr, assumptions) if expr.operands.num_entries() == 2: raise ValueError("pairwise_evaluation may only be used when there " "are more than 2 operands.") while expr.operands.num_entries() > 2: expr = eq.update(expr.association(0, length=2, assumptions=assumptions)) expr = eq.update(expr.inner_expr().operands[0].evaluation(assumptions)) eq.update(expr.evaluation(assumptions=assumptions)) return eq.relation
def vec_sum_elimination(self, field=None, **defaults_config): ''' For a VecSum in which the summand does not depend on the summation index, return an equality between this VecSum and the equivalent expression in which the VecSum is eliminated. For example, suppose self = VecSum(i, v, Interval(2, 4)). Then self.vec_sum_elimination() would return |- self = 3*v where the 3*v is actually ScalarMult(3, v). The method works only for a VecSum over a single summation index, and simply returns self = self if the VecSum elimination is not possible due to the summand being dependent on the index of summation. ''' expr = self summation_index = expr.index eq = TransRelUpdater(expr) if summation_index not in free_vars(expr.summand): vec_space_membership = expr.summand.deduce_in_vec_space( field=field, assumptions = defaults.assumptions + expr.conditions.entries) _V_sub = vec_space_membership.domain _K_sub = VecSpaces.known_field(_V_sub) _j_sub = expr.condition.domain.lower_bound _k_sub = expr.condition.domain.upper_bound _v_sub = expr.summand from proveit.linear_algebra.addition import vec_sum_of_constant_vec eq.update(vec_sum_of_constant_vec.instantiate( {V: _V_sub, K: _K_sub, j: _j_sub, k: _k_sub, v: _v_sub})) else: print("VecSum cannot be eliminated. The summand {0} appears " "to depend on the index of summation {1}". format(expr.summand, summation_index)) return eq.relation
def doReducedSimplification(self, assumptions=USE_DEFAULTS, **kwargs): ''' Derive and return this multiplication expression equated with a simpler form. Deals with disassociating any nested multiplications,simplifying negations, and factors of one, in that order. doReducedEvaluation deals with factors of 0. ''' expr = self eq = TransRelUpdater( self, assumptions) # for convenience updating our equation # Ungroup the expression (disassociate nested multiplications). idx = 0 length = len(expr.operands) - 1 while idx < length: # loop through all operands if isinstance(expr.operands[idx], Mult): # if it is grouped, ungroup it expr = eq.update(expr.disassociation(idx, assumptions)) else: idx += 1 length = len(expr.operands) # Simplify negations -- factor them out. expr = eq.update(expr.negSimplifications(assumptions)) if not isinstance(expr, Mult): # The expression may have changed to a negation after doing # negSimplification. Start the simplification of this new # expression fresh at this point. eq.update(expr.simplification(assumptions)) return eq.relation # Eliminate any factors of one. expr = eq.update(expr.oneEliminations(assumptions)) return eq.relation
def do_reduced_simplification(self, assumptions=USE_DEFAULTS, **kwargs): ''' Perform simplifications of a Divide expression after the operands have individually been simplified. Cancels common factors... ''' from proveit.numbers import one expr = self # for convenience updating our equation eq = TransRelUpdater(expr, assumptions) # perform cancelations where possible expr = eq.update(expr.cancelations(assumptions)) if not isinstance(expr, Div): # complete cancelation. return eq.relation if self.denominator == one: # eliminate division by one eq.update(expr.eliminate_divide_by_one(assumptions)) return eq.relation # no more division simplifications. return eq.relation
def shallow_simplification(self, *, must_evaluate=False, **defaults_config): ''' Returns a proven simplification equation for this ScalarMult expression assuming the operands have been simplified. Handles: (1) Doubly-nested scalar multiplication -- for example, taking ScalarMult(a, ScalarMult(b, v)) to ScalarMult(ScalarMult(a, b), v) (2) ScalarMult identity -- for example, taking ScalarMult(1, v) to v. ''' from proveit.numbers import Complex if (InSet(self.scalar, Complex).proven() and InSet(self.scaled, Complex).proven()): # If the operands are both complex numbers, this will # ScalarMult will reduce to number multiplication. return self.number_mult_reduction() expr = self # A convenience to allow successive updating of the equation # via transitivities (starting with self=self). eq = TransRelUpdater(self) # (1) Simplify doubly-nested scalar multiplication if isinstance(expr.scaled, ScalarMult): # Reduce a double-nested scalar multiplication. expr = eq.update(self.double_scaling_reduction()) # (2) Simplify multiplicative identity # (if expr is still a ScalarMult) if isinstance(expr, ScalarMult) and expr.scalar == one: expr = eq.update(expr.scalar_one_elimination(preserve_all=True)) return eq.relation
def doReducedEvaluation(self, assumptions=USE_DEFAULTS): ''' Derive and return this multiplication expression equated with an irreducible value. Handle the trivial case of a zero factor or do pairwise evaluation after simplifying negations and eliminating one factors. ''' from ._theorems_ import multZeroLeft, multZeroRight, multZeroAny from proveit.logic import isIrreducibleValue, SimplificationError from proveit.number import zero # First check for any zero factors -- quickest way to do an evaluation. try: zeroIdx = self.operands.index(zero) if len(self.operands)==2: if zeroIdx==0: return multZeroLeft.specialize({x:self.operands[1]}, assumptions=assumptions) else: return multZeroRight.specialize({x:self.operands[0]}, assumptions=assumptions) Afactors = self.operands[:zeroIdx] Bfactors = self.operands[zeroIdx+1:] return multZeroAny.specialize({m:len(Afactors), n:len(Bfactors), AA:Afactors, BB:Bfactors}, assumptions=assumptions) except (ValueError, ProofFailure): pass # No such "luck" regarding a simple multiplication by zero. expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self, assumptions) # Simplify negations -- factor them out. expr = eq.update(expr.negSimplifications(assumptions)) if not isinstance(expr, Mult): # The expression may have changed to a negation after doing # negSimplification. Start the simplification of this new # expression fresh at this point. eq.update(expr.evaluation(assumptions)) return eq.relation # Eliminate any factors of one. expr = eq.update(expr.oneEliminations(assumptions)) if isIrreducibleValue(expr): return eq.relation # done if len(self.operands) > 2: eq.update(pairwiseEvaluation(expr, assumptions)) return eq.relation raise SimplificationError("Unable to evaluate %s"%str(self))
def evaluate_add_digit(self, assumptions=USE_DEFAULTS): """ Evaluates each addition within the DecimalSequence """ from proveit import TransRelUpdater, ExprTuple from proveit.numbers import Add from . import deci_sequence_reduction expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self, assumptions) for i, digit in enumerate(self.digits): if isinstance(digit, Add): # only implemented for addition. _m = expr.digits[:i].num_elements(assumptions=assumptions) _n = digit.operands.num_elements(assumptions=assumptions) _k = expr.digits[i + 1:].num_elements(assumptions=assumptions) # _a = expr.inner_expr().operands[:i] _b = digit.operands _c = digit.evaluation(assumptions=assumptions).rhs # _d = expr.inner_expr().operands[i + 1:] _a = expr.digits[:i] _d = expr.digits[i + 1:] expr = eq.update( deci_sequence_reduction.instantiate( { m: _m, n: _n, k: _k, a: _a, b: _b, c: _c, d: _d }, assumptions=assumptions)) return eq.relation
def reduction(self, assumptions=USE_DEFAULTS): ''' Deduce that this enum Set expression is equal to the Set's support -- i.e. equal to a Set with all multiplicities reduced to 1. For example, the Set(a, a, b, b, c, d)={a, a, b, b, c, d} is equal to its support {a, b, c, d}. The deduction is achieved by successively applying the element-by-element reduction_elem() method until no further reduction is possible. ''' from proveit import TransRelUpdater eq = TransRelUpdater(self, assumptions) current_operands = list(self.operands) # the following does not preserve the order, but we really # just want the size of the support set desired_operands = set(self.operands) desired_num_operands = len(set(self.operands)) expr = self while len(current_operands) > desired_num_operands: expr = eq.update(expr.reduction_elem(assumptions=assumptions)) current_operands = expr.operands return eq.relation
def oneEliminations(self, assumptions=USE_DEFAULTS): ''' Equivalence method that derives a simplification in which factors of one are eliminated. For example: x*1*y*1*z*1 = x*y*z ''' from proveit.number import one expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self, assumptions) # Work in reverse order so indices don't need to be updated. for rev_idx, operand in enumerate(reversed(self.operands)): if operand==one: idx = len(self.operands) - rev_idx - 1 expr = eq.update(expr.oneElimination(idx, assumptions)) if not isinstance(expr, Mult): break # can't do an elimination if reduced to a single term. return eq.relation
def reduce_exprRange(self, assumptions=USE_DEFAULTS): ''' Tries to reduce a decimal sequence containing an ExprRange. For example, reduce #(3 4 2 .. 4 repeats .. 2 3) to 3422223 ''' from proveit import TransRelUpdater from proveit.core_expr_types.tuples import n_repeats_reduction from proveit.numbers.numerals.decimals import deci_sequence_reduction_ER was_range_reduction_disabled = ( ExprRange in defaults.disabled_auto_reduction_types) expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self, assumptions) idx = 0 for i, digit in enumerate(self.digits): # i is the current index in the original expression # idx is the current index in the transformed expression (eq.relation) if isinstance(digit, ExprRange) and isinstance( digit.body, Numeral): import proveit.numbers.numerals.decimals # _m = expr.digits[:i].num_elements(assumptions) # _n = digit.end_index # _k = expr.digits[i + 1:].num_elements(assumptions) # _a = expr.digits[:i] # _b = digit.body # _d = expr.digits[i + 1:] _m = eq.relation.rhs.digits[:idx].num_elements(assumptions) _n = digit.end_index _k = expr.digits[i + 1:].num_elements(assumptions) _a = eq.relation.rhs.digits[:idx] _b = digit.body _d = expr.digits[i + 1:] # if digit.end_index.as_int() >= 10: # Automatically reduce an Expression range of # a single numeral to an Expression tuple # (3 .. 4 repeats.. 3) = 3333 # #(2 3 4 (5 ..3 repeats.. 5) 6 7 8) = 234555678 while _n.as_int() > 9: _x = digit.body _c = n_repeats_reduction.instantiate( { n: _n, x: _x }, assumptions=assumptions).rhs eq.update( deci_sequence_reduction_ER.instantiate( { m: _m, n: _n, k: _k, a: _a, b: _b, c: _c, d: _d }, assumptions=assumptions)) _n = num(_n.as_int() - 1) idx += 1 #_n = digit.end_index len_thm = proveit.numbers.numerals.decimals \ .__getattr__('reduce_%s_repeats' % _n) _x = digit.body _c = len_thm.instantiate({x: _x}, assumptions=assumptions).rhs idx += _n.as_int() if _n == one: # we have to disable ExprRange reduction in this instance # because Prove-It knows that an ExprRange of one element is just that element # but we need to preserve the left hand side of the equation without this reduction defaults.disabled_auto_reduction_types.add(ExprRange) eq.update( deci_sequence_reduction_ER.instantiate( { m: _m, n: _n, k: _k, a: _a, b: _b, c: _c, d: _d }, assumptions=assumptions)) if not was_range_reduction_disabled: defaults.disabled_auto_reduction_types.remove( ExprRange) else: eq.update( deci_sequence_reduction_ER.instantiate( { m: _m, n: _n, k: _k, a: _a, b: _b, c: _c, d: _d }, assumptions=assumptions)) else: idx += 1 return eq.relation
def factors_extraction(self, field=None, **defaults_config): ''' Derive an equality between this VecSum and the result when all possible leading scalar factors have been extracted and moved to the front of the VecSum (for example, in the case where the summand of the VecSum is a ScalarMult) and all possible tensor product factors have been moved outside the VecSum (in front if possible, or afterward if necessary). For example, we could take the VecSum vec_sum = VecSum(ScalarMult(a, TensorProd(x, f(i), y))), where the index of summation is i, and call vec_sum.factor_extraction() to obtain: |- vec_sum = ScalarMult(a, TensorProd(x, VecSum(f(i)), y)) Note that any factors inside the summand that depend on the index of summation cannot be pulled out from inside the VecSum, and thus pose limitations on the result. Note that this method only works when self has a single index of summation, and only when self has a summand that is a ScalarMult or TensorProd. Later versions of this method should provide mechanisms to specify factors to extract from, and/or leave behind in, the VecSum. ''' expr = self summation_index = expr.index assumptions = defaults.assumptions + expr.conditions.entries assumptions_with_conditions = ( defaults.assumptions + expr.conditions.entries) # for convenience in updating our equation: # this begins with eq.relation as expr = expr eq = TransRelUpdater(expr) # If the summand is a ScalarMult, perform a # shallow_simplification(), which will remove nested # ScalarMults and multiplicative identities. This is # intended to simplify without changing too much the # intent of the user. This might even transform the # ScalarMult object into something else. from proveit.linear_algebra import ScalarMult, TensorProd if isinstance(expr.summand, ScalarMult): expr = eq.update( expr.inner_expr().summand.shallow_simplification()) if isinstance(expr.summand, ScalarMult): # had to re-check, b/c the shallow_simplification might # have transformed the ScalarMult into the scaled object tensor_prod_summand = False # not clearly useful; review please the_scalar = expr.summand.scalar elif isinstance(expr.summand, TensorProd): tensor_prod_summand = True # not clearly useful; review please if isinstance(expr.summand, ScalarMult): if summation_index not in free_vars(expr.summand.scalar): # it doesn't matter what the scalar is; the whole thing # can be pulled out in front of the VecSum from proveit.linear_algebra.scalar_multiplication import ( distribution_over_vec_sum) summand_in_vec_space = expr.summand.deduce_in_vec_space( field=field, assumptions=assumptions_with_conditions) _V_sub = summand_in_vec_space.domain _K_sub = VecSpaces.known_field(_V_sub) _b_sub = expr.indices _j_sub = _b_sub.num_elements() _f_sub = Lambda(expr.indices, expr.summand.scaled) _Q_sub = Lambda(expr.indices, expr.condition) _k_sub = expr.summand.scalar imp = distribution_over_vec_sum.instantiate( {V: _V_sub, K: _K_sub, b: _b_sub, j: _j_sub, f: _f_sub, Q: _Q_sub, k: _k_sub}, assumptions=assumptions_with_conditions) expr = eq.update(imp.derive_consequent( assumptions=assumptions_with_conditions).derive_reversed()) else: # The scalar portion is dependent on summation index. # If the scalar itself is a Mult of things, go through # and pull to the front of the Mult all individual # factors that are not dependent on the summation index. if isinstance(expr.summand.scalar, Mult): # Repeatedly pull index-independent factors # # to the front of the Mult factors # # prepare to count the extractable and # unextractable factors _num_factored = 0 _num_unfactored = len(expr.summand.scalar.operands.entries) # go through factors from back to front for the_factor in reversed( expr.summand.scalar.operands.entries): if summation_index not in free_vars(the_factor): expr = eq.update( expr.inner_expr().summand.scalar.factorization( the_factor, assumptions=assumptions_with_conditions, preserve_all=True)) _num_factored += 1 _num_unfactored -= 1 # group the factorable factors if _num_factored > 0: expr = eq.update( expr.inner_expr().summand.scalar.association( 0, _num_factored, assumptions=assumptions_with_conditions, preserve_all=True)) # group the unfactorable factors if _num_unfactored > 1: expr = eq.update( expr.inner_expr().summand.scalar.association( 1, _num_unfactored, assumptions=assumptions_with_conditions, preserve_all=True)) # finally, extract any factorable scalar factors if _num_factored > 0: from proveit.linear_algebra.scalar_multiplication import ( distribution_over_vec_sum_with_scalar_mult) # Mult._simplification_directives_.ungroup = False # _V_sub = VecSpaces.known_vec_space(expr, field=field) summand_in_vec_space = ( expr.summand.deduce_in_vec_space( field = field, assumptions = assumptions_with_conditions)) _V_sub = summand_in_vec_space.domain _K_sub = VecSpaces.known_field(_V_sub) _b_sub = expr.indices _j_sub = _b_sub.num_elements() _f_sub = Lambda(expr.indices, expr.summand.scaled) _Q_sub = Lambda(expr.indices, expr.condition) _c_sub = Lambda(expr.indices, expr.summand.scalar.operands[1]) _k_sub = expr.summand.scalar.operands[0] # when instantiating, we set preserve_expr=expr; # otherwise auto_simplification disassociates inside # the Mult. impl = distribution_over_vec_sum_with_scalar_mult.instantiate( {V:_V_sub, K:_K_sub, b: _b_sub, j: _j_sub, f: _f_sub, Q: _Q_sub, c:_c_sub, k: _k_sub}, preserve_expr=expr, assumptions=assumptions_with_conditions) expr = eq.update(impl.derive_consequent( assumptions=assumptions_with_conditions). derive_reversed()) else: # The scalar component is dependent on summation # index but is not a Mult. # Revert everything and return self = self. print("Found summation index {0} in the scalar {1} " "and the scalar is not a Mult object.". format(summation_index, expr.summand.scalar)) eq = TransRelUpdater(self) # ============================================================ # # VECTOR FACTORS # # ============================================================ # # After the scalar factors (if any) have been dealt with, # proceed with the vector factors in any remaining TensorProd # in the summand. # Notice that we are not guaranteed at this point that we even # have a TensorProd to factor, and if we do have a TensorProd # we have not identified the non-index-dependent factors to # extract. # After processing above for scalar factors, we might now have # (1) expr = VecSum (we didn't find scalar factors to extract), # inside of which we might have a ScalarMult or a TensorProd; # or (2) expr = ScalarMult (we found some scalar factors to # extract), with a VecSum as the scaled component. if isinstance(expr, VecSum): expr = eq.update(expr.tensor_prod_factoring()) elif isinstance(expr, ScalarMult) and isinstance(expr.scaled, VecSum): expr = eq.update(expr.inner_expr().scaled.tensor_prod_factoring()) return eq.relation
def factorization(self, the_factors, pull="left", group_factors=True, group_remainder=True, **defaults_config): ''' Return the proven factorization (equality with the factored form) from pulling the factor(s) from this division to the "left" or "right". If there are multiple occurrences, the first occurrence is used. If group_factors is True, the factors are grouped together as a sub-product. The group_remainder parameter is not relevant here but kept for consistency with other factorization methods. Examples: [(a*b)/(c*d)].factorization((a/c)) proves (a*b)/(c*d) = (a/c)*(b/d) [(a*b)/(c*d)].factorization((1/c)) proves (a*b)/(c*d) = (1/c)*(a*b/d) [(a*b)/(c*d)].factorization(a, pull='right') proves (a*b)/(c*d) = (b/(c*d))*a [a/(c*d)].factorization(a, pull='right') proves a/(c*d) = (1/(c*d))*a [(a*b)/d].factorization((a/d), pull='right') proves (a*b)/d = b*(a/d) ''' from proveit.numbers import one, Mult from . import mult_frac_left, mult_frac_right, prod_of_fracs expr = self eq = TransRelUpdater(expr) if the_factors == self: return eq.relation # self = self if isinstance(the_factors, Div): the_factor_numer = the_factors.numerator the_factor_denom = the_factors.denominator else: the_factor_numer = the_factors the_factor_denom = one replacements = [] # Factor out a fraction. if expr.denominator == the_factor_denom: # Factor (x/z) from (x*y)/z. # x or y may be 1. if the_factor_numer not in (one, expr.numerator): expr = eq.update(expr.inner_expr().numerator.factorization( the_factors.numerator, pull=pull, group_factors=True, group_remainder=True, preserve_all=True)) if pull == 'left': # factor (x*y)/z into (x/z)*y thm = mult_frac_left if the_factor_numer == one: # factor y/z into (1/z)*y _x = one _y = expr.numerator replacements.append( Mult(_x, _y).one_elimination(0, preserve_all=True)) else: # factor (x*y)/z into x*(y/z) thm = mult_frac_right if the_factor_numer == one: # factor x/z into x*(1/z) _x = expr.numerator _y = one replacements.append( Mult(_x, _y).one_elimination(1, preserve_all=True)) if the_factor_numer != one: assert expr.numerator.operands.num_entries() == 2 _x = expr.numerator.operands.entries[0] _y = expr.numerator.operands.entries[1] _z = expr.denominator eq.update( thm.instantiate({ x: _x, y: _y, z: _z }, replacements=replacements)) else: # Factor (x*y)/(z*w) into (x/z)*(y/w). thm = prod_of_fracs if the_factor_denom not in (one, expr.denominator): expr = eq.update(expr.inner_expr().denominator.factorization( the_factor_denom, pull=pull, group_factors=True, preserve_all=True)) assert expr.denominator.operands.num_entries() == 2 _z = expr.denominator.operands.entries[0] _w = expr.denominator.operands.entries[1] elif (pull == 'left') == (the_factor_denom == one): # Factor (x*y)/w into x*(y/w). _z = one _w = expr.denominator replacements.append( Mult(_z, _w).one_elimination(0, preserve_all=True)) else: # Factor (x*y)/z into (x/z)*y. _z = expr.denominator _w = one replacements.append( Mult(_z, _w).one_elimination(1, preserve_all=True)) # Factor the numerator parts unless there is a 1 numerator. if the_factor_numer not in (one, expr.numerator): expr = eq.update(expr.inner_expr().numerator.factorization( the_factor_numer, pull=pull, group_factors=True, group_remainder=True, preserve_all=True)) assert expr.numerator.operands.num_entries() == 2 # Factor (x*y)/(z*w) into (x/z)*(y/w) _x = expr.numerator.operands.entries[0] _y = expr.numerator.operands.entries[1] elif (pull == 'left') == (the_factor_numer == one): # Factor y/(z*w) into (1/z)*(y/w) _x = one _y = expr.numerator replacements.append( Mult(_x, _y).one_elimination(0, preserve_all=True)) else: # Factor x/(y*z) into (x/y)*(1/z) _x = expr.numerator _y = one replacements.append( Mult(_x, _y).one_elimination(1, preserve_all=True)) # create POSSIBLE replacements for inadvertently generated # fractions of the form _x/1 (i.e. _z = 1) # or _y/1 (i.e. _w = 1): if _z == one: replacements.append( frac(_x, _z).divide_by_one_elimination(preserve_all=True)) if _w == one: replacements.append( frac(_y, _w).divide_by_one_elimination(preserve_all=True)) eq.update( thm.instantiate({ x: _x, y: _y, z: _z, w: _w }, replacements=replacements, preserve_expr=expr)) return eq.relation
def generic_permutation(expr, new_order=None, cycles=None, assumptions=USE_DEFAULTS): ''' Deduce that the expression expr is equal to a new_expr which is the same class and in which the operands at indices 0, 1, …, n-1 have been reordered as specified EITHER by the new_order list OR by the cycles list parameter. For example, if expr were the enumerated Set S = {a, b, c, d}, then: generic_permutation(S, new_order=[0, 2, 3, 1]) and generic_permutation(S, cycles=[(1, 2, 3)]) would both return |- {a, b, c, d} = {a, c, d, b}. This generic_permutation method can work with any expression type which enjoys commutativity of its operands and has established the corresponding permutation_move() method and related axioms or theorems. ''' # check validity of default param usage: should have new_order list # OR list of cycles, but not both if new_order is None and cycles is None: raise ValueError("Need to specify either a new ordering in " "the form of new_order = list OR " "cycles = list of tuples.") if new_order is not None and cycles is not None: raise ValueError("Need to specify EITHER new_order OR cycles, " "but not both.") # check validity of provided index values: # (1) each index i needs to be 0 ≤ i ≤ n-1; # (2) set of indices provided in new_order needs to be complete, # i.e. consisting of ints 0, 1, 2, …, n-1 (but we allow cycle # notation to omit length-1 cycles) # (3) cannot have repeated indices expected_number_of_indices = expr.operands.num_entries() expected_indices_set = set(range(0, expected_number_of_indices)) # might be able to condense some of new_order and cycles together # later, but somewhat challenging if we allow cycles to omit # length-1 cycles if new_order: given_indices_list = new_order given_indices_set = set(new_order) if len(given_indices_list) > expected_number_of_indices: raise ValueError("new_order specification contains too " "many items, listing {2} indices when " "it should list {0} indices from 0 to {1}".format( expected_number_of_indices, expected_number_of_indices - 1, len(new_order))) unexpected_indices_set = given_indices_set - expected_indices_set if len(unexpected_indices_set) != 0: raise IndexError("Index or indices out of bounds: {0}. " "new_order should be a list of indices " "i such that 0 ≤ i ≤ {1}.".format( unexpected_indices_set, expected_number_of_indices - 1)) missing_indices_set = expected_indices_set - given_indices_set if len(missing_indices_set) != 0: raise ValueError("new_order specification is missing " "indices: {}.".format(missing_indices_set)) if cycles: # collect the indices from the cycles given_indices_list = [] for cycle in cycles: for i in cycle: given_indices_list.append(i) given_indices_set = set(given_indices_list) if len(given_indices_list) > expected_number_of_indices: raise ValueError("cycles specification contains too " "many items, providing {0} indices when " "it should provide no more than {1} " "indices with values from 0 to {2}".format( len(given_indices_list), expected_number_of_indices, expected_number_of_indices - 1)) unexpected_indices_set = given_indices_set - expected_indices_set if len(unexpected_indices_set) != 0: raise IndexError("Index or indices out of bounds: {0}. " "cycles should only contain indices " "i such that 0 ≤ i ≤ {1}.".format( unexpected_indices_set, expected_number_of_indices - 1)) # For cycles, we allow user to omit length-1 cycles # so we do NOT need to check for missing indices. # Instead, once cycles have passed all the tests, # convert to new_order list new_order = list(range(0, expected_number_of_indices)) for cycle in cycles: temp_cycle_length = len(cycle) for i in range(0, temp_cycle_length): new_order[cycle[i]] = cycle[(i + 1) % temp_cycle_length] # quick check for duplicates in new_order (can't check it # earlier because there might be legitimate duplicates during # the cycles-to-new_order construction process): if len(new_order) != len(set(new_order)): raise IndexError("Index or indices duplicated in cycles " "specification. cycles should contain " "only a single instance of any specific " "index value.") # if user-supplied args check out, then we can continue, # regardless of whether user has supplied new_order or cycles from proveit import TransRelUpdater current_order = list(range(0, expected_number_of_indices)) desired_order = new_order # notice this isn't a deep copy # No need to explicitly check for a trivial (non)-permutation; # the eq.relation will hold this for us # for convenience while updating our equation eq = TransRelUpdater(expr, assumptions) while current_order != desired_order: # Use set comprehension to find 1st index where the # current_order and desired_order lists differ and determine # the desired_order value at that location temp_order_diff_info = next( (idx, x, y) for idx, (x, y) in enumerate(zip(current_order, desired_order)) if x != y) # extract the init and final indices for the permutation init_idx = current_order.index(temp_order_diff_info[2]) final_idx = temp_order_diff_info[0] expr = eq.update( expr.permutation_move(init_idx, final_idx, assumptions=assumptions)) # update current_order to reflect step-wise change current_order.remove(temp_order_diff_info[2]) current_order.insert(final_idx, temp_order_diff_info[2]) return eq.relation
def cancelation(self, term_to_cancel, assumptions=USE_DEFAULTS): ''' Deduce and return an equality between self and a form in which the given operand has been canceled on the numerator and denominator. For example, [(a*b)/(b*c)].cancelation(b) would return (a*b)/(b*c) = a / c ''' from proveit.numbers import Mult expr = self eq = TransRelUpdater(expr, assumptions) if self.numerator == self.denominator == term_to_cancel: # x/x = 1 from . import frac_cancel_complete return frac_cancel_complete.instantiate({ x: term_to_cancel }).checked(assumptions) if term_to_cancel != self.numerator: if (not isinstance(self.numerator, Mult) or term_to_cancel not in self.numerator.operands): raise ValueError("%s not in the denominator of %s" % (term_to_cancel, self)) # Factor the term_to_cancel from the numerator to the left. expr = eq.update(expr.inner_expr().numerator.factorization( term_to_cancel, group_factor=True, group_remainder=True, assumptions=assumptions)) if term_to_cancel != self.denominator: if (not isinstance(self.denominator, Mult) or term_to_cancel not in self.denominator.operands): raise ValueError("%s not in the denominator of %s" % (term_to_cancel, self)) # Factor the term_to_cancel from the denominator to the left. expr = eq.update(expr.inner_expr().denominator.factorization( term_to_cancel, group_factor=True, group_remainder=True, assumptions=assumptions)) if term_to_cancel == self.numerator: from . import frac_cancel_numer_left assert len(expr.denominator.operands) == 2, "Should be grouped" expr = eq.update( frac_cancel_numer_left.instantiate( { x: term_to_cancel, y: expr.denominator.operands[1] }, assumptions=assumptions)) return eq.relation elif term_to_cancel == self.denominator: from . import frac_cancel_denom_left assert len(expr.numerator.operands) == 2, "Should be grouped" expr = eq.update( frac_cancel_denom_left.instantiate( { x: term_to_cancel, y: expr.numerator.operands[1] }, assumptions=assumptions)) return eq.relation else: from . import frac_cancel_left expr = eq.update( frac_cancel_left.instantiate( { x: term_to_cancel, y: expr.numerator.operands[1], z: expr.denominator.operands[1] }, assumptions=assumptions)) return eq.relation
def shallow_simplification(self, *, must_evaluate=False, **defaults_config): ''' Returns a proven simplification equation for this Divide expression assuming the operands have been simplified. Specifically, cancels common factors and eliminates ones. ''' from proveit.logic import is_irreducible_value from proveit.numbers import one, Neg expr = self # for convenience updating our equation eq = TransRelUpdater(expr) # perform cancelations where possible expr = eq.update(expr.cancelations(preserve_all=True)) if not isinstance(expr, Div): # complete cancelation. return eq.relation if self.is_irreducible_value(): # already irreducible return Equals(self, self).conclude_via_reflexivity() if must_evaluate and not all( is_irreducible_value(operand) for operand in self.operands): for operand in self.operands: if not is_irreducible_value(operand): # The simplification of the operands may not have # worked hard enough. Let's work harder if we # must evaluate. operand.evaluation() return self.evaluation() if expr.denominator == one: # eliminate division by one expr = eq.update(expr.divide_by_one_elimination(preserve_all=True)) if (isinstance(expr, Div) and Div._simplification_directives_.factor_negation and isinstance(expr.numerator, Neg)): # we have something like (-a)/b but want -(a/b) expr = eq.update(expr.neg_extraction()) # return eq.relation # no more division simplifications. if (Div._simplification_directives_.reduce_zero_numerator and isinstance(expr, Div)): if ((expr.numerator == zero or Equals(expr.numerator, zero).proven()) and NotEquals(expr.denominator, zero).proven()): # we have something like 0/x so reduce to 0 expr = eq.update(expr.zero_numerator_reduction()) # finally, check if we have something like (x/(y/z)) # but! remember to check here if we still even have a Div expr # b/c some of the work above might have changed it to # something else! if (isinstance(expr, Div) and isinstance(expr.denominator, Div) and NotEquals(expr.denominator.numerator, zero).proven() and NotEquals(expr.denominator.denominator, zero).prove()): expr = eq.update(expr.div_in_denominator_reduction()) return eq.relation
def shallow_simplification(self, *, must_evaluate=False, **defaults_config): ''' Attempt to determine whether this disjunction, with simplified operands, evaluates to TRUE or FALSE under the given assumptions. If all operands have simplified to FALSE, the disjunction is FALSE. If any of the operands have simplified to TRUE, the disjunction may be TRUE (if the other operands are provably Boolean). If it can't be evaluated, and must_evaluate is False, ungroup nested disjunctions if that is an active simplification direction. Also, if applicable, perform a unary reduction: Or(A) = A. ''' from proveit.logic import (Equals, FALSE, TRUE, EvaluationError, is_irreducible_value) # load in truth-table evaluations from . import or_t_t, or_t_f, or_f_t, or_f_f if self.operands.num_entries() == 0: from proveit.logic.booleans.disjunction import \ empty_disjunction_eval # Or() = FALSE return empty_disjunction_eval # Check whether or not all of the operands are FALSE # or any are TRUE. all_are_false = True for operand in self.operands: if operand != FALSE: all_are_false = False if operand == TRUE: # If any simplified operand is TRUE, the disjunction # may only evaluate to TRUE if it can be evaluated. # Only use automation here if 'must_evaluate' is True. self.conclude(automation=must_evaluate) return Equals(self, TRUE).prove() # If all of the operands are FALSE, we can prove that the # conjunction is equal to FALSE. if all_are_false: self.conclude_negation() return Equals(self, FALSE).prove() if must_evaluate: if not all( is_irreducible_value(operand) for operand in self.operands): # The simplification of the operands may not have # worked hard enough. Let's work harder if we # must evaluate. for operand in self.operands: if not is_irreducible_value(operand): operand.evaluation() return self.evaluation() # Can't evaluate the conjunction if no operand was # FALSE but they aren't all TRUE. raise EvaluationError(self) if self.operands.is_single(): # Or(A) = A return self.unary_reduction() expr = self # for convenience updating our equation eq = TransRelUpdater(expr) if Or._simplification_directives_.ungroup: # ungroup the expression (disassociate nested disjunctions). _n = 0 length = expr.operands.num_entries() - 1 # loop through all operands while _n < length: operand = expr.operands[_n] if isinstance(operand, Or): # if it is grouped, ungroup it expr = eq.update( expr.disassociation(_n, auto_simplify=False)) length = expr.operands.num_entries() _n += 1 return Expression.shallow_simplification(self)
def factorization(self, the_factor, pull="left", group_factors=True, field=None, **defaults_config): ''' Deduce an equality between this VecAdd expression and a version in which either: (1) the scalar factor the_factor has been factored out in front (or possibly out behind) to produce a new ScalarMult; OR (2) the tensor product factor the_factor has been factored out in front (or possible out behind) to produce a new TensorProd. For example, if x = VecAdd(ScalarMult(a, v1), ScalarMult(a, v2)) then x.factorization(a) produces: |- x = ScalarMult(a, VecAdd(v1, v2)). Prove-It will need to know or be able to derive a vector space in which the vectors live. This method only works if the terms of the VecAdd are all ScalarMult objects or all TensorProd objects. In the case of all ScalarMult objects, any nested ScalarMult objects are first flattened if possible. Note: In the case of a VecAdd of all TensorProd objects, the lack of commutativity for tensor products limits any factorable tensor product factors to those occurring on the far left or far right of each tensor product term. Thus, for example, if x = VecAdd(TensorProd(v1, v2, v3), TensorProd(v1, v4, v5)) we can call x.factorization(v1) to obtain |- x = TensorProd(v1, VecAdd(TensorProd(v2, v3), TensorProd(v4, v5))), but we cannot factor v1 our of the expression y = VecAdd(TensorProd(v2, v1, v3), TensorProd(v4, v1, v5)) ''' expr = self eq = TransRelUpdater(expr) replacements = list(defaults.replacements) from proveit.linear_algebra import ScalarMult, TensorProd from proveit.numbers import one, Mult # Case (1) VecAdd(ScalarMult, ScalarMult, ..., ScalarMult) if all(isinstance(op, ScalarMult) for op in self.operands): # look for the_factor in each scalar; # code based on Add.factorization() _b = [] for _i in range(expr.terms.num_entries()): # remove nesting of ScalarMults term = expr.terms[_i].shallow_simplification().rhs expr = eq.update( expr.inner_expr().terms[_i].shallow_simplification()) # simplify the scalar part of the ScalarMult term = term.inner_expr().scalar.shallow_simplification().rhs expr = eq.update(expr.inner_expr().terms[_i].scalar. shallow_simplification()) if hasattr(term.scalar, 'factorization'): term_scalar_factorization = term.scalar.factorization( the_factor, pull, group_factors=group_factors, group_remainder=True, preserve_all=True) if not isinstance(term_scalar_factorization.rhs, Mult): raise ValueError( "Expecting right hand side of each factorization " "to be a product. Instead obtained: {}".format( term_scalar_factorization.rhs)) if pull == 'left': # the grouped remainder on the right _b.append( ScalarMult( term_scalar_factorization.rhs.operands[-1], term.scaled)) else: # the grouped remainder on the left _b.append( ScalarMult( term_scalar_factorization.rhs.operands[0], term.scaled)) # substitute in the factorized term expr = eq.update( term_scalar_factorization.substitution( expr.inner_expr().terms[_i].scalar, preserve_all=True)) else: if term.scalar != the_factor: raise ValueError( "Factor, %s, is not present in the term at " "index %d of %s!" % (the_factor, _i, self)) if pull == 'left': replacements.append( Mult(term.scalar, one).one_elimination(1)) else: replacements.append( Mult(one, term.scalar).one_elimination(0)) _b.append(ScalarMult(one, term.scaled)) if not group_factors and isinstance(the_factor, Mult): factor_sub = the_factor.operands else: factor_sub = ExprTuple(the_factor) # pull left/right not really relevant for the ScalarMult # cases; this simplification step still seems relevant if defaults.auto_simplify: # Simplify the remainder of the factorization if # auto-simplify is enabled. replacements.append(VecAdd(*_b).simplification()) from proveit import K, i, k, V, a # Perhaps here we could search through the operands to find # an appropriate VecSpace? Or maybe it doesn't matter? vec_space_membership = expr.operands[0].deduce_in_vec_space( field=field) _V_sub = vec_space_membership.domain _K_sub = VecSpaces.known_field(_V_sub) _i_sub = expr.operands.num_elements() _k_sub = the_factor _a_sub = ExprTuple(*_b) from proveit.linear_algebra.scalar_multiplication import ( distribution_over_vectors) distribution = distribution_over_vectors.instantiate( { V: _V_sub, K: _K_sub, i: _i_sub, k: _k_sub, a: _a_sub }, replacements=replacements) # need to connect the distributed version back to the # original self, via a shallow_simplification() of # each of the ScalarMult terms resulting in the distribution for _i in range(len(distribution.rhs.operands.entries)): distribution = (distribution.inner_expr().rhs.operands[_i]. shallow_simplify()) eq.update(distribution.derive_reversed()) # Case (2) VecAdd(TensorProd, TensorProd, ..., TensorProd) elif all(isinstance(op, TensorProd) for op in self.operands): # if hasattr(the_factor, 'operands'): # print("the_factor has operands: {}".format(the_factor.operands)) # the_factor_tuple = the_factor.operands.entries # else: # print("the_factor does not have operands: {}".format(the_factor)) # the_factor_tuple = (the_factor,) if isinstance(the_factor, TensorProd): the_factor_tuple = the_factor.operands.entries else: the_factor_tuple = (the_factor, ) # Setting the default_field here because the field # used manually in the association step somehow gets lost VecSpaces.default_field = field # look for the_factor in each TensorProd appearing in # the VecAdd operands, looking at the left vs. right # sides depending on the 'pull' direction specified _b = [] # to hold factors left behind for _i in range(expr.terms.num_entries()): # Notice we're not ready to deal with ExprRange # versions of Add operands here! # We are also implicitly assuming that each TensorProd # has at least two operands term = expr.terms[_i] if hasattr(term, 'operands'): term_tuple = term.operands.entries else: term_tuple = (term, ) if pull == 'left': # look for factor at left-most-side if the_factor_tuple != term_tuple[0:len(the_factor_tuple)]: raise ValueError( "VecAdd.factorization() expecting the_factor " "{0} to appear at the leftmost side of each " "addend, but {0} does not appear at the " "leftmost side of the addend {1}.".format( the_factor, term)) else: # we're OK, so save away the remainder of # factors from the rhs of the term, # and group any multi-term factor on the left if len(term_tuple[len(the_factor_tuple):]) == 1: _b.append(term_tuple[-1]) else: _b.append( TensorProd( *term_tuple[len(the_factor_tuple):])) # then create an associated version of the # expr to match the eventual thm instantiation # ALSO NEED TO DO THIS FOR THE RIGHT CASE expr = eq.update( expr.inner_expr().operands[_i].association( len(the_factor_tuple), len(term_tuple) - len(the_factor_tuple), preserve_all=True)) # perhaps we actually don't need the assoc step? # if len(the_factor_tuple) != 1: # expr = eq.update(expr.inner_expr().operands[_i]. # association(0, len(the_factor_tuple), # preserve_all=True)) elif pull == 'right': # look for factor at right-most-side if the_factor_tuple != term_tuple[-( len(the_factor_tuple)):]: raise ValueError( "VecAdd.factorization() expecting the_factor " "{0} to appear at the rightmost side of each " "addend, but {0} does not appear at the " "rightmost side of the addend {1}.".format( the_factor, term)) else: # we're OK, so save away the remainder of # factors from the lhs of the term, # and group any multi-term factor on the right if len(term_tuple[0:-(len(the_factor_tuple))]) == 1: _b.append(term_tuple[0]) else: _b.append( TensorProd( *term_tuple[0:-(len(the_factor_tuple))])) # then create an associated version of the # expr to match the eventual thm instantiation expr = eq.update( expr.inner_expr().operands[_i].association( 0, len(term_tuple) - len(the_factor_tuple), preserve_all=True)) # perhaps we actually don't need the assoc step? # if len(the_factor_tuple) != 1: # expr = eq.update(expr.inner_expr().operands[_i]. # association( # len(term_tuple)-len(the_factor_tuple), # len(the_factor_tuple), # preserve_all=True)) else: raise ValueError( "VecAdd.factorization() requires 'pull' argument " "to be specified as either 'left' or 'right'.") # now ready to instantiate the TensorProd/VecAdd # theorem: tensor_prod_distribution_over_add # and derive it's reversed result from proveit.linear_algebra.tensors import ( tensor_prod_distribution_over_add) from proveit import a, b, c, i, j, k, K, V from proveit.numbers import zero, one, num # useful to get ahead of time the num of operands # in the_factor and define the replacement # if hasattr(the_factor, 'operands'): # num_factor_entries = num(the_factor.operands.num_entries()) # factor_entries = the_factor.operands.entries # else: # num_factor_entries = one # factor_entries = (the_factor,) # useful to get ahead of time the num of operands # in the_factor and define the replacement if isinstance(the_factor, TensorProd): num_factor_entries = num(the_factor.operands.num_entries()) factor_entries = the_factor.operands.entries else: num_factor_entries = one factor_entries = (the_factor, ) # call deduce_in_vec_space() on the original self # instead of the current expr, otherwise we can run into # compications due to the associated sub-terms vec_space_membership = self.operands[0].deduce_in_vec_space( field=field) _V_sub = vec_space_membership.domain _K_sub = VecSpaces.known_field(_V_sub) if pull == 'left': # num of operands in left the_factor _i_sub = num_factor_entries # num of operands in right factor _k_sub = zero # the actual factor operands _a_sub = factor_entries # the other side is empty _c_sub = () elif pull == 'right': # left side is empty _i_sub = zero # right side has the factor _k_sub = num_factor_entries # left side is empty _a_sub = () # right side has the factor _c_sub = factor_entries _j_sub = num(len(_b)) _b_sub = ExprTuple(*_b) from proveit.linear_algebra.tensors import ( tensor_prod_distribution_over_add) impl = tensor_prod_distribution_over_add.instantiate( { V: _V_sub, K: _K_sub, i: _i_sub, j: _j_sub, k: _k_sub, a: _a_sub, b: _b_sub, c: _c_sub }, preserve_all=True) conseq = impl.derive_consequent() eq.update(conseq.derive_reversed()) else: print("Not yet an identified case. Sorry!") return eq.relation
def tensor_prod_factoring(self, idx=None, idx_beg=None, idx_end=None, field=None, **defaults_config): ''' For a VecSum with a TensorProd summand or ScalarMult summand with a scaled attribute being a TensorProd, factor out from the VecSum the TensorProd vectors other than the ones indicated by the (0-based) idx, or idx_beg and idx_end pair and return an equality between the original VecSum and the new TensorProd. For example, we could take the VecSum defined by vec_sum = VecSum(TensorProd(x, f(i), y, z)) and call vec_sum.tensor_prod_factoring(idx_beg=1, idx_end=2) to obtain: |- VecSum(TensorProd(x, f(i), y, z)) = TensorProd(x, VecSum(TensorProd(f(i), y)), z) This method should work even if the summand is a nested ScalarMult. Note that any vectors inside the TensorProd that depend on the index of summation cannot be pulled out of the VecSum and thus will cause the method to fail if not chosen to remain inside the VecSum. If all idx args are 'None', method will factor out all possible vector factors, including the case where all factors could be removed and the VecSum eliminated entirely. Note that this method only works when self has a single index of summation. ''' expr = self the_summand = self.summand eq = TransRelUpdater(expr) # Check that # (1) the_summand is a TensorProd # or (2) the_summand is a ScalarMult; # otherwise, this method does not apply from proveit.linear_algebra import ScalarMult, TensorProd if isinstance(the_summand, ScalarMult): # try shallow simplification first to remove nested # ScalarMults and multiplicative identities expr = eq.update(expr.inner_expr().summand.shallow_simplification()) the_summand = expr.summand if isinstance(the_summand, TensorProd): tensor_prod_expr = the_summand tensor_prod_summand = True tensor_prod_factors_list = list( the_summand.operands.entries) elif (isinstance(the_summand, ScalarMult) and isinstance(the_summand.scaled, TensorProd)): tensor_prod_expr = the_summand.scaled tensor_prod_summand = False tensor_prod_factors_list = list( the_summand.scaled.operands.entries) else: raise ValueError( "tensor_prod_factoring() requires the VecSum summand " "to be a TensorProd or a ScalarMult (with its 'scaled' " "attribute a TensorProd); instead the " "summand is {}".format(self.instance_expr)) if idx is None and idx_beg is None and idx_end is None: # prepare to take out all possible factors, including # the complete elimination of the VecSum if possible if expr.index not in free_vars(expr.summand): # summand does not depend on index of summation # so we can eliminate the VecSum entirely return expr.vec_sum_elimination(field=field) if expr.index in free_vars(tensor_prod_expr): # identify the extractable vs. non-extractable # TensorProd factors (and there must be at least # one such non-extractable factor) idx_beg = -1 idx_end = -1 for i in range(len(expr.summand.operands.entries)): if expr.index in free_vars(tensor_prod_expr.operands[i]): if idx_beg == -1: idx_beg = i idx_end = idx_beg else: idx_end = i else: # The alternative is that the summand is # a ScalarMult with the scalar (but not the scaled) # being dependent on the index of summation. It's not # obvious what's best to do in this case, but we set # things up to factor out all but the last of the # TensorProd factors (so we'll factor out at least # 1 factor) idx_beg = len(tensor_prod_expr.operands.entries) - 1 idx_end = idx_beg # Check that the provided idxs are within bounds # (it should refer to an actual TensorProd operand) num_vec_factors = len(tensor_prod_factors_list) if idx is not None and idx >= num_vec_factors: raise ValueError( "idx value {0} provided for tensor_prod_factoring() " "method is out-of-bounds; the TensorProd summand has " "{1} factors: {2}, and thus possibly indices 0-{3}". format(idx, len(tensor_prod_factors_list), tensor_prod_factors_list, len(tensor_prod_factors_list)-1)) if idx_beg is not None and idx_end is not None: if (idx_end < idx_beg or idx_beg >= num_vec_factors or idx_end >= num_vec_factors): raise ValueError( "idx_beg value {0} or idx_end value {1} (or both) " "provided for tensor_prod_factoring() " "method is/are out-of-bounds; the TensorProd summand " "has {2} factors: {3}, and thus possibly indices 0-{3}". format(idx_beg, idx_end, num_vec_factors, tensor_prod_factors_list,num_vec_factors-1)) if idx is not None: # take single idx as the default idx_beg = idx idx_end = idx # Check that the TensorProd factors to be factored out do not # rely on the VecSum index of summation summation_index = expr.index for i in range(num_vec_factors): if i < idx_beg or i > idx_end: the_factor = tensor_prod_factors_list[i] if summation_index in free_vars(the_factor): raise ValueError( "TensorProd factor {0} cannot be factored " "out of the given VecSum summation because " "it is a function of the summation index {1}.". format(the_factor, summation_index)) # Everything checks out as best we can tell, so prepare to # import and instantiate the appropriate theorem, # depending on whether: # (1) the_summand is a TensorProd, or # (2) the_summand is a ScalarMult (with a TensorProd 'scaled') if tensor_prod_summand: from proveit.linear_algebra.tensors import ( tensor_prod_distribution_over_summation) else: from proveit.linear_algebra.tensors import ( tensor_prod_distribution_over_summation_with_scalar_mult) if idx_beg != idx_end: # need to associate the elements and change idx value # but process is slightly different in the two cases if tensor_prod_summand: expr = eq.update(expr.inner_expr().summand.association( idx_beg, idx_end-idx_beg+1)) tensor_prod_expr = expr.summand else: expr = eq.update(expr.inner_expr().summand.scaled.association( idx_beg, idx_end-idx_beg+1)) tensor_prod_expr = expr.summand.scaled idx = idx_beg from proveit import K, f, Q, i, j, k, V, a, b, c, s # actually, maybe it doesn't matter and we can deduce the # vector space regardless: (Adding this temp 12/26/21) vec_space_membership = expr.summand.deduce_in_vec_space( field=field, assumptions = defaults.assumptions + expr.conditions.entries) _V_sub = vec_space_membership.domain # Substitutions regardless of Case _K_sub = VecSpaces.known_field(_V_sub) _b_sub = expr.indices _j_sub = _b_sub.num_elements() _Q_sub = Lambda(expr.indices, expr.condition) # Case-specific substitutions, using updated tensor_prod_expr: _a_sub = tensor_prod_expr.operands[:idx] _c_sub = tensor_prod_expr.operands[idx+1:] _f_sub = Lambda(expr.indices, tensor_prod_expr.operands[idx]) if not tensor_prod_summand: _s_sub = Lambda(expr.indices, expr.summand.scalar) # Case-dependent substitutions: _i_sub = _a_sub.num_elements() _k_sub = _c_sub.num_elements() if tensor_prod_summand: impl = tensor_prod_distribution_over_summation.instantiate( {K:_K_sub, f:_f_sub, Q:_Q_sub, i:_i_sub, j:_j_sub, k:_k_sub, V:_V_sub, a:_a_sub, b:_b_sub, c:_c_sub}, preserve_expr=expr) else: impl = (tensor_prod_distribution_over_summation_with_scalar_mult. instantiate( {K:_K_sub, f:_f_sub, Q:_Q_sub, i:_i_sub, j:_j_sub, k:_k_sub, V:_V_sub, a:_a_sub, b:_b_sub, c:_c_sub, s: _s_sub}, preserve_expr=expr)) expr = eq.update(impl.derive_consequent( assumptions = defaults.assumptions + expr.conditions.entries). derive_reversed()) return eq.relation
def digit_repetition_reduction(self, **defaults_config): ''' Tries to reduce a decimal sequence containing an ExprRange. For example, reduce #(3 4 2 .. 4 repeats .. 2 3) to 3422223 ''' from proveit import TransRelUpdater from proveit.core_expr_types.tuples import n_repeats_reduction from proveit.numbers.numerals.decimals import deci_sequence_reduction_ER expr = self # A convenience to allow successive update to the equation via transitivities. # (starting with self=self). eq = TransRelUpdater(self) idx = 0 for i, digit in enumerate(self.digits): # i is the current index in the original expression # idx is the current index in the transformed expression (eq.relation) if (isinstance(digit, ExprRange) and isinstance(digit.body, Numeral)): import proveit.numbers.numerals.decimals # _m = expr.digits[:i].num_elements(assumptions) # _n = digit.true_end_index # _k = expr.digits[i + 1:].num_elements(assumptions) # _a = expr.digits[:i] # _b = digit.body # _d = expr.digits[i + 1:] _m = eq.relation.rhs.digits[:idx].num_elements() _n = digit.true_end_index _k = expr.digits[i + 1:].num_elements() _a = eq.relation.rhs.digits[:idx] _b = digit.body _d = expr.digits[i + 1:] # if digit.true_end_index.as_int() >= 10: # Automatically reduce an Expression range of # a single numeral to an Expression tuple # (3 .. 4 repeats.. 3) = 3333 # #(2 3 4 (5 ..3 repeats.. 5) 6 7 8) = 234555678 while _n.as_int() > 9: _x = digit.body _c = n_repeats_reduction.instantiate({ n: _n, x: _x }, preserve_all=True).rhs eq.update( deci_sequence_reduction_ER.instantiate( { m: _m, n: _n, k: _k, a: _a, b: _b, c: _c, d: _d }, preserve_all=True)) _n = num(_n.as_int() - 1) idx += 1 #_n = digit.true_end_index len_thm = proveit.numbers.numerals.decimals \ .__getattr__('reduce_%s_repeats' % _n) _x = digit.body _c = len_thm.instantiate({x: _x}).rhs idx += _n.as_int() eq.update( deci_sequence_reduction_ER.instantiate( { m: _m, n: _n, k: _k, a: _a, b: _b, c: _c, d: _d }, preserve_all=True)) else: idx += 1 return eq.relation
def cancelation(self, term_to_cancel, **defaults_config): ''' Deduce and return an equality between self and a form in which the given operand has been canceled on the numerator and denominator. For example, [(a*b)/(b*c)].cancelation(b) would return (a*b)/(b*c) = a / c. Assumptions or previous work might be required to establish that the term_to_cancel is non-zero. ''' from proveit.numbers import Mult, one expr = self eq = TransRelUpdater(expr) if self.numerator == self.denominator == term_to_cancel: # x/x = 1 from . import frac_cancel_complete return frac_cancel_complete.instantiate({x: term_to_cancel}) if term_to_cancel != self.numerator: # try to catch Exp objects here as well? # after all, Exp(term_to_cancel, n) has factors! if (not isinstance(self.numerator, Mult) or term_to_cancel not in self.numerator.operands): raise ValueError("%s not in the numerator of %s" % (term_to_cancel, self)) # Factor the term_to_cancel from the numerator to the left. expr = eq.update(expr.inner_expr().numerator.factorization( term_to_cancel, group_factors=True, group_remainder=True, preserve_all=True)) if term_to_cancel != self.denominator: if (not isinstance(self.denominator, Mult) or term_to_cancel not in self.denominator.operands): raise ValueError("%s not in the denominator of %s" % (term_to_cancel, self)) # Factor the term_to_cancel from the denominator to the left. expr = eq.update(expr.inner_expr().denominator.factorization( term_to_cancel, group_factors=True, group_remainder=True, preserve_all=True)) if expr.numerator == expr.denominator == term_to_cancel: # Perhaps it reduced to the trivial x/x = 1 case via # auto-simplification. expr = eq.update(expr.cancelation(term_to_cancel)) return eq.relation else: # (x*y) / (x*z) = y/z with possible automatic reductions # via 1 eliminations. from . import frac_cancel_left replacements = list(defaults.replacements) if expr.numerator == term_to_cancel: numer_prod = Mult(term_to_cancel, one) _y = one replacements.append( numer_prod.one_elimination(1, preserve_expr=term_to_cancel)) else: _y = expr.numerator.operands[1] if expr.denominator == term_to_cancel: denom_prod = Mult(term_to_cancel, one) _z = one replacements.append( denom_prod.one_elimination(1, preserve_expr=term_to_cancel)) else: _z = expr.denominator.operands[1] expr = eq.update( frac_cancel_left.instantiate({ x: term_to_cancel, y: _y, z: _z }, replacements=replacements, preserve_expr=expr)) return eq.relation