def extractMyInitArgValue(self, argName): ''' Return the most proper initialization value for the initialization argument of the given name in order to reconstruct this Expression in its current style. ''' init_argname_mapping = self.__class__._init_argname_mapping_ argName = init_argname_mapping.get(argName, argName) if argName == 'operator': return self.operator # simply the operator elif argName == 'instanceVarOrVars': # return the joined instance variables according to style. return singleOrCompositeExpression( OperationOverInstances.explicitInstanceVars(self)) elif argName == 'instanceExpr': # return the inner instance expression after joining the # instance variables according to the style return OperationOverInstances.explicitInstanceExpr(self) elif argName == 'domain' or argName == 'domains': # return the proper single domain or list of domains if self.domain is None: return None domains = OperationOverInstances.explicitDomains(self) if domains == [self.domain] * len(domains): return self.domain if argName == 'domain' else None elif not None in domains: return ExprTuple(*domains) if argName == 'domains' else None return None elif argName == 'conditions': # return the joined conditions excluding domain conditions conditions = compositeExpression( OperationOverInstances.explicitConditions(self)) if len(conditions) == 0: conditions = tuple() # set to match the "default" return conditions
def __init__(self, operator, operand_or_operands, styles=None): ''' Create an operation with the given operator and operands. The operator must be a Label (a Variable or a Literal). If a composite expression is provided as the 'operand_or_operands' it is taken to be the 'operands' of the Operation; otherwise the 'operands' will be the the provided Expression wrapped as a single entry in an ExprTuple. When there is a single operand that is not an ExprRange, there will be an 'operand' attribute as well as 'operands' as an ExprTuple containing the one operand. ''' from proveit._core_.expression.composite import ( single_or_composite_expression, Composite, ExprTuple) from proveit._core_.expression.label.label import Label from .indexed_var import IndexedVar self.operator = operator operand_or_operands = single_or_composite_expression( operand_or_operands, do_singular_reduction=True) if isinstance(operand_or_operands, Composite): # a composite of multiple operands self.operands = operand_or_operands else: self.operands = ExprTuple(operand_or_operands) def raise_bad_operator_type(operator): raise TypeError('operator must be a Label or an indexed variable ' '(IndexedVar). %s is none of those.' % str(operator)) if (not isinstance(self.operator, Label) and not isinstance(self.operator, IndexedVar)): raise_bad_operator_type(self.operator) if (isinstance(self.operands, ExprTuple) and self.operands.is_single()): # This is a single operand. self.operand = self.operands[0] sub_exprs = (self.operator, self.operands) if isinstance(self, IndexedVar): core_type = 'IndexedVar' else: core_type = 'Operation' Expression.__init__(self, [core_type], sub_exprs, styles=styles)
def decorated_relation_prover(*args, **kwargs): from proveit._core_.expression.expr import Expression from proveit._core_.expression.composite import ExprRange, ExprTuple from proveit.relation import Relation # 'preserve' the 'self' or 'self.expr' expression so it will # be on the left side without simplification. _self = args[0] if isinstance(_self, Expression): expr = _self elif hasattr(_self, 'expr'): expr = _self.expr else: raise TypeError("@relation_prover, %s, expected to be a " "method for an Expression type or it must " "have an 'expr' attribute." % func) if 'preserve_expr' in kwargs: if 'preserved_exprs' in kwargs: kwargs['preserved_exprs'] = (kwargs['preserved_exprs'].union( [expr])) else: kwargs['preserved_exprs'] = (defaults.preserved_exprs.union( [expr])) else: kwargs['preserve_expr'] = expr # Use the regular @prover wrapper. proven_truth = decorated_prover(*args, **kwargs) # Check that the result is of the expected form. proven_expr = proven_truth.expr if not isinstance(proven_expr, Relation): raise TypeError("@relation_prover, %s, expected to prove a " "Relation expression, not %s of type %s." % (func, proven_expr, proven_expr.__class__)) expected_lhs = expr if isinstance(expr, ExprRange): expected_lhs = ExprTuple(expr) if proven_expr.lhs != expected_lhs: raise TypeError("@relation_prover, %s, expected to prove a " "relation with %s on its left side " "('lhs'). %s does not satisfy this " "requirement." % (func, expected_lhs, proven_expr)) # Make the style consistent with the original expression. if not proven_expr.lhs.has_same_style(expected_lhs): # Make the left side of the proven truth have a style # that matches the original expression. inner_lhs = proven_truth.inner_expr().lhs proven_truth = inner_lhs.with_matching_style(expected_lhs) return proven_truth
def _formatted(self, formatType, fence=False): ''' Format the OperationOverInstances according to the style which may join nested operations of the same type. ''' # override this default as desired explicitIvars = list(self.explicitInstanceVars() ) # the (joined) instance vars to show explicitly explicitConditions = ExprTuple(*self.explicitConditions( )) # the (joined) conditions to show explicitly after '|' explicitDomains = ExprTuple( *self.explicitDomains()) # the (joined) domains explicitInstanceExpr = self.explicitInstanceExpr( ) # left over after joining instnace vars according to the style hasExplicitIvars = (len(explicitIvars) > 0) hasExplicitConditions = (len(explicitConditions) > 0) hasMultiDomain = (len(explicitDomains) > 1 and explicitDomains != ExprTuple(*[self.domain] * len(explicitDomains))) outStr = '' formattedVars = ', '.join( [var.formatted(formatType, abbrev=True) for var in explicitIvars]) if formatType == 'string': if fence: outStr += '[' outStr += self.operator.formatted(formatType) + '_{' if hasExplicitIvars: if hasMultiDomain: outStr += '(' + formattedVars + ')' else: outStr += formattedVars if hasMultiDomain or self.domain is not None: outStr += ' in ' if hasMultiDomain: outStr += explicitDomains.formatted( formatType, operatorOrOperators='*', fence=False) else: outStr += self.domain.formatted(formatType, fence=False) if hasExplicitConditions: if hasExplicitIvars: outStr += " | " outStr += explicitConditions.formatted(formatType, fence=False) #outStr += ', '.join(condition.formatted(formatType) for condition in self.conditions if condition not in implicitConditions) outStr += '} ' + explicitInstanceExpr.formatted(formatType, fence=True) if fence: outStr += ']' if formatType == 'latex': if fence: outStr += r'\left[' outStr += self.operator.formatted(formatType) + '_{' if hasExplicitIvars: if hasMultiDomain: outStr += '(' + formattedVars + ')' else: outStr += formattedVars if hasMultiDomain or self.domain is not None: outStr += r' \in ' if hasMultiDomain: outStr += explicitDomains.formatted( formatType, operatorOrOperators=r'\times', fence=False) else: outStr += self.domain.formatted(formatType, fence=False) if hasExplicitConditions: if hasExplicitIvars: outStr += "~|~" outStr += explicitConditions.formatted(formatType, fence=False) #outStr += ', '.join(condition.formatted(formatType) for condition in self.conditions if condition not in implicitConditions) outStr += '}~' + explicitInstanceExpr.formatted(formatType, fence=True) if fence: outStr += r'\right]' return outStr
class Operation(Expression): # Map _operator_ Literals to corresponding Operation classes. # This is populated automatically when the _operator_ attribute # is accessed (see ExprType in proveit._core_.expression.expr). operation_class_of_operator = dict() @staticmethod def _clear_(): ''' Clear all references to Prove-It information under the Expression jurisdiction. All Expression classes that store Prove-It state information must implement _clear_ to clear that information. ''' Operation.operation_class_of_operator.clear() def __init__(self, operator, operand_or_operands, styles=None): ''' Create an operation with the given operator and operands. The operator must be a Label (a Variable or a Literal). If a composite expression is provided as the 'operand_or_operands' it is taken to be the 'operands' of the Operation; otherwise the 'operands' will be the the provided Expression wrapped as a single entry in an ExprTuple. When there is a single operand that is not an ExprRange, there will be an 'operand' attribute as well as 'operands' as an ExprTuple containing the one operand. ''' from proveit._core_.expression.composite import ( single_or_composite_expression, Composite, ExprTuple) from proveit._core_.expression.label.label import Label from .indexed_var import IndexedVar self.operator = operator operand_or_operands = single_or_composite_expression( operand_or_operands, do_singular_reduction=True) if isinstance(operand_or_operands, Composite): # a composite of multiple operands self.operands = operand_or_operands else: self.operands = ExprTuple(operand_or_operands) def raise_bad_operator_type(operator): raise TypeError('operator must be a Label or an indexed variable ' '(IndexedVar). %s is none of those.' % str(operator)) if (not isinstance(self.operator, Label) and not isinstance(self.operator, IndexedVar)): raise_bad_operator_type(self.operator) if (isinstance(self.operands, ExprTuple) and self.operands.is_single()): # This is a single operand. self.operand = self.operands[0] sub_exprs = (self.operator, self.operands) if isinstance(self, IndexedVar): core_type = 'IndexedVar' else: core_type = 'Operation' Expression.__init__(self, [core_type], sub_exprs, styles=styles) def style_options(self): ''' Return the StyleOptions object for the Operation. ''' from proveit._core_.expression.composite.expr_tuple import ExprTuple trivial_op = False if (isinstance(self.operands, ExprTuple) and len(self.operands.entries) > 0 and not self.operands.is_single()): # 'infix' is only a sensible option when there are # multiple operands as an ExprTuple. default_op_style = 'infix' else: # With no operands or 1 operand, infix is not an option. trivial_op = True default_op_style = 'function' options = StyleOptions(self) options.add_option( name='operation', description="'infix' or 'function' style formatting", default=default_op_style, related_methods=()) if not trivial_op: # Wrapping is only relevant if there is more than one # operand. options.add_option( name='wrap_positions', description=("position(s) at which wrapping is to occur; " "'2 n - 1' is after the nth operand, '2 n' is " "after the nth operation."), default='()', related_methods=('with_wrapping_at', 'with_wrap_before_operator', 'with_wrap_after_operator', 'wrap_positions')) options.add_option( name='justification', description=( "if any wrap positions are set, justify to the 'left', " "'center', or 'right'"), default='center', related_methods=('with_justification', )) return options def with_wrapping_at(self, *wrap_positions): return self.with_styles(wrap_positions='(' + ' '.join(str(pos) for pos in wrap_positions) + ')') def with_wrap_before_operator(self): if self.operands.num_entries() != 2: raise NotImplementedError( "'with_wrap_before_operator' only valid when there are 2 operands" ) return self.with_wrapping_at(1) def with_wrap_after_operator(self): if self.operands.num_entries() != 2: raise NotImplementedError( "'with_wrap_after_operator' only valid when there are 2 operands" ) return self.with_wrapping_at(2) def with_justification(self, justification): return self.with_styles(justification=justification) @classmethod def _implicitOperator(operation_class): if hasattr(operation_class, '_operator_'): return operation_class._operator_ return None @classmethod def extract_init_arg_value(cls, arg_name, operator, operands): ''' Given a name of one of the arguments of the __init__ method, return the corresponding value contained in the 'operands' composite expression (i.e., the operands of a constructed operation). Except when this is an OperationOverInstances, this method should be overridden if you cannot simply pass the operands directly into the __init__ method. ''' raise NotImplementedError( "'%s.extract_init_arg_value' must be appropriately implemented; " "__init__ arguments do not fall into a simple 'default' " "scenarios." % str(cls)) def extract_my_init_arg_value(self, arg_name): ''' Given a name of one of the arguments of the __init__ method, return the corresponding value appropriate for reconstructing self. The default calls the extract_init_arg_value static method. Overload this when the most proper way to construct the expression is style dependent. Then "remake_arguments" will use this overloaded method. "_make" will do it via the static method but will fix the styles afterwards. ''' return self.__class__.extract_init_arg_value(arg_name, self.operator, self.operands) def _extractMyInitArgs(self): ''' Call the _extract_init_args class method but will set "obj" to "self". This will cause extract_my_init_arg_value to be called instead of the extract_init_arg_value static method. ''' return self.__class__._extract_init_args(self.operator, self.operands, obj=self) @classmethod def _extract_init_args(cls, operator, operands, obj=None): ''' For a particular Operation class and given operator and operands, yield (name, value) pairs to pass into the initialization method for creating the operation consistent with the given operator and operands. First attempt to call 'extract_init_arg_value' (or 'extract_my_init_arg_value' if 'obj' is provided) for each of the __init__ arguments to determine how to generate the appropriate __init__ parameters from the given operators and operands. If that is not implemented, fall back to one of the following default scenarios. If the particular Operation class has an 'implicit operator' defined via an _operator_ attribute and the number of __init__ arguments matches the number of operands or it takes only a single *args variable-length argument list: construct the Operation by passing each operand individually. Otherwise, if there is no 'implicit operator' and __init__ accepts only two arguments (no variable-length or keyward arguments): construct the Operation by passeng the operation(s) as the first argument and operand(s) as the second argument. If the length of either of these is 1, then the single expression is passed rather than a composite. Otherwise, if there is no 'implicit operator' and __init__ accepts one formal argument and a variable-length argument and no keyword arguments, or a number of formal arguments equal to the number of operands plus 1 and no variable-length argument and no keyword arguments: construct the Operation by passing the operator(s) and each operand individually. ''' implicit_operator = cls._implicitOperator() matches_implicit_operator = (operator == implicit_operator) if implicit_operator is not None and not matches_implicit_operator: raise OperationError("An implicit operator may not be changed") args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, _ = \ inspect.getfullargspec(cls.__init__) args = args[1:] # skip over the 'self' arg if obj is None: def extract_init_arg_value_fn(arg): return cls.extract_init_arg_value(arg, operator, operands) else: extract_init_arg_value_fn = \ lambda arg: obj.extract_my_init_arg_value(arg) try: arg_vals = [extract_init_arg_value_fn(arg) for arg in args] if varargs is not None: arg_vals += extract_init_arg_value_fn(varargs) if defaults is None: defaults = [] for k, (arg, val) in enumerate(zip(args, arg_vals)): if len(defaults) - len(args) + k < 0: if not isinstance(val, Expression): raise TypeError( "extract_init_arg_val for %s should return an Expression but is returning a %s" % (arg, type(val))) yield val # no default specified; just supply the value, not the argument name else: if val == defaults[len(defaults) - len(args) + k]: continue # using the default value else: yield (arg, val) # override the default if varkw is not None: kw_arg_vals = extract_init_arg_value_fn(varkw) for arg, val in kw_arg_vals.items(): yield (arg, val) if kwonlyargs is not None: for kwonlyarg in kwonlyargs: val = extract_init_arg_value_fn(kwonlyarg) if val != kwonlydefaults[kwonlyarg]: yield (kwonlyarg, val) except NotImplementedError: # and (operation_class.extract_init_arg_value == # Operation.extract_init_arg_value): if (varkw is None): # try some default scenarios (that do not involve # keyword arguments) handle default implicit operator # case if implicit_operator and ( (len(args) == 0 and varargs is not None) or (len(args) == operands.num_entries() and varargs is None)): # yield each operand separately for operand in operands: yield operand return # handle default explicit operator case if (not implicit_operator) and (varkw is None): if varargs is None and len(args) == 2: # assume one argument for the operator and one # argument for the operands yield operator yield operands return elif ((varargs is not None and len(args) == 1) or (len(args) == operands.num_entries() + 1 and varargs is None)): # yield the operator and each operand separately yield operator for operand in operands: yield operand return raise NotImplementedError( "Must implement 'extract_init_arg_value' for the " "Operation of type %s if it does not fall into " "one of the default cases for 'extract_init_args'" % str(cls)) @classmethod def _make(operation_class, core_info, sub_expressions): ''' Make the appropriate Operation. core_info should equal ('Operation',). The first of the sub_expressions should be the operator and the second should be the operands. This implementation expects the Operation sub-class to have a class variable named '_operator_' that defines the Literal operator of the class. It will instantiate the Operation sub-class with just *operands and check that the operator is consistent. Override this method if a different behavior is desired. ''' if len(core_info) != 1 or core_info[0] != 'Operation': raise ValueError( "Expecting Operation core_info to contain exactly one item: 'Operation'" ) if len(sub_expressions) == 0: raise ValueError( 'Expecting at least one sub_expression for an Operation, for the operator' ) operator, operands = sub_expressions[0], sub_expressions[1] args = [] kw_args = dict() for arg in operation_class._extract_init_args(operator, operands): if isinstance(arg, Expression): args.append(arg) else: kw, val = arg kw_args[kw] = val return operation_class(*args, **kw_args) def remake_arguments(self): ''' Yield the argument values or (name, value) pairs that could be used to recreate the Operation. ''' for arg in self._extractMyInitArgs(): yield arg def remake_with_style_calls(self): ''' In order to reconstruct this Expression to have the same styles, what "with..." method calls are most appropriate? Return a tuple of strings with the calls to make. The default for the Operation class is to include appropriate 'with_wrapping_at' and 'with_justification' calls. ''' wrap_positions = self.wrap_positions() call_strs = [] if len(wrap_positions) > 0: call_strs.append('with_wrapping_at(' + ','.join(str(pos) for pos in wrap_positions) + ')') justification = self.get_style('justification', 'center') if justification != 'center': call_strs.append('with_justification("' + justification + '")') return call_strs def string(self, **kwargs): # When there is a single operand, we must use the "function"-style # formatting. if self.get_style('operation', 'function') == 'function': return self._function_formatted('string', **kwargs) return self._formatted('string', **kwargs) def latex(self, **kwargs): # When there is a single operand, we must use the "function"-style # formatting. if self.get_style('operation', 'function') == 'function': return self._function_formatted('latex', **kwargs) return self._formatted('latex', **kwargs) def wrap_positions(self): ''' Return a list of wrap positions according to the current style setting. ''' return [ int(pos_str) for pos_str in self.get_style( 'wrap_positions', '').strip('()').split(' ') if pos_str != '' ] def _function_formatted(self, format_type, **kwargs): from proveit._core_.expression.composite.expr_tuple import ExprTuple formatted_operator = self.operator.formatted(format_type, fence=True) if (hasattr(self, 'operand') and not isinstance(self.operand, ExprTuple)): return '%s(%s)' % (formatted_operator, self.operand.formatted(format_type, fence=False)) return '%s(%s)' % (formatted_operator, self.operands.formatted( format_type, fence=False, sub_fence=False)) def _formatted(self, format_type, **kwargs): ''' Format the operation in the form "A * B * C" where '*' is a stand-in for the operator that is obtained from self.operator.formatted(format_type). ''' if hasattr(self, 'operator'): return Operation._formattedOperation( format_type, self.operator, self.operands, wrap_positions=self.wrap_positions(), justification=self.get_style('justification', 'center'), **kwargs) else: return Operation._formattedOperation( format_type, self.operators, self.operands, wrap_positions=self.wrap_positions(), justification=self.get_style('justification', 'center'), **kwargs) @staticmethod def _formattedOperation(format_type, operator_or_operators, operands, wrap_positions, justification, implicit_first_operator=False, **kwargs): from proveit import ExprRange, ExprTuple, composite_expression if isinstance(operator_or_operators, Expression) and not isinstance( operator_or_operators, ExprTuple): operator = operator_or_operators # Single operator case. # Different formatting when there is 0 or 1 element, unless # it is an ExprRange. if operands.num_entries() < 2: if operands.num_entries() == 0 or not isinstance( operands[0], ExprRange): if format_type == 'string': return '[' + operator.string( fence=True) + '](' + operands.string( fence=False, sub_fence=False) + ')' else: return r'\left[' + operator.latex( fence=True) + r'\right]\left(' + operands.latex( fence=False, sub_fence=False) + r'\right)' raise ValueError("Unexpected format_type: " + str(format_type)) fence = kwargs.get('fence', False) sub_fence = kwargs.get('sub_fence', True) do_wrapping = len(wrap_positions) > 0 formatted_str = '' formatted_str += operands.formatted(format_type, fence=fence, sub_fence=sub_fence, operator_or_operators=operator, wrap_positions=wrap_positions, justification=justification) return formatted_str else: operators = operator_or_operators operands = composite_expression(operands) # Multiple operator case. # Different formatting when there is 0 or 1 element, unless it is # an ExprRange if operands.num_entries() < 2: if operands.num_entries() == 0 or not isinstance( operands[0], ExprRange): raise OperationError( "No defaut formatting with multiple operators and zero operands" ) fence = kwargs['fence'] if 'fence' in kwargs else False sub_fence = kwargs['sub_fence'] if 'sub_fence' in kwargs else True do_wrapping = len(wrap_positions) > 0 formatted_str = '' if fence: formatted_str = '(' if format_type == 'string' else r'\left(' if do_wrapping and format_type == 'latex': formatted_str += r'\begin{array}{%s} ' % justification[0] formatted_str += operands.formatted( format_type, fence=False, sub_fence=sub_fence, operator_or_operators=operators, implicit_first_operator=implicit_first_operator, wrap_positions=wrap_positions) if do_wrapping and format_type == 'latex': formatted_str += r' \end{array}' if fence: formatted_str += ')' if format_type == 'string' else r'\right)' return formatted_str def _replaced(self, repl_map, allow_relabeling, assumptions, requirements, equality_repl_requirements): ''' Returns this expression with sub-expressions substituted according to the replacement map (repl_map) dictionary. When an operater of an Operation is substituted by a Lambda map, the operation itself will be substituted with the Lambda map applied to the operands. For example, substituting f : (x,y) -> x+y on f(a, b) will result in a+b. When performing operation substitution with a range of parameters, the Lambda map application will require the number of these parameters to equal with the number of corresponding operand elements. For example, f : (a, b_1, ..., b_n) -> a*b_1 + ... + a*b_n n : 3 applied to f(w, x, y, z) will result in w*x + w*y + w*z provided that |(b_1, ..., b_3)| = |(x, y, z)| is proven. Assumptions may be needed to prove such requirements. Requirements will be appended to the 'requirements' list if one is provided. There are limitations with respect the Lambda map application involving iterated parameters when perfoming operation substitution in order to keep derivation rules (i.e., instantiation) simple. For details, see the ExprRange.substituted documentation. ''' from proveit import Lambda, ExprRange if len(repl_map) > 0 and (self in repl_map): # The full expression is to be substituted. return repl_map[self] # Perform substitutions for the operator(s) and operand(s). subbed_operator = \ self.operator.replaced(repl_map, allow_relabeling, assumptions, requirements, equality_repl_requirements) subbed_operands = \ self.operands.replaced(repl_map, allow_relabeling, assumptions, requirements, equality_repl_requirements) # Check if the operator is being substituted by a Lambda map in # which case we should perform full operation substitution. if isinstance(subbed_operator, Lambda): # Substitute the entire operation via a Lambda map # application. For example, f(x, y) -> x + y, # or g(a, b_1, ..., b_n) -> a * b_1 + ... + a * b_n. if isinstance(subbed_operator.body, ExprRange): raise ImproperReplacement( self, repl_map, "The function %s cannot be defined using this " "lambda, %s, that has an ExprRange for its body; " "that could lead to tuple length contradictions." % (self.operator, subbed_operator)) return Lambda._apply( subbed_operator.parameters, subbed_operator.body, *subbed_operands.entries, assumptions=assumptions, requirements=requirements, equality_repl_requirements=equality_repl_requirements) # If the operator is a literal operator of # an Operation class defined via an "_operator_" class # attribute, then create the Operation of that class. if subbed_operator in Operation.operation_class_of_operator: op_class = Operation.operation_class_of_operator[subbed_operator] if op_class != self.__class__: # Don't transfer the styles; they may not apply in # the same manner in the setting of the new # operation. subbed_sub_exprs = (subbed_operator, subbed_operands) substituted = op_class._checked_make( ['Operation'], sub_expressions=subbed_sub_exprs) return substituted._auto_reduced(assumptions, requirements, equality_repl_requirements) subbed_sub_exprs = (subbed_operator, subbed_operands) substituted = self.__class__._checked_make(self._core_info, subbed_sub_exprs) return substituted._auto_reduced(assumptions, requirements, equality_repl_requirements)
def __init__(self, operator, operand_or_operands=None, *, operands=None, styles): ''' Create an operation with the given operator and operands. The operator must be a Label (a Variable or a Literal). If a composite expression is provided as the 'operand_or_operands' it is taken to be the 'operands' of the Operation; otherwise the 'operands' will be the the provided Expression wrapped as a single entry in an ExprTuple. When there is a single operand that is not an ExprRange, there will be an 'operand' attribute as well as 'operands' as an ExprTuple containing the one operand. ''' from proveit._core_.expression.composite import ( composite_expression, single_or_composite_expression, Composite, ExprTuple, NamedExprs) from proveit._core_.expression.lambda_expr import Lambda from .indexed_var import IndexedVar if self.__class__ == Operation: raise TypeError("Do not create an object of type Operation; " "use a derived class (e.g., Function) instead.") self.operator = operator if (operand_or_operands == None) == (operands == None): if operands == None: raise ValueError( "Must supply either 'operand_or_operands' or 'operands' " "when constructing an Operation") else: raise ValueError( "Must supply 'operand_or_operands' or 'operands' but not " "both when constructing an Operation") if operands is not None: if not isinstance(operands, Expression): operands = composite_expression(operands) self.operands = operands else: orig_operand_or_operands = operand_or_operands operand_or_operands = single_or_composite_expression( operand_or_operands, do_singular_reduction=True) if isinstance(operand_or_operands, Composite): # a composite of multiple operands self.operands = operand_or_operands else: if isinstance(orig_operand_or_operands, Composite): self.operands = orig_operand_or_operands else: self.operands = ExprTuple(operand_or_operands) def raise_bad_operator_type(operator): raise TypeError("An operator may not be an explicit Lambda map " "like %s; this is necessary to avoid a Curry's " "paradox." % str(operator)) if isinstance(self.operator, Lambda): raise_bad_operator_type(self.operator) if (isinstance(self.operands, ExprTuple) and self.operands.is_single()): # This is a single operand. self.operand = self.operands[0] sub_exprs = (self.operator, self.operands) if isinstance(self, IndexedVar): core_type = 'IndexedVar' else: core_type = 'Operation' if isinstance(self.operands, NamedExprs): # Make attributes to make the keys of the NamedExprs # operands. for key in self.operands.keys(): setattr(self, key, self.operands[key]) Expression.__init__(self, [core_type], sub_exprs, styles=styles)
class Operation(Expression): # Map _operator_ Literals to corresponding Operation classes. # This is populated automatically when the _operator_ attribute # is accessed (see ExprType in proveit._core_.expression.expr). operation_class_of_operator = dict() @staticmethod def _clear_(): ''' Clear all references to Prove-It information under the Expression jurisdiction. All Expression classes that store Prove-It state information must implement _clear_ to clear that information. ''' Operation.operation_class_of_operator.clear() def __init__(self, operator, operand_or_operands=None, *, operands=None, styles): ''' Create an operation with the given operator and operands. The operator must be a Label (a Variable or a Literal). If a composite expression is provided as the 'operand_or_operands' it is taken to be the 'operands' of the Operation; otherwise the 'operands' will be the the provided Expression wrapped as a single entry in an ExprTuple. When there is a single operand that is not an ExprRange, there will be an 'operand' attribute as well as 'operands' as an ExprTuple containing the one operand. ''' from proveit._core_.expression.composite import ( composite_expression, single_or_composite_expression, Composite, ExprTuple, NamedExprs) from proveit._core_.expression.lambda_expr import Lambda from .indexed_var import IndexedVar if self.__class__ == Operation: raise TypeError("Do not create an object of type Operation; " "use a derived class (e.g., Function) instead.") self.operator = operator if (operand_or_operands == None) == (operands == None): if operands == None: raise ValueError( "Must supply either 'operand_or_operands' or 'operands' " "when constructing an Operation") else: raise ValueError( "Must supply 'operand_or_operands' or 'operands' but not " "both when constructing an Operation") if operands is not None: if not isinstance(operands, Expression): operands = composite_expression(operands) self.operands = operands else: orig_operand_or_operands = operand_or_operands operand_or_operands = single_or_composite_expression( operand_or_operands, do_singular_reduction=True) if isinstance(operand_or_operands, Composite): # a composite of multiple operands self.operands = operand_or_operands else: if isinstance(orig_operand_or_operands, Composite): self.operands = orig_operand_or_operands else: self.operands = ExprTuple(operand_or_operands) def raise_bad_operator_type(operator): raise TypeError("An operator may not be an explicit Lambda map " "like %s; this is necessary to avoid a Curry's " "paradox." % str(operator)) if isinstance(self.operator, Lambda): raise_bad_operator_type(self.operator) if (isinstance(self.operands, ExprTuple) and self.operands.is_single()): # This is a single operand. self.operand = self.operands[0] sub_exprs = (self.operator, self.operands) if isinstance(self, IndexedVar): core_type = 'IndexedVar' else: core_type = 'Operation' if isinstance(self.operands, NamedExprs): # Make attributes to make the keys of the NamedExprs # operands. for key in self.operands.keys(): setattr(self, key, self.operands[key]) Expression.__init__(self, [core_type], sub_exprs, styles=styles) def style_options(self): ''' Return the StyleOptions object for the Operation. ''' from proveit._core_.expression.composite.expr_tuple import ExprTuple trivial_op = not (isinstance(self.operands, ExprTuple) and len(self.operands.entries) > 0 and not self.operands.is_single()) options = StyleOptions(self) # Note: when there are no operands or 1 operand, 'infix' # is like the 'function' style except the operator is # wrapped in square braces. options.add_option( name='operation', description="'infix' or 'function' style formatting", default='infix', related_methods=()) if not trivial_op: # Wrapping is only relevant if there is more than one # operand. options.add_option( name='wrap_positions', description=("position(s) at which wrapping is to occur; " "'2 n - 1' is after the nth operand, '2 n' is " "after the nth operation."), default='()', related_methods=('with_wrapping_at', 'with_wrap_before_operator', 'with_wrap_after_operator', 'without_wrapping', 'wrap_positions')) options.add_option( name='justification', description=( "if any wrap positions are set, justify to the 'left', " "'center', or 'right'"), default='center', related_methods=('with_justification', )) return options def with_wrapping_at(self, *wrap_positions): return self.with_styles(wrap_positions='(' + ' '.join(str(pos) for pos in wrap_positions) + ')') def without_wrapping(self, *wrap_positions): return self.with_wrapping_at() def with_wrap_before_operator(self): if self.operands.num_entries() != 2: raise NotImplementedError( "'with_wrap_before_operator' only valid when there are 2 operands" ) return self.with_wrapping_at(1) def with_wrap_after_operator(self): if self.operands.num_entries() != 2: raise NotImplementedError( "'with_wrap_after_operator' only valid when there are 2 operands" ) return self.with_wrapping_at(2) def with_justification(self, justification): return self.with_styles(justification=justification) @classmethod def _implicit_operator(operation_class): if hasattr(operation_class, '_operator_'): return operation_class._operator_ return None @classmethod def extract_init_arg_value(cls, arg_name, operator, operands): ''' Given a name of one of the arguments of the __init__ method, return the corresponding value contained in the 'operands' composite expression (i.e., the operands of a constructed operation). Except when this is an OperationOverInstances, this method should be overridden if you cannot simply pass the operands directly into the __init__ method. ''' from proveit import NamedExprs if isinstance(operands, NamedExprs): # If the operands are NamedExprs, presume the arguments # correspond to the names of the sub-expressions. if arg_name in operands: return operands[arg_name] else: return None raise NotImplementedError( "'%s.extract_init_arg_value' must be appropriately implemented; " "__init__ arguments do not fall into a simple 'default' " "scenarios." % str(cls)) def extract_my_init_arg_value(self, arg_name): ''' Given a name of one of the arguments of the __init__ method, return the corresponding value appropriate for reconstructing self. The default calls the extract_init_arg_value static method. Overload this when the most proper way to construct the expression is style dependent. Then "remake_arguments" will use this overloaded method. "_make" will do it via the static method but will fix the styles afterwards. ''' return self.__class__.extract_init_arg_value(arg_name, self.operator, self.operands) def _extract_my_init_args(self): ''' Call the _extract_init_args class method but will set "obj" to "self". This will cause extract_my_init_arg_value to be called instead of the extract_init_arg_value static method. ''' return self.__class__._extract_init_args(self.operator, self.operands, obj=self) @classmethod def _extract_init_args(cls, operator, operands, obj=None): ''' For a particular Operation class and given operator and operands, yield (name, value) pairs to pass into the initialization method for creating the operation consistent with the given operator and operands. First attempt to call 'extract_init_arg_value' (or 'extract_my_init_arg_value' if 'obj' is provided) for each of the __init__ arguments to determine how to generate the appropriate __init__ parameters from the given operators and operands. If that is not implemented, fall back to one of the following default scenarios. If the particular Operation class has an 'implicit operator' defined via an _operator_ attribute and the number of __init__ arguments matches the number of operands or it takes only a single *args variable-length argument list: construct the Operation by passing each operand individually. Otherwise, if there is no 'implicit operator' and __init__ accepts only two arguments (no variable-length or keyward arguments): construct the Operation by passeng the operation(s) as the first argument and operand(s) as the second argument. If the length of either of these is 1, then the single expression is passed rather than a composite. Otherwise, if there is no 'implicit operator' and __init__ accepts one formal argument and a variable-length argument and no keyword arguments, or a number of formal arguments equal to the number of operands plus 1 and no variable-length argument and no keyword arguments: construct the Operation by passing the operator(s) and each operand individually. ''' from .function import Function from proveit._core_.expression.composite import (ExprTuple, Composite) implicit_operator = cls._implicit_operator() matches_implicit_operator = (operator == implicit_operator) if implicit_operator is not None and not matches_implicit_operator: raise OperationError("An implicit operator may not be changed " "(%s vs %s)" % (operator, implicit_operator)) sig = inspect.signature(cls.__init__) Parameter = inspect.Parameter init_params = sig.parameters if obj is None: def extract_init_arg_value_fn(arg): return cls.extract_init_arg_value(arg, operator, operands) else: extract_init_arg_value_fn = \ lambda arg: obj.extract_my_init_arg_value(arg) try: first_param = True for param_name, param in init_params.items(): if first_param: # Skip the 'self' parameter. first_param = False continue if param_name == 'styles': continue # skip the styles parameter param = init_params[param_name] val = extract_init_arg_value_fn(param_name) default = param.default if default is param.empty or val != default: # Override the default if there is one. if not isinstance(val, Expression): raise TypeError( "extract_init_arg_val for %s should return " "an Expression but is returning a %s" % (param_name, type(val))) if param.kind == Parameter.POSITIONAL_ONLY: yield val else: yield (param_name, val) except NotImplementedError: # Try some default scenarios that don't require # extract_init_arg_value_fn to be implemented. pos_params = [] var_params = None kwonly_params = [] varkw = None for name, param in init_params.items(): if param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD): pos_params.append(name) elif param.kind == Parameter.VAR_POSITIONAL: var_params = name elif param.kind == Parameter.KEYWORD_ONLY: kwonly_params.append(name) elif param.kind == Parameter.VAR_KEYWORD: varkw = name pos_params = pos_params[1:] # skip 'self' # Note: None keyword indicates that we should create a # generic Function for these all-in-one operands # (e.g. a variable representing an ExprTuple). if cls == Function: operands_kw = 'operands' else: operands_kw = None if (varkw is None): # handle default implicit operator case if implicit_operator and ( (len(pos_params) == 0 and var_params is not None) or (len(pos_params) == operands.num_entries() and var_params is None)): if isinstance(operands, ExprTuple): # yield each operand separately for operand in operands: yield operand return elif not isinstance(operands, Composite): # Create a generic Operation yield operator yield (operands_kw, operands) return # handle default explicit operator case elif (implicit_operator is None) and (varkw is None): if var_params is None and len(pos_params) == 2: # assume one argument for the operator and one # argument for the operands yield operator if isinstance(operands, Composite): yield operands else: yield (operands_kw, operands) return elif ((var_params is not None and len(pos_params) == 1) or (len(pos_params) == operands.num_entries() + 1 and var_params is None)): # yield the operator yield operator if isinstance(operands, ExprTuple): # yield each operand separately for operand in operands: yield operand return elif not isinstance(operands, Composite): yield (operands_kw, operands) return raise NotImplementedError( "Must implement 'extract_init_arg_value' for the " "Operation of type %s if it does not fall into " "one of the default cases for 'extract_init_args'" % str(cls)) @classmethod def _make(operation_class, core_info, sub_expressions, *, styles): ''' Make the appropriate Operation. core_info should equal ('Operation',). The first of the sub_expressions should be the operator and the second should be the operands. This implementation expects the Operation sub-class to have a class variable named '_operator_' that defines the Literal operator of the class. It will instantiate the Operation sub-class with just *operands and check that the operator is consistent. Override this method if a different behavior is desired. ''' from proveit._core_.expression.label.var import Variable from .function import Function if len(core_info) != 1 or core_info[0] != 'Operation': raise ValueError( "Expecting Operation core_info to contain exactly one item: 'Operation'" ) if len(sub_expressions) == 0: raise ValueError( 'Expecting at least one sub_expression for an Operation, for the operator' ) operator, operands = sub_expressions[0], sub_expressions[1] implicit_operator = operation_class._implicit_operator() if implicit_operator is not None and isinstance(operator, Variable): # Convert an implicit operator to a variable. return Function(operator, operands, styles=styles) if operation_class == Operation: return Function(operator, operands, styles=styles) args = [] kw_args = dict() for arg in operation_class._extract_init_args(operator, operands): if isinstance(arg, Expression): args.append(arg) else: kw, val = arg if kw == None: # kw=None used to indicate that # we should create a generic Function # and use the 'operands' keyword # (e.g. to represent an ExprTuple of # operands with a variable). operation_class = Function kw = 'operands' kw_args[kw] = val return operation_class(*args, **kw_args, styles=styles) def remake_arguments(self): ''' Yield the argument values or (name, value) pairs that could be used to recreate the Operation. ''' for arg in self._extract_my_init_args(): yield arg def remake_with_style_calls(self): ''' In order to reconstruct this Expression to have the same styles, what "with..." method calls are most appropriate? Return a tuple of strings with the calls to make. The default for the Operation class is to include appropriate 'with_wrapping_at' and 'with_justification' calls. ''' wrap_positions = self.wrap_positions() call_strs = [] if len(wrap_positions) > 0: call_strs.append('with_wrapping_at(' + ','.join(str(pos) for pos in wrap_positions) + ')') justification = self.get_style('justification', 'center') if justification != 'center': call_strs.append('with_justification("' + justification + '")') return call_strs def string(self, **kwargs): # When there is a single operand, we must use the "function"-style # formatting. from proveit._core_.expression.composite.expr_tuple import ExprTuple if (isinstance(self.operands, ExprTuple) and self.get_style('operation', 'function') == 'function'): return self._function_formatted('string', **kwargs) return self._formatted('string', **kwargs) def latex(self, **kwargs): # When there is a single operand, we must use the "function"-style # formatting. from proveit._core_.expression.composite.expr_tuple import ExprTuple if (isinstance(self.operands, ExprTuple) and self.get_style('operation', 'function') == 'function'): return self._function_formatted('latex', **kwargs) return self._formatted('latex', **kwargs) def wrap_positions(self): ''' Return a list of wrap positions according to the current style setting. ''' return [ int(pos_str) for pos_str in self.get_style( 'wrap_positions', '').strip('()').split(' ') if pos_str != '' ] def _function_formatted(self, format_type, **kwargs): from proveit._core_.expression.composite.expr_tuple import ExprTuple formatted_operator = self.operator.formatted(format_type, fence=True) lparen = r'\left(' if format_type == 'latex' else '(' rparen = r'\right)' if format_type == 'latex' else ')' if (hasattr(self, 'operand') and not isinstance(self.operand, ExprTuple)): formatted_operand = self.operand.formatted(format_type, fence=False) else: formatted_operand = self.operands.formatted(format_type, fence=False, sub_fence=False) return (formatted_operator + lparen + formatted_operand + rparen) def _formatted(self, format_type, **kwargs): ''' Format the operation in the form "A * B * C" where '*' is a stand-in for the operator that is obtained from self.operator.formatted(format_type). ''' return Operation._formatted_operation( format_type, self.operator, self.operands, implicit_first_operator=True, wrap_positions=self.wrap_positions(), justification=self.get_style('justification', 'center'), **kwargs) @staticmethod def _formatted_operation(format_type, operator_or_operators, operands, *, wrap_positions, justification, implicit_first_operator=True, **kwargs): from proveit import ExprRange, ExprTuple, composite_expression if (isinstance(operator_or_operators, Expression) and not isinstance(operator_or_operators, ExprTuple)): # Single operator case. operator = operator_or_operators if isinstance(operands, ExprTuple): # An ExprTuple of operands. # Different formatting when there is 0 or 1 element, unless # it is an ExprRange. if operands.num_entries() < 2: if operands.num_entries() == 0 or not isinstance( operands[0], ExprRange): if format_type == 'string': return ( '[' + operator.string(fence=True) + '](' + operands.string(fence=False, sub_fence=False) + ')') else: return ( r'\left[' + operator.latex(fence=True) + r'\right]\left(' + operands.latex(fence=False, sub_fence=False) + r'\right)') raise ValueError("Unexpected format_type: " + str(format_type)) fence = kwargs.get('fence', False) sub_fence = kwargs.get('sub_fence', True) do_wrapping = len(wrap_positions) > 0 formatted_str = '' formatted_str += operands.formatted( format_type, fence=fence, sub_fence=sub_fence, operator_or_operators=operator, implicit_first_operator=implicit_first_operator, wrap_positions=wrap_positions, justification=justification) return formatted_str else: # The operands ExprTuple are being represented by a Variable # (or Operation) equating to an ExprTuple. We will format this # similarly as when we have 1 element except we drop the # round parantheses. For example, [+]a, where a represents # the ExprTuple of operands for the '+' operation. if format_type == 'string': return '[' + operator.string( fence=True) + ']' + operands.string(fence=False, sub_fence=False) else: return (r'\left[' + operator.latex(fence=True) + r'\right]' + operands.latex(fence=False, sub_fence=False)) else: operators = operator_or_operators operands = composite_expression(operands) # Multiple operator case. # Different formatting when there is 0 or 1 element, unless it is # an ExprRange if operands.num_entries() < 2: if operands.num_entries() == 0 or not isinstance( operands[0], ExprRange): raise OperationError( "No defaut formatting with multiple operators and zero operands" ) fence = kwargs['fence'] if 'fence' in kwargs else False sub_fence = kwargs['sub_fence'] if 'sub_fence' in kwargs else True do_wrapping = len(wrap_positions) > 0 formatted_str = '' if fence: formatted_str = '(' if format_type == 'string' else r'\left(' if do_wrapping and format_type == 'latex': formatted_str += r'\begin{array}{%s} ' % justification[0] formatted_str += operands.formatted( format_type, fence=False, sub_fence=sub_fence, operator_or_operators=operators, implicit_first_operator=implicit_first_operator, wrap_positions=wrap_positions) if do_wrapping and format_type == 'latex': formatted_str += r' \end{array}' if fence: formatted_str += ')' if format_type == 'string' else r'\right)' return formatted_str def basic_replaced(self, repl_map, *, allow_relabeling=False, requirements=None): ''' Returns this expression with sub-expressions substituted according to the replacement map (repl_map) dictionary. When an operater of an Operation is substituted by a Lambda map, the operation itself will be substituted with the Lambda map applied to the operands. For example, substituting f : (x,y) -> x+y on f(a, b) will result in a+b. When performing operation substitution with a range of parameters, the Lambda map application will require the number of these parameters to equal with the number of corresponding operand elements. For example, f : (a, b_1, ..., b_n) -> a*b_1 + ... + a*b_n n : 3 applied to f(w, x, y, z) will result in w*x + w*y + w*z provided that |(b_1, ..., b_3)| = |(x, y, z)| is proven. Assumptions may be needed to prove such requirements. Requirements will be appended to the 'requirements' list if one is provided. There are limitations with respect the Lambda map application involving iterated parameters when perfoming operation substitution in order to keep derivation rules (i.e., instantiation) simple. For details, see the ExprRange.substituted documentation. ''' from proveit import Lambda, ExprRange if len(repl_map) > 0 and (self in repl_map): # The full expression is to be substituted. return repl_map[self] # Perform substitutions for the operator(s) and operand(s). subbed_operator = \ self.operator.basic_replaced( repl_map, allow_relabeling=allow_relabeling, requirements=requirements) subbed_operands = \ self.operands.basic_replaced( repl_map, allow_relabeling=allow_relabeling, requirements=requirements) # Check if the operator is being substituted by a Lambda map in # which case we should perform full operation substitution. if isinstance(subbed_operator, Lambda): # Substitute the entire operation via a Lambda map # application. For example, f(x, y) -> x + y, # or g(a, b_1, ..., b_n) -> a * b_1 + ... + a * b_n. if isinstance(subbed_operator.body, ExprRange): raise ImproperReplacement( self, repl_map, "The function %s cannot be defined using this " "lambda, %s, that has an ExprRange for its body; " "that could lead to tuple length contradictions." % (self.operator, subbed_operator)) return Lambda._apply(subbed_operator.parameters, subbed_operator.body, *subbed_operands.entries, requirements=requirements) # If the operator is a literal operator of # an Operation class defined via an "_operator_" class # attribute, then create the Operation of that class. if subbed_operator in Operation.operation_class_of_operator: op_class = Operation.operation_class_of_operator[subbed_operator] if op_class != self.__class__: # Don't transfer the styles; they may not apply in # the same manner in the setting of the new # operation. subbed_sub_exprs = (subbed_operator, subbed_operands) return op_class._checked_make( ['Operation'], sub_expressions=subbed_sub_exprs, style_preferences=self._style_data.styles) subbed_sub_exprs = (subbed_operator, subbed_operands) return self.__class__._checked_make( self._core_info, subbed_sub_exprs, style_preferences=self._style_data.styles) @equality_prover('evaluated', 'evaluate') def evaluation(self, **defaults_config): ''' If possible, return a Judgment of this expression equal to an irreducible value. This Operation.evaluation version simplifies the operands and then calls shallow_simplification with must_evaluat=True. ''' from proveit import UnsatisfiedPrerequisites, ProofFailure from proveit.logic import EvaluationError, SimplificationError # Try to simplify the operands first. reduction = self.simplification_of_operands( simplify_with_known_evaluations=True) # After making sure the operands have been simplified, # try 'shallow_simplification' with must_evaluate=True. try: if reduction.lhs == reduction.rhs: # _no_eval_check is a directive to the @equality_prover wrapper # to tell it not to check for an existing evaluation if we have # already checked. return self.shallow_simplification(must_evaluate=True, _no_eval_check=True) evaluation = reduction.rhs.shallow_simplification( must_evaluate=True) except (SimplificationError, UnsatisfiedPrerequisites, NotImplementedError, ProofFailure): raise EvaluationError(self) return reduction.apply_transitivity(evaluation) @equality_prover('simplified', 'simplify') def simplification(self, **defaults_config): ''' If possible, return a Judgment of this expression equal to a simplified form (according to strategies specified in proveit.defaults). This Operation.simplification version tries calling simplifies the operands and then calls 'shallow_simplification'. ''' # Try to simplify the operands first. reduction = self.simplification_of_operands() # After making sure the operands have been simplified, # try 'shallow_simplification'. # Use the 'reduction' as a replacement in case it is needed. # For example, consider # 1*b + 3*b # It's reduction is # 1*b + 3*b = b + 3*b # But in the shallow simplification, we'll do a factorization # that will exploit the "reduction" fact which wouldn't # otherwise be used because (1*b + 3*b) is a preserved # expression since simplification is an @equality_prover. if reduction.lhs == reduction.rhs: # _no_eval_check is a directive to the @equality_prover wrapper # to tell it not to check for an existing evaluation if we have # already checked. return self.shallow_simplification(_no_eval_check=True) else: simplification = reduction.rhs.shallow_simplification( replacements=[reduction]) return reduction.apply_transitivity(simplification) @equality_prover('simplified_operands', 'operands_simplify') def simplification_of_operands(self, **defaults_config): ''' Prove this Operation equal to a form in which its operands have been simplified. ''' from proveit.relation import TransRelUpdater from proveit import ExprRange, NamedExprs from proveit.logic import is_irreducible_value if any(isinstance(operand, ExprRange) for operand in self.operands): # If there is any ExprRange in the operands, simplify the # operands together as an ExprTuple. return self.inner_expr().operands[:].simplification() else: expr = self eq = TransRelUpdater(expr) with defaults.temporary() as temp_defaults: # No auto-simplification or replacements here; # just simplify operands one at a time. temp_defaults.preserve_all = True operands = self.operands if isinstance(operands, NamedExprs): # operands as NamedExprs for key in operands.keys(): operand = operands[key] if not is_irreducible_value(operand): inner_operand = getattr(expr.inner_expr(), key) expr = eq.update(inner_operand.simplification()) else: # operands as ExprTuple for k, operand in enumerate(operands): if not is_irreducible_value(operand): inner_operand = expr.inner_expr().operands[k] expr = eq.update(inner_operand.simplification()) return eq.relation @equality_prover('operator_substituted', 'operator_substitute') def operator_substitution(self, equality, **defaults_config): from proveit import f, g, n, x from proveit.core_expr_types.operations import (operator_substitution) _n = self.operands.num_elements() if equality.lhs == self.operator: return operator_substitution.instantiate({ n: _n, x: self.operands, f: equality.lhs, g: equality.rhs }) elif equality.rhs == self.operator: return operator_substitution.instantiate({ n: _n, x: self.operands, f: equality.rhs, g: equality.lhs }) else: raise ValueError("%s is not an appropriate 'equality' for " "operator_substitution on %s (the 'operator' " "is not matched on either side)" % (equality, self)) @equality_prover('operands_substituted', 'operands_substitute') def operands_substitution(self, equality, **defaults_config): from proveit import f, n, x, y from proveit.core_expr_types.operations import (operands_substitution) from proveit.logic.equality import substitution if equality.lhs == self.operands: _x, _y = equality.lhs, equality.rhs elif equality.rhs == self.operands: _x, _y = equality.rhs, equality.lhs else: raise ValueError("%s is not an appropriate 'equality' for " "operator_substitution on %s (the 'operator' " "is not matched on either side)" % (equality, self)) if self.operands.is_single(): # This is a simple single-operand substitution. return substitution.instantiate({ f: self.operator, x: _x[0], y: _y[0] }) # More general mult-operand substitution: _n = self.operands.num_elements() if equality.lhs == self.operands: return operands_substitution.instantiate({ n: _n, f: self.operator, x: equality.lhs, y: equality.rhs }) elif equality.rhs == self.operands: return operands_substitution.instantiate({ n: _n, f: self.operator, x: equality.rhs, y: equality.lhs }) else: raise ValueError("%s is not an appropriate 'equality' for " "operator_substitution on %s (the 'operator' " "is not matched on either side)" % (equality, self)) @equality_prover('sub_expr_substituted', 'sub_expr_substitute') def sub_expr_substitution(self, new_sub_exprs, **defaults_config): ''' Given new sub-expressions to replace existing sub-expressions, return the equality between this Expression and the new one with the new sub-expressions. ''' from proveit.logic import Equals from proveit.relation import TransRelUpdater assert len(new_sub_exprs) == 2, ( "Expecting 2 sub-expressions: operator and operands") eq = TransRelUpdater(self) expr = self if new_sub_exprs[0] != self.sub_expr(0): expr = eq.update( expr.operator_substitution( Equals(self.sub_expr(0), new_sub_exprs[0]))) if new_sub_exprs[1] != self.sub_expr(1): expr = eq.update( expr.operands_substitution( Equals(self.sub_expr(1), new_sub_exprs[1]))) return eq.relation def operands_are_irreducible(self): ''' Return True iff all of the operands of this Operation are irreducible. ''' from proveit.logic import is_irreducible_value return all( is_irreducible_value(operand) for operand in self.operands.entries)
def bundle(expr, bundle_thm, num_levels=2, *, assumptions=USE_DEFAULTS): ''' Given a nested OperationOverInstances, derive or equate an equivalent form in which a given number of nested levels is bundled together. Use the given theorem specific to the particular OperationOverInstances. For example, \forall_{x, y | Q(x, y)} \forall_{z | R(z)} P(x, y, z) can become \forall_{x, y, z | Q(x, y), R(z)} P(x, y, z) via bundle with num_levels=2. For example of the form of the theorem required, see proveit.logic.boolean.quantification.bundling or proveit.logic.boolean.quantification.bundling_equality. ''' from proveit.relation import TransRelUpdater from proveit.logic import Implies, Equals # Make a TransRelUpdater only if the bundle_thm yield an # equation, in which case we'll want the result to be an equation. eq = None bundled = expr while num_levels >= 2: if (not isinstance(bundled, OperationOverInstances) or not isinstance(bundled.instanceExpr, OperationOverInstances)): raise ValueError( "May only 'bundle' nested OperationOverInstances, " "not %s" % bundled) _m = bundled.instanceParams.length() _n = bundled.instanceExpr.instanceParams.length() _P = bundled.instanceExpr.instanceExpr _Q = bundled.effectiveCondition() _R = bundled.instanceExpr.effectiveCondition() m, n = bundle_thm.instanceVars P, Q, R = bundle_thm.instanceExpr.instanceVars correspondence = bundle_thm.instanceExpr.instanceExpr if isinstance(correspondence, Implies): if (not isinstance(correspondence.antecedent, OperationOverInstances) or not len(correspondence.consequent.instanceParams) == 2): raise ValueError("'bundle_thm', %s, does not have the " "expected form with the bundled form as " "the consequent of the implication, %s" % (bundle_thm, correspondence)) x_1_to_m, y_1_to_n = correspondence.consequent.instanceParams elif isinstance(correspondence, Equals): if not isinstance( correspondence.rhs, OperationOverInstances or not len(correspondence.antecedent.instanceParams) == 2): raise ValueError("'bundle_thm', %s, does not have the " "expected form with the bundled form on " "right of the an equality, %s" % (bundle_thm, correspondence)) x_1_to_m, y_1_to_n = correspondence.rhs.instanceParams all_params = bundled.instanceParams + bundled.instanceExpr.instanceParams Pxy = Function(P, all_params) Qx = Function(Q, bundled.instanceParams) Rxy = Function(R, all_params) x_1_to_m = x_1_to_m.replaced({m: _m}) y_1_to_n = y_1_to_n.replaced({n: _n}) instantiation = bundle_thm.instantiate( { m: _m, n: _n, ExprTuple(x_1_to_m): bundled.instanceParams, ExprTuple(y_1_to_n): bundled.instanceExpr.instanceParams, Pxy: _P, Qx: _Q, Rxy: _R }, assumptions=assumptions) if isinstance(instantiation.expr, Implies): bundled = instantiation.deriveConsequent() elif isinstance(instantiation.expr, Equals): if eq is None: eq = TransRelUpdater(bundled) try: bundled = eq.update(instantiation) except ValueError: raise ValueError( "Instantiation of bundle_thm %s is %s but " "should match %s on one side of the equation." % (bundle_thm, instantiation, bundled)) else: raise ValueError("Instantiation of bundle_thm %s is %s but " "should be an Implies or Equals expression." % (bundle_thm, instantiation)) num_levels -= 1 if eq is None: # Return the bundled result. return bundled else: # Return the equality between the original expression and # the bundled result. return eq.relation
def unbundle(expr, unbundle_thm, num_param_entries=(1, ), *, assumptions=USE_DEFAULTS): ''' Given a nested OperationOverInstances, derive or equate an equivalent form in which the parameter entries are split in number according to 'num_param_entries'. Use the given theorem specific to the particular OperationOverInstances. For example, \forall_{x, y, z | Q(x, y), R(z)} P(x, y, z) can become \forall_{x, y | Q(x, y)} \forall_{z | R(z)} P(x, y, z) via bundle with num_param_entries=(2, 1) or num_param_entries=(2,) -- the last number can be implied by the remaining number of parameters. For example of the form of the theorem required, see proveit.logic.boolean.quantification.unbundling or proveit.logic.boolean.quantification.bundling_equality. ''' from proveit.relation import TransRelUpdater from proveit.logic import Implies, Equals, And # Make a TransRelUpdater only if the bundle_thm yield an # equation, in which case we'll want the result to be an equation. eq = None unbundled = expr net_indicated_param_entries = sum(num_param_entries) num_actual_param_entries = len(expr.instanceParams) for n in num_param_entries: if not isinstance(n, int) or n <= 0: raise ValueError( "Each of 'num_param_entries', must be an " "integer greater than 0. %s fails this requirement." % (num_param_entries)) if net_indicated_param_entries > num_actual_param_entries: raise ValueError( "Sum of 'num_param_entries', %s=%d should not " "be greater than the number of parameter entries " "of %s for unbundling." % (num_param_entries, net_indicated_param_entries, expr)) if net_indicated_param_entries < num_actual_param_entries: diff = num_actual_param_entries - net_indicated_param_entries num_param_entries = list(num_param_entries) + [diff] else: num_param_entries = list(num_param_entries) while len(num_param_entries) > 1: n_last_entries = num_param_entries.pop(-1) first_params = ExprTuple(*unbundled.instanceParams[:-n_last_entries]) first_param_vars = {getParamVar(param) for param in first_params} remaining_params = \ ExprTuple(*unbundled.instanceParams[-n_last_entries:]) _m = first_params.length() _n = remaining_params.length() _P = unbundled.instanceExpr # Split up the conditions between the outer # OperationOverInstances and inner OperationOverInstances condition = unbundled.effectiveCondition() if isinstance(condition, And): _nQ = 0 for cond in condition.operands: cond_vars = free_vars(cond, err_inclusively=True) if first_param_vars.isdisjoint(cond_vars): break _nQ += 1 if _nQ == 0: _Q = And() elif _nQ == 1: _Q = condition.operands[0] else: _Q = And(*condition.operands[:_nQ]) _nR = len(condition.operands) - _nQ if _nR == 0: _R = And() elif _nR == 1: _R = condition.operands[-1] else: _R = And(*condition.operands[_nQ:]) elif first_param_vars.isdisjoint( free_vars(condition, err_inclusively=True)): _Q = condition _R = And() else: _Q = And() _R = condition m, n = unbundle_thm.instanceVars P, Q, R = unbundle_thm.instanceExpr.instanceVars correspondence = unbundle_thm.instanceExpr.instanceExpr if isinstance(correspondence, Implies): if (not isinstance(correspondence.antecedent, OperationOverInstances) or not len(correspondence.antecedent.instanceParams) == 2): raise ValueError("'unbundle_thm', %s, does not have the " "expected form with the bundled form as " "the antecedent of the implication, %s" % (unbundle_thm, correspondence)) x_1_to_m, y_1_to_n = correspondence.antecedent.instanceParams elif isinstance(correspondence, Equals): if not isinstance( correspondence.rhs, OperationOverInstances or not len(correspondence.antecedent.instanceParams) == 2): raise ValueError("'unbundle_thm', %s, does not have the " "expected form with the bundled form on " "right of the an equality, %s" % (unbundle_thm, correspondence)) x_1_to_m, y_1_to_n = correspondence.rhs.instanceParams else: raise ValueError("'unbundle_thm', %s, does not have the expected " "form with an equality or implication " "correspondence, %s" % (unbundle_thm, correspondence)) Qx = Function(Q, first_params) Rxy = Function(R, unbundled.instanceParams) Pxy = Function(P, unbundled.instanceParams) x_1_to_m = x_1_to_m.replaced({m: _m}) y_1_to_n = y_1_to_n.replaced({n: _n}) instantiation = unbundle_thm.instantiate( { m: _m, n: _n, ExprTuple(x_1_to_m): first_params, ExprTuple(y_1_to_n): remaining_params, Pxy: _P, Qx: _Q, Rxy: _R }, assumptions=assumptions) if isinstance(instantiation.expr, Implies): unbundled = instantiation.deriveConsequent() elif isinstance(instantiation.expr, Equals): if eq is None: eq = TransRelUpdater(unbundled) try: unbundled = eq.update(instantiation) except ValueError: raise ValueError( "Instantiation of bundle_thm %s is %s but " "should match %s on one side of the equation." % (unbundle_thm, instantiation, unbundled)) else: raise ValueError("Instantiation of bundle_thm %s is %s but " "should be an Implies or Equals expression." % (unbundle_thm, instantiation)) if eq is None: # Return the unbundled result. return unbundled else: # Return the equality between the original expression and # the unbundled result. return eq.relation
def _formatted(self, formatType, fence=False): ''' Format the OperationOverInstances according to the style which may join nested operations of the same type. ''' # override this default as desired explicitIparams = list(self.explicitInstanceParams()) explicitConditions = ExprTuple(*self.explicitConditions()) explicitDomains = ExprTuple(*self.explicitDomains()) instanceExpr = self.instanceExpr hasExplicitIparams = (len(explicitIparams) > 0) hasExplicitConditions = (len(explicitConditions) > 0) hasMultiDomain = (len(explicitDomains) > 1 and (not hasattr(self, 'domain') or explicitDomains != ExprTuple(*[self.domain] * len(explicitDomains)))) domain_conditions = ExprTuple(*self.domainConditions()) outStr = '' formattedParams = ', '.join([ param.formatted(formatType, abbrev=True) for param in explicitIparams ]) if formatType == 'string': if fence: outStr += '[' outStr += self.operator.formatted(formatType) + '_{' if hasExplicitIparams: if hasMultiDomain: outStr += domain_conditions.formatted( formatType, operatorOrOperators=',', fence=False) else: outStr += formattedParams if not hasMultiDomain and self.domain is not None: outStr += ' in ' if hasMultiDomain: outStr += explicitDomains.formatted( formatType, operatorOrOperators='*', fence=False) else: outStr += self.domain.formatted(formatType, fence=False) if hasExplicitConditions: if hasExplicitIparams: outStr += " | " outStr += explicitConditions.formatted(formatType, fence=False) #outStr += ', '.join(condition.formatted(formatType) for condition in self.conditions if condition not in implicitConditions) outStr += '} ' + instanceExpr.formatted(formatType, fence=True) if fence: outStr += ']' if formatType == 'latex': if fence: outStr += r'\left[' outStr += self.operator.formatted(formatType) + '_{' if hasExplicitIparams: if hasMultiDomain: outStr += domain_conditions.formatted( formatType, operatorOrOperators=',', fence=False) else: outStr += formattedParams if not hasMultiDomain and self.domain is not None: outStr += r' \in ' outStr += self.domain.formatted(formatType, fence=False) if hasExplicitConditions: if hasExplicitIparams: outStr += "~|~" outStr += explicitConditions.formatted(formatType, fence=False) #outStr += ', '.join(condition.formatted(formatType) for condition in self.conditions if condition not in implicitConditions) outStr += '}~' + instanceExpr.formatted(formatType, fence=True) if fence: outStr += r'\right]' return outStr
def __init__(self, operator, instanceParamOrParams, instanceExpr, *, domain=None, domains=None, condition=None, conditions=None, styles=None, _lambda_map=None): ''' Create an Operation for the given operator that is applied over instances of the given instance parameter(s), instanceParamOrParams, for the given instance Expression, instanceExpr under the given conditions. That is, the operation operates over all possibilities of given Variable(s) wherever the condition(s) is/are satisfied. Examples include forall, exists, summation, etc. instanceParamOrParams may be singular or plural (iterable). Each parameter may be a Variable or Iter over IndexedVars (just as a Lambda parameter). An OperationOverInstances is effected as an Operation over a Lambda map with a conditional body. If a 'domain' is supplied, additional conditions are generated that each instance parameter is in the domain "set": InSet(x_i, domain), where x_i is for each instance parameter. If, instead, 'domains' are supplied, then each instance parameter is supplied with its own domain (one for each instance parameter). Whether the OperationOverInstances is constructed with domain/domains explicitly, or they are provided as conditions in the proper order does not matter. Essentially, the 'domain' concept is simply a convenience for conditions of this form and may be formatted using a shorthand notation. For example, "forall_{x in S | Q(x)} P(x)" is a shorthand notation for "forall_{x | x in S, Q(x)} P(x)". _lambda_map is used internally for efficiently rebuilding an OperationOverInstances expression. ''' from proveit.logic import InSet from proveit._core_.expression.lambda_expr.lambda_expr import getParamVar if styles is None: styles = dict() if condition is not None: if conditions is not None: raise ValueError("Cannot specify both 'conditions' and " "'condition'") conditions = (condition, ) elif conditions is None: conditions = tuple() if _lambda_map is not None: # Use the provided 'lambda_map' instead of creating one. from proveit.logic import And lambda_map = _lambda_map instance_params = lambda_map.parameters if isinstance(lambda_map.body, Conditional): # Has conditions. instanceExpr = lambda_map.body.value if (isinstance(lambda_map.body.condition, And) and not lambda_map.body.condition.operands.singular()): conditions = compositeExpression( lambda_map.body.condition.operands) else: conditions = compositeExpression(lambda_map.body.condition) else: # No conditions. instanceExpr = lambda_map.body conditions = ExprTuple() else: # We will need to generate the Lambda sub-expression. # Do some initial preparations w.r.t. instanceParams, domain(s), and # conditions. instance_params = compositeExpression(instanceParamOrParams) if len(instance_params) == 0: raise ValueError( "Expecting at least one instance parameter when " "constructing an OperationOverInstances") # Add appropriate conditions for the domains: if domain is not None: # prepend domain conditions if domains is not None: raise ValueError( "Provide a single domain or multiple domains, " "not both") if not isinstance(domain, Expression): raise TypeError( "The domain should be an 'Expression' type") domains = [domain] * len(instance_params) if domains is not None: # Prepend domain conditions. Note that although we start with # all domain conditions at the beginning, # some may later get pushed back as "inner conditions" # (see below), if len(domains) != len(instance_params): raise ValueError( "When specifying multiple domains, the number " "should be the same as the number of instance " "variables.") for domain in domains: if domain is None: raise ValueError( "When specifying multiple domains, none " "of them can be the None value") domain_conditions = [] for iparam, domain in zip(instance_params, domains): if isinstance(iparam, ExprRange): condition = ExprRange(iparam.parameter, InSet(iparam.body, domain), iparam.start_index, iparam.end_index) else: condition = InSet(iparam, domain) domain_conditions.append(condition) conditions = domain_conditions + list(conditions) domain = domains[ 0] # domain of the outermost instance variable conditions = compositeExpression(conditions) # domain(s) may be implied via the conditions. If domain(s) were # supplied, this should simply reproduce them from the conditions that # were prepended. domain = domains = None # These may be reset below if there are ... if (len(conditions) >= len(instance_params)): domains = [ _extract_domain_from_condition(ivar, cond) for ivar, cond in zip(instance_params, conditions) ] if all(domain is not None for domain in domains): # Used if we have a single instance variable domain = domains[0] else: domains = None if _lambda_map is None: # Now do the actual lambda_map creation if len(instance_params) == 1: instanceParamOrParams = instance_params[0] # Generate the Lambda sub-expression. lambda_map = OperationOverInstances._createOperand( instanceParamOrParams, instanceExpr, conditions) self.instanceExpr = instanceExpr '''Expression corresponding to each 'instance' in the OperationOverInstances''' self.instanceParams = instance_params if len(instance_params) > 1: '''Instance parameters of the OperationOverInstance.''' self.instanceVars = [ getParamVar(parameter) for parameter in instance_params ] self.instanceParamOrParams = self.instanceParams self.instanceVarOrVars = self.instanceVars '''Instance parameter variables of the OperationOverInstance.''' if domains is not None: self.domains = domains # Domain for each instance variable '''Domains of the instance parameters (may be None)''' else: self.domain = None else: self.instanceParam = instance_params[0] '''Outermost instance parameter of the OperationOverInstance.''' self.instanceVar = getParamVar(self.instanceParam) self.instanceParamOrParams = self.instanceParam self.instanceVarOrVars = self.instanceVar '''Outermost instance parameter variable of the OperationOverInstance.''' self.domain = domain '''Domain of the outermost instance parameter (may be None)''' self.conditions = conditions '''Conditions applicable to the outermost instance variable (or iteration of indexed variables) of the OperationOverInstance. May include an implicit 'domain' condition.''' if isinstance(lambda_map.body, Conditional): self.condition = lambda_map.body.condition Operation.__init__(self, operator, lambda_map, styles=styles)
def _replaced(self, repl_map, allow_relabeling, assumptions, requirements, equality_repl_requirements): ''' Returns this expression with sub-expressions substituted according to the replacement map (repl_map) dictionary. When an operater of an Operation is substituted by a Lambda map, the operation itself will be substituted with the Lambda map applied to the operands. For example, substituting f : (x,y) -> x+y on f(a, b) will result in a+b. When performing operation substitution with a range of parameters, the Lambda map application will require the number of these parameters to equal with the number of corresponding operand elements. For example, f : (a, b_1, ..., b_n) -> a*b_1 + ... + a*b_n n : 3 applied to f(w, x, y, z) will result in w*x + w*y + w*z provided that |(b_1, ..., b_3)| = |(x, y, z)| is proven. Assumptions may be needed to prove such requirements. Requirements will be appended to the 'requirements' list if one is provided. There are limitations with respect the Lambda map application involving iterated parameters when perfoming operation substitution in order to keep derivation rules (i.e., instantiation) simple. For details, see the ExprRange.substituted documentation. ''' from proveit import (Lambda, singleOrCompositeExpression, compositeExpression, ExprTuple, ExprRange) if len(repl_map) > 0 and (self in repl_map): # The full expression is to be substituted. return repl_map[self] # Perform substitutions for the operator(s) and operand(s). subbed_operator_or_operators = \ self.operator_or_operators.replaced(repl_map, allow_relabeling, assumptions, requirements, equality_repl_requirements) subbed_operand_or_operands = \ self.operand_or_operands.replaced(repl_map, allow_relabeling, assumptions, requirements, equality_repl_requirements) subbed_operators = compositeExpression(subbed_operator_or_operators) # Check if the operator is being substituted by a Lambda map in # which case we should perform full operation substitution. if len(subbed_operators) == 1: subbed_operator = subbed_operators[0] if isinstance(subbed_operator, Lambda): # Substitute the entire operation via a Lambda map # application. For example, f(x, y) -> x + y, # or g(a, b_1, ..., b_n) -> a * b_1 + ... + a * b_n. if isinstance(subbed_operator.body, ExprRange): raise ImproperReplacement( self, repl_map, "The function %s cannot be defined using this " "lambda, %s, that has an ExprRange for its body; " "that could lead to tuple length contradictions." % (self.operator, subbed_operator)) if len(self.operands)==1 and \ not isinstance(self.operands[0], ExprRange): # A single operand case (even if that operand # happens to be a tuple). subbed_operands = [subbed_operand_or_operands] else: subbed_operands = subbed_operand_or_operands return Lambda._apply( subbed_operator.parameters, subbed_operator.body, *subbed_operands, assumptions=assumptions, requirements=requirements, equality_repl_requirements=equality_repl_requirements) had_singular_operand = hasattr(self, 'operand') if (had_singular_operand and isinstance(subbed_operand_or_operands, ExprTuple) and not isinstance(self.operand_or_operands, ExprTuple)): # If a singular operand is replaced with an ExprTuple, # we must wrap an extra ExprTuple around it to indicate # that it is still a singular operand with the operand # as the ExprTuple (rather than expanding to multiple # operands). subbed_operand_or_operands = ExprTuple(subbed_operand_or_operands) else: # Possibly collapse multiple operands to a single operand # via "do_singular_reduction=True". subbed_operand_or_operands = singleOrCompositeExpression( subbed_operand_or_operands, do_singular_reduction=True) # Remake the Expression with substituted operator and/or # operands if len(subbed_operators) == 1: # If it is a single operator that is a literal operator of # an Operation class defined via an "_operator_" class # attribute, then create the Operation of that class. operator = subbed_operators[0] if operator in Operation.operationClassOfOperator: op_class = Operation.operationClassOfOperator[operator] if op_class != self.__class__: # Don't transfer the styles; they may not apply in # the same manner in the setting of the new # operation. subbed_sub_exprs = (operator, subbed_operand_or_operands) substituted = op_class._checked_make( ['Operation'], styles=None, subExpressions=subbed_sub_exprs) return substituted._auto_reduced( assumptions, requirements, equality_repl_requirements) subbed_sub_exprs = (subbed_operator_or_operators, subbed_operand_or_operands) substituted = self.__class__._checked_make(self._coreInfo, self.getStyles(), subbed_sub_exprs) return substituted._auto_reduced(assumptions, requirements, equality_repl_requirements)
class Iter(Expression): ''' An Iter Expression represents an iteration of Expressions to be inserted into a containing composite Expression. Upon substitution, it automatically expands to if it contains an Indexed expression whose variable is substituted with a tuple or array. To ensure such an expansion is unambiguous and sensibly defined, Iter expressions must only contain Indexed variables with indices that are 'simple' functions of an iteration (the parameter itself or a sum of terms with one term as the parameter). For example a_1*b_1 + ... + a_n*b_n when substituted with a:(x_1, ..., x_j, y_1, ..., y_k) b:(z_1, ..., z_k, x_1, ..., x_j) under the assumption that j<k will be transformed into x_1*z_1 + ... + x_j*z_j + y_1*z_{j+1} + ... + y_{k-j}*z_{j+(k-j)} + y_{k-j+1}*x_1 + ... + y_{k-j+j}*x_j ''' def __init__(self, parameter_or_parameters, body, start_index_or_indices, end_index_or_indices, styles=None, requirements=tuple(), _lambda_map=None): ''' Create an Iter that represents an iteration of the body for the parameter(s) ranging from the start index/indices to the end index/indices. A Lambda expression will be created as its sub-expression that maps the parameter(s) to the body with conditions that restrict the parameter(s) to the appropriate interval. _lambda_map is used internally for efficiently rebuilding an Iter. ''' from proveit.logic import InSet from proveit.number import Interval if _lambda_map is not None: # Use the provided 'lambda_map' instead of creating one. lambda_map = _lambda_map pos_args = (parameter_or_parameters, body, start_index_or_indices, end_index_or_indices) if pos_args != (None, None, None, None): raise ValueError( "Positional arguments of the Init constructor " "should be None if lambda_map is provided.") parameters = lambda_map.parameters body = lambda_map.body conditions = lambda_map.conditions if len(conditions) != len(parameters): raise ValueError( "Inconsistent number of conditions and lambda " "map parameters") start_indices, end_indices = [], [] for param, condition in zip(parameters, conditions): invalid_condition_msg = ( "Not the right kind of lambda_map condition " "for an iteration") if not isinstance(condition, InSet) or condition.element != param: raise ValueError(invalid_condition_msg) domain = condition.domain if not isinstance(domain, Interval): raise ValueError(invalid_condition_msg) start_index, end_index = domain.lowerBound, domain.upperBound start_indices.append(start_index) end_indices.append(end_index) self.start_indices = ExprTuple(*start_indices) self.end_indices = ExprTuple(*end_indices) if len(parameters) == 1: self.start_index = self.start_indices[0] self.end_index = self.end_indices[0] self.start_index_or_indices = self.start_index self.end_index_or_indices = self.end_index else: self.start_index_or_indices = self.start_indices self.end_index_or_indices = self.end_indices else: parameters = compositeExpression(parameter_or_parameters) start_index_or_indices = singleOrCompositeExpression( start_index_or_indices) if isinstance(start_index_or_indices, ExprTuple) and len(start_index_or_indices) == 1: start_index_or_indices = start_index_or_indices[0] self.start_index_or_indices = start_index_or_indices if isinstance(start_index_or_indices, Composite): # a composite of multiple indices self.start_indices = self.start_index_or_indices else: # a single index self.start_index = self.start_index_or_indices # wrap a single index in a composite for convenience self.start_indices = compositeExpression( self.start_index_or_indices) end_index_or_indices = singleOrCompositeExpression( end_index_or_indices) if isinstance(end_index_or_indices, ExprTuple) and len(end_index_or_indices) == 1: end_index_or_indices = end_index_or_indices[0] self.end_index_or_indices = end_index_or_indices if isinstance(self.end_index_or_indices, Composite): # a composite of multiple indices self.end_indices = self.end_index_or_indices else: # a single index self.end_index = self.end_index_or_indices # wrap a single index in a composite for convenience self.end_indices = compositeExpression( self.end_index_or_indices) conditions = [] for param, start_index, end_index in zip(parameters, self.start_indices, self.end_indices): conditions.append( InSet(param, Interval(start_index, end_index))) lambda_map = Lambda(parameters, body, conditions=conditions) self.ndims = len(self.start_indices) if self.ndims != len(self.end_indices): raise ValueError( "Inconsistent number of 'start' and 'end' indices") if len(parameters) != len(self.start_indices): raise ValueError( "Inconsistent number of indices and lambda map parameters") Expression.__init__(self, ['Iter'], [lambda_map], styles=styles, requirements=requirements) self.lambda_map = lambda_map self._checkIndexedRestriction(body) """ def __init__(self, lambda_map, start_index_or_indices, end_index_or_indices, styles=None, requirements=tuple()): if not isinstance(lambda_map, Lambda): raise TypeError('When creating an Iter Expression, the lambda_map argument must be a Lambda expression') if isinstance(start_index_or_indices, ExprTuple) and len(start_index_or_indices)==1: start_index_or_indices = start_index_or_indices[0] self.start_index_or_indices = singleOrCompositeExpression(start_index_or_indices) self.ndims = len(self.start_indices) if self.ndims != len(self.end_indices): raise ValueError("Inconsistent number of 'start' and 'end' indices") if len(lambda_map.parameters) != len(self.start_indices): raise ValueError("Inconsistent number of indices and lambda map parameters") Expression.__init__(self, ['Iter'], [lambda_map, self.start_index_or_indices, self.end_index_or_indices], styles=styles, requirements=requirements) self.lambda_map = lambda_map """ def _checkIndexedRestriction(self, subExpr): ''' An iteration is restricted to not contain any Indexed variable that is a complicated function of the iteration parameter. Specifically, for each parameter, the index of an Indexed variable may be a function solely of that parameter or that parameter added with terms that don't contain the parameter. For example, you can have x_1, ..., x_n and you can have x_{1+k}, ..., x_{n+k} or x_{k+1}, ..., x_{k+n} but not x_{1^2}, ..., x_{n^2} or x_{2*1}, ..., x_{2*n}. ''' from .indexed import Indexed from proveit.number import Add if isinstance(subExpr, Indexed): for index in subExpr.indices: for parameter in self.lambda_map.parameters: if index == parameter: # It is fine for the index to simply be the # parameter. That is a simple case. continue if isinstance(index, Add): terms_to_check = list(index.operands) if parameter in terms_to_check: # Remove first occurrence of the parameter. terms_to_check.remove(parameter) for term in terms_to_check: if parameter in term.freeVars(): # Invalid because the parameter occurs # either in multiple terms of the index # or in a non-trivial form (not simply # as itself). raise InvalidIterationError(index, parameter) # Valid: the parameter occurs solely as a term # of the index. continue # Move on to the next check. if parameter in index.freeVars(): # The parameter occurs in the index in a form # that is not valid: raise InvalidIterationError(index, parameter) # Recursively check sub expressions of the sub expression. for sub_sub_expr in subExpr.subExprIter(): self._checkIndexedRestriction(sub_sub_expr) @classmethod def _make(subClass, coreInfo, styles, subExpressions): if subClass != Iter: MakeNotImplemented(subClass) if len(coreInfo) != 1 or coreInfo[0] != 'Iter': raise ValueError( "Expecting Iter coreInfo to contain exactly one item: 'Iter'") lambda_map = subExpressions[0] return Iter(None, None, None, None, _lambda_map=lambda_map).withStyles(**styles) def remakeArguments(self): ''' Yield the argument values or (name, value) pairs that could be used to recreate the Indexed. ''' yield self.lambda_map.parameter_or_parameters yield self.lambda_map.body yield self.start_index_or_indices yield self.end_index_or_indices def first(self): ''' Return the first instance of the iteration (and store for future use). ''' if not hasattr(self, '_first'): expr_map = { parameter: index for parameter, index in zip(self.lambda_map.parameters, self.start_indices) } self._first = self.lambda_map.body.substituted(expr_map) return self._first def last(self): ''' Return the last instance of the iteration (and store for futurle use). ''' if not hasattr(self, '_last'): expr_map = { parameter: index for parameter, index in zip(self.lambda_map.parameters, self.end_indices) } self._last = self.lambda_map.body.substituted(expr_map) return self._last def string(self, **kwargs): return self.formatted('string', **kwargs) def latex(self, **kwargs): return self.formatted('latex', **kwargs) def formatted(self, formatType, fence=False, subFence=True, operator=None, **kwargs): outStr = '' if operator is None: formatted_operator = ',' # comma is the default formatted operator elif isinstance(operator, str): formatted_operator = operator else: formatted_operator = operator.formatted(formatType) formatted_sub_expressions = [ subExpr.formatted(formatType, fence=subFence) for subExpr in (self.first(), self.last()) ] formatted_sub_expressions.insert( 1, '\ldots' if formatType == 'latex' else '...') # put the formatted operator between each of formattedSubExpressions if fence: outStr += '(' if formatType == 'string' else r'\left(' outStr += formatted_operator.join(formatted_sub_expressions) if fence: outStr += ')' if formatType == 'string' else r'\right)' return outStr def getInstance(self, index_or_indices, assumptions=USE_DEFAULTS, requirements=None): ''' Return the iteration instance with the given indices as an Expression, using the given assumptions as needed to interpret the indices expression. Required truths, proven under the given assumptions, that were used to make this interpretation will be appended to the given 'requirements' (if provided). ''' from proveit.number import LessEq if self.ndims == 1: indices = [index_or_indices] else: indices = index_or_indices if requirements is None: # requirements won't be passed back in this case requirements = [] # first make sure that the indices are in the iteration range for index, start, end in zip(indices, self.start_indices, self.end_indices): for first, second in ((start, index), (index, end)): relation = None try: relation = LessEq.sort([first, second], reorder=False, assumptions=assumptions) except: raise IterationError( "Indices not provably within the iteration range: %s <= %s" % (first, second)) requirements.append(relation) # map to the desired instance return self.lambda_map.mapped(*indices) def substituted(self, exprMap, relabelMap=None, reservedVars=None, assumptions=USE_DEFAULTS, requirements=None): ''' Returns this expression with the substitutions made according to exprMap and/or relabeled according to relabelMap. Attempt to automatically expand the iteration if any Indexed sub-expressions substitute their variable for a composite (list or tensor). Indexed should index variables that represent composites, but substituting the composite is a signal that an outer iteration should be expanded. An exception is raised if this fails. ''' from .composite import _generateCoordOrderAssumptions from proveit import ProofFailure, ExprArray from proveit.logic import Equals, InSet from proveit.number import Less, LessEq, dist_add, \ zero, one, dist_subtract, Naturals, Integers from .composite import _simplifiedCoord from proveit._core_.expression.expr import _NoExpandedIteration from proveit._core_.expression.label.var import safeDummyVars self._checkRelabelMap(relabelMap) if relabelMap is None: relabelMap = dict() assumptions = defaults.checkedAssumptions(assumptions) new_requirements = [] iter_params = self.lambda_map.parameters iter_body = self.lambda_map.body ndims = self.ndims subbed_start = self.start_indices.substituted(exprMap, relabelMap, reservedVars, assumptions, new_requirements) subbed_end = self.end_indices.substituted(exprMap, relabelMap, reservedVars, assumptions, new_requirements) #print("iteration substituted", self, subbed_start, subbed_end) # Need to handle the change in scope within the lambda # expression. We won't use 'new_params'. They aren't relavent # after an expansion, this won't be used. new_params, inner_expr_map, inner_assumptions, inner_reservations \ = self.lambda_map._innerScopeSub(exprMap, relabelMap, reservedVars, assumptions, new_requirements) # Get sorted substitution parameter start and end # values demarcating how the entry array must be split up for # each axis. all_entry_starts = [None] * ndims all_entry_ends = [None] * ndims do_expansion = False for axis in range(ndims): try: empty_eq = Equals(dist_add(subbed_end[axis], one), subbed_start[axis]) try: # Check if this is an empty iteration which # happens when end+1=start. empty_eq.prove(assumptions, automation=False) all_entry_starts[axis] = all_entry_ends[axis] = [] do_expansion = True continue except ProofFailure: pass param_vals = \ iter_body._iterSubParamVals(axis, iter_params[axis], subbed_start[axis], subbed_end[axis], inner_expr_map, relabelMap, inner_reservations, inner_assumptions, new_requirements) assert param_vals[0] == subbed_start[axis] if param_vals[-1] != subbed_end[axis]: # The last of the param_vals should either be # subbed_end[axis] or known to be # subbed_end[axis]+1. Let's double-check. eq = Equals(dist_add(subbed_end[axis], one), param_vals[-1]) eq.prove(assumptions, automation=False) # Populate the entry starts and ends using the # param_vals which indicate that start of each contained # entry plus the end of this iteration. all_entry_starts[axis] = [] all_entry_ends[axis] = [] for left, right in zip(param_vals[:-1], param_vals[1:]): all_entry_starts[axis].append(left) try: eq = Equals(dist_add(left, one), right) eq.prove(assumptions, automation=False) new_requirements.append( eq.prove(assumptions, automation=False)) # Simple single-entry case: the start and end # are the same. entry_end = left except: # Not the simple case; perform the positive # integrality check. requirement = InSet(dist_subtract(right, left), Naturals) # Knowing the simplification may help prove the # requirement. _simplifiedCoord(requirement, assumptions, []) try: new_requirements.append( requirement.prove(assumptions)) except ProofFailure as e: raise IterationError("Failed to prove requirement " "%s:\n%s" % (requirement, e)) if right == subbed_end[axis]: # This last entry is the inclusive end # rather than past the end, so it is an # exception. entry_end = right else: # Subtract one from the start of the next # entyr to get the end of this entry. entry_end = dist_subtract(right, one) entry_end = _simplifiedCoord( entry_end, assumptions, requirements) all_entry_ends[axis].append(entry_end) # See if we should add the end value as an extra # singular entry. If param_vals[-1] is at the inclusive # end, then we have a singular final entry. if param_vals[-1] == subbed_end[axis]: end_val = subbed_end[axis] all_entry_starts[axis].append(end_val) all_entry_ends[axis].append(end_val) else: # Otherwise, the last param_val will be one after # the inclusive end which we will want to use below # when building the last iteration entry. all_entry_starts[axis].append(param_vals[-1]) do_expansion = True except EmptyIterException: # Indexing over a negative or empty range. The only way this # should be allowed is if subbed_end+1=subbed_start. Equals(dist_add(subbed_end[axis], one), subbed_start[axis]).prove(assumptions) all_entry_starts[axis] = all_entry_ends[axis] = [] do_expansion = True except _NoExpandedIteration: pass if do_expansion: # There are Indexed sub-Expressions whose variable is # being replaced with a Composite, so let us # expand the iteration for all of the relevant # iteration ranges. # Sort the argument value ranges. # We must have "substition parameter values" along each # axis: if None in all_entry_starts or None in all_entry_ends: raise IterationError("Must expand all axes or none of the " "axes, when substituting %s" % str(self)) # Generate the expanded tuple/array as the substition # of 'self'. shape = [len(all_entry_ends[axis]) for axis in range(ndims)] entries = ExprArray.make_empty_entries(shape) indices_by_axis = [range(extent) for extent in shape] #print('shape', shape, 'indices_by_axis', indices_by_axis, 'sub_param_vals', sub_param_vals) extended_inner_assumptions = list(inner_assumptions) for axis_starts in all_entry_starts: # Generate assumptions that order the # successive entry start parameter values # must be natural numbers. (This is a requirement for # iteration instances and is a simple fact of # succession for single entries.) extended_inner_assumptions.extend( _generateCoordOrderAssumptions(axis_starts)) # Maintain lists of parameter values that come before each given entry. #prev_param_vals = [[] for axis in range(ndims)] # Iterate over each of the new entries, obtaining indices # into sub_param_vals for the start parameters of the entry. for entry_indices in itertools.product(*indices_by_axis): entry_starts = [axis_starts[i] for axis_starts, i in \ zip(all_entry_starts, entry_indices)] entry_ends = [axis_ends[i] for axis_ends, i in \ zip(all_entry_ends, entry_indices)] is_singular_entry = True for entry_start, entry_end in zip(entry_starts, entry_ends): # Note that empty ranges will be skipped because # equivalent parameter values should be skipped in # the param_vals above. if entry_start != entry_end: # Not a singular entry along this axis, so # it is not a singular entry. We must do an # iteration for this entry. is_singular_entry = False if is_singular_entry: # Single element entry. # Generate the entry by making appropriate # parameter substitutions for the iteration body. entry_inner_expr_map = dict(inner_expr_map) entry_inner_expr_map.update({ param: arg for param, arg in zip(iter_params, entry_starts) }) for param in iter_params: relabelMap.pop(param, None) entry = iter_body.substituted(entry_inner_expr_map, relabelMap, inner_reservations, extended_inner_assumptions, new_requirements) else: # Iteration entry. # Shift the iteration parameter so that the # iteration will have the same start-indices # for this sub-range (like shifting a viewing # window, moving the origin to the start of the # sub-range). # Generate "safe" new parameters (the Variables are # not used for anything that might conflict). # Avoid using free variables from these expressions: unsafe_var_exprs = [self] unsafe_var_exprs.extend(exprMap.values()) unsafe_var_exprs.extend(relabelMap.values()) unsafe_var_exprs.extend(entry_starts) unsafe_var_exprs.extend(entry_ends) new_params = safeDummyVars(ndims, *unsafe_var_exprs) # Make assumptions that places the parameter(s) in the # appropriate range and at an integral coordinate position. # Note, it is possible that this actually represents an # empty range and that these assumptions are contradictory; # but this still suits our purposes regardless. # Also, we will choose to shift the parameter so it # starts at the start index of the iteration. range_expr_map = dict(inner_expr_map) range_assumptions = [] shifted_entry_ends = [] for axis, (param, new_param, entry_start, entry_end) \ in enumerate(zip(iter_params, new_params, entry_starts, entry_ends)): start_idx = self.start_indices[axis] shift = dist_subtract(entry_start, start_idx) shift = _simplifiedCoord(shift, assumptions, new_requirements) if shift != zero: shifted_param = dist_add(new_param, shift) else: shifted_param = new_param range_expr_map[param] = shifted_param shifted_end = dist_subtract(entry_end, shift) shifted_end = _simplifiedCoord(shifted_end, assumptions, new_requirements) shifted_entry_ends.append(shifted_end) assumption = InSet(new_param, Integers) range_assumptions.append(assumption) assumption = LessEq(entry_start, shifted_param) range_assumptions.append(assumption) # Assume differences with each of the previous # range starts are natural numbers as should be # the case given requirements that have been # met. next_index = entry_indices[axis] + 1 prev_starts = all_entry_starts[axis][:next_index] for prev_start in prev_starts: assumption = InSet( dist_subtract(shifted_param, prev_start), Naturals) range_assumptions.append(assumption) next_start = all_entry_starts[axis][next_index] assumption = Less(shifted_param, next_start) range_assumptions.append(assumption) # Perform the substitution. # The fact that our "new parameters" are "safe" # alleviates the need to reserve anything extra. range_lambda_body = iter_body.substituted( range_expr_map, relabelMap, reservedVars, extended_inner_assumptions + range_assumptions, new_requirements) # Any requirements that involve the new parameters # are a direct consequence of the iteration range # and are not external requirements: new_requirements = \ [requirement for requirement in new_requirements if requirement.freeVars().isdisjoint(new_params)] entry = Iter(new_params, range_lambda_body, self.start_indices, shifted_entry_ends) # Set this entry in the entries array. ExprArray.set_entry(entries, entry_indices, entry) ''' # Iteration entry. # Shift the iteration parameter so that the # iteration will have the same start-indices # for this sub-range (like shifting a viewing # window, moving the origin to the start of the # sub-range). # Generate "safe" new parameters (the Variables are # not used for anything that might conflict). # Avoid using free variables from these expressions: unsafe_var_exprs = [self] unsafe_var_exprs.extend(exprMap.values()) unsafe_var_exprs.extend(relabelMap.values()) unsafe_var_exprs.extend(entry_start_vals) unsafe_var_exprs.extend(entry_end_vals) new_params = safeDummyVars(len(iter_params), *unsafe_var_exprs) # Make the appropriate substitution mapping # and add appropriate assumptions for the iteration # parameter(s). range_expr_map = dict(inner_expr_map) range_assumptions = [] for start_idx, param, new_param, range_start, range_end \ in zip(subbed_start, iter_params, new_params, entry_start_vals, entry_end_vals): shifted_param = Add(new_param, subtract(range_start, start_idx)) shifted_param = _simplifiedCoord(shifted_param, assumptions, requirements) range_expr_map[param] = shifted_param # Include assumptions that the parameters are # in the proper range. assumption = LessEq(start_idx, new_param) range_assumptions.append(assumption) assumption = InSet(subtract(new_param, start_idx), Naturals) #assumption = LessEq(new_param, # subtract(range_end, start_idx)) assumption = LessEq(new_param, range_end) range_assumptions.append(assumption) # Perform the substitution. # The fact that our "new parameters" are "safe" # alleviates the need to reserve anything extra. range_lambda_body = iter_body.substituted(range_expr_map, relabelMap, reservedVars, inner_assumptions+range_assumptions, new_requirements) # Any requirements that involve the new parameters # are a direct consequence of the iteration range # and are not external requirements: new_requirements = \ [requirement for requirement in new_requirements if requirement.freeVars().isdisjoint(new_params)] range_lambda_map = Lambda(new_params, range_lambda_body) # Obtain the appropriate end indices. end_indices = \ [_simplifiedCoord(subtract(range_end, start_idx), assumptions, new_requirements) for start_idx, range_end in zip(subbed_start, entry_end_vals)] entry = Iter(range_lambda_map, subbed_start, end_indices) # Set this entry in the entries array. ExprArray.set_entry(entries, entry_start_indices, entry) ''' subbed_self = compositeExpression(entries) else: # No Indexed sub-Expressions whose variable is # replaced with a Composite, so let us not expand the # iteration. Just do an ordinary substitution. new_requirements = [] # Fresh new requirements. subbed_map = self.lambda_map.substituted(exprMap, relabelMap, reservedVars, assumptions, new_requirements) subbed_self = Iter(subbed_map.parameters, subbed_map.body, subbed_start, subbed_end) for requirement in new_requirements: # Make sure requirements don't use reserved variable in a # nested scope. requirement._restrictionChecked(reservedVars) if requirements is not None: requirements += new_requirements # append new requirements return subbed_self
def __init__(self, parameter_or_parameters, body, start_index_or_indices, end_index_or_indices, styles=None, requirements=tuple(), _lambda_map=None): ''' Create an Iter that represents an iteration of the body for the parameter(s) ranging from the start index/indices to the end index/indices. A Lambda expression will be created as its sub-expression that maps the parameter(s) to the body with conditions that restrict the parameter(s) to the appropriate interval. _lambda_map is used internally for efficiently rebuilding an Iter. ''' from proveit.logic import InSet from proveit.number import Interval if _lambda_map is not None: # Use the provided 'lambda_map' instead of creating one. lambda_map = _lambda_map pos_args = (parameter_or_parameters, body, start_index_or_indices, end_index_or_indices) if pos_args != (None, None, None, None): raise ValueError( "Positional arguments of the Init constructor " "should be None if lambda_map is provided.") parameters = lambda_map.parameters body = lambda_map.body conditions = lambda_map.conditions if len(conditions) != len(parameters): raise ValueError( "Inconsistent number of conditions and lambda " "map parameters") start_indices, end_indices = [], [] for param, condition in zip(parameters, conditions): invalid_condition_msg = ( "Not the right kind of lambda_map condition " "for an iteration") if not isinstance(condition, InSet) or condition.element != param: raise ValueError(invalid_condition_msg) domain = condition.domain if not isinstance(domain, Interval): raise ValueError(invalid_condition_msg) start_index, end_index = domain.lowerBound, domain.upperBound start_indices.append(start_index) end_indices.append(end_index) self.start_indices = ExprTuple(*start_indices) self.end_indices = ExprTuple(*end_indices) if len(parameters) == 1: self.start_index = self.start_indices[0] self.end_index = self.end_indices[0] self.start_index_or_indices = self.start_index self.end_index_or_indices = self.end_index else: self.start_index_or_indices = self.start_indices self.end_index_or_indices = self.end_indices else: parameters = compositeExpression(parameter_or_parameters) start_index_or_indices = singleOrCompositeExpression( start_index_or_indices) if isinstance(start_index_or_indices, ExprTuple) and len(start_index_or_indices) == 1: start_index_or_indices = start_index_or_indices[0] self.start_index_or_indices = start_index_or_indices if isinstance(start_index_or_indices, Composite): # a composite of multiple indices self.start_indices = self.start_index_or_indices else: # a single index self.start_index = self.start_index_or_indices # wrap a single index in a composite for convenience self.start_indices = compositeExpression( self.start_index_or_indices) end_index_or_indices = singleOrCompositeExpression( end_index_or_indices) if isinstance(end_index_or_indices, ExprTuple) and len(end_index_or_indices) == 1: end_index_or_indices = end_index_or_indices[0] self.end_index_or_indices = end_index_or_indices if isinstance(self.end_index_or_indices, Composite): # a composite of multiple indices self.end_indices = self.end_index_or_indices else: # a single index self.end_index = self.end_index_or_indices # wrap a single index in a composite for convenience self.end_indices = compositeExpression( self.end_index_or_indices) conditions = [] for param, start_index, end_index in zip(parameters, self.start_indices, self.end_indices): conditions.append( InSet(param, Interval(start_index, end_index))) lambda_map = Lambda(parameters, body, conditions=conditions) self.ndims = len(self.start_indices) if self.ndims != len(self.end_indices): raise ValueError( "Inconsistent number of 'start' and 'end' indices") if len(parameters) != len(self.start_indices): raise ValueError( "Inconsistent number of indices and lambda map parameters") Expression.__init__(self, ['Iter'], [lambda_map], styles=styles, requirements=requirements) self.lambda_map = lambda_map self._checkIndexedRestriction(body)