def __call__(self, x, y, id=None, best=0, k=False): self._file = open(self._filename,'a') super(LoggingMonitor,self).__call__(x, y, id, k=k) if self._yinterval is not numpy.inf and \ int((self._step-1) % self._yinterval) == 0: if not list_or_tuple_or_ndarray(y): y = "%f" % self._ik(self._y[-1], k) elif self._all: y = "%s" % self._ik(self._y[-1], k) else: y = "%f" % self._ik(self._y[-1][best], k) if not list_or_tuple_or_ndarray(x): x = "[%f]" % self._x[-1] elif self._all: xa = self._x[-1] if not list_or_tuple_or_ndarray(xa): x = "[%f]" % xa else: x = "%s" % xa else: xb = self._x[-1][best] if not list_or_tuple_or_ndarray(xb): x = "[%f]" % xb else: x = "%s" % xb step = [self._step-1] if id is not None: step.append(id) self._file.write(" %s %s %s\n" % (tuple(step), y, x)) self._file.close() return
def __call__(self, x, y, id=None, best=0, k=False): super(VerboseLoggingMonitor,self).__call__(x, y, id, best, k=k) if self._vyinterval is not numpy.inf and \ int((self._step-1) % self._vyinterval) == 0: if not list_or_tuple_or_ndarray(y): who = '' y = " %f" % self._ik(self._y[-1], k) elif self._all: who = '' y = " %s" % self._ik(self._y[-1], k) else: who = ' best' y = " %f" % self._ik(self._y[-1][best], k) msg = "Generation %d has%s Chi-Squared:%s" % (self._step-1,who,y) if id is not None: msg = "[id: %d] " % (id) + msg print msg if self._vxinterval is not numpy.inf and \ int((self._step-1) % self._vxinterval) == 0: if not list_or_tuple_or_ndarray(x): who = '' x = " %f" % self._x[-1] elif self._all: who = '' x = "\n %s" % self._x[-1] else: who = ' best' x = "\n %s" % self._x[-1][best] msg = "Generation %d has%s fit parameters:%s" % (self._step-1,who,x) if id is not None: msg = "[id: %d] " % (id) + msg print msg return
def __call__(self, x, y, id=None, best=0, k=False): super(VerboseLoggingMonitor,self).__call__(x, y, id, best, k=k) if self._vyinterval is not numpy.inf and \ int((self._step-1) % self._vyinterval) == 0: if not list_or_tuple_or_ndarray(y): who = '' y = " %f" % self._ik(self._y[-1], k) elif self._all: who = '' y = " %s" % self._ik(self._y[-1], k) else: who = ' best' y = " %f" % self._ik(self._y[-1][best], k) msg = "Generation %d has%s Chi-Squared:%s" % (self._step-1,who,y) if id is not None: msg = "[id: %d] " % (id) + msg print(msg) if self._vxinterval is not numpy.inf and \ int((self._step-1) % self._vxinterval) == 0: if not list_or_tuple_or_ndarray(x): who = '' x = " %f" % self._x[-1] elif self._all: who = '' x = "\n %s" % self._x[-1] else: who = ' best' x = "\n %s" % self._x[-1][best] msg = "Generation %d has%s fit parameters:%s" % (self._step-1,who,x) if id is not None: msg = "[id: %d] " % (id) + msg print(msg) return
def generate_penalty(conditions, ptype=None, **kwds): """Converts a penalty constraint function to a mystic.penalty function. Inputs: conditions -- a penalty constraint function, or list of constraint functions ptype -- a mystic.penalty type, or a list of mystic.penalty types of the same length as the given conditions For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> ineqf,eqf = generate_conditions(constraints, nvars=3) >>> penalty = generate_penalty((ineqf,eqf)) >>> penalty([1.,2.,0.]) 25.0 >>> penalty([1.,2.,0.5]) 0.0 Additional Inputs: k -- penalty multiplier h -- iterative multiplier """ # allow for single condition, list of conditions, or nested list if not list_or_tuple_or_ndarray(conditions): conditions = list((conditions, )) else: pass #XXX: should be fine... conditions = list(flatten(conditions)) # allow for single ptype, list of ptypes, or nested list if ptype is None: ptype = [] from mystic.penalty import quadratic_equality, quadratic_inequality for condition in conditions: if 'inequality' in condition.__name__: ptype.append(quadratic_inequality) else: ptype.append(quadratic_equality) elif not list_or_tuple_or_ndarray(ptype): ptype = list((ptype, )) * len(conditions) else: pass #XXX: is already a list, should be the same len as conditions ptype = list(flatten(ptype)) # iterate through penalties, building a compound penalty function pf = lambda x: 0.0 pfdoc = "" for penalty, condition in zip(ptype, conditions): pfdoc += "%s: %s\n" % (penalty.__name__, condition.__doc__) apply = penalty(condition, **kwds) pf = apply(pf) pf.__doc__ = pfdoc.rstrip('\n') pf.__name__ = 'penalty' return pf
def generate_penalty(conditions, ptype=None, **kwds): """Converts a penalty constraint function to a mystic.penalty function. Inputs: conditions -- a penalty constraint function, or list of constraint functions ptype -- a mystic.penalty type, or a list of mystic.penalty types of the same length as the given conditions For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> ineqf,eqf = generate_conditions(constraints, nvars=3) >>> penalty = generate_penalty((ineqf,eqf)) >>> penalty([1.,2.,0.]) 25.0 >>> penalty([1.,2.,0.5]) 0.0 Additional Inputs: k -- penalty multiplier h -- iterative multiplier """ # allow for single condition, list of conditions, or nested list if not list_or_tuple_or_ndarray(conditions): conditions = list((conditions,)) else: pass #XXX: should be fine... conditions = list(flatten(conditions)) # allow for single ptype, list of ptypes, or nested list if ptype is None: ptype = [] from mystic.penalty import quadratic_equality, quadratic_inequality for condition in conditions: if 'inequality' in condition.__name__: ptype.append(quadratic_inequality) else: ptype.append(quadratic_equality) elif not list_or_tuple_or_ndarray(ptype): ptype = list((ptype,))*len(conditions) else: pass #XXX: is already a list, should be the same len as conditions ptype = list(flatten(ptype)) # iterate through penalties, building a compound penalty function pf = lambda x:0.0 pfdoc = "" for penalty, condition in zip(ptype, conditions): pfdoc += "%s: %s\n" % (penalty.__name__, condition.__doc__) apply = penalty(condition, **kwds) pf = apply(pf) pf.__doc__ = pfdoc.rstrip('\n') pf.__name__ = 'penalty' return pf
def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring,'_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i],variables[indices[i]]) return mystring
def get_variables(constraints, variables='x'): """extract a list of the string variable names from constraints string Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x1 + x2 = x3*4 ... x3 = x2*x4''' >>> get_variables(constraints) ['x1', 'x2', 'x3', 'x4'] Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. For example: >>> constraints = ''' ... y = min(u,v) - z*sin(x) ... z = x**2 + 1.0 ... u = v*z''' >>> get_variables(constraints, list('pqrstuvwxyz')) ['u', 'v', 'x', 'y', 'z'] """ if list_or_tuple_or_ndarray(variables): equations = replace_variables(constraints, variables, '_') vars = get_variables(equations, '_') indices = [int(v.strip('_')) for v in vars] varnamelist = [] from numpy import sort for i in sort(indices): varnamelist.append(variables[i]) return varnamelist import re target = variables + '[0-9]+' varnamelist = [] equation_list = constraints.splitlines() for equation in equation_list: vars = re.findall(target, equation) for var in vars: if var not in varnamelist: varnamelist.append(var) return varnamelist
def get_variables(constraints, variables='x'): """extract a list of the string variable names from constraints string Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x1 + x2 = x3*4 ... x3 = x2*x4''' >>> get_variables(constraints) ['x1', 'x2', 'x3', 'x4'] Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. For example: >>> constraints = ''' ... y = min(u,v) - z*sin(x) ... z = x**2 + 1.0 ... u = v*z''' >>> get_variables(constraints, list('pqrstuvwxyz')) ['u', 'v', 'x', 'y', 'z'] """ if list_or_tuple_or_ndarray(variables): equations = replace_variables(constraints,variables,'_') vars = get_variables(equations,'_') indices = [int(v.strip('_')) for v in vars] varnamelist = [] from numpy import sort for i in sort(indices): varnamelist.append(variables[i]) return varnamelist import re target = variables+'[0-9]+' varnamelist = [] equation_list = constraints.splitlines() for equation in equation_list: vars = re.findall(target,equation) for var in vars: if var not in varnamelist: varnamelist.append(var) return varnamelist
def replace_variables(constraints, variables=None, markers='$'): """Replace variables in constraints string with a marker. Returns a modified constraints string. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). variables -- list of variable name strings. The variable names will be replaced in the order that they are provided, where if the default marker "$i" is used, the first variable will be replaced with "$0", the second with "$1", and so on. For example: >>> variables = ['spam', 'eggs'] >>> constraints = '''spam + eggs - 42''' >>> print replace_variables(constraints, variables, 'x') 'x0 + x1 - 42' Additional Inputs: markers -- desired variable name. Default is '$'. A list of variable name strings is also accepted for when desired variable names don't have the same base. For example: >>> variables = ['x1','x2','x3'] >>> constraints = "min(x1*x2) - sin(x3)" >>> print replace_variables(constraints, variables, ['x','y','z']) 'min(x*y) - sin(z)' """ if variables is None: variables = [] elif isinstance(variables, str): variables = list((variables, )) # substitite one list of strings for another if list_or_tuple_or_ndarray(markers): equations = replace_variables(constraints, variables, '_') vars = get_variables(equations, '_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): equations = equations.replace(vars[i], markers[indices[i]]) return equations # Sort by decreasing length of variable name, so that if one variable name # is a substring of another, that won't be a problem. variablescopy = variables[:] def comparator(x, y): return len(y) - len(x) variablescopy.sort(comparator) # Figure out which index goes with which variable. indices = [] for item in variablescopy: indices.append(variables.index(item)) # Default is markers='$', as '$' is not a special symbol in Python, # and it is unlikely a user will choose it for a variable name. if markers in variables: marker = '_$$$$$$$$$$' # even less likely... else: marker = markers '''Bug demonstrated here: >>> equation = """x3 = max(y,x) + x""" >>> vars = ['x','y','z','x3'] >>> print replace_variables(equation,vars) $4 = ma$1($2,$1) + $1 ''' #FIXME: don't parse if __name__ in __builtins__, globals, or locals? for i in indices: #FIXME: or better, use 're' pattern matching constraints = constraints.replace(variables[i], marker + str(i)) return constraints.replace(marker, markers)
def generate_penalty(conditions, ptype=None, join=None, **kwds): """Converts a penalty constraint function to a mystic.penalty function. Inputs: conditions -- a penalty constraint function, or list of constraint functions ptype -- a mystic.penalty type, or a list of mystic.penalty types of the same length as the given conditions join -- either (and_, or_) from mystic.coupler, or None. The default is to iteratively apply the penalties. For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> ineqf,eqf = generate_conditions(constraints, nvars=3) >>> penalty = generate_penalty((ineqf,eqf)) >>> penalty([1.,2.,0.]) 25.0 >>> penalty([1.,2.,0.5]) 0.0 Additional Inputs: k -- penalty multiplier h -- iterative multiplier """ # allow for single condition, list of conditions, or nested list if not list_or_tuple_or_ndarray(conditions): conditions = list((conditions,)) else: pass #XXX: should be fine... # discover the nested structure of conditions and ptype nc = nt = 0 if ptype is None or not list_or_tuple_or_ndarray(ptype): nt = -1 else: while tuple(flatten(conditions, nc)) != tuple(flatten(conditions)): nc += 1 while tuple(flatten(ptype, nt)) != tuple(flatten(ptype)): nt += 1 if join is None: pass # don't use 'and/or' to join the conditions #elif nc >= 2: # join when is tuple of tuples of conditions else: # always use join, if given (instead of only if nc >= 2) if nt >= nc: # there as many or more nested ptypes than conditions p = iter(ptype) return join(*(generate_penalty(c, next(p), **kwds) for c in conditions)) return join(*(generate_penalty(c, ptype, **kwds) for c in conditions)) # flatten everything and produce the penalty conditions = list(flatten(conditions)) # allow for single ptype, list of ptypes, or nested list if ptype is None: ptype = [] from mystic.penalty import quadratic_equality, quadratic_inequality for condition in conditions: if 'inequality' in condition.__name__: ptype.append(quadratic_inequality) else: ptype.append(quadratic_equality) elif not list_or_tuple_or_ndarray(ptype): ptype = list((ptype,))*len(conditions) else: pass #XXX: is already a list, should be the same len as conditions ptype = list(flatten(ptype)) # iterate through penalties, building a compound penalty function pf = lambda x:0.0 pfdoc = "" for penalty, condition in zip(ptype, conditions): pfdoc += "%s: %s\n" % (penalty.__name__, condition.__doc__) apply = penalty(condition, **kwds) pf = apply(pf) pf.__doc__ = pfdoc.rstrip('\n') pf.__name__ = 'penalty' return pf
def _solve_nonlinear(constraints, variables='x', target=None, **kwds): """Build a constraints function given a string of nonlinear constraints. Returns a constraints function. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = '''x1 = x3*3. + x0*x2''' >>> print(_solve_nonlinear(constraints)) x0 = (x1 - 3.0*x3)/x2 >>> constraints = ''' ... spread([x0,x1]) - 1.0 = mean([x0,x1]) ... mean([x0,x1,x2]) = x2''' >>> print(_solve_nonlinear(constraints)) x0 = -0.5 + 0.5*x2 x1 = 0.5 + 1.5*x2 Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. target -- list providing the order for which the variables will be solved. If there are "N" constraint equations, the first "N" variables given will be selected as the dependent variables. By default, increasing order is used. For example: >>> constraints = ''' ... spread([x0,x1]) - 1.0 = mean([x0,x1]) ... mean([x0,x1,x2]) = x2''' >>> print(_solve_nonlinear(constraints, target=['x1'])) x1 = -0.833333333333333 + 0.166666666666667*x2 x0 = -0.5 + 0.5*x2 Further Inputs: locals -- a dictionary of additional variables used in the symbolic constraints equations, and their desired values. """ nvars = None permute = False # if True, return all permutations warn = True # if True, don't suppress warnings verbose = False # if True, print details from _classify_variables #-----------------------undocumented------------------------------- permute = kwds['permute'] if 'permute' in kwds else permute warn = kwds['warn'] if 'warn' in kwds else warn verbose = kwds['verbose'] if 'verbose' in kwds else verbose #------------------------------------------------------------------ if target in [None, False]: target = [] elif isinstance(target, str): target = target.split(',') else: target = list(target) # not the best for ndarray, but should work from mystic.symbolic import replace_variables, get_variables if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables, '_') varname = '_' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # create function to replace "_" with original variables def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring,'_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i],variables[indices[i]]) return mystring locals = kwds['locals'] if 'locals' in kwds else None if locals is None: locals = {} eqns = constraints.splitlines() # Remove empty strings: actual_eqns = [] for j in range(len(eqns)): if eqns[j].strip(): actual_eqns.append(eqns[j].strip()) orig_eqns = actual_eqns[:] neqns = len(actual_eqns) xperms = [varname+str(i) for i in range(ndim)] if target: [target.remove(i) for i in target if i not in xperms] [target.append(i) for i in xperms if i not in target] _target = [] [_target.append(i) for i in target if i not in _target] target = _target target = tuple(target) xperms = list(permutations(xperms)) #XXX: takes a while if nvars is ~10 if target: # Try the suggested order first. xperms.remove(target) xperms.insert(0, target) complete_list = [] constraints_function_list = [] # Some of the permutations will give the same answer; # look into reducing the number of repeats? for perm in xperms: # Sort the list actual_eqns so any equation containing x0 is first, etc. sorted_eqns = [] actual_eqns_copy = orig_eqns[:] usedvars = [] for variable in perm: # range(ndim): for eqn in actual_eqns_copy: if eqn.find(variable) != -1: sorted_eqns.append(eqn) actual_eqns_copy.remove(eqn) usedvars.append(variable) break if actual_eqns_copy: # Append the remaining equations for item in actual_eqns_copy: sorted_eqns.append(item) actual_eqns = sorted_eqns # Append the remaining variables to usedvars tempusedvar = usedvars[:] tempusedvar.sort() nmissing = ndim - len(tempusedvar) for m in range(nmissing): usedvars.append(varname + str(len(tempusedvar) + m)) #FIXME: not sure if the code below should be totally trusted... for i in range(neqns): # Trying to use xi as a pivot. Loop through the equations # looking for one containing xi. _target = usedvars[i%len(usedvars)] #XXX: ...to make it len of neqns for eqn in actual_eqns[i:]: invertedstring = _solve_single(eqn, variables=varname, target=_target, warn=warn) if invertedstring: warn = False break if invertedstring is None: continue #XXX: ...when _solve_single fails # substitute into the remaining equations. the equations' order # in the list newsystem is like in a linear coefficient matrix. newsystem = ['']*neqns j = actual_eqns.index(eqn) newsystem[j] = invertedstring #XXX: ...was eqn. I think correct now othereqns = actual_eqns[:j] + actual_eqns[j+1:] for othereqn in othereqns: expression = invertedstring.split("=")[1] fixed = othereqn.replace(_target, '(' + expression + ')') k = actual_eqns.index(othereqn) newsystem[k] = fixed actual_eqns = newsystem #XXX: potentially carrying too many eqns # Invert so that it can be fed properly to generate_constraint simplified = [] for eqn in actual_eqns[:len(usedvars)]: #XXX: ...needs to be same len _target = usedvars[actual_eqns.index(eqn)] mysoln = _solve_single(eqn, variables=varname, target=_target, warn=warn) if mysoln: simplified.append(mysoln) simplified = restore(variables, '\n'.join(simplified).rstrip()) if permute: complete_list.append(simplified) continue if verbose: print(_classify_variables(simplified, variables, ndim)) return simplified warning='Warning: an error occurred in building the constraints.' if warn: print(warning) if verbose: print(_classify_variables(simplified, variables, ndim)) if permute: #FIXME: target='x3,x1' may order correct, while 'x1,x3' doesn't filter = []; results = [] for i in complete_list: _eqs = '\n'.join(sorted(i.split('\n'))) if _eqs and (_eqs not in filter): filter.append(_eqs) results.append(i) return tuple(results) #FIXME: somehow 'rhs = xi' can be in results return simplified
def penalty_parser(constraints, variables='x', nvars=None): """parse symbolic constraints into penalty constraints. Returns a tuple of inequality constraints and a tuple of equality constraints. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> penalty_parser(constraints, nvars=3) (('-(x[0] - (0.))',), ('x[2] - (x[0]/2.)',)) Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x2' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ #from mystic.tools import src #ndim = len(get_variables(src(func), variables)) if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # Parse the constraints string lines = constraints.splitlines() eqconstraints = [] ineqconstraints = [] for line in lines: if line.strip(): fixed = line # Iterate in reverse in case ndim > 9. indices = list(range(ndim)) indices.reverse() for i in indices: fixed = fixed.replace(varname + str(i), 'x[' + str(i) + ']') constraint = fixed.strip() # Replace 'spread', 'mean', and 'variance' (uses numpy, not mystic) if constraint.find('spread(') != -1: constraint = constraint.replace('spread(', 'ptp(') if constraint.find('mean(') != -1: constraint = constraint.replace('mean(', 'average(') if constraint.find('variance(') != -1: constraint = constraint.replace('variance(', 'var(') # Sorting into equality and inequality constraints, and making all # inequality constraints in the form expression <= 0. and all # equality constraints of the form expression = 0. split = constraint.split('>') direction = '>' if len(split) == 1: split = constraint.split('<') direction = '<' if len(split) == 1: split = constraint.split('=') direction = '=' if len(split) == 1: print "Invalid constraint: ", constraint eqn = {'lhs':split[0].rstrip('=').strip(), \ 'rhs':split[-1].lstrip('=').strip()} expression = '%(lhs)s - (%(rhs)s)' % eqn if direction == '=': eqconstraints.append(expression) elif direction == '<': ineqconstraints.append(expression) else: ineqconstraints.append('-(' + expression + ')') return tuple(ineqconstraints), tuple(eqconstraints)
def _prepare_sympy(constraints, variables='x', nvars=None): """Parse an equation string and prepare input for sympy. Returns a tuple of sympy-specific input: (code for variable declaration, left side of equation string, right side of equation string, list of variables, and the number of sympy equations). Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x0 = x4**2 ... x4 - x3 = 0. ... x4 - x0 = x2''' >>> code, lhs, rhs, vars, neqn = _prepare_sympy(constraints, nvars=5) >>> print(code) x0=Symbol('x0') x1=Symbol('x1') x2=Symbol('x2') x3=Symbol('x3') x4=Symbol('x4') rand = Symbol('rand') >>> print("%s %s" % (lhs, rhs)) ['x0 ', 'x4 - x3 ', 'x4 - x0 '] [' x4**2', ' 0.', ' x2'] >>> print("%s in %s eqns" % (vars, neqn)) x0,x1,x2,x3,x4, in 3 eqns Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x1' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ if ">" in constraints or "<" in constraints: raise NotImplementedError("cannot simplify inequalities") from mystic.symbolic import replace_variables, get_variables #XXX: if constraints contain x0,x1,x3 for 'x', should x2 be in code,xlist? if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables, markers='_') varname = '_' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # split constraints_str into lists of left hand sides and right hand sides eacheqn = constraints.splitlines() neqns = 0 left = [] right = [] for eq in eacheqn: #XXX: Le/Ge instead of Eq; Max/Min... (NotImplemented ?) splitlist = eq.replace('==','=').split('=') #FIXME: no inequalities if len(splitlist) == 2: #FIXME: first convert >/< to min/max ? # If equation is blank on one side, raise error. if len(splitlist[0].strip()) == 0 or len(splitlist[1].strip()) == 0: print("%r is not an equation!" % eq) # Raise exception? else: left.append(splitlist[0]) right.append(splitlist[1]) neqns += 1 # If equation doesn't have one equal sign, raise error. if len(splitlist) != 2 and len(splitlist) != 1: print("%r is not an equation!" % eq) # Raise exception? # First create list of x variables xlist = "" for i in range(ndim): xn = varname + str(i) xlist += xn + "," # Start constructing the code string code = "" for i in range(ndim): xn = varname + str(i) code += xn + '=' + "Symbol('" + xn + "')\n" code += "rand = Symbol('rand')\n" return code, left, right, xlist, neqns
def _classify_variables(constraints, variables='x', nvars=None): """Takes a string of constraint equations and determines which variables are dependent, independent, and unconstrained. Assumes there are no duplicate equations. Returns a dictionary with keys: 'dependent', 'independent', and 'unconstrained', and with values that enumerate the variables that match each variable type. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x0 = x4**2 ... x2 = x3 + x4''' >>> _classify_variables(constraints, nvars=5) {'dependent':['x0','x2'], 'independent':['x3','x4'], 'unconstrained':['x1']} >>> constraints = ''' ... x0 = x4**2 ... x4 - x3 = 0. ... x4 - x0 = x2''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2','x4'], 'independent': ['x3'], 'unconstrained': ['x1']} Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x1' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ if ">" in constraints or "<" in constraints: raise NotImplementedError("cannot classify inequalities") from mystic.symbolic import replace_variables, get_variables #XXX: use solve? or first if not in form xi = ... ? if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars eqns = constraints.splitlines() indices = list(range(ndim)) dep = [] indep = [] for eqn in eqns: # find which variables are used if eqn: for var in range(ndim): if indices.count(var) != 0: if eqn.find(varname + str(var)) != -1: indep.append(var) indices.remove(var) indep.sort() _dep = [] for eqn in eqns: # find which variables are on the LHS if eqn: split = eqn.split('=') for var in indep: if split[0].find(varname + str(var)) != -1: _dep.append(var) indep.remove(var) break _dep.sort() indep = _dep + indep # prefer variables found on LHS for eqn in eqns: # find one dependent variable per equation _dep = [] _indep = indep[:] if eqn: for var in _indep: if eqn.find(varname + str(var)) != -1: _dep.append(var) _indep.remove(var) if _dep: dep.append(_dep[0]) indep.remove(_dep[0]) #FIXME: 'equivalent' equations not ignored (e.g. x2=x2; or x2=1, 2*x2=2) """These are good: >>> constraints = ''' ... x0 = x4**2 ... x2 - x4 - x3 = 0.''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2'], 'independent': ['x3','x4'], 'unconstrained': ['x1']} >>> constraints = ''' ... x0 + x2 = 0. ... x0 + 2*x2 = 0.''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2'], 'independent': [], 'unconstrained': ['x1','x3','x4']} This is a bug: >>> constraints = ''' ... x0 + x2 = 0. ... 2*x0 + 2*x2 = 0.''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2'], 'independent': [], 'unconstrained': ['x1','x3','x4']} """ #XXX: should simplify first? dep.sort() indep.sort() # return the actual variable names (not the indices) if varname == variables: # then was single variable variables = [varname+str(i) for i in range(ndim)] dep = [variables[i] for i in dep] indep = [variables[i] for i in indep] indices = [variables[i] for i in indices] d = {'dependent':dep, 'independent':indep, 'unconstrained':indices} return d
def generate_constraint(conditions, ctype=None, **kwds): """Converts a constraint solver to a mystic.constraints function. Inputs: conditions -- a constraint solver, or list of constraint solvers ctype -- a mystic.constraints type, or a list of mystic.constraints types of the same length as the given conditions NOTES: This simple constraint generator doesn't check for conflicts in conditions, but simply applies conditions in the given order. This constraint generator assumes that a single variable has been isolated on the left-hand side of each constraints equation, thus all constraints are of the form "x_i = f(x)". This solver picks speed over robustness, and thus relies on the user to formulate the constraints so that they do not conflict. For example: >>> constraints = ''' ... x0 = cos(x1) + 2. ... x1 = x2*2.''' >>> solv = generate_solvers(constraints) >>> constraint = generate_constraint(solv) >>> constraint([1.0, 0.0, 1.0]) [1.5838531634528576, 2.0, 1.0] Standard python math conventions are used. For example, if an 'int' is used in a constraint equation, one or more variable may be evaluate to an 'int' -- this can affect solved values for the variables. For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> solv = generate_solvers(constraints, nvars=3) >>> print solv[0].__doc__ 'x[2] = x[0]/2.' >>> print solv[1].__doc__ 'x[0] = max(0., x[0])' >>> constraint = generate_constraint(solv) >>> constraint([1,2,3]) [1, 2, 0.5] >>> constraint([-1,2,-3]) [0.0, 2, 0.0] """ # allow for single condition, list of conditions, or nested list if not list_or_tuple_or_ndarray(conditions): conditions = list((conditions,)) else: pass #XXX: should be fine... conditions = list(flatten(conditions)) # allow for single ctype, list of ctypes, or nested list if ctype is None: from mystic.coupler import inner #XXX: outer ? ctype = list((inner,))*len(conditions) elif not list_or_tuple_or_ndarray(ctype): ctype = list((ctype,))*len(conditions) else: pass #XXX: is already a list, should be the same len as conditions ctype = list(flatten(ctype)) # iterate through solvers, building a compound constraints solver cf = lambda x:x cfdoc = "" for wrapper, condition in zip(ctype, conditions): cfdoc += "%s: %s\n" % (wrapper.__name__, condition.__doc__) apply = wrapper(condition, **kwds) cf = apply(cf) cf.__doc__ = cfdoc.rstrip('\n') cf.__name__ = 'constraint' return cf
def _solve_single(constraint, variables='x', target=None, **kwds): """Solve a symbolic constraints equation for a single variable. Inputs: constraint -- a string of symbolic constraints. Only a single constraint equation should be provided, and must be an equality constraint. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> equation = "x1 - 3. = x0*x2" >>> print _solve_single(equation) x0 = -(3.0 - x1)/x2 Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. target -- list providing the order for which the variables will be solved. By default, increasing order is used. For example: >>> equation = "x1 - 3. = x0*x2" >>> print _solve_single(equation, target='x1') x1 = 3.0 + x0*x2 Further Inputs: locals -- a dictionary of additional variables used in the symbolic constraints equations, and their desired values. """ #XXX: an very similar version of this code is found in _solve_linear XXX# # for now, we abort on multi-line equations or inequalities if len(constraint.replace('==', '=').split('=')) != 2: raise NotImplementedError, "requires a single valid equation" if ">" in constraint or "<" in constraint: raise NotImplementedError, "cannot simplify inequalities" nvars = None permute = False # if True, return all permutations warn = True # if True, don't supress warnings verbose = False # if True, print debug info #-----------------------undocumented------------------------------- permute = kwds['permute'] if 'permute' in kwds else permute warn = kwds['warn'] if 'warn' in kwds else warn verbose = kwds['verbose'] if 'verbose' in kwds else verbose #------------------------------------------------------------------ if target in [None, False]: target = [] elif isinstance(target, str): target = target.split(',') else: target = list(target) # not the best for ndarray, but should work from mystic.symbolic import replace_variables, get_variables if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraint, variables, markers='_') varname = '_' ndim = len(variables) for i in range(len(target)): if variables.count(target[i]): target[i] = replace_variables(target[i], variables, markers='_') else: constraints = constraint # constraints used below varname = variables # varname used below instead of variables myvar = get_variables(constraint, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # create function to replace "_" with original variables def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring, '_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i], variables[indices[i]]) return mystring # default is _locals with sympy imported _locals = {} locals = kwds['locals'] if 'locals' in kwds else None if locals is None: locals = {} try: code = """from sympy import Eq, Symbol;""" code += """from sympy import solve as symsol;""" code = compile(code, '<string>', 'exec') exec code in _locals except ImportError: # Equation will not be simplified." if warn: print "Warning: sympy not installed." return constraint # default is _locals with numpy and math imported # numpy throws an 'AttributeError', but math passes error to sympy code = """from numpy import *; from math import *;""" # prefer math code += """from numpy import mean as average;""" # use np.mean not average code += """from numpy import var as variance;""" # look like mystic.math code += """from numpy import ptp as spread;""" # look like mystic.math code = compile(code, '<string>', 'exec') exec code in _locals _locals.update(locals) #XXX: allow this? code, left, right, xlist, neqns = _prepare_sympy(constraints, varname, ndim) eqlist = "" for i in range(1, neqns + 1): eqn = 'eq' + str(i) eqlist += eqn + "," code += eqn + '= Eq(' + left[i - 1] + ',' + right[i - 1] + ')\n' eqlist = eqlist.rstrip(',') # get full list of variables in 'targeted' order xperms = xlist.split(',')[:-1] targeted = target[:] [targeted.remove(i) for i in targeted if i not in xperms] [targeted.append(i) for i in xperms if i not in targeted] _target = [] [_target.append(i) for i in targeted if i not in _target] targeted = _target targeted = tuple(targeted) ######################################################################## # solve each xi: symsol(single_equation, [x0,x1,...,xi,...,xn]) # returns: {x0: f(xn,...), x1: f(xn,...), ..., xn: f(...,x0)} if permute or not target: #XXX: the goal is solving *only one* equation code += '_xlist = %s\n' % ','.join(targeted) code += '_elist = [symsol([' + eqlist + '], [i]) for i in _xlist]\n' code += '_elist = [i if isinstance(i, dict) else {j:i[-1][-1]} for j,i in zip(_xlist,_elist)]\n' code += 'soln = {}\n' code += '[soln.update(i) for i in _elist if i]\n' else: code += 'soln = symsol([' + eqlist + '], [' + target[0] + '])\n' #code += 'soln = symsol([' + eqlist + '], [' + targeted[0] + '])\n' code += 'soln = soln if isinstance(soln, dict) else {' + target[ 0] + ': soln[-1][-1]}\n' ######################################################################## if verbose: print code code = compile(code, '<string>', 'exec') try: exec code in globals(), _locals soln = _locals['soln'] if not soln: if warn: print "Warning: target variable is not valid" soln = {} except NotImplementedError: # catch 'multivariate' error for older sympy if warn: print "Warning: could not simplify equation." return constraint #FIXME: resolve diff with _solve_linear except NameError, error: # catch when variable is not defined if warn: print "Warning:", error soln = {}
def constraints_parser(constraints, variables='x', nvars=None): """parse symbolic constraints into a tuple of constraints solver equations. The left-hand side of each constraint must be simplified to support assignment. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> constraints_parser(constraints, nvars=3) ('x[2] = x[0]/2.', 'x[0] = max(0., x[0])') Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x2' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ #from mystic.tools import src #ndim = len(get_variables(src(func), variables)) if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars def _process_line(line): 'string processing from line to expression' # Iterate in reverse in case ndim > 9. indices = list(range(ndim)) indices.reverse() for i in indices: line = line.replace(varname + str(i), 'x[' + str(i) + ']') constraint = line.strip() # Replace 'ptp', 'average', and 'var' (uses mystic, not numpy) if constraint.find('ptp(') != -1: constraint = constraint.replace('ptp(', 'spread(') if constraint.find('average(') != -1: constraint = constraint.replace('average(', 'mean(') if constraint.find('var(') != -1: constraint = constraint.replace('var(', 'variance(') if constraint.find('prod(') != -1: constraint = constraint.replace('prod(', 'product(') return constraint def _process_expression(expression): ' allow mystic.math.measures impose_* on LHS ' lhs,rhs = expression.split('=') if lhs.find('spread(') != -1: lhs = lhs.split('spread')[-1] rhs = ' impose_spread( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('mean(') != -1: lhs = lhs.split('mean')[-1] rhs = ' impose_mean( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('variance(') != -1: lhs = lhs.split('variance')[-1] rhs = ' impose_variance( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('sum(') != -1: lhs = lhs.split('sum')[-1] rhs = ' impose_sum( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('product(') != -1: lhs = lhs.split('product')[-1] rhs = ' impose_product( (' + rhs.lstrip() + '),' + lhs + ')' return lhs,rhs # Parse the constraints string lines = constraints.splitlines() parsed = [] #XXX: in penalty_parser is eqconstraints, ineqconstraints xLHS, xRHS = [],[] for line in lines: if line.strip(): constraint = _process_line(line) # Skip whenever '!=' is not the comparator if '!=' != comparator(constraint): continue eta = ' * (_tol(%(rhs)s,tol,rel) * 1.1) '#XXX: better 1.1*e_ or ??? # collect the LHS and RHS of all != cases, to use later. split = constraint.split('!=') #XXX: better to use 1? eta? or ??? expression = '%(lhs)s = %(lhs)s + equal(%(lhs)s,%(rhs)s)' + eta eqn = {'lhs':split[0].rstrip('=').strip(), \ 'rhs':split[-1].lstrip('=').strip()} xLHS.append(eqn['lhs']) xRHS.append(eqn['rhs']) lhs, rhs = _process_expression(expression % eqn) parsed.append("=".join((lhs,rhs))) # iterate again, actually processing strings knowing where the '!=' are. for line in lines: if line.strip(): constraint = _process_line(line) # Skip whenever '!=' is the comparator eps = comparator(constraint) if eps == '!=': continue #XXX: use 1.1? or ??? eta = '(_tol(%(rhs)s,tol,rel) * any(equal(%(rhs)s,%(neq)s)))' # Use eta whenever '<=' or '>=' is comparator eta = (' + ' + eta) if '>=' == eps else ((' - ' + eta) if '<=' == eps else '') #XXX: '>' in eps, or '>=' == eps? # Use epsilon whenever '<' or '>' is comparator eps = ' + e_ ' if eps == '>' else (' - e_ ' if eps == '<' else '') # convert "<" to min(LHS, RHS) and ">" to max(LHS,RHS) split = constraint.split('>') expression = '%(lhs)s = max(%(rhs)s, %(lhs)s)' if len(split) == 1: # didn't contain '>' or '!=' split = constraint.split('<') expression = '%(lhs)s = min(%(rhs)s, %(lhs)s)' if len(split) == 1: # didn't contain '>', '<', or '!=' split = constraint.split('=') expression = '%(lhs)s = %(rhs)s' if len(split) == 1: # didn't contain '>', '<', '!=', or '=' print "Invalid constraint: ", constraint eqn = {'lhs':split[0].rstrip('=').strip(), \ 'rhs':split[-1].lstrip('=').strip()} # get list of LHS,RHS that != forces not to appear eqn['neq'] = '[' + ','.join(j for (i,j) in zip(xLHS+xRHS,xRHS+xLHS) if eqn['lhs'] == i) + ']' eqn['rhs'] += eps.replace('e_', '_tol(%(rhs)s,tol,rel)' % eqn) \ or eta % eqn expression = "=".join(_process_expression(expression % eqn)) parsed.append(expression) return tuple(reversed(parsed))
def constraints_parser(constraints, variables='x', nvars=None): """parse symbolic constraints into a tuple of constraints solver equations. The left-hand side of each constraint must be simplified to support assignment. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> constraints_parser(constraints, nvars=3) ('x[2] = x[0]/2.', 'x[0] = max(0., x[0])') Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x2' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ #from mystic.tools import src #ndim = len(get_variables(src(func), variables)) if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # Parse the constraints string lines = constraints.splitlines() parsed = [] #XXX: in penalty_parser is eqconstraints, ineqconstraints for line in lines: if line.strip(): fixed = line # Iterate in reverse in case ndim > 9. indices = list(range(ndim)) indices.reverse() for i in indices: fixed = fixed.replace(varname + str(i), 'x[' + str(i) + ']') constraint = fixed.strip() # Replace 'ptp', 'average', and 'var' (uses mystic, not numpy) if constraint.find('ptp(') != -1: constraint = constraint.replace('ptp(', 'spread(') if constraint.find('average(') != -1: constraint = constraint.replace('average(', 'mean(') if constraint.find('var(') != -1: constraint = constraint.replace('var(', 'variance(') if constraint.find('prod(') != -1: constraint = constraint.replace('prod(', 'product(') #XXX: below this line the code is different than penalty_parser # convert "<" to min(LHS, RHS) and ">" to max(LHS,RHS) split = constraint.split('>') expression = '%(lhs)s = max(%(rhs)s, %(lhs)s)' if len(split) == 1: # didn't contain '>' split = constraint.split('<') expression = '%(lhs)s = min(%(rhs)s, %(lhs)s)' if len(split) == 1: # didn't contain '>' or '<' split = constraint.split('=') expression = '%(lhs)s = %(rhs)s' if len(split) == 1: # didn't contain '>', '<', or '=' print "Invalid constraint: ", constraint eqn = {'lhs':split[0].rstrip('=').strip(), \ 'rhs':split[-1].lstrip('=').strip()} expression = expression % eqn # allow mystic.math.measures impose_* on LHS lhs, rhs = expression.split('=') if lhs.find('spread(') != -1: lhs = lhs.split('spread')[-1] rhs = ' impose_spread( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('mean(') != -1: lhs = lhs.split('mean')[-1] rhs = ' impose_mean( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('variance(') != -1: lhs = lhs.split('variance')[-1] rhs = ' impose_variance( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('sum(') != -1: lhs = lhs.split('sum')[-1] rhs = ' impose_sum( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('product(') != -1: lhs = lhs.split('product')[-1] rhs = ' impose_product( (' + rhs.lstrip() + '),' + lhs + ')' expression = "=".join([lhs, rhs]) parsed.append(expression) return tuple(parsed)
def _solve_nonlinear(constraints, variables='x', target=None, **kwds): """Build a constraints function given a string of nonlinear constraints. Returns a constraints function. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = '''x1 = x3*3. + x0*x2''' >>> print _solve_nonlinear(constraints) x0 = (x1 - 3.0*x3)/x2 >>> constraints = ''' ... spread([x0,x1]) - 1.0 = mean([x0,x1]) ... mean([x0,x1,x2]) = x2''' >>> print _solve_nonlinear(constraints) x0 = -0.5 + 0.5*x2 x1 = 0.5 + 1.5*x2 Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. target -- list providing the order for which the variables will be solved. If there are "N" constraint equations, the first "N" variables given will be selected as the dependent variables. By default, increasing order is used. For example: >>> constraints = ''' ... spread([x0,x1]) - 1.0 = mean([x0,x1]) ... mean([x0,x1,x2]) = x2''' >>> print _solve_nonlinear(constraints, target=['x1']) x1 = -0.833333333333333 + 0.166666666666667*x2 x0 = -0.5 + 0.5*x2 Further Inputs: locals -- a dictionary of additional variables used in the symbolic constraints equations, and their desired values. """ nvars = None permute = False # if True, return all permutations warn = True # if True, don't supress warnings verbose = False # if True, print details from _classify_variables #-----------------------undocumented------------------------------- permute = kwds['permute'] if 'permute' in kwds else permute warn = kwds['warn'] if 'warn' in kwds else warn verbose = kwds['verbose'] if 'verbose' in kwds else verbose #------------------------------------------------------------------ if target in [None, False]: target = [] elif isinstance(target, str): target = target.split(',') else: target = list(target) # not the best for ndarray, but should work from mystic.symbolic import replace_variables, get_variables if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables, '_') varname = '_' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # create function to replace "_" with original variables def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring,'_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i],variables[indices[i]]) return mystring locals = kwds['locals'] if 'locals' in kwds else None if locals is None: locals = {} eqns = constraints.splitlines() # Remove empty strings: actual_eqns = [] for j in range(len(eqns)): if eqns[j].strip(): actual_eqns.append(eqns[j].strip()) orig_eqns = actual_eqns[:] neqns = len(actual_eqns) xperms = [varname+str(i) for i in range(ndim)] if target: [target.remove(i) for i in target if i not in xperms] [target.append(i) for i in xperms if i not in target] _target = [] [_target.append(i) for i in target if i not in _target] target = _target target = tuple(target) xperms = list(permutations(xperms)) #XXX: takes a while if nvars is ~10 if target: # Try the suggested order first. xperms.remove(target) xperms.insert(0, target) complete_list = [] constraints_function_list = [] # Some of the permutations will give the same answer; # look into reducing the number of repeats? for perm in xperms: # Sort the list actual_eqns so any equation containing x0 is first, etc. sorted_eqns = [] actual_eqns_copy = orig_eqns[:] usedvars = [] for variable in perm: # range(ndim): for eqn in actual_eqns_copy: if eqn.find(variable) != -1: sorted_eqns.append(eqn) actual_eqns_copy.remove(eqn) usedvars.append(variable) break if actual_eqns_copy: # Append the remaining equations for item in actual_eqns_copy: sorted_eqns.append(item) actual_eqns = sorted_eqns # Append the remaining variables to usedvars tempusedvar = usedvars[:] tempusedvar.sort() nmissing = ndim - len(tempusedvar) for m in range(nmissing): usedvars.append(varname + str(len(tempusedvar) + m)) for i in range(neqns): # Trying to use xi as a pivot. Loop through the equations # looking for one containing xi. _target = usedvars[i] for eqn in actual_eqns[i:]: invertedstring = _solve_single(eqn, variables=varname, target=_target, warn=warn) if invertedstring: warn = False break # substitute into the remaining equations. the equations' order # in the list newsystem is like in a linear coefficient matrix. newsystem = ['']*neqns j = actual_eqns.index(eqn) newsystem[j] = eqn othereqns = actual_eqns[:j] + actual_eqns[j+1:] for othereqn in othereqns: expression = invertedstring.split("=")[1] fixed = othereqn.replace(_target, '(' + expression + ')') k = actual_eqns.index(othereqn) newsystem[k] = fixed actual_eqns = newsystem # Invert so that it can be fed properly to generate_constraint simplified = [] for eqn in actual_eqns: _target = usedvars[actual_eqns.index(eqn)] mysoln = _solve_single(eqn, variables=varname, target=_target, warn=warn) if mysoln: simplified.append(mysoln) simplified = restore(variables, '\n'.join(simplified).rstrip()) if permute: complete_list.append(simplified) continue if verbose: print _classify_variables(simplified, variables, ndim) return simplified warning='Warning: an error occurred in building the constraints.' if warn: print warning if verbose: print _classify_variables(simplified, variables, ndim) if permute: #FIXME: target='x3,x1' may order correct, while 'x1,x3' doesn't filter = []; results = [] for i in complete_list: _eqs = '\n'.join(sorted(i.split('\n'))) if _eqs and (_eqs not in filter): filter.append(_eqs) results.append(i) return tuple(results) #FIXME: somehow 'rhs = xi' can be in results return simplified
def _solve_single(constraint, variables='x', target=None, **kwds): """Solve a symbolic constraints equation for a single variable. Inputs: constraint -- a string of symbolic constraints. Only a single constraint equation should be provided, and must be an equality constraint. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> equation = "x1 - 3. = x0*x2" >>> print _solve_single(equation) x0 = -(3.0 - x1)/x2 Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. target -- list providing the order for which the variables will be solved. By default, increasing order is used. For example: >>> equation = "x1 - 3. = x0*x2" >>> print _solve_single(equation, target='x1') x1 = 3.0 + x0*x2 Further Inputs: locals -- a dictionary of additional variables used in the symbolic constraints equations, and their desired values. """ #XXX: an very similar version of this code is found in _solve_linear XXX# # for now, we abort on multi-line equations or inequalities if len(constraint.replace('==','=').split('=')) != 2: raise NotImplementedError, "requires a single valid equation" if ">" in constraint or "<" in constraint: raise NotImplementedError, "cannot simplify inequalities" nvars = None permute = False # if True, return all permutations warn = True # if True, don't supress warnings verbose = False # if True, print debug info #-----------------------undocumented------------------------------- permute = kwds['permute'] if 'permute' in kwds else permute warn = kwds['warn'] if 'warn' in kwds else warn verbose = kwds['verbose'] if 'verbose' in kwds else verbose #------------------------------------------------------------------ if target in [None, False]: target = [] elif isinstance(target, str): target = target.split(',') else: target = list(target) # not the best for ndarray, but should work from mystic.symbolic import replace_variables, get_variables if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraint, variables, markers='_') varname = '_' ndim = len(variables) for i in range(len(target)): if variables.count(target[i]): target[i] = replace_variables(target[i],variables,markers='_') else: constraints = constraint # constraints used below varname = variables # varname used below instead of variables myvar = get_variables(constraint, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # create function to replace "_" with original variables def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring,'_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i],variables[indices[i]]) return mystring # default is _locals with sympy imported _locals = {} locals = kwds['locals'] if 'locals' in kwds else None if locals is None: locals = {} try: code = """from sympy import Eq, Symbol;""" code += """from sympy import solve as symsol;""" code = compile(code, '<string>', 'exec') exec code in _locals except ImportError: # Equation will not be simplified." if warn: print "Warning: sympy not installed." return constraint # default is _locals with numpy and math imported # numpy throws an 'AttributeError', but math passes error to sympy code = """from numpy import *; from math import *;""" # prefer math code += """from numpy import mean as average;""" # use np.mean not average code += """from numpy import var as variance;""" # look like mystic.math code += """from numpy import ptp as spread;""" # look like mystic.math code = compile(code, '<string>', 'exec') exec code in _locals _locals.update(locals) #XXX: allow this? code,left,right,xlist,neqns = _prepare_sympy(constraints, varname, ndim) eqlist = "" for i in range(1, neqns+1): eqn = 'eq' + str(i) eqlist += eqn + "," code += eqn + '= Eq(' + left[i-1] + ',' + right[i-1] + ')\n' eqlist = eqlist.rstrip(',') # get full list of variables in 'targeted' order xperms = xlist.split(',')[:-1] targeted = target[:] [targeted.remove(i) for i in targeted if i not in xperms] [targeted.append(i) for i in xperms if i not in targeted] _target = [] [_target.append(i) for i in targeted if i not in _target] targeted = _target targeted = tuple(targeted) ######################################################################## # solve each xi: symsol(single_equation, [x0,x1,...,xi,...,xn]) # returns: {x0: f(xn,...), x1: f(xn,...), ..., xn: f(...,x0)} if permute or not target: #XXX: the goal is solving *only one* equation code += '_xlist = %s\n' % ','.join(targeted) code += '_elist = [symsol(['+eqlist+'], [i]) for i in _xlist]\n' code += '_elist = [i if isinstance(i, dict) else {j:i[-1][-1]} for j,i in zip(_xlist,_elist)]\n' code += 'soln = {}\n' code += '[soln.update(i) for i in _elist if i]\n' else: code += 'soln = symsol([' + eqlist + '], [' + target[0] + '])\n' #code += 'soln = symsol([' + eqlist + '], [' + targeted[0] + '])\n' code += 'soln = soln if isinstance(soln, dict) else {' + target[0] + ': soln[-1][-1]}\n' ######################################################################## if verbose: print code code = compile(code, '<string>', 'exec') try: exec code in globals(), _locals soln = _locals['soln'] if not soln: if warn: print "Warning: target variable is not valid" soln = {} except NotImplementedError: # catch 'multivariate' error for older sympy if warn: print "Warning: could not simplify equation." return constraint #FIXME: resolve diff with _solve_linear except NameError, error: # catch when variable is not defined if warn: print "Warning:", error soln = {}
def generate_constraint(conditions, ctype=None, **kwds): """Converts a constraint solver to a mystic.constraints function. Inputs: conditions -- a constraint solver, or list of constraint solvers ctype -- a mystic.constraints type, or a list of mystic.constraints types of the same length as the given conditions NOTES: This simple constraint generator doesn't check for conflicts in conditions, but simply applies conditions in the given order. This constraint generator assumes that a single variable has been isolated on the left-hand side of each constraints equation, thus all constraints are of the form "x_i = f(x)". This solver picks speed over robustness, and thus relies on the user to formulate the constraints so that they do not conflict. For example: >>> constraints = ''' ... x0 = cos(x1) + 2. ... x1 = x2*2.''' >>> solv = generate_solvers(constraints) >>> constraint = generate_constraint(solv) >>> constraint([1.0, 0.0, 1.0]) [1.5838531634528576, 2.0, 1.0] Standard python math conventions are used. For example, if an 'int' is used in a constraint equation, one or more variable may be evaluate to an 'int' -- this can affect solved values for the variables. For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> solv = generate_solvers(constraints, nvars=3) >>> print solv[0].__doc__ 'x[2] = x[0]/2.' >>> print solv[1].__doc__ 'x[0] = max(0., x[0])' >>> constraint = generate_constraint(solv) >>> constraint([1,2,3]) [1, 2, 0.5] >>> constraint([-1,2,-3]) [0.0, 2, 0.0] """ # allow for single condition, list of conditions, or nested list if not list_or_tuple_or_ndarray(conditions): conditions = list((conditions, )) else: pass #XXX: should be fine... conditions = list(flatten(conditions)) # allow for single ctype, list of ctypes, or nested list if ctype is None: from mystic.coupler import inner #XXX: outer ? ctype = list((inner, )) * len(conditions) elif not list_or_tuple_or_ndarray(ctype): ctype = list((ctype, )) * len(conditions) else: pass #XXX: is already a list, should be the same len as conditions ctype = list(flatten(ctype)) # iterate through solvers, building a compound constraints solver cf = lambda x: x cfdoc = "" for wrapper, condition in zip(ctype, conditions): cfdoc += "%s: %s\n" % (wrapper.__name__, condition.__doc__) apply = wrapper(condition, **kwds) cf = apply(cf) cf.__doc__ = cfdoc.rstrip('\n') cf.__name__ = 'constraint' return cf
def _prepare_sympy(constraints, variables='x', nvars=None): """Parse an equation string and prepare input for sympy. Returns a tuple of sympy-specific input: (code for variable declaration, left side of equation string, right side of equation string, list of variables, and the number of sympy equations). Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x0 = x4**2 ... x4 - x3 = 0. ... x4 - x0 = x2''' >>> code, lhs, rhs, vars, neqn = _prepare_sympy(constraints, nvars=5) >>> print code x0=Symbol('x0') x1=Symbol('x1') x2=Symbol('x2') x3=Symbol('x3') x4=Symbol('x4') rand = Symbol('rand') >>> print lhs, rhs ['x0 ', 'x4 - x3 ', 'x4 - x0 '] [' x4**2', ' 0.', ' x2'] print "%s in %s eqns" % (vars, neqn) x0,x1,x2,x3,x4, in 3 eqns Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x1' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ if ">" in constraints or "<" in constraints: raise NotImplementedError, "cannot simplify inequalities" from mystic.symbolic import replace_variables, get_variables #XXX: if constraints contain x0,x1,x3 for 'x', should x2 be in code,xlist? if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables, markers='_') varname = '_' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # split constraints_str into lists of left hand sides and right hand sides eacheqn = constraints.splitlines() neqns = 0 left = [] right = [] for eq in eacheqn: #XXX: Le/Ge instead of Eq; Max/Min... (NotImplemented ?) splitlist = eq.replace('==','=').split('=') #FIXME: no inequalities if len(splitlist) == 2: #FIXME: first convert >/< to min/max ? # If equation is blank on one side, raise error. if len(splitlist[0].strip()) == 0 or len(splitlist[1].strip()) == 0: print eq, "is not an equation!" # Raise exception? else: left.append(splitlist[0]) right.append(splitlist[1]) neqns += 1 # If equation doesn't have one equal sign, raise error. if len(splitlist) != 2 and len(splitlist) != 1: print eq, "is not an equation!" # Raise exception? # First create list of x variables xlist = "" for i in range(ndim): xn = varname + str(i) xlist += xn + "," # Start constructing the code string code = "" for i in range(ndim): xn = varname + str(i) code += xn + '=' + "Symbol('" + xn + "')\n" code += "rand = Symbol('rand')\n" return code, left, right, xlist, neqns
def replace_variables(constraints, variables=None, markers='$'): """Replace variables in constraints string with a marker. Returns a modified constraints string. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). variables -- list of variable name strings. The variable names will be replaced in the order that they are provided, where if the default marker "$i" is used, the first variable will be replaced with "$0", the second with "$1", and so on. For example: >>> variables = ['spam', 'eggs'] >>> constraints = '''spam + eggs - 42''' >>> print replace_variables(constraints, variables, 'x') 'x0 + x1 - 42' Additional Inputs: markers -- desired variable name. Default is '$'. A list of variable name strings is also accepted for when desired variable names don't have the same base. For example: >>> variables = ['x1','x2','x3'] >>> constraints = "min(x1*x2) - sin(x3)" >>> print replace_variables(constraints, variables, ['x','y','z']) 'min(x*y) - sin(z)' """ if variables is None: variables = [] elif isinstance(variables, str): variables = list((variables,)) # substitite one list of strings for another if list_or_tuple_or_ndarray(markers): equations = replace_variables(constraints,variables,'_') vars = get_variables(equations,'_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): equations = equations.replace(vars[i],markers[indices[i]]) return equations # Sort by decreasing length of variable name, so that if one variable name # is a substring of another, that won't be a problem. variablescopy = variables[:] def comparator(x, y): return len(y) - len(x) variablescopy.sort(comparator) # Figure out which index goes with which variable. indices = [] for item in variablescopy: indices.append(variables.index(item)) # Default is markers='$', as '$' is not a special symbol in Python, # and it is unlikely a user will choose it for a variable name. if markers in variables: marker = '_$$$$$$$$$$' # even less likely... else: marker = markers '''Bug demonstrated here: >>> equation = """x3 = max(y,x) + x""" >>> vars = ['x','y','z','x3'] >>> print replace_variables(equation,vars) $4 = ma$1($2,$1) + $1 ''' #FIXME: don't parse if __name__ in __builtins__, globals, or locals? for i in indices: #FIXME: or better, use 're' pattern matching constraints = constraints.replace(variables[i], marker + str(i)) return constraints.replace(marker, markers)
def _classify_variables(constraints, variables='x', nvars=None): """Takes a string of constraint equations and determines which variables are dependent, independent, and unconstrained. Assumes there are no duplicate equations. Returns a dictionary with keys: 'dependent', 'independent', and 'unconstrained', and with values that enumerate the variables that match each variable type. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x0 = x4**2 ... x2 = x3 + x4''' >>> _classify_variables(constraints, nvars=5) {'dependent':['x0','x2'], 'independent':['x3','x4'], 'unconstrained':['x1']} >>> constraints = ''' ... x0 = x4**2 ... x4 - x3 = 0. ... x4 - x0 = x2''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2','x4'], 'independent': ['x3'], 'unconstrained': ['x1']} Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x1' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ if ">" in constraints or "<" in constraints: raise NotImplementedError, "cannot classify inequalities" from mystic.symbolic import replace_variables, get_variables #XXX: use solve? or first if not in form xi = ... ? if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars eqns = constraints.splitlines() indices = range(ndim) dep = [] indep = [] for eqn in eqns: # find which variables are used if eqn: for var in range(ndim): if indices.count(var) != 0: if eqn.find(varname + str(var)) != -1: indep.append(var) indices.remove(var) indep.sort() _dep = [] for eqn in eqns: # find which variables are on the LHS if eqn: split = eqn.split('=') for var in indep: if split[0].find(varname + str(var)) != -1: _dep.append(var) indep.remove(var) break _dep.sort() indep = _dep + indep # prefer variables found on LHS for eqn in eqns: # find one dependent variable per equation _dep = [] _indep = indep[:] if eqn: for var in _indep: if eqn.find(varname + str(var)) != -1: _dep.append(var) _indep.remove(var) if _dep: dep.append(_dep[0]) indep.remove(_dep[0]) #FIXME: 'equivalent' equations not ignored (e.g. x2=x2; or x2=1, 2*x2=2) """These are good: >>> constraints = ''' ... x0 = x4**2 ... x2 - x4 - x3 = 0.''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2'], 'independent': ['x3','x4'], 'unconstrained': ['x1']} >>> constraints = ''' ... x0 + x2 = 0. ... x0 + 2*x2 = 0.''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2'], 'independent': [], 'unconstrained': ['x1','x3','x4']} This is a bug: >>> constraints = ''' ... x0 + x2 = 0. ... 2*x0 + 2*x2 = 0.''' >>> _classify_variables(constraints, nvars=5) {'dependent': ['x0','x2'], 'independent': [], 'unconstrained': ['x1','x3','x4']} """ #XXX: should simplify first? dep.sort() indep.sort() # return the actual variable names (not the indicies) if varname == variables: # then was single variable variables = [varname+str(i) for i in range(ndim)] dep = [variables[i] for i in dep] indep = [variables[i] for i in indep] indices = [variables[i] for i in indices] d = {'dependent':dep, 'independent':indep, 'unconstrained':indices} return d
def _solve_linear(constraints, variables='x', target=None, **kwds): """Solve a system of symbolic linear constraints equations. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x0 - x2 = 2. ... x2 = x3*2.''' >>> print _solve_linear(constraints) x2 = 2.0*x3 x0 = 2.0 + 2.0*x3 Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. target -- list providing the order for which the variables will be solved. If there are "N" constraint equations, the first "N" variables given will be selected as the dependent variables. By default, increasing order is used. For example: >>> constraints = ''' ... x0 - x2 = 2. ... x2 = x3*2.''' >>> print _solve_linear(constraints, target=['x3','x2']) x3 = -1.0 + 0.5*x0 x2 = -2.0 + x0 Further Inputs: locals -- a dictionary of additional variables used in the symbolic constraints equations, and their desired values. """ nvars = None permute = False # if True, return all permutations warn = True # if True, don't supress warnings verbose = False # if True, print debug info #-----------------------undocumented------------------------------- permute = kwds['permute'] if 'permute' in kwds else permute warn = kwds['warn'] if 'warn' in kwds else warn verbose = kwds['verbose'] if 'verbose' in kwds else verbose #------------------------------------------------------------------ if target in [None, False]: target = [] elif isinstance(target, str): target = target.split(',') else: target = list(target) # not the best for ndarray, but should work from mystic.symbolic import replace_variables, get_variables if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] _constraints = replace_variables(constraints, variables, '_') varname = '_' ndim = len(variables) for i in range(len(target)): if variables.count(target[i]): target[i] = replace_variables(target[i], variables, markers='_') else: _constraints = constraints varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # create function to replace "_" with original variables def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring, '_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i], variables[indices[i]]) return mystring # default is _locals with sympy imported _locals = {} locals = kwds['locals'] if 'locals' in kwds else None if locals is None: locals = {} # if sympy not installed, return original constraints try: code = """from sympy import Eq, Symbol;""" code += """from sympy import solve as symsol;""" code = compile(code, '<string>', 'exec') exec code in _locals except ImportError: # Equation will not be simplified." if warn: print "Warning: sympy not installed." return constraints # default is _locals with numpy and math imported # numpy throws an 'AttributeError', but math passes error to sympy code = """from numpy import *; from math import *;""" # prefer math code += """from numpy import mean as average;""" # use np.mean not average code += """from numpy import var as variance;""" # look like mystic.math code += """from numpy import ptp as spread;""" # look like mystic.math code = compile(code, '<string>', 'exec') exec code in _locals _locals.update(locals) #XXX: allow this? code, left, right, xlist, neqns = _prepare_sympy(_constraints, varname, ndim) eqlist = "" for i in range(1, neqns + 1): eqn = 'eq' + str(i) eqlist += eqn + "," code += eqn + '= Eq(' + left[i - 1] + ',' + right[i - 1] + ')\n' eqlist = eqlist.rstrip(',') # get full list of variables in 'targeted' order xperms = xlist.split(',')[:-1] targeted = target[:] [targeted.remove(i) for i in targeted if i not in xperms] [targeted.append(i) for i in xperms if i not in targeted] _target = [] [_target.append(i) for i in targeted if i not in _target] targeted = _target targeted = tuple(targeted) if permute: # Form constraints equations for each permutation. # This will change the order of the x variables passed to symsol() # to get different variables solved for. xperms = list( permutations(xperms)) #XXX: takes a while if nvars is ~10 if target: # put the tuple with the 'targeted' order first xperms.remove(targeted) xperms.insert(0, targeted) else: xperms = [tuple(targeted)] solns = [] for perm in xperms: _code = code xlist = ','.join(perm).rstrip(',') #XXX: if not all, use target ? # solve dependent xi: symsol([linear_system], [x0,x1,...,xi,...,xn]) # returns: {x0: f(xn,...), x1: f(xn,...), ...} _code += 'soln = symsol([' + eqlist + '], [' + xlist + '])' #XXX: need to convert/check soln similarly as in _solve_single ? if verbose: print _code _code = compile(_code, '<string>', 'exec') try: exec _code in globals(), _locals soln = _locals['soln'] if not soln: if warn: print "Warning: could not simplify equation." soln = {} except NotImplementedError: # catch 'multivariate' error if warn: print "Warning: could not simplify equation." soln = {} except NameError, error: # catch when variable is not defined if warn: print "Warning:", error soln = {} if verbose: print soln solved = "" for key, value in soln.iteritems(): solved += str(key) + ' = ' + str(value) + '\n' if solved: solns.append(restore(variables, solved.rstrip()))
def _solve_linear(constraints, variables='x', target=None, **kwds): """Solve a system of symbolic linear constraints equations. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints must be equality constraints only. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x0 - x2 = 2. ... x2 = x3*2.''' >>> print _solve_linear(constraints) x2 = 2.0*x3 x0 = 2.0 + 2.0*x3 Additional Inputs: variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. target -- list providing the order for which the variables will be solved. If there are "N" constraint equations, the first "N" variables given will be selected as the dependent variables. By default, increasing order is used. For example: >>> constraints = ''' ... x0 - x2 = 2. ... x2 = x3*2.''' >>> print _solve_linear(constraints, target=['x3','x2']) x3 = -1.0 + 0.5*x0 x2 = -2.0 + x0 Further Inputs: locals -- a dictionary of additional variables used in the symbolic constraints equations, and their desired values. """ nvars = None permute = False # if True, return all permutations warn = True # if True, don't supress warnings verbose = False # if True, print debug info #-----------------------undocumented------------------------------- permute = kwds['permute'] if 'permute' in kwds else permute warn = kwds['warn'] if 'warn' in kwds else warn verbose = kwds['verbose'] if 'verbose' in kwds else verbose #------------------------------------------------------------------ if target in [None, False]: target = [] elif isinstance(target, str): target = target.split(',') else: target = list(target) # not the best for ndarray, but should work from mystic.symbolic import replace_variables, get_variables if list_or_tuple_or_ndarray(variables): if nvars is not None: variables = variables[:nvars] _constraints = replace_variables(constraints, variables, '_') varname = '_' ndim = len(variables) for i in range(len(target)): if variables.count(target[i]): target[i] = replace_variables(target[i],variables,markers='_') else: _constraints = constraints varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars is not None: ndim = nvars # create function to replace "_" with original variables def restore(variables, mystring): if list_or_tuple_or_ndarray(variables): vars = get_variables(mystring,'_') indices = [int(v.strip('_')) for v in vars] for i in range(len(vars)): mystring = mystring.replace(vars[i],variables[indices[i]]) return mystring # default is _locals with sympy imported _locals = {} locals = kwds['locals'] if 'locals' in kwds else None if locals is None: locals = {} # if sympy not installed, return original constraints try: code = """from sympy import Eq, Symbol;""" code += """from sympy import solve as symsol;""" code = compile(code, '<string>', 'exec') exec code in _locals except ImportError: # Equation will not be simplified." if warn: print "Warning: sympy not installed." return constraints # default is _locals with numpy and math imported # numpy throws an 'AttributeError', but math passes error to sympy code = """from numpy import *; from math import *;""" # prefer math code += """from numpy import mean as average;""" # use np.mean not average code += """from numpy import var as variance;""" # look like mystic.math code += """from numpy import ptp as spread;""" # look like mystic.math code = compile(code, '<string>', 'exec') exec code in _locals _locals.update(locals) #XXX: allow this? code,left,right,xlist,neqns = _prepare_sympy(_constraints, varname, ndim) eqlist = "" for i in range(1, neqns+1): eqn = 'eq' + str(i) eqlist += eqn + "," code += eqn + '= Eq(' + left[i-1] + ',' + right[i-1] + ')\n' eqlist = eqlist.rstrip(',') # get full list of variables in 'targeted' order xperms = xlist.split(',')[:-1] targeted = target[:] [targeted.remove(i) for i in targeted if i not in xperms] [targeted.append(i) for i in xperms if i not in targeted] _target = [] [_target.append(i) for i in targeted if i not in _target] targeted = _target targeted = tuple(targeted) if permute: # Form constraints equations for each permutation. # This will change the order of the x variables passed to symsol() # to get different variables solved for. xperms = list(permutations(xperms)) #XXX: takes a while if nvars is ~10 if target: # put the tuple with the 'targeted' order first xperms.remove(targeted) xperms.insert(0, targeted) else: xperms = [tuple(targeted)] solns = [] for perm in xperms: _code = code xlist = ','.join(perm).rstrip(',') #XXX: if not all, use target ? # solve dependent xi: symsol([linear_system], [x0,x1,...,xi,...,xn]) # returns: {x0: f(xn,...), x1: f(xn,...), ...} _code += 'soln = symsol([' + eqlist + '], [' + xlist + '])' #XXX: need to convert/check soln similarly as in _solve_single ? if verbose: print _code _code = compile(_code, '<string>', 'exec') try: exec _code in globals(), _locals soln = _locals['soln'] if not soln: if warn: print "Warning: could not simplify equation." soln = {} except NotImplementedError: # catch 'multivariate' error if warn: print "Warning: could not simplify equation." soln = {} except NameError, error: # catch when variable is not defined if warn: print "Warning:", error soln = {} if verbose: print soln solved = "" for key, value in soln.iteritems(): solved += str(key) + ' = ' + str(value) + '\n' if solved: solns.append( restore(variables, solved.rstrip()) )
def penalty_parser(constraints, variables='x', nvars=None): """parse symbolic constraints into penalty constraints. Returns a tuple of inequality constraints and a tuple of equality constraints. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> penalty_parser(constraints, nvars=3) (('-(x[0] - (0.))',), ('x[2] - (x[0]/2.)',)) Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x2' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ #from mystic.tools import src #ndim = len(get_variables(src(func), variables)) if list_or_tuple_or_ndarray(variables): if nvars != None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars != None: ndim = nvars # Parse the constraints string lines = constraints.splitlines() eqconstraints = [] ineqconstraints = [] for line in lines: if line.strip(): fixed = line # Iterate in reverse in case ndim > 9. indices = list(range(ndim)) indices.reverse() for i in indices: fixed = fixed.replace(varname + str(i), 'x[' + str(i) + ']') constraint = fixed.strip() # Replace 'spread', 'mean', and 'variance' (uses numpy, not mystic) if constraint.find('spread(') != -1: constraint = constraint.replace('spread(', 'ptp(') if constraint.find('mean(') != -1: constraint = constraint.replace('mean(', 'average(') if constraint.find('variance(') != -1: constraint = constraint.replace('variance(', 'var(') # Sorting into equality and inequality constraints, and making all # inequality constraints in the form expression <= 0. and all # equality constraints of the form expression = 0. split = constraint.split('>') direction = '>' if len(split) == 1: split = constraint.split('<') direction = '<' if len(split) == 1: split = constraint.split('=') direction = '=' if len(split) == 1: print "Invalid constraint: ", constraint eqn = {'lhs':split[0].rstrip('=').strip(), \ 'rhs':split[-1].lstrip('=').strip()} expression = '%(lhs)s - (%(rhs)s)' % eqn if direction == '=': eqconstraints.append(expression) elif direction == '<': ineqconstraints.append(expression) else: ineqconstraints.append('-(' + expression + ')') return tuple(ineqconstraints), tuple(eqconstraints)
def constraints_parser(constraints, variables='x', nvars=None): """parse symbolic constraints into a tuple of constraints solver equations. The left-hand side of each constraint must be simplified to support assignment. Inputs: constraints -- a string of symbolic constraints, with one constraint equation per line. Constraints can be equality and/or inequality constraints. Standard python syntax should be followed (with the math and numpy modules already imported). For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> constraints_parser(constraints, nvars=3) ('x[2] = x[0]/2.', 'x[0] = max(0., x[0])') Additional Inputs: nvars -- number of variables. Includes variables not explicitly given by the constraint equations (e.g. 'x2' in the example above). variables -- desired variable name. Default is 'x'. A list of variable name strings is also accepted for when desired variable names don't have the same base, and can include variables that are not found in the constraints equation string. """ #from mystic.tools import src #ndim = len(get_variables(src(func), variables)) if list_or_tuple_or_ndarray(variables): if nvars != None: variables = variables[:nvars] constraints = replace_variables(constraints, variables) varname = '$' ndim = len(variables) else: varname = variables # varname used below instead of variables myvar = get_variables(constraints, variables) if myvar: ndim = max([int(v.strip(varname)) for v in myvar]) + 1 else: ndim = 0 if nvars != None: ndim = nvars # Parse the constraints string lines = constraints.splitlines() parsed = [] #XXX: in penalty_parser is eqconstraints, ineqconstraints for line in lines: if line.strip(): fixed = line # Iterate in reverse in case ndim > 9. indices = list(range(ndim)) indices.reverse() for i in indices: fixed = fixed.replace(varname + str(i), 'x[' + str(i) + ']') constraint = fixed.strip() # Replace 'ptp', 'average', and 'var' (uses mystic, not numpy) if constraint.find('ptp(') != -1: constraint = constraint.replace('ptp(', 'spread(') if constraint.find('average(') != -1: constraint = constraint.replace('average(', 'mean(') if constraint.find('var(') != -1: constraint = constraint.replace('var(', 'variance(') if constraint.find('prod(') != -1: constraint = constraint.replace('prod(', 'product(') #XXX: below this line the code is different than penalty_parser # convert "<" to min(LHS, RHS) and ">" to max(LHS,RHS) split = constraint.split('>') expression = '%(lhs)s = max(%(rhs)s, %(lhs)s)' if len(split) == 1: # didn't contain '>' split = constraint.split('<') expression = '%(lhs)s = min(%(rhs)s, %(lhs)s)' if len(split) == 1: # didn't contain '>' or '<' split = constraint.split('=') expression = '%(lhs)s = %(rhs)s' if len(split) == 1: # didn't contain '>', '<', or '=' print "Invalid constraint: ", constraint eqn = {'lhs':split[0].rstrip('=').strip(), \ 'rhs':split[-1].lstrip('=').strip()} expression = expression % eqn # allow mystic.math.measures impose_* on LHS lhs,rhs = expression.split('=') if lhs.find('spread(') != -1: lhs = lhs.split('spread')[-1] rhs = ' impose_spread( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('mean(') != -1: lhs = lhs.split('mean')[-1] rhs = ' impose_mean( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('variance(') != -1: lhs = lhs.split('variance')[-1] rhs = ' impose_variance( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('sum(') != -1: lhs = lhs.split('sum')[-1] rhs = ' impose_sum( (' + rhs.lstrip() + '),' + lhs + ')' if lhs.find('product(') != -1: lhs = lhs.split('product')[-1] rhs = ' impose_product( (' + rhs.lstrip() + '),' + lhs + ')' expression = "=".join([lhs,rhs]) parsed.append(expression) return tuple(parsed)
def generate_constraint(conditions, ctype=None, join=None, **kwds): """Converts a constraint solver to a mystic.constraints function. Inputs: conditions -- a constraint solver, or list of constraint solvers ctype -- a mystic.coupler type, or a list of mystic.coupler types of the same length as the given conditions join -- either (and_, or_) from mystic.constraints, or None. The default is to iteratively apply the constraints. NOTES: This simple constraint generator doesn't check for conflicts in conditions, but simply applies conditions in the given order. This constraint generator assumes that a single variable has been isolated on the left-hand side of each constraints equation, thus all constraints are of the form "x_i = f(x)". This solver picks speed over robustness, and thus relies on the user to formulate the constraints so that they do not conflict. For example: >>> constraints = ''' ... x0 = cos(x1) + 2. ... x1 = x2*2.''' >>> solv = generate_solvers(constraints) >>> constraint = generate_constraint(solv) >>> constraint([1.0, 0.0, 1.0]) [1.5838531634528576, 2.0, 1.0] Standard python math conventions are used. For example, if an 'int' is used in a constraint equation, one or more variable may be evaluate to an 'int' -- this can affect solved values for the variables. For example: >>> constraints = ''' ... x2 = x0/2. ... x0 >= 0.''' >>> solv = generate_solvers(constraints, nvars=3) >>> print solv[0].__doc__ 'x[2] = x[0]/2.' >>> print solv[1].__doc__ 'x[0] = max(0., x[0])' >>> constraint = generate_constraint(solv) >>> constraint([1,2,3]) [1, 2, 0.5] >>> constraint([-1,2,-3]) [0.0, 2, 0.0] """ # allow for single condition, list of conditions, or nested list if not list_or_tuple_or_ndarray(conditions): conditions = list((conditions,)) else: pass #XXX: should be fine... # discover the nested structure of conditions and ctype nc = nt = 0 if ctype is None or not list_or_tuple_or_ndarray(ctype): nt = -1 else: while tuple(flatten(conditions, nc)) != tuple(flatten(conditions)): nc += 1 while tuple(flatten(ctype, nt)) != tuple(flatten(ctype)): nt += 1 if join is None: pass # don't use 'and/or' to join the conditions #elif nc >= 2: # join when is tuple of tuples of conditions else: # always use join, if given (instead of only if nc >= 2) if nt >= nc: # there as many or more nested ctypes than conditions p = iter(ctype) return join(*(generate_constraint(c, next(p), **kwds) for c in conditions)) return join(*(generate_constraint(c, ctype, **kwds) for c in conditions)) # flatten everything and produce the constraint conditions = list(flatten(conditions)) # allow for single ctype, list of ctypes, or nested list if ctype is None: from mystic.coupler import inner #XXX: outer ? ctype = list((inner,))*len(conditions) elif not list_or_tuple_or_ndarray(ctype): ctype = list((ctype,))*len(conditions) else: pass #XXX: is already a list, should be the same len as conditions ctype = list(flatten(ctype)) # iterate through solvers, building a compound constraints solver cf = lambda x:x cfdoc = "" for wrapper, condition in zip(ctype, conditions): cfdoc += "%s: %s\n" % (wrapper.__name__, condition.__doc__) apply = wrapper(condition, **kwds) cf = apply(cf) cf.__doc__ = cfdoc.rstrip('\n') cf.__name__ = 'constraint' return cf