def speedTest4(mutate = 2): """Test wrt sympy. Results - sympy is 10 to 24 times faster without using arrays (ouch!). - diffpy.srfit.equation is slightly slower when using arrays, but not considerably worse than versus numpy alone. """ from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 20, 0.05) qsig = 0.01 sigma = 0.003 eqstr = """\ b1 + b2*x + b3*x**2 + b4*x**3 + b5*x**4 + b6*x**5 + b7*x**6 + b8*x**7\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) from sympy import var, exp, lambdify from numpy import polyval b1, b2, b3, b4, b5, b6, b7, b8, xx = vars = var("b1 b2 b3 b4 b5 b6 b7 b8 xx") f = lambdify(vars, polyval([b1, b2, b3, b4, b5, b6, b7, b8], xx), "numpy") tnpy = 0 teq = 0 # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [1.0]*(len(eq.args)) args.append(x) # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() # Time the different functions with these arguments teq += timeFunction(eq, *(args[:-1])) tnpy += timeFunction(f, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "sympy: ", tnpy/numcalls print "equation: ", teq/numcalls print "ratio: ", teq/tnpy return
def speedTest4(mutate=2): """Test wrt sympy. Results - sympy is 10 to 24 times faster without using arrays (ouch!). - diffpy.srfit.equation is slightly slower when using arrays, but not considerably worse than versus numpy alone. """ from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 20, 0.05) eqstr = """\ b1 + b2*x + b3*x**2 + b4*x**3 + b5*x**4 + b6*x**5 + b7*x**6 + b8*x**7\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) from sympy import var, lambdify from numpy import polyval b1, b2, b3, b4, b5, b6, b7, b8, xx = vars = var( "b1 b2 b3 b4 b5 b6 b7 b8 xx") f = lambdify(vars, polyval([b1, b2, b3, b4, b5, b6, b7, b8], xx), "numpy") tnpy = 0 teq = 0 # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [1.0] * (len(eq.args)) args.append(x) # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() # Time the different functions with these arguments teq += timeFunction(eq, *(args[:-1])) tnpy += timeFunction(f, *args) print("Average call time (%i calls, %i mutations/call):" % (numcalls, mutate)) print("sympy: ", tnpy / numcalls) print("equation: ", teq / numcalls) print("ratio: ", teq / tnpy) return
def profileTest(): from diffpy.srfit.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 10, 0.001) qsig = 0.01 sigma = 0.003 eqstr = """\ b1 + b2*x + b3*x**2 + b4*x**3 + b5*x**4 + b6*x**5 + b7*x**6 + b8*x**7\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) mutate = 8 numargs = len(eq.args) choices = range(numargs) args = [0.1] * numargs # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() eq(*args) return
def profileTest(): from diffpy.srfit.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 10, 0.001) qsig = 0.01 sigma = 0.003 eqstr = """\ b1 + b2*x + b3*x**2 + b4*x**3 + b5*x**4 + b6*x**5 + b7*x**6 + b8*x**7\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) mutate = 8 numargs = len(eq.args) choices = range(numargs) args = [0.1]*numargs # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() eq(*args) return
def weightedTest(mutate=2): """Show the benefits of a properly balanced equation tree.""" from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 10, 0.01) qsig = 0.01 sigma = 0.003 eqstr = """\ b1 + b2*x + b3*x**2 + b4*x**3 + b5*x**4 + b6*x**5 + b7*x**6 + b8*x**7\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) #scale = visitors.NodeWeigher() #eq.root.identify(scale) #print scale.output from numpy import polyval def f(b1, b2, b3, b4, b5, b6, b7, b8): return polyval([b8, b7, b6, b5, b4, b3, b2, b1], x) tnpy = 0 teq = 0 # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [0.1] * numargs # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() #print args # Time the different functions with these arguments teq += timeFunction(eq, *args) tnpy += timeFunction(f, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "numpy: ", tnpy / numcalls print "equation: ", teq / numcalls print "ratio: ", teq / tnpy return
def speedTest3(mutate=2): """Test wrt sympy. Results - sympy is 10 to 24 times faster without using arrays (ouch!). - diffpy.srfit.equation is slightly slower when using arrays, but not considerably worse than versus numpy alone. """ from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 20, 0.05) qsig = 0.01 sigma = 0.003 eqstr = """\ A0*exp(-(x*qsig)**2)*(exp(-((x-1.0)/sigma1)**2)+exp(-((x-2.0)/sigma2)**2))\ + polyval(list(b1, b2, b3, b4, b5, b6, b7, b8), x)\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.qsig.setValue(qsig) eq.sigma1.setValue(sigma) eq.sigma2.setValue(sigma) eq.A0.setValue(1.0) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) from sympy import var, exp, lambdify from numpy import polyval A0, qsig, sigma1, sigma2, b1, b2, b3, b4, b5, b6, b7, b8, xx = vars = var( "A0 qsig sigma1 sigma2 b1 b2 b3 b4 b5 b6 b7 b8 xx") f = lambdify( vars, A0 * exp(-(xx * qsig)**2) * (exp(-((xx - 1.0) / sigma1)**2) + exp(-((xx - 2.0) / sigma2)**2)) + polyval([b1, b2, b3, b4, b5, b6, b7, b8], xx), "numpy") tnpy = 0 teq = 0 # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [1.0] * (len(eq.args)) args.append(x) # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() # Time the different functions with these arguments teq += timeFunction(eq, *(args[:-1])) tnpy += timeFunction(f, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "sympy: ", tnpy / numcalls print "equation: ", teq / numcalls print "ratio: ", teq / tnpy return
def speedTest2(mutate=2): from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 20, 0.05) qsig = 0.01 sigma = 0.003 eqstr = """\ A0*exp(-(x*qsig)**2)*(exp(-((x-1.0)/sigma1)**2)+exp(-((x-2.0)/sigma2)**2))\ + polyval(list(b1, b2, b3, b4, b5, b6, b7, b8), x)\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.qsig.setValue(qsig) eq.sigma1.setValue(sigma) eq.sigma2.setValue(sigma) eq.A0.setValue(1.0) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) from numpy import exp from numpy import polyval def f(A0, qsig, sigma1, sigma2, b1, b2, b3, b4, b5, b6, b7, b8): return A0 * exp(-(x * qsig)**2) * (exp(-( (x - 1.0) / sigma1)**2) + exp(-((x - 2.0) / sigma2)**2)) + polyval( [b8, b7, b6, b5, b4, b3, b2, b1], x) tnpy = 0 teq = 0 # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [0.0] * (len(eq.args)) # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() # Time the different functions with these arguments tnpy += timeFunction(f, *args) teq += timeFunction(eq, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "numpy: ", tnpy / numcalls print "equation: ", teq / numcalls print "ratio: ", teq / tnpy return
class RecipeOrganizer(_recipeorganizer_interface, RecipeContainer): """Extended base class for organizing pieces of a FitRecipe. This class extends RecipeContainer by organizing constraints and Restraints, as well as Equations that can be used in Constraint and Restraint equations. These constraints and Restraints can be placed at any level and a flattened list of them can be retrieved with the _getConstraints and _getRestraints methods. Attributes name -- A name for this organizer. Names should be unique within a RecipeOrganizer and should be valid attribute names. _calculators -- A managed dictionary of Calculators, indexed by name. _parameters -- A managed OrderedDict of contained Parameters. _constraints -- A dictionary of Constraints, indexed by the constrained Parameter. Constraints can be added using the 'constrain' method. _restraints -- A set of Restraints. Restraints can be added using the 'restrain' method. _eqfactory -- A diffpy.srfit.equation.builder.EquationFactory instance that is used create Equations from string. Properties names -- Variable names (read only). See getNames. values -- Variable values (read only). See getValues. Raises ValueError if the name is not a valid attribute identifier """ def __init__(self, name): RecipeContainer.__init__(self, name) self._restraints = set() self._constraints = {} self._eqfactory = EquationFactory() self._calculators = {} self._manage(self._calculators) return # Parameter management def _newParameter(self, name, value, check=True): """Add a new Parameter to the container. This creates a new Parameter and adds it to the container using the _addParameter method. Returns the Parameter. """ p = Parameter(name, value) self._addParameter(p, check) return p def _addParameter(self, par, check=True): """Store a Parameter. Parameters added in this way are registered with the _eqfactory. par -- The Parameter to be stored. check -- If True (default), a ValueError is raised a Parameter of the specified name has already been inserted. Raises ValueError if the Parameter has no name. Raises ValueError if the Parameter has the same name as a contained RecipeContainer. """ # Store the Parameter RecipeContainer._addObject(self, par, self._parameters, check) # Register the Parameter self._eqfactory.registerArgument(par.name, par) return def _removeParameter(self, par): """Remove a parameter. This de-registers the Parameter with the _eqfactory. The Parameter will remain part of built equations. Note that constraints and restraints involving the Parameter are not modified. Raises ValueError if par is not part of the RecipeOrganizer. """ self._removeObject(par, self._parameters) self._eqfactory.deRegisterBuilder(par.name) return def registerCalculator(self, f, argnames=None): """Register a Calculator so it can be used within equation strings. A Calculator is an elaborate function that can organize Parameters. This creates a function with this class that can be used within string equations. The resulting equation can be used in a string with arguments like a function or without, in which case the values of the Parameters created from argnames will be be used to compute the value. f -- The Calculator to register. argnames -- The names of the arguments to f (list or None). If this is None, then the argument names will be extracted from the function. """ self._eqfactory.registerOperator(f.name, f) self._addObject(f, self._calculators) # Register arguments of the calculator if argnames is None: func_code = f.__call__.im_func.func_code argnames = list(func_code.co_varnames) argnames = argnames[1 : func_code.co_argcount] for pname in argnames: if pname not in self._eqfactory.builders: par = self._newParameter(pname, 0) else: par = self.get(pname) f.addLiteral(par) # Now return an equation object eq = self._eqfactory.makeEquation(f.name) return eq def registerFunction(self, f, name=None, argnames=None): """Register a function so it can be used within equation strings. This creates a function with this class that can be used within string equations. The resulting equation does not require the arguments to be passed in the equation string, as this will be handled automatically. f -- The callable to register. If this is an Equation instance, then all that needs to be provied is a name. name -- The name of the function to be used in equations. If this is None (default), the method will try to determine the name of the function automatically. argnames -- The names of the arguments to f (list or None). If this is None (default), then the argument names will be extracted from the function. Note that name and argnames can be extracted from regular python functions (of type 'function'), bound class methods and callable classes. Raises TypeError if name or argnames cannot be automatically extracted. Raises TypeError if an automatically extracted name is '<lambda>'. Raises ValueError if f is an Equation object and name is None. Returns the callable Equation object. """ # If the function is an equation, we treat it specially. This is # required so that the objects observed by the root get observed if the # Equation is used within another equation. It is assumed that a plain # function is not observable. if isinstance(f, Equation): if name is None: m = "Equation must be given a name" raise ValueError(m) self._eqfactory.registerOperator(name, f) return f #### Introspection code if name is None or argnames is None: import inspect func_code = None # This will let us offset the argument list to eliminate 'self' offset = 0 # check regular functions if inspect.isfunction(f): func_code = f.func_code # check class method elif inspect.ismethod(f): func_code = f.im_func.func_code offset = 1 # check functor elif hasattr(f, "__call__") and hasattr(f.__call__, "im_func"): func_code = f.__call__.im_func.func_code offset = 1 else: m = "Cannot extract name or argnames" raise ValueError(m) # Extract the name if name is None: name = func_code.co_name if name == "<lambda>": m = "You must supply a name name for a lambda function" raise ValueError(m) # Extract the arguments if argnames is None: argnames = list(func_code.co_varnames) argnames = argnames[offset : func_code.co_argcount] #### End introspection code # Make missing Parameters for pname in argnames: if pname not in self._eqfactory.builders: self._newParameter(pname, 0) # Initialize and register from diffpy.srfit.fitbase.calculator import Calculator if isinstance(f, Calculator): for pname in argnames: par = self.get(pname) f.addLiteral(par) self._eqfactory.registerOperator(name, f) else: self._eqfactory.registerFunction(name, f, argnames) # Now we can create the Equation and return it to the user. eq = self._eqfactory.makeEquation(name) return eq def registerStringFunction(self, fstr, name, ns={}): """Register a string function. This creates a function with this class that can be used within string equations. The resulting equation does not require the arguments to be passed in the function string, as this will be handled automatically. fstr -- A string equation to register. name -- The name of the function to be used in equations. ns -- A dictionary of Parameters, indexed by name, that are used in fstr, but not part of the FitRecipe (default {}). Raises ValueError if ns uses a name that is already used for another managed object. Raises ValueError if the function name is the name of another managed object. Returns the callable Equation object. """ # Build the equation instance. eq = equationFromString(fstr, self._eqfactory, ns=ns, buildargs=True) eq.name = name # Register any new Parameters. for par in self._eqfactory.newargs: self._addParameter(par) # Register the equation as a callable function. argnames = eq.argdict.keys() return self.registerFunction(eq, name, argnames) def evaluateEquation(self, eqstr, ns={}): """Evaluate a string equation. eqstr -- A string equation to evaluate. The equation is evaluated at the current value of the registered Parameters. ns -- A dictionary of Parameters, indexed by name, that are used in fstr, but not part of the FitRecipe (default {}). Raises ValueError if ns uses a name that is already used for a variable. """ eq = equationFromString(eqstr, self._eqfactory, ns) return eq() def constrain(self, par, con, ns={}): """Constrain a parameter to an equation. Note that only one constraint can exist on a Parameter at a time. par -- The name of a Parameter or a Parameter to constrain. con -- A string representation of the constraint equation or a Parameter to constrain to. A constraint equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in the ns argument, or if they are managed by this object. ns -- A dictionary of Parameters, indexed by name, that are used in the parameter, but not part of this object (default {}). Raises ValueError if ns uses a name that is already used for a variable. Raises ValueError if par is a string but not part of this object or in ns. Raises ValueError if par is marked as constant. """ if isinstance(par, basestring): name = par par = self.get(name) if par is None: par = ns.get(name) if par is None: raise ValueError("The parameter cannot be found") if par.const: raise ValueError("The parameter '%s' is constant" % par) if isinstance(con, basestring): eqstr = con eq = equationFromString(con, self._eqfactory, ns) else: eq = Equation(root=con) eqstr = con.name eq.name = "_constraint_%s" % par.name # Make and store the constraint con = Constraint() con.constrain(par, eq) # Store the equation string so it can be shown later. con.eqstr = eqstr self._constraints[par] = con # Our configuration changed self._updateConfiguration() return def isConstrained(self, par): """Determine if a Parameter is constrained in this object. par -- The name of a Parameter or a Parameter to check. """ if isinstance(par, basestring): name = par par = self.get(name) return par in self._constraints def unconstrain(self, *pars): """Unconstrain a Parameter. This removes any constraints on a Parameter. *pars -- The names of Parameters or Parameters to unconstrain. Raises ValueError if the Parameter is not constrained. """ update = False for par in pars: if isinstance(par, basestring): name = par par = self.get(name) if par is None: raise ValueError("The parameter cannot be found") if par in self._constraints: self._constraints[par].unconstrain() del self._constraints[par] update = True if update: # Our configuration changed self._updateConfiguration() else: raise ValueError("The parameter is not constrained") return def getConstrainedPars(self, recurse=False): """Get a list of constrained managed Parameters in this object. recurse -- Recurse into managed objects and retrive their constrained Parameters as well (default False). """ const = self._getConstraints(recurse) return const.keys() def clearConstraints(self, recurse=False): """Clear all constraints managed by this organizer. recurse -- Recurse into managed objects and clear all constraints found there as well. This removes constraints that are held in this organizer, no matter where the constrained parameters are from. """ if self._constraints: self.unconstrain(*self._constraints) if recurse: f = lambda m: hasattr(m, "clearConstraints") for m in ifilter(f, self._iterManaged()): m.clearConstraints(recurse) return def restrain(self, res, lb=-inf, ub=inf, sig=1, scaled=False, ns={}): """Restrain an expression to specified bounds res -- An equation string or Parameter to restrain. lb -- The lower bound on the restraint evaluation (default -inf). ub -- The lower bound on the restraint evaluation (default inf). sig -- The uncertainty on the bounds (default 1). scaled -- A flag indicating if the restraint is scaled (multiplied) by the unrestrained point-average chi^2 (chi^2/numpoints) (default False). ns -- A dictionary of Parameters, indexed by name, that are used in the equation string, but not part of the RecipeOrganizer (default {}). The penalty is calculated as (max(0, lb - val, val - ub)/sig)**2 and val is the value of the calculated equation. This is multipled by the average chi^2 if scaled is True. Raises ValueError if ns uses a name that is already used for a Parameter. Raises ValueError if res depends on a Parameter that is not part of the RecipeOrganizer and that is not defined in ns. Returns the Restraint object for use with the 'unrestrain' method. """ if isinstance(res, basestring): eqstr = res eq = equationFromString(res, self._eqfactory, ns) else: eq = Equation(root=res) eqstr = res.name # Make and store the restraint res = Restraint(eq, lb, ub, sig, scaled) res.eqstr = eqstr self.addRestraint(res) return res def addRestraint(self, res): """Add a Restraint instance to the RecipeOrganizer. res -- A Restraint instance. """ self._restraints.add(res) # Our configuration changed. Notify observers. self._updateConfiguration() return def unrestrain(self, *ress): """Remove a Restraint from the RecipeOrganizer. *ress -- Restraints returned from the 'restrain' method or added with the 'addRestraint' method. """ update = False restuple = tuple(self._restraints) for res in ress: if res in restuple: self._restraints.remove(res) update = True if update: # Our configuration changed self._updateConfiguration() return def clearRestraints(self, recurse=False): """Clear all restraints. recurse -- Recurse into managed objects and clear all restraints found there as well. """ self.unrestrain(*self._restraints) if recurse: f = lambda m: hasattr(m, "clearRestraints") for m in ifilter(f, self._iterManaged()): m.clearRestraints(recurse) return def _getConstraints(self, recurse=True): """Get the constrained Parameters for this and managed sub-objects.""" constraints = {} if recurse: f = lambda m: hasattr(m, "_getConstraints") for m in ifilter(f, self._iterManaged()): constraints.update(m._getConstraints(recurse)) constraints.update(self._constraints) return constraints def _getRestraints(self, recurse=True): """Get the Restraints for this and embedded ParameterSets. This returns a set of Restraint objects. """ restraints = set(self._restraints) if recurse: f = lambda m: hasattr(m, "_getRestraints") for m in ifilter(f, self._iterManaged()): restraints.update(m._getRestraints(recurse)) return restraints def _validate(self): """Validate my state. This performs RecipeContainer validations. This validates contained Restraints and Constraints. Raises AttributeError if validation fails. """ RecipeContainer._validate(self) iterable = chain(iter(self._restraints), self._constraints.itervalues()) self._validateOthers(iterable) return # For printing the configured recipe to screen def _formatManaged(self, indent=""): """Format fit hierarchy for showing. Returns the lines of the formatted string in a list. """ dashedline = 79 * "-" lines = [] formatstr = "%-20s %s" lines.append((indent + self.name)[:79]) # Show parameters if self._parameters: lines.append((indent + dashedline)[:79]) items = self._parameters.items() items.sort() lines.extend((indent + formatstr % (n, p.value))[:79] for n, p in items) indent += " " for obj in self._iterManaged(): if hasattr(obj, "_formatManaged"): tlines = obj._formatManaged(indent) lines.append("") lines.extend(tlines) return lines def _formatConstraints(self): """Format constraints for showing. This collects constraints on all levels of the hierarchy and displays them with respect to this level. Returns the lines of the formatted string in a list. """ cdict = self._getConstraints() # Find each constraint and format the equation clines = [] for par, con in cdict.items(): loc = self._locateManagedObject(par) if loc: locstr = ".".join(o.name for o in loc) clines.append("%s <-- %s" % (locstr, con.eqstr)) else: clines.append("%s <-- %s" % (par.name, con.eqstr)) if clines: clines.sort() dashedline = 79 * "-" clines.insert(0, dashedline) clines.insert(0, "Constraints") return clines def _formatRestraints(self): """Format restraints for showing. This collects restraints on all levels of the hierarchy and displays them with respect to this level. Returns the lines of the formatted string in a list. """ rset = self._getRestraints() rlines = [] for res in rset: line = "%s: lb = %f, ub = %f, sig = %f, scaled = %s" % (res.eqstr, res.lb, res.ub, res.sig, res.scaled) rlines.append(line) if rlines: rlines.sort() dashedline = 79 * "-" rlines.insert(0, dashedline) rlines.insert(0, "Restraints") return rlines def show(self): """Show the configuration on screen. This will print a summary of all contained objects. """ # Show sub objects and their parameters lines = [] tlines = self._formatManaged() lines.extend(tlines) # FIXME - parameter names in equations not particularly informative # Show constraints tlines = self._formatConstraints() lines.append("") lines.extend(tlines) # FIXME - parameter names in equations not particularly informative # Show restraints tlines = self._formatRestraints() lines.append("") lines.extend(tlines) print "\n".join(lines) return
class RecipeOrganizer(_recipeorganizer_interface, RecipeContainer): """Extended base class for organizing pieces of a FitRecipe. This class extends RecipeContainer by organizing constraints and Restraints, as well as Equations that can be used in Constraint and Restraint equations. These constraints and Restraints can be placed at any level and a flattened list of them can be retrieved with the _getConstraints and _getRestraints methods. Attributes name -- A name for this organizer. Names should be unique within a RecipeOrganizer and should be valid attribute names. _calculators -- A managed dictionary of Calculators, indexed by name. _parameters -- A managed OrderedDict of contained Parameters. _constraints -- A dictionary of Constraints, indexed by the constrained Parameter. Constraints can be added using the 'constrain' method. _restraints -- A set of Restraints. Restraints can be added using the 'restrain' method. _eqfactory -- A diffpy.srfit.equation.builder.EquationFactory instance that is used create Equations from string. Properties names -- Variable names (read only). See getNames. values -- Variable values (read only). See getValues. Raises ValueError if the name is not a valid attribute identifier """ def __init__(self, name): RecipeContainer.__init__(self, name) self._restraints = set() self._constraints = {} self._eqfactory = EquationFactory() self._calculators = {} self._manage(self._calculators) return # Parameter management def _newParameter(self, name, value, check=True): """Add a new Parameter to the container. This creates a new Parameter and adds it to the container using the _addParameter method. Returns the Parameter. """ p = Parameter(name, value) self._addParameter(p, check) return p def _addParameter(self, par, check=True): """Store a Parameter. Parameters added in this way are registered with the _eqfactory. par -- The Parameter to be stored. check -- If True (default), a ValueError is raised a Parameter of the specified name has already been inserted. Raises ValueError if the Parameter has no name. Raises ValueError if the Parameter has the same name as a contained RecipeContainer. """ # Store the Parameter RecipeContainer._addObject(self, par, self._parameters, check) # Register the Parameter self._eqfactory.registerArgument(par.name, par) return def _removeParameter(self, par): """Remove a parameter. This de-registers the Parameter with the _eqfactory. The Parameter will remain part of built equations. Note that constraints and restraints involving the Parameter are not modified. Raises ValueError if par is not part of the RecipeOrganizer. """ self._removeObject(par, self._parameters) self._eqfactory.deRegisterBuilder(par.name) return def registerCalculator(self, f, argnames=None): """Register a Calculator so it can be used within equation strings. A Calculator is an elaborate function that can organize Parameters. This creates a function with this class that can be used within string equations. The resulting equation can be used in a string with arguments like a function or without, in which case the values of the Parameters created from argnames will be be used to compute the value. f -- The Calculator to register. argnames -- The names of the arguments to f (list or None). If this is None, then the argument names will be extracted from the function. """ self._eqfactory.registerOperator(f.name, f) self._addObject(f, self._calculators) # Register arguments of the calculator if argnames is None: func_code = f.__call__.im_func.func_code argnames = list(func_code.co_varnames) argnames = argnames[1:func_code.co_argcount] for pname in argnames: if pname not in self._eqfactory.builders: par = self._newParameter(pname, 0) else: par = self.get(pname) f.addLiteral(par) # Now return an equation object eq = self._eqfactory.makeEquation(f.name) return eq def registerFunction(self, f, name=None, argnames=None): """Register a function so it can be used within equation strings. This creates a function with this class that can be used within string equations. The resulting equation does not require the arguments to be passed in the equation string, as this will be handled automatically. f -- The callable to register. If this is an Equation instance, then all that needs to be provied is a name. name -- The name of the function to be used in equations. If this is None (default), the method will try to determine the name of the function automatically. argnames -- The names of the arguments to f (list or None). If this is None (default), then the argument names will be extracted from the function. Note that name and argnames can be extracted from regular python functions (of type 'function'), bound class methods and callable classes. Raises TypeError if name or argnames cannot be automatically extracted. Raises TypeError if an automatically extracted name is '<lambda>'. Raises ValueError if f is an Equation object and name is None. Returns the callable Equation object. """ # If the function is an equation, we treat it specially. This is # required so that the objects observed by the root get observed if the # Equation is used within another equation. It is assumed that a plain # function is not observable. if isinstance(f, Equation): if name is None: m = "Equation must be given a name" raise ValueError(m) self._eqfactory.registerOperator(name, f) return f #### Introspection code if name is None or argnames is None: import inspect func_code = None # This will let us offset the argument list to eliminate 'self' offset = 0 # check regular functions if inspect.isfunction(f): func_code = f.func_code # check class method elif inspect.ismethod(f): func_code = f.im_func.func_code offset = 1 # check functor elif hasattr(f, "__call__") and hasattr(f.__call__, 'im_func'): func_code = f.__call__.im_func.func_code offset = 1 else: m = "Cannot extract name or argnames" raise ValueError(m) # Extract the name if name is None: name = func_code.co_name if name == '<lambda>': m = "You must supply a name name for a lambda function" raise ValueError(m) # Extract the arguments if argnames is None: argnames = list(func_code.co_varnames) argnames = argnames[offset:func_code.co_argcount] #### End introspection code # Make missing Parameters for pname in argnames: if pname not in self._eqfactory.builders: self._newParameter(pname, 0) # Initialize and register from diffpy.srfit.fitbase.calculator import Calculator if isinstance(f, Calculator): for pname in argnames: par = self.get(pname) f.addLiteral(par) self._eqfactory.registerOperator(name, f) else: self._eqfactory.registerFunction(name, f, argnames) # Now we can create the Equation and return it to the user. eq = self._eqfactory.makeEquation(name) return eq def registerStringFunction(self, fstr, name, ns={}): """Register a string function. This creates a function with this class that can be used within string equations. The resulting equation does not require the arguments to be passed in the function string, as this will be handled automatically. fstr -- A string equation to register. name -- The name of the function to be used in equations. ns -- A dictionary of Parameters, indexed by name, that are used in fstr, but not part of the FitRecipe (default {}). Raises ValueError if ns uses a name that is already used for another managed object. Raises ValueError if the function name is the name of another managed object. Returns the callable Equation object. """ # Build the equation instance. eq = equationFromString(fstr, self._eqfactory, ns=ns, buildargs=True) eq.name = name # Register any new Parameters. for par in self._eqfactory.newargs: self._addParameter(par) # Register the equation as a callable function. argnames = eq.argdict.keys() return self.registerFunction(eq, name, argnames) def evaluateEquation(self, eqstr, ns={}): """Evaluate a string equation. eqstr -- A string equation to evaluate. The equation is evaluated at the current value of the registered Parameters. ns -- A dictionary of Parameters, indexed by name, that are used in fstr, but not part of the FitRecipe (default {}). Raises ValueError if ns uses a name that is already used for a variable. """ eq = equationFromString(eqstr, self._eqfactory, ns) return eq() def constrain(self, par, con, ns={}): """Constrain a parameter to an equation. Note that only one constraint can exist on a Parameter at a time. par -- The name of a Parameter or a Parameter to constrain. con -- A string representation of the constraint equation or a Parameter to constrain to. A constraint equation must consist of numpy operators and "known" Parameters. Parameters are known if they are in the ns argument, or if they are managed by this object. ns -- A dictionary of Parameters, indexed by name, that are used in the parameter, but not part of this object (default {}). Raises ValueError if ns uses a name that is already used for a variable. Raises ValueError if par is a string but not part of this object or in ns. Raises ValueError if par is marked as constant. """ if isinstance(par, basestring): name = par par = self.get(name) if par is None: par = ns.get(name) if par is None: raise ValueError("The parameter cannot be found") if par.const: raise ValueError("The parameter '%s' is constant" % par) if isinstance(con, basestring): eqstr = con eq = equationFromString(con, self._eqfactory, ns) else: eq = Equation(root=con) eqstr = con.name eq.name = "_constraint_%s" % par.name # Make and store the constraint con = Constraint() con.constrain(par, eq) # Store the equation string so it can be shown later. con.eqstr = eqstr self._constraints[par] = con # Our configuration changed self._updateConfiguration() return def isConstrained(self, par): """Determine if a Parameter is constrained in this object. par -- The name of a Parameter or a Parameter to check. """ if isinstance(par, basestring): name = par par = self.get(name) return (par in self._constraints) def unconstrain(self, *pars): """Unconstrain a Parameter. This removes any constraints on a Parameter. *pars -- The names of Parameters or Parameters to unconstrain. Raises ValueError if the Parameter is not constrained. """ update = False for par in pars: if isinstance(par, basestring): name = par par = self.get(name) if par is None: raise ValueError("The parameter cannot be found") if par in self._constraints: self._constraints[par].unconstrain() del self._constraints[par] update = True if update: # Our configuration changed self._updateConfiguration() else: raise ValueError("The parameter is not constrained") return def getConstrainedPars(self, recurse=False): """Get a list of constrained managed Parameters in this object. recurse -- Recurse into managed objects and retrive their constrained Parameters as well (default False). """ const = self._getConstraints(recurse) return const.keys() def clearConstraints(self, recurse=False): """Clear all constraints managed by this organizer. recurse -- Recurse into managed objects and clear all constraints found there as well. This removes constraints that are held in this organizer, no matter where the constrained parameters are from. """ if self._constraints: self.unconstrain(*self._constraints) if recurse: f = lambda m: hasattr(m, "clearConstraints") for m in ifilter(f, self._iterManaged()): m.clearConstraints(recurse) return def restrain(self, res, lb=-inf, ub=inf, sig=1, scaled=False, ns={}): """Restrain an expression to specified bounds res -- An equation string or Parameter to restrain. lb -- The lower bound on the restraint evaluation (default -inf). ub -- The lower bound on the restraint evaluation (default inf). sig -- The uncertainty on the bounds (default 1). scaled -- A flag indicating if the restraint is scaled (multiplied) by the unrestrained point-average chi^2 (chi^2/numpoints) (default False). ns -- A dictionary of Parameters, indexed by name, that are used in the equation string, but not part of the RecipeOrganizer (default {}). The penalty is calculated as (max(0, lb - val, val - ub)/sig)**2 and val is the value of the calculated equation. This is multipled by the average chi^2 if scaled is True. Raises ValueError if ns uses a name that is already used for a Parameter. Raises ValueError if res depends on a Parameter that is not part of the RecipeOrganizer and that is not defined in ns. Returns the Restraint object for use with the 'unrestrain' method. """ if isinstance(res, basestring): eqstr = res eq = equationFromString(res, self._eqfactory, ns) else: eq = Equation(root=res) eqstr = res.name # Make and store the restraint res = Restraint(eq, lb, ub, sig, scaled) res.eqstr = eqstr self.addRestraint(res) return res def addRestraint(self, res): """Add a Restraint instance to the RecipeOrganizer. res -- A Restraint instance. """ self._restraints.add(res) # Our configuration changed. Notify observers. self._updateConfiguration() return def unrestrain(self, *ress): """Remove a Restraint from the RecipeOrganizer. *ress -- Restraints returned from the 'restrain' method or added with the 'addRestraint' method. """ update = False restuple = tuple(self._restraints) for res in ress: if res in restuple: self._restraints.remove(res) update = True if update: # Our configuration changed self._updateConfiguration() return def clearRestraints(self, recurse=False): """Clear all restraints. recurse -- Recurse into managed objects and clear all restraints found there as well. """ self.unrestrain(*self._restraints) if recurse: f = lambda m: hasattr(m, "clearRestraints") for m in ifilter(f, self._iterManaged()): m.clearRestraints(recurse) return def _getConstraints(self, recurse=True): """Get the constrained Parameters for this and managed sub-objects.""" constraints = {} if recurse: f = lambda m: hasattr(m, "_getConstraints") for m in ifilter(f, self._iterManaged()): constraints.update(m._getConstraints(recurse)) constraints.update(self._constraints) return constraints def _getRestraints(self, recurse=True): """Get the Restraints for this and embedded ParameterSets. This returns a set of Restraint objects. """ restraints = set(self._restraints) if recurse: f = lambda m: hasattr(m, "_getRestraints") for m in ifilter(f, self._iterManaged()): restraints.update(m._getRestraints(recurse)) return restraints def _validate(self): """Validate my state. This performs RecipeContainer validations. This validates contained Restraints and Constraints. Raises AttributeError if validation fails. """ RecipeContainer._validate(self) iterable = chain(iter(self._restraints), self._constraints.itervalues()) self._validateOthers(iterable) return # For printing the configured recipe to screen def _formatManaged(self, indent=""): """Format fit hierarchy for showing. Returns the lines of the formatted string in a list. """ dashedline = 79 * '-' lines = [] formatstr = "%-20s %s" lines.append((indent + self.name)[:79]) # Show parameters if self._parameters: lines.append((indent + dashedline)[:79]) items = self._parameters.items() items.sort() lines.extend( (indent + formatstr % (n, p.value))[:79] for n, p in items) indent += " " for obj in self._iterManaged(): if hasattr(obj, "_formatManaged"): tlines = obj._formatManaged(indent) lines.append("") lines.extend(tlines) return lines def _formatConstraints(self): """Format constraints for showing. This collects constraints on all levels of the hierarchy and displays them with respect to this level. Returns the lines of the formatted string in a list. """ cdict = self._getConstraints() # Find each constraint and format the equation clines = [] for par, con in cdict.items(): loc = self._locateManagedObject(par) if loc: locstr = ".".join(o.name for o in loc) clines.append("%s <-- %s" % (locstr, con.eqstr)) else: clines.append("%s <-- %s" % (par.name, con.eqstr)) if clines: clines.sort() dashedline = 79 * '-' clines.insert(0, dashedline) clines.insert(0, "Constraints") return clines def _formatRestraints(self): """Format restraints for showing. This collects restraints on all levels of the hierarchy and displays them with respect to this level. Returns the lines of the formatted string in a list. """ rset = self._getRestraints() rlines = [] for res in rset: line = "%s: lb = %f, ub = %f, sig = %f, scaled = %s"%\ (res.eqstr, res.lb, res.ub, res.sig, res.scaled) rlines.append(line) if rlines: rlines.sort() dashedline = 79 * '-' rlines.insert(0, dashedline) rlines.insert(0, "Restraints") return rlines def show(self): """Show the configuration on screen. This will print a summary of all contained objects. """ # Show sub objects and their parameters lines = [] tlines = self._formatManaged() lines.extend(tlines) # FIXME - parameter names in equations not particularly informative # Show constraints tlines = self._formatConstraints() lines.append("") lines.extend(tlines) # FIXME - parameter names in equations not particularly informative # Show restraints tlines = self._formatRestraints() lines.append("") lines.extend(tlines) print "\n".join(lines) return
def weightedTest(mutate = 2): """Show the benefits of a properly balanced equation tree.""" from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 10, 0.01) qsig = 0.01 sigma = 0.003 eqstr = """\ b1 + b2*x + b3*x**2 + b4*x**3 + b5*x**4 + b6*x**5 + b7*x**6 + b8*x**7\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) #scale = visitors.NodeWeigher() #eq.root.identify(scale) #print scale.output from numpy import polyval def f(b1, b2, b3, b4, b5, b6, b7, b8): return polyval([b8, b7, b6, b5,b4,b3,b2,b1],x) tnpy = 0 teq = 0 import random # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [0.1]*numargs # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() #print args # Time the different functions with these arguments teq += timeFunction(eq, *args) tnpy += timeFunction(f, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "numpy: ", tnpy/numcalls print "equation: ", teq/numcalls print "ratio: ", teq/tnpy return
def speedTest3(mutate = 2): """Test wrt sympy. Results - sympy is 10 to 24 times faster without using arrays (ouch!). - diffpy.srfit.equation is slightly slower when using arrays, but not considerably worse than versus numpy alone. """ from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 20, 0.05) qsig = 0.01 sigma = 0.003 eqstr = """\ A0*exp(-(x*qsig)**2)*(exp(-((x-1.0)/sigma1)**2)+exp(-((x-2.0)/sigma2)**2))\ + polyval(list(b1, b2, b3, b4, b5, b6, b7, b8), x)\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.qsig.setValue(qsig) eq.sigma1.setValue(sigma) eq.sigma2.setValue(sigma) eq.A0.setValue(1.0) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) from sympy import var, exp, lambdify from numpy import polyval A0, qsig, sigma1, sigma2, b1, b2, b3, b4, b5, b6, b7, b8, xx = vars = var("A0 qsig sigma1 sigma2 b1 b2 b3 b4 b5 b6 b7 b8 xx") f = lambdify(vars, A0*exp(-(xx*qsig)**2)*(exp(-((xx-1.0)/sigma1)**2)+exp(-((xx-2.0)/sigma2)**2)) + polyval([b1, b2, b3, b4, b5, b6, b7, b8], xx), "numpy") tnpy = 0 teq = 0 import random # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [1.0]*(len(eq.args)) args.append(x) # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() # Time the different functions with these arguments teq += timeFunction(eq, *(args[:-1])) tnpy += timeFunction(f, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "sympy: ", tnpy/numcalls print "equation: ", teq/numcalls print "ratio: ", teq/tnpy return
def speedTest2(mutate = 2): from diffpy.srfit.equation.builder import EquationFactory factory = EquationFactory() x = numpy.arange(0, 20, 0.05) qsig = 0.01 sigma = 0.003 eqstr = """\ A0*exp(-(x*qsig)**2)*(exp(-((x-1.0)/sigma1)**2)+exp(-((x-2.0)/sigma2)**2))\ + polyval(list(b1, b2, b3, b4, b5, b6, b7, b8), x)\ """ factory.registerConstant("x", x) eq = factory.makeEquation(eqstr) eq.qsig.setValue(qsig) eq.sigma1.setValue(sigma) eq.sigma2.setValue(sigma) eq.A0.setValue(1.0) eq.b1.setValue(0) eq.b2.setValue(1) eq.b3.setValue(2.0) eq.b4.setValue(2.0) eq.b5.setValue(2.0) eq.b6.setValue(2.0) eq.b7.setValue(2.0) eq.b8.setValue(2.0) from numpy import exp from numpy import polyval def f(A0, qsig, sigma1, sigma2, b1, b2, b3, b4, b5, b6, b7, b8): return A0*exp(-(x*qsig)**2)*(exp(-((x-1.0)/sigma1)**2)+exp(-((x-2.0)/sigma2)**2)) + polyval([b8, b7, b6, b5,b4,b3,b2,b1],x) tnpy = 0 teq = 0 import random # Randomly change variables numargs = len(eq.args) choices = range(numargs) args = [0.0]*(len(eq.args)) # The call-loop random.seed() numcalls = 1000 for _i in xrange(numcalls): # Mutate values n = mutate if n == 0: n = random.choice(choices) c = choices[:] for _j in xrange(n): idx = random.choice(c) c.remove(idx) args[idx] = random.random() # Time the different functions with these arguments tnpy += timeFunction(f, *args) teq += timeFunction(eq, *args) print "Average call time (%i calls, %i mutations/call):" % (numcalls, mutate) print "numpy: ", tnpy/numcalls print "equation: ", teq/numcalls print "ratio: ", teq/tnpy return