def double_scaling_reduction(self, **defaults_config): from . import doubly_scaled_as_singly_scaled if not isinstance(self.scaled, ScalarMult): raise ValueError("'double_scaling_reduction' is only applicable " "for a doubly nested ScalarMult") # Reduce doubly-nested ScalarMult _x = self.scaled.scaled # _V = VecSpaces.known_vec_space(_x) # the following is a little klunky, but trying to avoid the # use of a default field=Real if we're actually dealing with # complex scalars somewhere in the vector from proveit import free_vars if any([InSet(elem, Complex).proven() for elem in free_vars(self)]): _V = VecSpaces.known_vec_space(self, field=Complex) else: _V = VecSpaces.known_vec_space(self) _K = VecSpaces.known_field(_V) _alpha = self.scalar _beta = self.scaled.scalar return doubly_scaled_as_singly_scaled.instantiate({ K: _K, V: _V, x: _x, alpha: _alpha, beta: _beta })
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 shallow_simplification(self, *, must_evaluate=False, **defaults_config): ''' Returns a proven simplification equation for this Sum expression assuming the operands have been simplified. For the trivial case of summing over only one item (currently implemented just for a Interval where the endpoints are equal), derive and return this summation expression equated with the simplified form of the single term. Assumptions may be necessary to deduce necessary conditions for the simplification. NEEDS UPDATING ''' from proveit.logic import TRUE, SimplificationError from . import sum_single, trivial_sum if (isinstance(self.domain, Interval) and self.domain.lower_bound == self.domain.upper_bound): if hasattr(self, 'index'): return sum_single.instantiate({ Function(f, self.index): self.summand, a: self.domain.lower_bound }) if (isinstance(self.domain, Interval) and self.instance_param not in free_vars(self.summand) and self.non_domain_condition() == TRUE): # Trivial sum: summand independent of parameter. _a = self.domain.lower_bound _b = self.domain.upper_bound _x = self.summand return trivial_sum.instantiate({a: _a, b: _b, x: _x}) raise SimplificationError( "Sum simplification only implemented for a summation over an " "integer Interval of one instance variable where the upper " "and lower bounds are the same.")
def factor(self, theFactor, pull="left", groupFactor=False, groupRemainder=None, assumptions=frozenset()): ''' Pull out a common factor from a summation, pulling it either to the "left" or "right". If groupFactor is True and theFactor is a product, it will be grouped together as a sub-product. groupRemainder is not relevant kept for compatibility with other factor methods. 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. ''' from proveit.number.multiplication.theorems import distributeThroughSummationRev from proveit.number import Mult if not free_vars(theFactor).isdisjoint(self.indices): raise Exception( 'Cannot factor anything involving summation indices out of a summation' ) # We may need to factor the summand within the summation summand_assumptions = assumptions | { InSet(index, self.domain) for index in self.indices } summandFactorEq = self.summand.factor(theFactor, pull, groupFactor=False, groupRemainder=True, assumptions=summand_assumptions) summandInstanceEquivalence = summandFactorEq.generalize( self.indices, domain=self.domain).checked(assumptions) eq = Equation( self.instanceSubstitution(summandInstanceEquivalence).checked( assumptions)) factorOperands = theFactor.operands if isinstance(theFactor, Mult) else theFactor xDummy, zDummy = self.safeDummyVars(2) # Now do the actual factoring by reversing distribution if pull == 'left': Pop, Pop_sub = Operation( P, self.indices), summandFactorEq.rhs.operands[-1] xSub = factorOperands zSub = [] elif pull == 'right': Pop, Pop_sub = Operation( P, self.indices), summandFactorEq.rhs.operands[0] xSub = [] zSub = factorOperands # We need to deduce that theFactor is in Complexes and that all instances of Pop_sup are in Complexes. deduceInComplexes(factorOperands, assumptions=assumptions) deduceInComplexes(Pop_sub, assumptions=assumptions | {InSet(idx, self.domain) for idx in self.indices}).generalize( self.indices, domain=self.domain).checked(assumptions) # Now we specialize distributThroughSummationRev spec1 = distributeThroughSummationRev.specialize({ Pop: Pop_sub, S: self.domain, yEtc: self.indices, xEtc: Etcetera(MultiVariable(xDummy)), zEtc: Etcetera(MultiVariable(zDummy)) }).checked() eq.update(spec1.deriveConclusion().specialize({ Etcetera(MultiVariable(xDummy)): xSub, Etcetera(MultiVariable(zDummy)): zSub })) if groupFactor and len(factorOperands) > 1: eq.update( eq.eqExpr.rhs.group(endIdx=len(factorOperands), assumptions=assumptions)) return eq.eqExpr #.checked(assumptions)
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 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_factor, pull="left", group_factor=True, group_remainder=None, assumptions=USE_DEFAULTS): ''' If group_factor is True and the_factor is a product, it will be grouped together as a sub-product. group_remainder is not relevant kept for compatibility with other factor methods. Returns the equality that equates self to this new version. Give any assumptions necessary to prove that the operands are in Complex so that the associative and commutation theorems are applicable. ''' from proveit.numbers.multiplication import distribute_through_summation from proveit.numbers import Mult if not free_vars(the_factor).isdisjoint(self.indices): raise Exception( 'Cannot factor anything involving summation indices out of a summation' ) expr = self # for convenience updating our equation eq = TransRelUpdater(expr, assumptions) assumptions = defaults.checked_assumptions(assumptions) # We may need to factor the summand within the summation summand_assumptions = assumptions + self.condition summand_factorization = self.summand.factorization( the_factor, pull, group_factor=False, group_remainder=True, assumptions=summand_assumptions) summand_instance_equivalence = summand_factor_eq.generalize( self.indices, domain=self.domain).checked(assumptions) eq = Equation( self.instance_substitution(summand_instance_equivalence).checked( assumptions)) factor_operands = the_factor.operands if isinstance( the_factor, Mult) else the_factor x_dummy, z_dummy = self.safe_dummy_vars(2) # Now do the actual factoring by reversing distribution if pull == 'left': Pop, Pop_sub = Operation( P, self.indices), summand_factor_eq.rhs.operands[-1] x_sub = factor_operands z_sub = [] elif pull == 'right': Pop, Pop_sub = Operation( P, self.indices), summand_factor_eq.rhs.operands[0] x_sub = [] z_sub = factor_operands # We need to deduce that the_factor is in Complex and that all # instances of Pop_sup are in Complex. deduce_in_complex(factor_operands, assumptions=assumptions) deduce_in_complex(Pop_sub, assumptions=assumptions | {InSet(idx, self.domain) for idx in self.indices}).generalize( self.indices, domain=self.domain).checked(assumptions) # Now we instantiate distribut_through_summation_rev spec1 = distribute_through_summation_rev.instantiate({ Pop: Pop_sub, S: self.domain, y_etc: self.indices, x_etc: Etcetera(Multi_variable(x_dummy)), z_etc: Etcetera(Multi_variable(z_dummy)) }).checked() eq.update(spec1.derive_conclusion().instantiate({ Etcetera(Multi_variable(x_dummy)): x_sub, Etcetera(Multi_variable(z_dummy)): z_sub })) if group_factor and factor_operands.num_entries() > 1: eq.update( eq.eq_expr.rhs.group(end_idx=factor_operands.num_entries(), assumptions=assumptions)) return eq.eq_expr # .checked(assumptions)
def factorization(self, the_factors, pull="left", group_factors=True, group_remainder=None, **defaults_config): ''' Return the proven factorization (equality with the factored form) from pulling the factor(s) from this summation to the "left" or "right". If group_factors is True, the factors will be grouped together as a sub-product. group_remainder is not relevant kept for compatibility with other factor methods. ''' from proveit import ExprTuple, var_range, IndexedVar from proveit.numbers.multiplication import distribute_through_summation from proveit.numbers import Mult, one if not isinstance(the_factors, Expression): # If 'the_factors' is not an Expression, assume it is # an iterable and make it a Mult. the_factors = Mult(*the_factors) if not free_vars(the_factors).isdisjoint(self.instance_params): raise ValueError( 'Cannot factor anything involving summation indices ' 'out of a summation') expr = self # for convenience updating our equation eq = TransRelUpdater(expr) # We may need to factor the summand within the summation summand_assumptions = defaults.assumptions + self.conditions.entries summand_factorization = self.summand.factorization( the_factors, pull, group_factors=group_factors, group_remainder=True, assumptions=summand_assumptions) if summand_factorization.lhs != summand_factorization.rhs: gen_summand_factorization = summand_factorization.generalize( self.instance_params, conditions=self.conditions) expr = eq.update( expr.instance_substitution(gen_summand_factorization, preserve_all=True)) if not group_factors and isinstance(the_factors, Mult): factors = the_factors.factors else: factors = ExprTuple(the_factors) if pull == 'left': _a = factors _c = ExprTuple() summand_remainder = expr.summand.factors[-1] elif pull == 'right': _a = ExprTuple() _c = factors summand_remainder = expr.summand.factors[0] else: raise ValueError("'pull' must be 'left' or 'right', not %s" % pull) _b = self.instance_params _i = _a.num_elements() _j = _b.num_elements() _k = _c.num_elements() _f = Lambda(expr.instance_params, summand_remainder) _Q = Lambda(expr.instance_params, expr.condition) _impl = distribute_through_summation.instantiate( { i: _i, j: _j, k: _k, f: _f, Q: _Q, b: _b }, preserve_all=True) quantified_eq = _impl.derive_consequent(preserve_all=True) eq.update(quantified_eq.instantiate({a: _a, c: _c}, preserve_all=True)) return eq.relation