From 91fba7c2f6d4faad24605ce2abf20bc5655e80b0 Mon Sep 17 00:00:00 2001 From: Caden Myers Date: Mon, 12 Oct 2026 11:56:16 -0400 Subject: [PATCH] Deprecate camelcase functions in srfit.equation and refactor the tests for pytest --- news/equation-tests.rst | 41 ++ src/diffpy/srfit/equation/builder.py | 604 +++++++++++----- src/diffpy/srfit/equation/equationmod.py | 197 ++++-- .../srfit/equation/literals/__init__.py | 2 + src/diffpy/srfit/equation/literals/abcs.py | 2 +- .../srfit/equation/literals/argument.py | 62 +- src/diffpy/srfit/equation/literals/literal.py | 41 +- .../srfit/equation/literals/operators.py | 59 +- .../srfit/equation/visitors/__init__.py | 123 +++- .../srfit/equation/visitors/argfinder.py | 54 +- src/diffpy/srfit/equation/visitors/printer.py | 67 +- src/diffpy/srfit/equation/visitors/swapper.py | 58 +- .../srfit/equation/visitors/validator.py | 47 +- src/diffpy/srfit/equation/visitors/visitor.py | 99 ++- src/diffpy/srfit/fitbase/fitcontribution.py | 12 +- src/diffpy/srfit/fitbase/profilegenerator.py | 2 +- src/diffpy/srfit/fitbase/recipeorganizer.py | 24 +- tests/conftest.py | 28 +- .../deprecated/test_equation_deprecations.py | 454 ++++++++++++ tests/test_builder.py | 657 ++++++++++++------ tests/test_constraint.py | 8 +- tests/test_contribution.py | 20 +- tests/test_equation.py | 397 ++++++----- tests/test_literals.py | 495 +++++++------ tests/test_recipeorganizer.py | 8 +- tests/test_restraint.py | 4 +- tests/test_speed.py | 44 +- tests/test_visitors.py | 439 ++++++------ 28 files changed, 2766 insertions(+), 1282 deletions(-) create mode 100644 news/equation-tests.rst create mode 100644 tests/deprecated/test_equation_deprecations.py diff --git a/news/equation-tests.rst b/news/equation-tests.rst new file mode 100644 index 00000000..82165964 --- /dev/null +++ b/news/equation-tests.rst @@ -0,0 +1,41 @@ +**Added:** + +* Add ``make_equation``, ``register_constant``, ``register_argument``, + ``register_operator``, ``register_builder`` and ``deregister_builder`` to + ``EquationFactory``, and ``wrap_argument``, ``wrap_operator``, + ``wrap_function`` and ``get_builder`` to ``diffpy.srfit.equation.builder``. +* Add ``Equation.set_root``, ``Operator.add_literal`` and ``make_operator`` to + ``diffpy.srfit.equation``. +* Add ``get_args``, ``get_expression`` and ``pretty_print`` to + ``diffpy.srfit.equation.visitors``, and ``on_argument``, ``on_operator`` and + ``on_equation`` to ``Visitor``. + +**Changed:** + +* ``OperatorABC`` now requires ``add_literal`` instead of ``addLiteral``, and + ``Visitor`` subclasses now override ``on_argument``, ``on_operator`` and + ``on_equation`` instead of ``onArgument``, ``onOperator`` and + ``onEquation``. + +**Deprecated:** + +* ``EquationFactory.makeEquation``, ``registerConstant``, ``registerArgument``, + ``registerOperator``, ``registerBuilder`` and ``deRegisterBuilder``, + ``wrapArgument``, ``wrapOperator``, ``wrapFunction``, ``getBuilder``, + ``Equation.setRoot``, ``Operator.addLiteral``, ``makeOperator``, + ``getArgs``, ``getExpression``, ``prettyPrint`` and ``Visitor.onArgument``, + ``onOperator`` and ``onEquation`` are deprecated in favor of their + snake_case replacements. The old names still work and warn, and will be + removed in version 4.0.0. + +**Removed:** + +* + +**Fixed:** + +* Fix the test check for observers left on global equation builders. + +**Security:** + +* diff --git a/src/diffpy/srfit/equation/builder.py b/src/diffpy/srfit/equation/builder.py index 764b0d98..3969b6eb 100644 --- a/src/diffpy/srfit/equation/builder.py +++ b/src/diffpy/srfit/equation/builder.py @@ -22,28 +22,28 @@ the factory. An example using the EquationFactory: -The makeEquation method turns the string-representation of an equation into +The make_equation method turns the string-representation of an equation into a callable object. > factory = EquationFactory() -> eq = factory.makeEquation("A*sin(a*x)") +> eq = factory.make_equation("A*sin(a*x)") will create an equation that evaluates as "A*sin(a*x)". The equation takes no arguments. Custom Arguments and constants can be included in the equation: -> factory.registerConstant("offset", 3) +> factory.register_constant("offset", 3) > A = Argument(name = "A", value = 1.0) -> factory.registerArgument("A", A) -> eq = factory.makeEquation("A*sin(a*x) + offset") +> factory.register_argument("A", A) +> eq = factory.make_equation("A*sin(a*x) + offset") This includes a constant offset in the equation and makes sure that the user-defined Argument is in the equation. This can be used to assure that the same instance of an Argument appears in multiple equations. Other literals can be registered in a similar fashion. The BaseBuilder class does the hard work of making an equation from a string in -EquationFactory.makeEquation. BaseBuilder can be used directly to create +EquationFactory.make_equation. BaseBuilder can be used directly to create equations. BaseBuilder is specified in the ArgumentBuilder and OperatorBuilder classes. You can create builders from Literals or equations by using the -"wrap" methods within this module or by using the builder classes directly. +"wrap" functions within this module or by using the builder classes directly. With a collection of BaseBuilder objects, one can simply write the equation using normal python syntax: @@ -51,7 +51,7 @@ > a = ArgumentBuilder(name = "a") > x = ArgumentBuilder(name = "x") > # sin is defined in this module as an OperatorBuilder -> sin = getBuilder("sin") +> sin = get_builder("sin") > beq = A*sin(a*x) > eq = beq.get_equation() @@ -64,16 +64,16 @@ way. BaseBuilder can make use of user-defined functions. Any callable python -object can be wrapped as an OperatorBuilder with the wrapFunction method. For -example. +object can be wrapped as an OperatorBuilder with the wrap_function function. +For example. > _f = lambda a, b : (a-b)/(a+b) -> f = wrapFunction("f", _f) +> f = wrap_function("f", _f) > # Using BaseBuilder > a = ArgumentBuilder(name = "a") > b = ArgumentBuilder(name = "b") > c = ArgumentBuilder(name = "c") > beq = c*f(a,b) -> eq = beq.makeEquation() +> eq = beq.get_equation() """ import inspect @@ -94,6 +94,10 @@ "BaseBuilder", "ArgumentBuilder", "OperatorBuilder", + "wrap_argument", + "wrap_operator", + "wrap_function", + "get_builder", "wrapArgument", "wrapOperator", "wrapFunction", @@ -108,6 +112,7 @@ _builders = {} EquationFactory_base = "diffpy.srfit.equation.builder.EquationFactory" +builder_base = "diffpy.srfit.equation.builder" removal_version = "4.0.0" registerFunction_dep_msg = build_deprecation_message( @@ -116,6 +121,51 @@ "register_function", removal_version, ) +makeEquation_dep_msg = build_deprecation_message( + EquationFactory_base, "makeEquation", "make_equation", removal_version +) +registerConstant_dep_msg = build_deprecation_message( + EquationFactory_base, + "registerConstant", + "register_constant", + removal_version, +) +registerArgument_dep_msg = build_deprecation_message( + EquationFactory_base, + "registerArgument", + "register_argument", + removal_version, +) +registerOperator_dep_msg = build_deprecation_message( + EquationFactory_base, + "registerOperator", + "register_operator", + removal_version, +) +registerBuilder_dep_msg = build_deprecation_message( + EquationFactory_base, + "registerBuilder", + "register_builder", + removal_version, +) +deRegisterBuilder_dep_msg = build_deprecation_message( + EquationFactory_base, + "deRegisterBuilder", + "deregister_builder", + removal_version, +) +wrapArgument_dep_msg = build_deprecation_message( + builder_base, "wrapArgument", "wrap_argument", removal_version +) +wrapOperator_dep_msg = build_deprecation_message( + builder_base, "wrapOperator", "wrap_operator", removal_version +) +wrapFunction_dep_msg = build_deprecation_message( + builder_base, "wrapFunction", "wrap_function", removal_version +) +getBuilder_dep_msg = build_deprecation_message( + builder_base, "getBuilder", "get_builder", removal_version +) class EquationFactory(object): @@ -123,59 +173,61 @@ class EquationFactory(object): Attributes ---------- - builders - A dictionary of BaseBuilders registered with the - factory, indexed by name. - newargs - A set of new arguments created by makeEquation. This is - redefined whenever makeEquation is called. - equations - Set of equations that have been built by the - EquationFactory. + builders : dict + The BaseBuilders registered with the factory, indexed by name. + newargs : set + The new arguments created by the last call to ``make_equation``. + equations : set + The equations that have been built by the factory. """ symbols = ("+", "-", "*", "/", "**", "%", "|") ignore = ("(", ",", ")") def __init__(self): - """Initialize. - - This registers "pi" and "e" as constants within the factory. - """ + """Initialize the factory and register "pi" and "e" as + constants.""" self.builders = dict(_builders) self.newargs = set() self.equations = set() - self.registerConstant("pi", numpy.pi) - self.registerConstant("e", numpy.e) + self.register_constant("pi", numpy.pi) + self.register_constant("e", numpy.e) return - def makeEquation( + def make_equation( self, eqstr, buildargs=True, argclass=literals.Argument, argkw={} ): """Make an equation from an equation string. Parameters ---------- - eqstr - An equation in string form using standard python - syntax. The equation string can use any function - registered literal or function, including numpy ufuncs - that are automatically registered. - buildargs - A flag indicating whether missing arguments can be - created by the Factory (default True). If False, then - the a ValueError will be raised if there are undefined - arguments in the eqstr. Built arguments will be of type - argclass. - argclass - Class to use when creating new Arguments (default - diffpy.srfit.equation.literals.Argument). The class - constructor must accept the 'name' key word. - argkw - Key word dictionary to pass to the argclass constructor - (default {}). - - Returns a callable Literal representing the equation string. + eqstr : str + The equation in string form using standard python syntax. The + equation string can use any registered literal or function, + including the numpy ufuncs that are registered automatically. + buildargs : bool, optional + The flag that is True (default) to let the factory create + missing arguments. If False, undefined names in ``eqstr`` + raise a ValueError. + argclass : type, optional + The class used to create new arguments. Its constructor must + accept the ``name`` keyword. Default is + ``diffpy.srfit.equation.literals.Argument``. + argkw : dict, optional + The keyword arguments passed to the ``argclass`` constructor. + Default is an empty dict. + + Returns + ------- + Equation + The callable equation built from ``eqstr``. + + Raises + ------ + ValueError + If ``buildargs`` is False and ``eqstr`` uses undefined names. + SyntaxError + If ``eqstr`` is not valid python syntax. """ self._prepare_builders(eqstr, buildargs, argclass, argkw) beq = eval(eqstr, {}, self.builders) @@ -188,23 +240,43 @@ def makeEquation( self.equations.add(eq) return eq - def registerConstant(self, name, value): + def register_constant(self, name, value): """Register a named constant with the factory. - Returns the registered builder. + Parameters + ---------- + name : str + The name of the constant. + value : object + The value of the constant. + + Returns + ------- + ArgumentBuilder + The registered builder. """ arg = literals.Argument(name=name, value=value, const=True) - return self.registerArgument(name, arg) + return self.register_argument(name, arg) - def registerArgument(self, name, arg): + def register_argument(self, name, arg): """Register a named Argument with the factory. - Returns the registered builder. + Parameters + ---------- + name : str + The name to register the argument under. + arg : Argument + The argument to register. + + Returns + ------- + ArgumentBuilder + The registered builder. """ - argbuilder = wrapArgument(name, arg) - return self.registerBuilder(name, argbuilder) + argbuilder = wrap_argument(name, arg) + return self.register_builder(name, argbuilder) - def registerOperator(self, name, op): + def register_operator(self, name, op): """Register an Operator literal with the factory. Operators can be used with or without arguments (or parentheses) @@ -214,10 +286,20 @@ def registerOperator(self, name, op): operator is already populated with arguments, and those will be used. - Returns the registered builder. + Parameters + ---------- + name : str + The name to register the operator under. + op : Operator + The operator to register. + + Returns + ------- + OperatorBuilder + The registered builder. """ - opbuilder = wrapOperator(name, op) - return self.registerBuilder(name, opbuilder) + opbuilder = wrap_operator(name, op) + return self.register_builder(name, opbuilder) def register_function(self, name, func, argnames): """Register a named function with the factory. @@ -227,9 +309,9 @@ def register_function(self, name, func, argnames): Parameters ---------- name : str - The name of the function + The name of the function. func : callable - The callable python object + The callable python object. argnames : list of str The argument names for func. If these names do not correspond to builders, then new constants with value 0 @@ -242,29 +324,17 @@ def register_function(self, name, func, argnames): """ for n in argnames: if n not in self.builders: - self.registerConstant(n, 0) - opbuilder = wrapFunction(name, func, len(argnames)) + self.register_constant(n, 0) + opbuilder = wrap_function(name, func, len(argnames)) for argname in argnames: builder = self.builders[argname] argliteral = builder.literal - opbuilder.literal.addLiteral(argliteral) - registered_builder = self.registerBuilder(name, opbuilder) + opbuilder.literal.add_literal(argliteral) + registered_builder = self.register_builder(name, opbuilder) return registered_builder - @deprecated(registerFunction_dep_msg) - def registerFunction(self, name, func, argnames): - """This function has been deprecated and will be removed in - version 4.0.0. - - Please use - diffpy.srfit.equation.builder.EquationFactory.register_function - instead. - """ - return self.register_function(name, func, argnames) - - def registerBuilder(self, name, builder): - """Register builder in this module so it can be used in - makeEquation. + def register_builder(self, name, builder): + """Register a builder so it can be used in ``make_equation``. If an extant builder with the given name is already registered, this will replace all instances of the old builder's literal in @@ -272,8 +342,26 @@ def registerBuilder(self, name, builder): that this may lead to errors if one of the replacements causes a self-reference. - Raises ValueError if the new builder's literal causes a self- - reference in an existing equation. + Parameters + ---------- + name : str + The name to register the builder under. + builder : BaseBuilder + The builder to register. + + Returns + ------- + BaseBuilder + The registered builder. + + Raises + ------ + TypeError + If ``name`` is not a string or ``builder`` is not a + BaseBuilder. + ValueError + If the new builder's literal causes a self-reference in an + existing equation. """ if not isinstance(name, str): raise TypeError("Name must be a string") @@ -297,11 +385,16 @@ def registerBuilder(self, name, builder): self.builders[name] = builder return builder - def deRegisterBuilder(self, name): - """De-register a builder by name. + def deregister_builder(self, name): + """Deregister a builder by name. This does not change the equations that use the Literal wrapped - by the builder. + by the builder. Nothing happens if no builder has this name. + + Parameters + ---------- + name : str + The name of the builder to deregister. """ if name in self.builders: del self.builders[name] @@ -317,7 +410,10 @@ def wipeout(self, eq): indirect pickling with the factory because of observer callback function. - No return value. + Parameters + ---------- + eq : Equation or None + The equation to invalidate. Nothing happens if this is None. """ if eq is None: assert eq not in self.equations @@ -326,9 +422,88 @@ def wipeout(self, eq): # invalidate this equation to clean up any observer relations of # objects in the factory towards its literals tree. nan = literals.Argument("nan", value=numpy.nan, const=True) - eq.setRoot(nan) + eq.set_root(nan) return + @deprecated(makeEquation_dep_msg) + def makeEquation( + self, eqstr, buildargs=True, argclass=literals.Argument, argkw={} + ): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.make_equation + instead. + """ + return self.make_equation(eqstr, buildargs, argclass, argkw) + + @deprecated(registerConstant_dep_msg) + def registerConstant(self, name, value): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.register_constant + instead. + """ + return self.register_constant(name, value) + + @deprecated(registerArgument_dep_msg) + def registerArgument(self, name, arg): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.register_argument + instead. + """ + return self.register_argument(name, arg) + + @deprecated(registerOperator_dep_msg) + def registerOperator(self, name, op): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.register_operator + instead. + """ + return self.register_operator(name, op) + + @deprecated(registerFunction_dep_msg) + def registerFunction(self, name, func, argnames): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.register_function + instead. + """ + return self.register_function(name, func, argnames) + + @deprecated(registerBuilder_dep_msg) + def registerBuilder(self, name, builder): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.register_builder + instead. + """ + return self.register_builder(name, builder) + + @deprecated(deRegisterBuilder_dep_msg) + def deRegisterBuilder(self, name): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.builder.EquationFactory.deregister_builder + instead. + """ + return self.deregister_builder(name) + def _prepare_builders(self, eqstr, buildargs, argclass, argkw): """Prepare builders so that equation string can be evaluated. @@ -338,26 +513,22 @@ def _prepare_builders(self, eqstr, buildargs, argclass, argkw): Parameters ---------- - eqstr - An equation in string as specified in the makeEquation - method. - buildargs - A flag indicating whether missing arguments can be - created by the factory. If False, then the a ValueError - will be raised if there are undefined arguments in the - eqstr. - argclass - Class to use when creating new Arguments. The class - constructor must accept the 'name' key word. - argkw - Key word dictionary to pass to the argclass - constructor. - - Raises ValueError if new arguments must be created, but this is - disallowed due to the buildargs flag. - Raises SyntaxError if the equation string uses invalid syntax. - - Returns a dictionary of the name, BaseBuilder pairs. + eqstr : str + The equation string, as in ``make_equation``. + buildargs : bool + The flag that is True to let the factory create missing + arguments. + argclass : type + The class used to create new arguments. + argkw : dict + The keyword arguments passed to the ``argclass`` constructor. + + Raises + ------ + ValueError + If new arguments must be created but ``buildargs`` is False. + SyntaxError + If the equation string uses invalid syntax. """ eqargs = self._get_undefined_args(eqstr) @@ -374,7 +545,7 @@ def _prepare_builders(self, eqstr, buildargs, argclass, argkw): arg = argclass(name=argname, **argkw) argbuilder = ArgumentBuilder(name=argname, arg=arg) newargs.add(arg) - self.registerBuilder(argname, argbuilder) + self.register_builder(argname, argbuilder) self.newargs = newargs @@ -450,17 +621,17 @@ class BaseBuilder(object): Attributes ---------- - literal + literal : Literal or None The equation Literal being built by this instance. """ def __init__(self): - """Initialize.""" self.literal = None return def __call__(self, *args): - """Raises exception for easier debugging.""" + """Raise a TypeError, because a BaseBuilder takes no + arguments.""" m = "%s (%s) cannot accept arguments" % ( self.literal.name, self.__class__.__name__, @@ -472,6 +643,11 @@ def get_equation(self): The equation will given the name "_eq_" where "" is the name of the root node. + + Returns + ------- + Equation + The equation whose root is this builder's literal. """ # We need to make a name for this, so we name it after its root name = "_eq_%s" % self.literal.name @@ -506,7 +682,7 @@ def __eval_binary(self, other, OperatorClass, onleft=True): # ordering is preserved. if onleft: # Add the literals to the operator - op.addLiteral(self.literal) + op.add_literal(self.literal) # Deal with constants if isinstance(other, BaseBuilder): @@ -515,11 +691,11 @@ def __eval_binary(self, other, OperatorClass, onleft=True): literal = other else: literal = literals.Argument(value=other, const=True) - op.addLiteral(literal) + op.add_literal(literal) if not onleft: # Add the literals to the operator - op.addLiteral(self.literal) + op.add_literal(self.literal) # Create a new OperatorBuilder for the Operator opbuilder = OperatorBuilder(op.name) @@ -529,7 +705,7 @@ def __eval_binary(self, other, OperatorClass, onleft=True): def __eval_unary(self, OperatorClass): """Evaluate a unary function.""" op = OperatorClass() - op.addLiteral(self.literal) + op.add_literal(self.literal) opbuilder = OperatorBuilder(op.name) opbuilder.literal = op return opbuilder @@ -587,30 +763,26 @@ class ArgumentBuilder(BaseBuilder): Equation builder objects can be composed like a normal function where the arguments can be other BaseBuilder instances or constants. + Parameters + ---------- + value : object, optional + The value of the new Argument. Default is None. + name : str, optional + The name of the new Argument. Default is None. + const : bool, optional + The flag that is True if the new Argument is constant. Default is + False. + arg : Argument, optional + The existing Argument to wrap. If None (default), a new Argument + is created from ``value``, ``name`` and ``const``. + Attributes ---------- - literal + literal : Argument The Argument wrapped by this instance. """ def __init__(self, value=None, name=None, const=False, arg=None): - """Create an ArgumentBuilder instance, containing a new - Argument. - - Parameters - ---------- - value - The value of the wrapped Argument (float, default None) - name - The name of the wrapped Argument (string, default None) - const - Flag indicating whether the Argument is constant (bool, - default False) - arg - A pre-defined Argument to use. If this is None (default), - then a new Argument will be created from value, name and - const. - """ BaseBuilder.__init__(self) if arg is None: self.literal = literals.Argument( @@ -627,27 +799,24 @@ def __init__(self, value=None, name=None, const=False, arg=None): class OperatorBuilder(BaseBuilder): """BaseBuilder wrapper around an Operator literal. + Parameters + ---------- + name : str + The name of the wrapped Operator. + op : Operator, optional + The Operator to wrap. If None (default), ``name`` is taken to be + the name of a numpy ufunc, which defines the Operator when the + builder is called. + Attributes ---------- - literal + literal : Operator or None The Operator wrapped by this instance. - name - The name of the operator to be wrapped + name : str + The name of the wrapped Operator. """ def __init__(self, name, op=None): - """Wrap an Operator or a function by name. - - Parameters - ---------- - name - The name of the wrapped Operator - op - If specified, this sets the literal attribute as this - operator (default None). Otherwise, the name is assumed to - be that of a numpy ufunc, which is used to specify the - Operator. - """ BaseBuilder.__init__(self) self.name = name self.literal = op @@ -660,10 +829,19 @@ def __call__(self, *args): Parameters ---------- - args - Arguments of the operation. + *args + The arguments of the operation. - Raises ValueError if self.literal.nin >= 0 and len(args) != op.nin + Returns + ------- + OperatorBuilder + The new builder for the operation applied to ``args``. + + Raises + ------ + ValueError + If the operator takes a fixed number of arguments and + ``args`` has a different length. """ newobj = OperatorBuilder(self.name) @@ -674,7 +852,7 @@ def __call__(self, *args): self.literal = literals.UFuncOperator(ufunc) # Here the Operator is already specified. We can copy its attributes # to a new Operator inside of the new OperatorBuilder. - op = literals.makeOperator( + op = literals.make_operator( name=self.literal.name, symbol=self.literal.symbol, nin=self.literal.nin, @@ -697,7 +875,7 @@ def __call__(self, *args): if not isinstance(arg, BaseBuilder): name = self.name + "_%i" % i arg = ArgumentBuilder(value=arg, name=name, const=True) - newobj.literal.addLiteral(arg.literal) + newobj.literal.add_literal(arg.literal) return newobj @@ -707,35 +885,65 @@ def __call__(self, *args): # Utility functions -def wrapArgument(name, arg): - """Wrap an Argument as a builder.""" +def wrap_argument(name, arg): + """Wrap an Argument as a builder. + + Parameters + ---------- + name : str + The name of the argument. It is not used, because the builder + takes its name from ``arg``. + arg : Argument + The argument to wrap. + + Returns + ------- + ArgumentBuilder + The builder that wraps ``arg``. + """ argbuilder = ArgumentBuilder(arg=arg) return argbuilder -def wrapOperator(name, op): - """Wrap an Operator as a builder.""" +def wrap_operator(name, op): + """Wrap an Operator as a builder. + + Parameters + ---------- + name : str + The name of the builder. + op : Operator + The operator to wrap. + + Returns + ------- + OperatorBuilder + The builder that wraps ``op``. + """ opbuilder = OperatorBuilder(name, op) return opbuilder -def wrapFunction(name, func, nin=2, nout=1): +def wrap_function(name, func, nin=2, nout=1): """Wrap a function in an OperatorBuilder instance. Parameters ---------- - name - The name of the function - func - A callable python object - nin - The number of input arguments (default 2) - nout - The number of return values (default 1) - - Returns the OperatorBuilder instance that wraps the function. + name : str + The name of the function. + func : callable + The callable python object. + nin : int, optional + The number of input arguments. Default is 2. + nout : int, optional + The number of return values. Default is 1. + + Returns + ------- + OperatorBuilder + The builder that wraps the function. """ - op = literals.makeOperator( + op = literals.make_operator( name=name, symbol=name, nin=nin, nout=nout, operation=func ) @@ -745,11 +953,67 @@ def wrapFunction(name, func, nin=2, nout=1): return opbuilder -def getBuilder(name): - """Get an operator from the global builders dictionary.""" +def get_builder(name): + """Get an operator builder from the global builders dictionary. + + Parameters + ---------- + name : str + The name of the builder, such as "sin" or "add". + + Returns + ------- + OperatorBuilder + The global builder registered under ``name``. + + Raises + ------ + KeyError + If no global builder has this name. + """ return _builders[name] +@deprecated(wrapArgument_dep_msg) +def wrapArgument(name, arg): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.builder.wrap_argument instead. + """ + return wrap_argument(name, arg) + + +@deprecated(wrapOperator_dep_msg) +def wrapOperator(name, op): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.builder.wrap_operator instead. + """ + return wrap_operator(name, op) + + +@deprecated(wrapFunction_dep_msg) +def wrapFunction(name, func, nin=2, nout=1): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.builder.wrap_function instead. + """ + return wrap_function(name, func, nin, nout) + + +@deprecated(getBuilder_dep_msg) +def getBuilder(name): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.builder.get_builder instead. + """ + return get_builder(name) + + def __wrap_numpy_operators(): """Export all numpy operators as OperatorBuilder instances in the module namespace.""" diff --git a/src/diffpy/srfit/equation/equationmod.py b/src/diffpy/srfit/equation/equationmod.py index fa5918d0..845a94a7 100644 --- a/src/diffpy/srfit/equation/equationmod.py +++ b/src/diffpy/srfit/equation/equationmod.py @@ -15,18 +15,26 @@ """The Equation class for holding and evaluating an equation. Equation is a functor that holds a Literal tree that defines an -equation. It's __call__ method evaluates the equation at the most recent +equation. Its __call__ method evaluates the equation at the most recent value of its Arguments. The non-constant arguments are accessible as attributes of the Equation instance and can be passed as arguments to __call__. -Example > # make a Literal tree. Here's a simple one > add = -AdditionOperator() > a = Argument(name="a") # Don't forget to name -these! > b = Argument(name="b") > add.addLiteral(a) > add.addLiteral(b) -> # make an Equation instance and pass the root > eq = Equation(root = -add) > eq(a=3, b=4) # returns 7 > eq(a=2) # remembers b=4, returns 6 > -eq.a.set_value(-3) > eq.b.set_value(3) > eq() # uses last assignment of -a and b, returns 0 +Example:: + + # make a Literal tree. Here's a simple one + add = AdditionOperator() + a = Argument(name="a") # Don't forget to name these! + b = Argument(name="b") + add.add_literal(a) + add.add_literal(b) + # make an Equation instance and pass the root + eq = Equation(root=add) + eq(a=3, b=4) # returns 7 + eq(a=2) # remembers b=4, returns 6 + eq.a.set_value(-3) + eq.b.set_value(3) + eq() # uses last assignment of a and b, returns 0 See the class documentation for more information. """ @@ -37,7 +45,14 @@ from diffpy.srfit.equation.literals.literal import Literal from diffpy.srfit.equation.literals.operators import Operator -from diffpy.srfit.equation.visitors import getArgs, swap, validate +from diffpy.srfit.equation.visitors import get_args, swap, validate +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +equation_base = "diffpy.srfit.equation.Equation" +removal_version = "4.0.0" +setRoot_dep_msg = build_deprecation_message( + equation_base, "setRoot", "set_root", removal_version +) class Equation(Operator): @@ -55,33 +70,33 @@ class Equation(Operator): they evaluate as the root node, but provide a calling interface that accepts new argument values for the literal tree. + Parameters + ---------- + name : str, optional + The name of the Equation. If None (default) and ``root`` is + given, the name is "eq_". + root : Literal, optional + The root node of the Literal tree. If None (default), call + ``set_root`` to set it. + Attributes ---------- - root - The root Literal of the equation tree - argdict - An OrderedDict of Arguments from the root. - args - Property that gets the values of argdict. - - Operator Attributes - ------------------- - args - List of Literal arguments, set with 'addLiteral' - name - A name for this operator. e.g. "add" or "sin" - nin - Number of inputs (<1 means this is variable) - nout - Number of outputs - operation - Function that performs the operation. e.g. numpy.add. - symbol - The symbolic representation. e.g. "+" or "sin". - _value - The value of the Operator. - value - Property for 'get_value'. + root : Literal or None + The root Literal of the equation tree. + argdict : OrderedDict + The non-constant Arguments of the tree, indexed by name. + args : list + The values of ``argdict``, in the same order. + name : str + The name of the Equation. + nin : int + The number of non-constant Arguments. + nout : int + The number of outputs, which is always 1. + symbol : str + The symbolic representation, which is the same as ``name``. + value : object + The value of the Equation, read with ``get_value``. """ # define abstract attributes from the Operator base. @@ -89,17 +104,6 @@ class Equation(Operator): nout = 1 def __init__(self, name=None, root=None): - """Initialize. - - Parameters - ---------- - name - A name for this Equation. - root - The root node of the Literal tree (default None). If root - is not passed here, you must call the 'setRoot' method to - set or change the root node. - """ # Operator stuff. We circumvent Operator.__init__ since we're using # args as a property. We cannot set it, as the Operator tries to do. if name is None and root is not None: @@ -108,7 +112,7 @@ def __init__(self, name=None, root=None): self.root = None self.argdict = OrderedDict() if root is not None: - self.setRoot(root) + self.set_root(root) return @property @@ -121,7 +125,17 @@ def operation(self, *args, **kw): Same as the __call__ method. This method is used via the Operator interface. - Return the result of __call__(*args, **kw). + Parameters + ---------- + *args + The new values of the arguments, in the order of ``args``. + **kw + The new values of the arguments, by name. + + Returns + ------- + object + The value of the Equation. """ return self.__call__(*args, **kw) @@ -150,11 +164,18 @@ def __dir__(self): rv = sorted(rv) return rv - def setRoot(self, root): + def set_root(self, root): """Set the root of the Literal tree. - Raises: - ValueError if errors are found in the Literal tree. + Parameters + ---------- + root : Literal + The new root of the Literal tree. + + Raises + ------ + ValueError + If errors are found in the Literal tree. """ # Validate the new root validate(root) @@ -169,7 +190,7 @@ def setRoot(self, root): self._flush(other=(self,)) # Get the args - args = getArgs(root, getconsts=False) + args = get_args(root, getconsts=False) self.argdict = OrderedDict([(arg.name, arg) for arg in args]) # Set Operator attributes @@ -177,6 +198,15 @@ def setRoot(self, root): return + @deprecated(setRoot_dep_msg) + def setRoot(self, root): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.equation.Equation.set_root instead. + """ + return self.set_root(root) + def __call__(self, *args, **kw): """Call the equation. @@ -185,7 +215,23 @@ def __call__(self, *args, **kw): by the args attribute. The Equation will remember values set in this way. - Raises ValueError when a passed argument cannot be found + Parameters + ---------- + *args + The new values of the arguments, in the order of ``args``. + **kw + The new values of the arguments, by name. + + Returns + ------- + object + The value of the Equation. + + Raises + ------ + ValueError + If there are more positional values than arguments, or a + keyword does not name an argument. """ # Process args for idx, val in enumerate(args): @@ -207,23 +253,58 @@ def __call__(self, *args, **kw): def swap(self, oldlit, newlit): """Swap a literal in the equation for another. - Note that this may change the root and the operation interface + Note that this may change the root and the operation interface. + + Parameters + ---------- + oldlit : Literal + The literal to replace. + newlit : Literal + The literal to put in its place. + + Raises + ------ + ValueError + If ``newlit`` causes a self-reference. """ newroot = swap(self.root, oldlit, newlit) - self.setRoot(newroot) + self.set_root(newroot) return # Operator methods - def addLiteral(self, literal): - """Cannot add a literal to an Equation.""" + def add_literal(self, literal): + """Raise a RuntimeError, because an Equation takes its literals + from its root. + + Parameters + ---------- + literal : Literal + The literal that cannot be added. + + Raises + ------ + RuntimeError + Always. + """ raise RuntimeError("Cannot add literals to an Equation.") # Visitors can treat us differently than an Operator. def identify(self, visitor): - """Identify self to a visitor.""" - return visitor.onEquation(self) + """Identify this Equation to a visitor. + + Parameters + ---------- + visitor : Visitor + The visitor to identify to. + + Returns + ------- + object + The result of ``visitor.on_equation(self)``. + """ + return visitor.on_equation(self) # End of file diff --git a/src/diffpy/srfit/equation/literals/__init__.py b/src/diffpy/srfit/equation/literals/__init__.py index 862cac3b..a688cb98 100644 --- a/src/diffpy/srfit/equation/literals/__init__.py +++ b/src/diffpy/srfit/equation/literals/__init__.py @@ -42,6 +42,7 @@ "UFuncOperator", "ArrayOperator", "PolyvalOperator", + "make_operator", "makeOperator", ] @@ -65,6 +66,7 @@ SubtractionOperator, SumOperator, UFuncOperator, + make_operator, makeOperator, ) diff --git a/src/diffpy/srfit/equation/literals/abcs.py b/src/diffpy/srfit/equation/literals/abcs.py index d930c1a5..138cb858 100644 --- a/src/diffpy/srfit/equation/literals/abcs.py +++ b/src/diffpy/srfit/equation/literals/abcs.py @@ -79,7 +79,7 @@ class OperatorABC(LiteralABC): """ @abstractmethod - def addLiteral(self, literal): + def add_literal(self, literal): """Add a literal argument to the operator.""" pass diff --git a/src/diffpy/srfit/equation/literals/argument.py b/src/diffpy/srfit/equation/literals/argument.py index a8d387b6..d8600eeb 100644 --- a/src/diffpy/srfit/equation/literals/argument.py +++ b/src/diffpy/srfit/equation/literals/argument.py @@ -27,45 +27,73 @@ class Argument(Literal, ArgumentABC): - """Argument class. + """A leaf of an equation tree, holding a variable or a constant. + + Parameters + ---------- + name : str, optional + The name of the Argument. Default is None. + value : object, optional + The initial value of the Argument. Default is None. + const : bool, optional + The flag that is True if the Argument is a constant. Default is + False. Attributes ---------- - name - A name for this Argument. - const - A flag indicating whether this is considered a constant. - Constants may be given special treatment by the Visitors. - _value - The value of the Argument. Modified with 'set_value'. - value - Property for 'get_value' and 'set_value'. + name : str or None + The name of the Argument. + const : bool + The flag that is True if the Argument is a constant. Visitors + may treat constants differently. + value : object + The value of the Argument, read with ``get_value`` and changed + with ``set_value``. """ const = None def __init__(self, name=None, value=None, const=False): - """Initialization.""" Literal.__init__(self, name) self.const = const self.value = value return def identify(self, visitor): - """Identify self to a visitor.""" - return visitor.onArgument(self) + """Identify this Argument to a visitor. + + Parameters + ---------- + visitor : Visitor + The visitor to identify to. + + Returns + ------- + object + The result of ``visitor.on_argument(self)``. + """ + return visitor.on_argument(self) def get_value(self): - """Get the value of this Literal.""" + """Get the value of this Argument. + + Returns + ------- + object + The current value of the Argument. + """ return self._value def set_value(self, val): - """Set the value of the Literal. + """Set the value of this Argument. + + Observers are notified only if the new value differs from the + current one. Parameters ---------- - val - The value to assign + val : object + The value to assign. """ if isinstance(self._value, ndarray) or isinstance(val, ndarray): notequiv = not array_equal(self._value, val) diff --git a/src/diffpy/srfit/equation/literals/literal.py b/src/diffpy/srfit/equation/literals/literal.py index 1f3b7b6d..6fbabb5b 100644 --- a/src/diffpy/srfit/equation/literals/literal.py +++ b/src/diffpy/srfit/equation/literals/literal.py @@ -16,7 +16,7 @@ Literals are base pieces of the equation hierarchy. The 'identify' method identifies the Literal to a visitor by calling the identifying -method of the vistior. +method of the visitor. """ __all__ = ["Literal"] @@ -38,26 +38,38 @@ class Literal(Observable, LiteralABC): Literal derives from Observable. See diffpy.srfit.util.observable. + Parameters + ---------- + name : str, optional + The name of the Literal. If None (default), the class attribute + ``name`` is used. + Attributes ---------- - name - A name for this Literal (default None). - _value - The value of the Literal. + name : str or None + The name of the Literal. + _value : object + The cached value of the Literal, or None if it must be + recomputed. """ name = None _value = None def __init__(self, name=None): - """Initialization.""" Observable.__init__(self) if name is not None: self.name = name return def get_value(self): - """Get the value of the Literal.""" + """Get the value of the Literal. + + Raises + ------ + NotImplementedError + Always, because derived classes must define this method. + """ raise NotImplementedError("Define in derived class") @deprecated(getValue_dep_msg) @@ -71,12 +83,23 @@ def getValue(self): return self.get_value() def identify(self, visitor): - """Identify self to a visitor.""" + """Identify this Literal to a visitor. + + Parameters + ---------- + visitor : Visitor + The visitor to identify to. + + Raises + ------ + NotImplementedError + Always, because derived classes must define this method. + """ m = "'%s' must override 'identify'" % self.__class__.__name__ raise NotImplementedError(m) def _flush(self, other): - """Invalidate my state and notify observers.""" + """Clear the cached value and notify observers.""" if self._value is None: return self._value = None diff --git a/src/diffpy/srfit/equation/literals/operators.py b/src/diffpy/srfit/equation/literals/operators.py index 810dd79e..8d0d83dc 100644 --- a/src/diffpy/srfit/equation/literals/operators.py +++ b/src/diffpy/srfit/equation/literals/operators.py @@ -19,10 +19,10 @@ evaluated by the Evaluator visitor, or otherwise inspected. The Operator class contains all the information necessary to be -identified and evaluated by a Visitor. Thus, a single onOperator method +identified and evaluated by a Visitor. Thus, a single on_operator method exists in the Visitor base class. Other Operators can be derived from Operator (see AdditionOperator), but they all identify themselves with -the Visitor.onOperator method. +the Visitor.on_operator method. """ __all__ = [ @@ -39,12 +39,25 @@ "UFuncOperator", "ArrayOperator", "PolyvalOperator", + "make_operator", + "makeOperator", ] import numpy from diffpy.srfit.equation.literals.abcs import OperatorABC from diffpy.srfit.equation.literals.literal import Literal +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +operator_base = "diffpy.srfit.equation.literals.Operator" +literals_base = "diffpy.srfit.equation.literals" +removal_version = "4.0.0" +addLiteral_dep_msg = build_deprecation_message( + operator_base, "addLiteral", "add_literal", removal_version +) +makeOperator_dep_msg = build_deprecation_message( + literals_base, "makeOperator", "make_operator", removal_version +) class Operator(Literal, OperatorABC): @@ -74,7 +87,7 @@ class Operator(Literal, OperatorABC): ---------- args : list The list of `Literal` arguments. Read-only, use the - `addLiteral` method to change its content. + `add_literal` method to change its content. """ # Private Attributes @@ -102,15 +115,23 @@ def __init__(self, name=None): def identify(self, visitor): """Identify self to a visitor.""" - return visitor.onOperator(self) + return visitor.on_operator(self) - def addLiteral(self, literal): + def add_literal(self, literal): """Add a literal to this operator. Note that order of operation matters. The first literal added is the leftmost argument. The last is the rightmost. - Raises ValueError if the literal causes a self-reference. + Parameters + ---------- + literal : Literal + The literal to add as the next argument. + + Raises + ------ + ValueError + If ``literal`` causes a self-reference. """ # Make sure we don't have self-reference self._loop_check(literal) @@ -119,6 +140,16 @@ def addLiteral(self, literal): self._flush(other=(self,)) return + @deprecated(addLiteral_dep_msg) + def addLiteral(self, literal): + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use diffpy.srfit.equation.literals.Operator.add_literal + instead. + """ + return self.add_literal(literal) + def get_value(self): """Get or evaluate the value of the operator.""" if self._value is None: @@ -171,7 +202,7 @@ class BinaryOperator(Operator): class CustomOperator(Operator): """Concrete class for a user-defined Operator. - Use the `makeOperator` factory function to create an instance. + Use the `make_operator` factory function to create an instance. """ # declare all abstract attributes from the Operator base. @@ -182,7 +213,7 @@ class CustomOperator(Operator): pass -def makeOperator(name, symbol, operation, nin, nout): +def make_operator(name, symbol, operation, nin, nout): """Return a new custom operator object. Parameters @@ -197,7 +228,7 @@ def makeOperator(name, symbol, operation, nin, nout): nin : int Number of input arguments for the operator. Any number of arguments is allowed when -1. - nout : in + nout : int Number of outputs returned by the `operation`. Returns @@ -213,6 +244,16 @@ def makeOperator(name, symbol, operation, nin, nout): return op +@deprecated(makeOperator_dep_msg) +def makeOperator(name, symbol, operation, nin, nout): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.literals.make_operator instead. + """ + return make_operator(name, symbol, operation, nin, nout) + + # Some specified operators diff --git a/src/diffpy/srfit/equation/visitors/__init__.py b/src/diffpy/srfit/equation/visitors/__init__.py index 82239eb4..a331e368 100644 --- a/src/diffpy/srfit/equation/visitors/__init__.py +++ b/src/diffpy/srfit/equation/visitors/__init__.py @@ -24,40 +24,61 @@ http://en.wikipedia.org/wiki/Visitor_pattern). """ -from __future__ import print_function - from diffpy.srfit.equation.visitors.argfinder import ArgFinder from diffpy.srfit.equation.visitors.printer import Printer from diffpy.srfit.equation.visitors.swapper import Swapper from diffpy.srfit.equation.visitors.validator import Validator - - -def getArgs(literal, getconsts=True): +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +visitors_base = "diffpy.srfit.equation.visitors" +removal_version = "4.0.0" +getArgs_dep_msg = build_deprecation_message( + visitors_base, "getArgs", "get_args", removal_version +) +getExpression_dep_msg = build_deprecation_message( + visitors_base, "getExpression", "get_expression", removal_version +) +prettyPrint_dep_msg = build_deprecation_message( + visitors_base, "prettyPrint", "pretty_print", removal_version +) + + +def get_args(literal, getconsts=True): """Get the Arguments of a Literal tree. Parameters ---------- - getconsts - If True (default), then Arguments designated as constant - are also retrieved. - - Returns a list of Arguments searched for depth-first. + literal : Literal + The root of the Literal tree. + getconsts : bool, optional + The flag that is True (default) to also get the Arguments that + are constant. + + Returns + ------- + list of Argument + The Arguments, found depth-first. """ v = ArgFinder(getconsts) return literal.identify(v) -def getExpression(literal, eqskip=None): - """Get math expression string from the Literal tree object. +def get_expression(literal, eqskip=None): + """Get the math expression string of a Literal tree. Parameters ---------- - eqskip - regular expression pattern for Equation objects that should - be printed under their own name. Expand all Equation objects - when None. - - Return string. + literal : Literal + The root of the Literal tree. + eqskip : str, optional + The regular expression pattern for Equation objects that should + be written under their own name. If None (default), all + Equation objects are expanded. + + Returns + ------- + str + The expression of the Literal tree. """ v = Printer() v.eqskip = eqskip @@ -65,16 +86,30 @@ def getExpression(literal, eqskip=None): return rv -def prettyPrint(literal): - """Print a Literal tree.""" - print(getExpression(literal)) +def pretty_print(literal): + """Print the math expression of a Literal tree. + + Parameters + ---------- + literal : Literal + The root of the Literal tree. + """ + print(get_expression(literal)) return def validate(literal): """Validate a Literal tree. - Raises ValueError if the tree contains errors. + Parameters + ---------- + literal : Literal + The root of the Literal tree. + + Raises + ------ + ValueError + If the tree contains errors. """ v = Validator() errors = literal.identify(v) @@ -91,10 +126,52 @@ def swap(literal, oldlit, newlit): Corrections are done in-place unless literal is oldlit, in which case the return value is newlit. - Returns the literal tree with oldlit swapped for newlit. + Parameters + ---------- + literal : Literal + The root of the Literal tree. + oldlit : Literal + The literal to replace. + newlit : Literal + The literal to put in its place. + + Returns + ------- + Literal + The Literal tree with ``oldlit`` swapped for ``newlit``. """ if literal is oldlit: return newlit v = Swapper(oldlit, newlit) literal.identify(v) return literal + + +@deprecated(getArgs_dep_msg) +def getArgs(literal, getconsts=True): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.visitors.get_args instead. + """ + return get_args(literal, getconsts) + + +@deprecated(getExpression_dep_msg) +def getExpression(literal, eqskip=None): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.visitors.get_expression instead. + """ + return get_expression(literal, eqskip) + + +@deprecated(prettyPrint_dep_msg) +def prettyPrint(literal): + """This function has been deprecated and will be removed in version + 4.0.0. + + Please use diffpy.srfit.equation.visitors.pretty_print instead. + """ + return pretty_print(literal) diff --git a/src/diffpy/srfit/equation/visitors/argfinder.py b/src/diffpy/srfit/equation/visitors/argfinder.py index 13ba085a..86b10957 100644 --- a/src/diffpy/srfit/equation/visitors/argfinder.py +++ b/src/diffpy/srfit/equation/visitors/argfinder.py @@ -14,7 +14,7 @@ ############################################################################## """Visitor for extracting the Argument entries in a Literal tree. -ArgFinder extracts all Arguments from a literal true. +ArgFinder extracts all Arguments from a literal tree. """ __all__ = ["ArgFinder"] @@ -25,23 +25,21 @@ class ArgFinder(Visitor): """ArgFinder extracts Arguments from a Literal tree. + Parameters + ---------- + getconsts : bool, optional + The flag that is True (default) to also collect constant + Arguments. + Attributes ---------- - args - The list of collected Arguments - getconsts - Flag indicating whether to grab constant arguments. + args : list + The collected Arguments. + getconsts : bool + The flag that is True to also collect constant Arguments. """ def __init__(self, getconsts=True): - """Initialize. - - Parameters - ---------- - getconsts - Flag indicating whether to parse constant arguments - (default True). - """ self.args = [] self.getconsts = getconsts return @@ -51,14 +49,36 @@ def reset(self): self.args = [] return - def onArgument(self, arg): - """Process an Argument node.""" + def on_argument(self, arg): + """Collect an Argument node. + + Parameters + ---------- + arg : Argument + The Argument to collect. + + Returns + ------- + list + The Arguments collected so far. + """ if self.getconsts or not arg.const: self.args.append(arg) return self.args - def onOperator(self, op): - """Process an Operator node.""" + def on_operator(self, op): + """Collect the Arguments of an Operator node. + + Parameters + ---------- + op : Operator + The Operator whose Arguments are collected. + + Returns + ------- + list + The Arguments collected so far. + """ for arg in op.args: arg.identify(self) return self.args diff --git a/src/diffpy/srfit/equation/visitors/printer.py b/src/diffpy/srfit/equation/visitors/printer.py index 4fcb717b..ce9d2bc6 100644 --- a/src/diffpy/srfit/equation/visitors/printer.py +++ b/src/diffpy/srfit/equation/visitors/printer.py @@ -31,11 +31,11 @@ class Printer(Visitor): Attributes ---------- - eqskip - regular expression pattern for Equation objects that should - not be expanded, but printed under their own name. Expand - all equations when None. - output + eqskip : str or None + The regular expression pattern for Equation objects that should + not be expanded, but printed under their own name. Expand all + equations when None. + output : str The output generated by the printer. """ @@ -43,12 +43,11 @@ class Printer(Visitor): _eqpat = None def __init__(self): - """Initialize.""" self.reset() return def reset(self): - """Reset the out put string.""" + """Reset the output string.""" self.output = "" return @@ -71,10 +70,21 @@ def eqskip(self, value): self._eqpat = re.compile(value) return - def onArgument(self, arg): - """Process an Argument node. + def on_argument(self, arg): + """Write an Argument node. + The name is written, or the value if the Argument has no name. No assumption is made about the argument type. + + Parameters + ---------- + arg : Argument + The Argument to write. + + Returns + ------- + str + The output so far. """ if arg.name is None: self.output += str(arg.value) @@ -82,8 +92,23 @@ def onArgument(self, arg): self.output += str(arg.name) return self.output - def onOperator(self, op): - """Process an Operator node.""" + def on_operator(self, op): + """Write an Operator node. + + Binary operators whose symbol differs from their name are + written in infix form, such as "(a + b)". Others are written as + a function call, such as "sin(a)". + + Parameters + ---------- + op : Operator + The Operator to write. + + Returns + ------- + str + The output so far. + """ # We have to deal with infix operators if op.name != op.symbol and op.nin == 2: self._on_infix(op) @@ -99,13 +124,27 @@ def onOperator(self, op): self.output += ")" return self.output - def onEquation(self, eq): - """Process an Equation node.""" + def on_equation(self, eq): + """Write an Equation node. + + The Equation is written by name if it matches ``eqskip``, and + expanded otherwise. + + Parameters + ---------- + eq : Equation + The Equation to write. + + Returns + ------- + str + The output so far. + """ skipthis = ( self._eqpat is not None and eq.name and self._eqpat.match(eq.name) ) if skipthis: - self.onArgument(eq) + self.on_argument(eq) else: eq.root.identify(self) return self.output diff --git a/src/diffpy/srfit/equation/visitors/swapper.py b/src/diffpy/srfit/equation/visitors/swapper.py index 6d957611..2e923c1b 100644 --- a/src/diffpy/srfit/equation/visitors/swapper.py +++ b/src/diffpy/srfit/equation/visitors/swapper.py @@ -27,25 +27,22 @@ class Swapper(Visitor): Note that this cannot swap out a root node of a literal tree. This case must be tested for explicitly. + Parameters + ---------- + oldlit : Literal + The literal to be replaced. + newlit : Literal + The literal to be placed into the literal tree. + Attributes ---------- - newlit + newlit : Literal The literal to be placed into the literal tree. - oldlit + oldlit : Literal The literal to be replaced. """ def __init__(self, oldlit, newlit): - """Initialize. - - Parameters - ---------- - oldlit - The literal to be replaced. - newlit - The literal to be placed into the literal tree. See the - class for how the replacement takes place. - """ self.newlit = newlit self.oldlit = oldlit @@ -53,22 +50,37 @@ class for how the replacement takes place. return - def onArgument(self, arg): + def on_argument(self, arg): """Process an Argument node. Tell the parent to swap the old Argument with the replacement Literal. + + Parameters + ---------- + arg : Argument + The Argument to check. """ if arg is self.oldlit: self._swap = True return - def onOperator(self, op): + def on_operator(self, op): """Process an Operator node. Tell the parent to swap the old Operator with the replacement - Literal. + Literal, and swap any of its arguments in place. + + Parameters + ---------- + op : Operator + The Operator to check and traverse. + + Raises + ------ + ValueError + If the replacement Literal causes a self-reference. """ # Check to see if we need to swap out this Operator. If so, then we # don't need to traverse into its arguments. @@ -120,10 +132,20 @@ def onOperator(self, op): return - def onEquation(self, eq): + def on_equation(self, eq): """Process an Equation node. This looks at the equation itself as well as the root. + + Parameters + ---------- + eq : Equation + The Equation to check and traverse. + + Raises + ------ + ValueError + If the replacement Literal causes a self-reference. """ if eq is self.oldlit: self._swap = True @@ -131,7 +153,7 @@ def onEquation(self, eq): # If the newlit is the root, then swap that out and move on. if eq.root is self.oldlit: - eq.setRoot(self.newlit) + eq.set_root(self.newlit) return # Now move into the equation. We have to do a _loop_check to make sure @@ -140,7 +162,7 @@ def onEquation(self, eq): eq.root.identify(self) # Reset the root in case anything changed underneath. - eq.setRoot(eq.root) + eq.set_root(eq.root) return diff --git a/src/diffpy/srfit/equation/visitors/validator.py b/src/diffpy/srfit/equation/visitors/validator.py index 32d9682b..f9d78568 100644 --- a/src/diffpy/srfit/equation/visitors/validator.py +++ b/src/diffpy/srfit/equation/visitors/validator.py @@ -36,29 +36,38 @@ class Validator(Visitor): Attributes ---------- - errors - List of strings describing errors. - _nin - Variable for counting the number input arguments for an - operator. + errors : list of str + The descriptions of the errors found. + _nin : int + The number of outputs of the last node visited, used to count + the inputs of its parent operator. """ def __init__(self): - """Initialize.""" self.reset() return def reset(self): - """Click the clicker and reset non-public data.""" + """Clear the errors and the input count.""" self.errors = [] self._nin = 0 return - def onArgument(self, arg): - """Process an Argument node. + def on_argument(self, arg): + """Check an Argument node. The Argument must be an instance of ArgumentABC from - diffpy.srfit.equation.literals.abcs + diffpy.srfit.equation.literals.abcs. + + Parameters + ---------- + arg : Argument + The Argument to check. + + Returns + ------- + list of str + The errors found so far. """ if not isinstance(arg, ArgumentABC): m = msg % (arg, ArgumentABC.__name__) @@ -66,11 +75,23 @@ def onArgument(self, arg): self._nin = 1 return self.errors - def onOperator(self, op): - """Process an Operator node. + def on_operator(self, op): + """Check an Operator node and its arguments. The Operator must be an instance of OperatorABC from - diffpy.srfit.equation.literals.abcs + diffpy.srfit.equation.literals.abcs, be single-valued, have a + name, symbol and operation, and receive as many inputs as it + takes. + + Parameters + ---------- + op : Operator + The Operator to check. + + Returns + ------- + list of str + The errors found so far. """ if not isinstance(op, OperatorABC): m = msg % (op, OperatorABC.__name__) diff --git a/src/diffpy/srfit/equation/visitors/visitor.py b/src/diffpy/srfit/equation/visitors/visitor.py index 965c5a92..bb3a655c 100644 --- a/src/diffpy/srfit/equation/visitors/visitor.py +++ b/src/diffpy/srfit/equation/visitors/visitor.py @@ -28,28 +28,109 @@ __all__ = ["Visitor"] +from diffpy.utils._deprecator import build_deprecation_message, deprecated + +visitor_base = "diffpy.srfit.equation.visitors.visitor.Visitor" +removal_version = "4.0.0" +onArgument_dep_msg = build_deprecation_message( + visitor_base, "onArgument", "on_argument", removal_version +) +onOperator_dep_msg = build_deprecation_message( + visitor_base, "onOperator", "on_operator", removal_version +) +onEquation_dep_msg = build_deprecation_message( + visitor_base, "onEquation", "on_equation", removal_version +) + class Visitor(object): """Abstract class for all visitors to a Literal tree. - See implemented visitors for examples of use. + Subclasses must override ``on_argument`` and ``on_operator``. See + implemented visitors for examples of use. """ + def on_argument(self, arg): + """Process an Argument node. + + Parameters + ---------- + arg : Argument + The Argument to process. + + Raises + ------ + NotImplementedError + Always, because subclasses must override this method. + """ + return self._abstract("on_argument") + + def on_operator(self, op): + """Process an Operator node. + + Parameters + ---------- + op : Operator + The Operator to process. + + Raises + ------ + NotImplementedError + Always, because subclasses must override this method. + """ + return self._abstract("on_operator") + + def on_equation(self, eq): + """Process an Equation node. + + Equations are specialized Operators. They don't need to be + specifically supported by a Visitor, so by default they are + passed to ``on_operator``. + + Parameters + ---------- + eq : Equation + The Equation to process. + + Returns + ------- + object + The result of ``on_operator(eq)``. + """ + return self.on_operator(eq) + + @deprecated(onArgument_dep_msg) def onArgument(self, arg): - """Process an Argument node.""" - return self._abstract("onArgument") + """This function has been deprecated and will be removed in + version 4.0.0. + Please use + diffpy.srfit.equation.visitors.visitor.Visitor.on_argument + instead. + """ + return self.on_argument(arg) + + @deprecated(onOperator_dep_msg) def onOperator(self, op): - """Process an Operator node.""" - return self._abstract("onOperator") + """This function has been deprecated and will be removed in + version 4.0.0. + + Please use + diffpy.srfit.equation.visitors.visitor.Visitor.on_operator + instead. + """ + return self.on_operator(op) + @deprecated(onEquation_dep_msg) def onEquation(self, eq): - """Process an Equation node. + """This function has been deprecated and will be removed in + version 4.0.0. - Equations are specialized Operators. They don't need to be - specifically supported by a Visitor. + Please use + diffpy.srfit.equation.visitors.visitor.Visitor.on_equation + instead. """ - return self.onOperator(eq) + return self.on_equation(eq) # throw an exception def _abstract(self, method): diff --git a/src/diffpy/srfit/fitbase/fitcontribution.py b/src/diffpy/srfit/fitbase/fitcontribution.py index d135205d..73350efd 100644 --- a/src/diffpy/srfit/fitbase/fitcontribution.py +++ b/src/diffpy/srfit/fitbase/fitcontribution.py @@ -253,7 +253,7 @@ def add_profile_generator(self, gen, name=None): # Register the generator with the equation factory and add it as a # managed object. - self._eqfactory.registerOperator(name, gen) + self._eqfactory.register_operator(name, gen) self._add_object(gen, self._generators, True) # If we have a profile, set the profile of the generator. @@ -315,7 +315,7 @@ def set_equation(self, eqstr, ns={}): self._add_parameter(par) # Register eq as an operator - self._eqfactory.registerOperator("eq", eq) + self._eqfactory.register_operator("eq", eq) self._eqfactory.wipeout(self._eq) self._eq = eq @@ -346,11 +346,11 @@ def get_equation(self): The normalized math expression, or an empty string if the profile equation has not been set yet. """ - from diffpy.srfit.equation.visitors import getExpression + from diffpy.srfit.equation.visitors import get_expression rv = "" if self._eq is not None: - rv = getExpression(self._eq) + rv = get_expression(self._eq) return rv @deprecated(getequation_dep_msg) @@ -432,11 +432,11 @@ def get_residual_equation(self): The normalized math formula, or an empty string if the residual equation has not been configured yet. """ - from diffpy.srfit.equation.visitors import getExpression + from diffpy.srfit.equation.visitors import get_expression rv = "" if self._reseq is not None: - rv = getExpression(self._reseq, eqskip="eq$") + rv = get_expression(self._reseq, eqskip="eq$") return rv @deprecated(getresidualequation_dep_msg) diff --git a/src/diffpy/srfit/fitbase/profilegenerator.py b/src/diffpy/srfit/fitbase/profilegenerator.py index 597d5437..178f898c 100644 --- a/src/diffpy/srfit/fitbase/profilegenerator.py +++ b/src/diffpy/srfit/fitbase/profilegenerator.py @@ -90,7 +90,7 @@ class ProfileGenerator(Operator, ParameterSet): Operator Attributes ------------------- args - List of Literal arguments, set with 'addLiteral' + List of Literal arguments, set with 'add_literal' name A name for this operator. e.g. "add" or "sin" nin diff --git a/src/diffpy/srfit/fitbase/recipeorganizer.py b/src/diffpy/srfit/fitbase/recipeorganizer.py index 0e4947f4..8e7cc77a 100644 --- a/src/diffpy/srfit/fitbase/recipeorganizer.py +++ b/src/diffpy/srfit/fitbase/recipeorganizer.py @@ -805,7 +805,7 @@ def _add_parameter(self, parameter, check=True): RecipeContainer._add_object(self, parameter, self._parameters, check) # Register the Parameter - self._eqfactory.registerArgument(parameter.name, parameter) + self._eqfactory.register_argument(parameter.name, parameter) return def _remove_parameter(self, parameter): @@ -823,7 +823,7 @@ def _remove_parameter(self, parameter): If `parameter` is not part of the RecipeOrganizer. """ self._remove_object(parameter, self._parameters) - self._eqfactory.deRegisterBuilder(parameter.name) + self._eqfactory.deregister_builder(parameter.name) return def register_calculator(self, calculator, argnames=None): @@ -850,7 +850,7 @@ def register_calculator(self, calculator, argnames=None): Equation The callable Equation object wrapping `calculator`. """ - self._eqfactory.registerOperator(calculator.name, calculator) + self._eqfactory.register_operator(calculator.name, calculator) self._add_object(calculator, self._calculators) # Register arguments of the calculator if argnames is None: @@ -863,10 +863,10 @@ def register_calculator(self, calculator, argnames=None): parameter = self._new_parameter(pname, 0) else: parameter = self.get(pname) - calculator.addLiteral(parameter) + calculator.add_literal(parameter) # Now return an equation object - eq = self._eqfactory.makeEquation(calculator.name) + eq = self._eqfactory.make_equation(calculator.name) return eq @deprecated(registerCalculator_deprecation_msg) @@ -933,7 +933,7 @@ def register_function(self, function, name=None, argnames=None): "Specify a name with the 'name' argument." ) raise ValueError(m) - self._eqfactory.registerOperator(name, function) + self._eqfactory.register_operator(name, function) return function # Introspection code @@ -993,13 +993,13 @@ def register_function(self, function, name=None, argnames=None): if isinstance(function, Calculator): for pname in argnames: parameter = self.get(pname) - function.addLiteral(parameter) - self._eqfactory.registerOperator(name, function) + function.add_literal(parameter) + self._eqfactory.register_operator(name, function) else: self._eqfactory.register_function(name, function, argnames) # Now we can create the Equation and return it to the user. - equation_object = self._eqfactory.makeEquation(name) + equation_object = self._eqfactory.make_equation(name) return equation_object @@ -1770,13 +1770,13 @@ def get_equation_from_string( # Register the ns parameters in the equation factory for name, arg in ns.items(): - factory.registerArgument(name, arg) + factory.register_argument(name, arg) - eq = factory.makeEquation(eqstr, buildargs, argclass, argkw) + eq = factory.make_equation(eqstr, buildargs, argclass, argkw) # Clean the ns parameters for name in ns: - factory.deRegisterBuilder(name) + factory.deregister_builder(name) return eq diff --git a/tests/conftest.py b/tests/conftest.py index d19f976e..115edb90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,23 +107,25 @@ def _makeArgs(num): @pytest.fixture(scope="session") -def noObserversInGlobalBuilders(): - def _no_observers_in_global_builders(): - """True if no observer function leaks to global builder objects. - - Ensure objects are not immortal due to a reference from static - value. +def find_builders_with_observers(): + def _find_builders_with_observers(): + """Return the names of global builders whose literal has + observers. + + A global builder lives for the whole session, so an observer + attached to it keeps that observer's equation alive forever. + Call this at the end of a test; the result should be empty. """ from diffpy.srfit.equation.builder import _builders - rv = True - for n, b in _builders.items(): - if b.literal and b.literal._observers: - rv = False - break - return rv + builders_with_observers = [ + name + for name, builder in _builders.items() + if builder.literal and builder.literal._observers + ] + return builders_with_observers - return _no_observers_in_global_builders() + return _find_builders_with_observers @pytest.fixture(scope="session") diff --git a/tests/deprecated/test_equation_deprecations.py b/tests/deprecated/test_equation_deprecations.py new file mode 100644 index 00000000..f1701072 --- /dev/null +++ b/tests/deprecated/test_equation_deprecations.py @@ -0,0 +1,454 @@ +"""Tests for the deprecated camelCase names in diffpy.srfit.equation. + +Each test repeats the cases of the matching test for the new name, but +calls the deprecated name and also checks that it warns. + +Remove this file in version 4.0.0, together with the deprecated names. +""" + +import re + +import numpy +import pytest + +import diffpy.srfit.equation.builder as builder +import diffpy.srfit.equation.literals as literals +import diffpy.srfit.equation.visitors as visitors +from diffpy.srfit.equation.equationmod import Equation + + +def expected_message(base, old_name, new_name): + return re.escape( + f"'{base}.{old_name}' is deprecated and will be removed in version " + f"4.0.0. Please use '{base}.{new_name}' instead." + ) + + +LITERAL = "diffpy.srfit.equation.literals.Literal" +LITERALS = "diffpy.srfit.equation.literals" +OPERATOR = "diffpy.srfit.equation.literals.Operator" +EQUATION = "diffpy.srfit.equation.Equation" +EQUATION_FACTORY = "diffpy.srfit.equation.builder.EquationFactory" +BASE_BUILDER = "diffpy.srfit.equation.builder.BaseBuilder" +BUILDER = "diffpy.srfit.equation.builder" +VISITORS = "diffpy.srfit.equation.visitors" +VISITOR = "diffpy.srfit.equation.visitors.visitor.Visitor" + + +def _make_sum(literal_kind): + """Return a literal whose value is 1.5 + 2.0 = 3.5.""" + if literal_kind == "argument": + return literals.Argument(name="a", value=3.5) + operator = literals.AdditionOperator() + operator.add_literal(literals.Argument(name="a", value=1.5)) + operator.add_literal(literals.Argument(name="b", value=2.0)) + if literal_kind == "operator": + return operator + return Equation(name="eq", root=operator) + + +# Literal: see test_literals.py::test_get_value. +@pytest.mark.parametrize( + "input_literal_kind", + [ + # C1: An Argument holds the value itself. + # Expected: Warns and returns 3.5. + "argument", + # C2: An Operator computes the value from its literals. + # Expected: Warns and returns 3.5. + "operator", + # C3: An Equation evaluates the operator at its root. + # Expected: Warns and returns 3.5. + "equation", + ], +) +def test_getValue(input_literal_kind): + literal = _make_sum(input_literal_kind) + with pytest.warns( + DeprecationWarning, + match=expected_message(LITERAL, "getValue", "get_value"), + ): + actual_value = literal.getValue() + expected_value = 3.5 + assert actual_value == expected_value + + +# EquationFactory: see +# test_builder.py::test_register_function_replaces_existing. +@pytest.mark.parametrize( + "input_replacement, expected_value", + [ + # C1: g is replaced by the Python function 0.5 * v1. + # Expected: Warns, and "g" evaluates to 0.5. + ("python_function", 0.5), + # C2: g is replaced by the equation "v1 + v2". + # Expected: Warns, and "g" evaluates to 3. + ("equation", 3), + ], +) +def test_registerFunction(make_args, input_replacement, expected_value): + factory = builder.EquationFactory() + v1, v2, v3, v4 = make_args(4) + for argument in (v1, v2, v3, v4): + factory.register_argument(argument.name, argument) + factory.register_function( + "g", + lambda v1, v2, v3, v4: (v1 + v2) * (v3 + v4), + ["v1", "v2", "v3", "v4"], + ) + equation = factory.make_equation("g") + if input_replacement == "python_function": + new_function, new_arg_names = (lambda v1: 0.5 * v1), ["v1"] + else: + new_function = factory.make_equation("v1 + v2") + new_arg_names = new_function.argdict.keys() + with pytest.warns( + DeprecationWarning, + match=expected_message( + EQUATION_FACTORY, "registerFunction", "register_function" + ), + ): + function_builder = factory.registerFunction( + "g", new_function, new_arg_names + ) + actual_root = equation.root + assert actual_root is function_builder.literal + actual_value = equation() + assert actual_value == pytest.approx(expected_value) + + +# BaseBuilder: see test_builder.py::test_build_equation_from_builders. +def test_getEquation(): + # C1: The equation A*sin(a*x) is written with builder objects, with + # a=1, A=2 and x an array. + # Expected: Warns. Its arguments are A and a, and its value is + # 2*sin(x). + sin = builder.get_builder("sin") + a = builder.ArgumentBuilder(name="a", value=1) + A = builder.ArgumentBuilder(name="A", value=2) + x = numpy.arange(0, numpy.pi, 0.1) + with pytest.warns( + DeprecationWarning, + match=expected_message(BASE_BUILDER, "getEquation", "get_equation"), + ): + equation = (A * sin(a * x)).getEquation() + actual_arg_names = [arg.name for arg in equation.args] + expected_arg_names = ["A", "a"] + assert actual_arg_names == expected_arg_names + actual_values = equation() + expected_values = 2 * numpy.sin(x) + assert numpy.array_equal(actual_values, expected_values) + + +def _make_factory_call_args(method_name): + """Return fresh arguments for an EquationFactory method.""" + if method_name == "make_equation": + return ("v1 + 2",) + if method_name == "register_constant": + return ("c", 3) + if method_name == "register_argument": + return ("a", literals.Argument(name="a", value=3)) + if method_name == "register_operator": + return ("op", _make_sum("operator")) + if method_name == "register_builder": + return ("b", builder.ArgumentBuilder(name="b", value=3)) + return ("v1",) + + +def _call_factory_method(method_name, args): + """Call a method of a factory that has v1 = 1 registered. + + Return the names the factory has afterward and the value of what + the method returned. + """ + factory = builder.EquationFactory() + factory.register_argument("v1", literals.Argument(name="v1", value=1)) + result = getattr(factory, method_name)(*args) + if result is None: + value = None + elif isinstance(result, Equation): + value = result() + else: + value = result.literal.value + return set(factory.builders), value + + +# EquationFactory: each deprecated method does the same as its new name, +# on a factory that has v1 = 1 registered. See test_builder.py. +@pytest.mark.parametrize( + "input_old_name, input_new_name", + [ + # C1: makeEquation builds "v1 + 2". + # Expected: Warns and gives the same result as make_equation. + ("makeEquation", "make_equation"), + # C2: registerConstant registers c = 3. + # Expected: Warns and gives the same result as register_constant. + ("registerConstant", "register_constant"), + # C3: registerArgument registers the argument a = 3. + # Expected: Warns and gives the same result as register_argument. + ("registerArgument", "register_argument"), + # C4: registerOperator registers the operator op = 1.5 + 2.0. + # Expected: Warns and gives the same result as register_operator. + ("registerOperator", "register_operator"), + # C5: registerBuilder registers a builder for b = 3. + # Expected: Warns and gives the same result as register_builder. + ("registerBuilder", "register_builder"), + # C6: deRegisterBuilder removes v1. + # Expected: Warns and gives the same result as deregister_builder. + ("deRegisterBuilder", "deregister_builder"), + ], +) +def test_equation_factory_deprecations(input_old_name, input_new_name): + with pytest.warns( + DeprecationWarning, + match=expected_message( + EQUATION_FACTORY, input_old_name, input_new_name + ), + ): + actual_old_result = _call_factory_method( + input_old_name, _make_factory_call_args(input_new_name) + ) + actual_new_result = _call_factory_method( + input_new_name, _make_factory_call_args(input_new_name) + ) + assert actual_old_result == actual_new_result + + +def _make_builder_call_args(function_name): + """Return fresh arguments for a builder module function.""" + if function_name == "wrap_argument": + return ("a", literals.Argument(name="a", value=3)) + if function_name == "wrap_operator": + return ("op", _make_sum("operator")) + if function_name == "wrap_function": + return ("f", numpy.add, 2, 1) + return ("sin",) + + +def _describe_builder(wrapped_builder): + literal = wrapped_builder.literal + return type(wrapped_builder), type(literal), literal.name + + +# builder module: each deprecated function does the same as its new name. +# See test_builder.py. +@pytest.mark.parametrize( + "input_old_name, input_new_name", + [ + # C1: wrapArgument wraps the argument a = 3. + # Expected: Warns and gives the same builder as wrap_argument. + ("wrapArgument", "wrap_argument"), + # C2: wrapOperator wraps the operator op = 1.5 + 2.0. + # Expected: Warns and gives the same builder as wrap_operator. + ("wrapOperator", "wrap_operator"), + # C3: wrapFunction wraps numpy.add with two inputs. + # Expected: Warns and gives the same builder as wrap_function. + ("wrapFunction", "wrap_function"), + # C4: getBuilder gets the global builder for sin. + # Expected: Warns and gives the same builder as get_builder. + ("getBuilder", "get_builder"), + ], +) +def test_builder_module_deprecations(input_old_name, input_new_name): + with pytest.warns( + DeprecationWarning, + match=expected_message(BUILDER, input_old_name, input_new_name), + ): + actual_old_builder = getattr(builder, input_old_name)( + *_make_builder_call_args(input_new_name) + ) + actual_new_builder = getattr(builder, input_new_name)( + *_make_builder_call_args(input_new_name) + ) + actual_old_result = _describe_builder(actual_old_builder) + actual_new_result = _describe_builder(actual_new_builder) + assert actual_old_result == actual_new_result + + +def _set_equation_root(method_name): + equation = Equation(name="eq", root=_make_sum("operator")) + getattr(equation, method_name)(literals.Argument(name="v", value=5)) + return [arg.name for arg in equation.args], equation() + + +# Equation: see test_equation.py::test_equation_set_root. +def test_setRoot(): + # C1: An equation with root 1.5 + 2.0 has its root set to v = 5. + # Expected: Warns, and the equation has the same arguments and value + # as after set_root. + with pytest.warns( + DeprecationWarning, + match=expected_message(EQUATION, "setRoot", "set_root"), + ): + actual_old_result = _set_equation_root("setRoot") + actual_new_result = _set_equation_root("set_root") + assert actual_old_result == actual_new_result + + +def _add_literals(method_name): + operator = literals.AdditionOperator() + getattr(operator, method_name)(literals.Argument(name="a", value=1)) + getattr(operator, method_name)(literals.Argument(name="b", value=2)) + return [arg.name for arg in operator.args], operator.value + + +# Operator: see test_literals.py::test_operator_value_after_argument_change. +def test_addLiteral(): + # C1: The arguments a = 1 and b = 2 are added to an add operator. + # Expected: Warns, and the operator has the same arguments and value + # as after add_literal. + with pytest.warns( + DeprecationWarning, + match=expected_message(OPERATOR, "addLiteral", "add_literal"), + ): + actual_old_result = _add_literals("addLiteral") + actual_new_result = _add_literals("add_literal") + assert actual_old_result == actual_new_result + + +# Equation: see test_equation.py::test_equation_add_literal_bad. +def test_addLiteral_equation_bad(): + # C1: A literal is added to an equation. + # Expected: Warns and raises a RuntimeError. + equation = Equation(name="eq", root=_make_sum("operator")) + with ( + pytest.warns( + DeprecationWarning, + match=expected_message(OPERATOR, "addLiteral", "add_literal"), + ), + pytest.raises(RuntimeError), + ): + equation.addLiteral(literals.Argument(name="c", value=3)) + + +def _describe_operator(operator): + return ( + type(operator), + operator.name, + operator.symbol, + operator.operation, + operator.nin, + operator.nout, + ) + + +# literals module: see test_literals.py::test_operator_init. +def test_makeOperator(): + # C1: An add operator is made. + # Expected: Warns and gives the same operator as make_operator. + operator_kwargs = dict( + name="add", symbol="+", operation=numpy.add, nin=2, nout=1 + ) + with pytest.warns( + DeprecationWarning, + match=expected_message(LITERALS, "makeOperator", "make_operator"), + ): + actual_old_operator = literals.makeOperator(**operator_kwargs) + actual_new_operator = literals.make_operator(**operator_kwargs) + actual_old_result = _describe_operator(actual_old_operator) + actual_new_result = _describe_operator(actual_new_operator) + assert actual_old_result == actual_new_result + + +def _make_embedded_tree(): + """Return the tree inner + c, where the equation inner = a + b.""" + top = literals.AdditionOperator() + top.add_literal(Equation(name="inner", root=_make_sum("operator"))) + top.add_literal(literals.Argument(name="c", value=1.0, const=True)) + return top + + +def _describe_args(args): + return [arg.name for arg in args] + + +# visitors module: each deprecated function does the same as its new name +# on the tree inner + c. See test_visitors.py. +@pytest.mark.parametrize( + "input_old_name, input_new_name, input_kwargs, input_describe", + [ + # C1: getArgs gets the arguments of the tree. + # Expected: Warns and gives the same arguments as get_args. + ("getArgs", "get_args", {}, _describe_args), + # C2: getArgs gets the non-constant arguments of the tree. + # Expected: Warns and gives the same arguments as get_args. + ("getArgs", "get_args", {"getconsts": False}, _describe_args), + # C3: getExpression writes the tree out in full. + # Expected: Warns and gives the same string as get_expression. + ("getExpression", "get_expression", {}, str), + # C4: getExpression writes the tree with "inner" by name. + # Expected: Warns and gives the same string as get_expression. + ("getExpression", "get_expression", {"eqskip": "inner"}, str), + ], +) +def test_visitors_module_deprecations( + input_old_name, input_new_name, input_kwargs, input_describe +): + with pytest.warns( + DeprecationWarning, + match=expected_message(VISITORS, input_old_name, input_new_name), + ): + actual_old_result = input_describe( + getattr(visitors, input_old_name)( + _make_embedded_tree(), **input_kwargs + ) + ) + actual_new_result = input_describe( + getattr(visitors, input_new_name)( + _make_embedded_tree(), **input_kwargs + ) + ) + assert actual_old_result == actual_new_result + + +# visitors module: see test_visitors.py::test_pretty_print. +def test_prettyPrint(capsys): + # C1: The tree inner + c is printed. + # Expected: Warns and prints the same line as pretty_print. + with pytest.warns( + DeprecationWarning, + match=expected_message(VISITORS, "prettyPrint", "pretty_print"), + ): + visitors.prettyPrint(_make_embedded_tree()) + actual_old_output = capsys.readouterr().out + visitors.pretty_print(_make_embedded_tree()) + actual_new_output = capsys.readouterr().out + assert actual_old_output == actual_new_output + + +def _make_visited_literal(method_name): + if method_name == "on_argument": + return literals.Argument(name="a", value=1.5) + if method_name == "on_operator": + return _make_sum("operator") + return Equation(name="eq", root=_make_sum("operator")) + + +# Visitor: each deprecated method on the Visitor base forwards to its new +# name in a subclass. A Printer writes the visited literal. +@pytest.mark.parametrize( + "input_old_name, input_new_name", + [ + # C1: onArgument writes the argument a. + # Expected: Warns and writes the same as on_argument. + ("onArgument", "on_argument"), + # C2: onOperator writes the operator a + b. + # Expected: Warns and writes the same as on_operator. + ("onOperator", "on_operator"), + # C3: onEquation writes the equation eq = a + b. + # Expected: Warns and writes the same as on_equation. + ("onEquation", "on_equation"), + ], +) +def test_visitor_deprecations(input_old_name, input_new_name): + with pytest.warns( + DeprecationWarning, + match=expected_message(VISITOR, input_old_name, input_new_name), + ): + actual_old_output = getattr(visitors.Printer(), input_old_name)( + _make_visited_literal(input_new_name) + ) + actual_new_output = getattr(visitors.Printer(), input_new_name)( + _make_visited_literal(input_new_name) + ) + assert actual_old_output == actual_new_output diff --git a/tests/test_builder.py b/tests/test_builder.py index d4b38fd7..2884fb13 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -12,7 +12,7 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Tests for refinableobj module.""" +"""Tests for the builder module.""" import numpy import pytest @@ -21,264 +21,471 @@ import diffpy.srfit.equation.literals as literals -def testRegisterArg(make_args, noObserversInGlobalBuilders): - - factory = builder.EquationFactory() - - v1 = make_args(1)[0] - - b1 = factory.registerArgument("v1", v1) - assert factory.builders["v1"] is b1 - assert b1.literal is v1 - - eq = factory.makeEquation("v1") - - assert v1 is eq.args[0] - assert 1 == len(eq.args) - - # Try to parse an equation with buildargs turned off - with pytest.raises(ValueError): - factory.makeEquation("v1 + v2", False) +def _gaussian(x, sigma): + return numpy.sqrt(numpy.exp(-0.5 * (x / sigma) ** 2)) - # Make sure we can still use constants - eq = factory.makeEquation("v1 + 2", False) - assert v1 is eq.args[0] - assert 1 == len(eq.args) - assert noObserversInGlobalBuilders - return +def _make_factory_with_function(make_args): + """Return a factory where g(v1, v2, v3, v4) = (v1 + v2) * (v3 + v4). -def testRegisterOperator(make_args, noObserversInGlobalBuilders): - """Try to use an operator without arguments in an equation.""" + The arguments v1 to v4 have the values 1 to 4, so g is 21. The fifth + argument from make_args, v5 = 5, is returned unregistered. + """ factory = builder.EquationFactory() - v1, v2, v3, v4 = make_args(4) - - op = literals.AdditionOperator() - - op.addLiteral(v1) - op.addLiteral(v2) - - factory.registerArgument("v3", v3) - factory.registerArgument("v4", v4) - factory.registerOperator("op", op) + v1, v2, v3, v4, v5 = make_args(5) + for argument in (v1, v2, v3, v4): + factory.register_argument(argument.name, argument) - # Build an equation where op is treated as a terminal node - eq = factory.makeEquation("op") - assert 3 == pytest.approx(eq()) + def g(v1, v2, v3, v4): + return (v1 + v2) * (v3 + v4) - eq = factory.makeEquation("v3*op") - assert 9 == pytest.approx(eq()) + function_builder = factory.register_function( + "g", g, ["v1", "v2", "v3", "v4"] + ) + return factory, function_builder.literal, (v1, v2, v3, v4, v5), g - # Now use the op like a function - eq = factory.makeEquation("op(v3, v4)") - assert 7 == pytest.approx(eq()) - # Make sure we can still access op as itself. - eq = factory.makeEquation("op") - assert 3 == pytest.approx(eq()) +def test_register_argument(make_args): + # C1: An argument is registered under the name "v1". + # Expected: The returned builder is stored under that name and wraps + # the argument, and an equation "v1" has the argument as its only + # argument. + factory = builder.EquationFactory() + v1 = make_args(1)[0] + argument_builder = factory.register_argument("v1", v1) + actual_builder = factory.builders["v1"] + expected_builder = argument_builder + assert actual_builder == expected_builder + actual_literal = argument_builder.literal + expected_literal = v1 + assert actual_literal == expected_literal + actual_args = factory.make_equation("v1").args + expected_args = [v1] + assert actual_args == expected_args + + +# deregister_builder removes a name from the factory. +@pytest.mark.parametrize( + "input_name, expected_names_removed", + [ + # C1: The registered argument v1 is deregistered. + # Expected: v1 is no longer a builder of the factory. + ("v1", {"v1"}), + # C2: A name that was never registered is deregistered. + # Expected: Nothing happens, and no error is raised. + ("missing", set()), + ], +) +def test_deregister_builder(make_args, input_name, expected_names_removed): + factory = builder.EquationFactory() + v1 = make_args(1)[0] + factory.register_argument("v1", v1) + names_before = set(factory.builders) + factory.deregister_builder(input_name) + actual_names_removed = names_before - set(factory.builders) + assert actual_names_removed == expected_names_removed - assert noObserversInGlobalBuilders - return +# With buildargs=False, the factory builds from registered arguments only. +def test_make_equation_without_building_args(make_args): + factory = builder.EquationFactory() + v1 = make_args(1)[0] + factory.register_argument("v1", v1) + actual_args = factory.make_equation("v1 + 2", False).args + expected_args = [v1] + assert actual_args == expected_args -def testSwapping(make_args, noObserversInGlobalBuilders): - def g1(v1, v2, v3, v4): - return (v1 + v2) * (v3 + v4) +# With buildargs=False, the factory may not create new arguments, so an +# equation using an unregistered name raises a ValueError. +def test_make_equation_without_building_args_bad(make_args): + factory = builder.EquationFactory() + v1 = make_args(1)[0] + factory.register_argument("v1", v1) + with pytest.raises(ValueError): + factory.make_equation("v1 + v2", False) + + +# A registered operator op = v1 + v2 (= 3) can be used as a value or called +# as a function. The arguments v3 = 3 and v4 = 4 are also registered. +@pytest.mark.parametrize( + "input_equations, expected_value", + [ + # C1: "op" on its own. + # Expected: The operator's value, 3. + (["op"], 3), + # C2: "v3*op". + # Expected: 3 * 3 = 9. + (["v3*op"], 9), + # C3: "op(v3, v4)" calls the operator with new arguments. + # Expected: 3 + 4 = 7. + (["op(v3, v4)"], 7), + # C4: "op(v3, v4)" is built first, then "op" on its own. + # Expected: "op" still uses its own arguments, so 3. + (["op(v3, v4)", "op"], 3), + ], +) +def test_register_operator(make_args, input_equations, expected_value): + factory = builder.EquationFactory() + v1, v2, v3, v4 = make_args(4) + operator = literals.AdditionOperator() + operator.add_literal(v1) + operator.add_literal(v2) + factory.register_argument("v3", v3) + factory.register_argument("v4", v4) + factory.register_operator("op", operator) + for equation_string in input_equations: + equation = factory.make_equation(equation_string) + actual_value = equation() + assert actual_value == expected_value + + +def test_register_function(make_args): + # C1: A Python function g is registered with the arguments v1 to v4. + # Expected: The builder's operator wraps g, takes v1 to v4 and has the + # value 21, and an equation "g" has that operator as its root. + factory, operator, (v1, v2, v3, v4, _), g = _make_factory_with_function( + make_args + ) + actual_operation = operator.operation + expected_operation = g + assert actual_operation == expected_operation + actual_args = operator.args + expected_args = [v1, v2, v3, v4] + assert actual_args == expected_args + actual_operator_value = operator.value + expected_value = 21 + assert actual_operator_value == expected_value + equation = factory.make_equation("g") + actual_root = equation.root + expected_root = operator + assert actual_root == expected_root + actual_equation_value = equation() + assert actual_equation_value == expected_value + + +def test_register_argument_replaces_existing(make_args): + # C1: v5 is registered under the name "v4", which g already uses. + # Expected: The new builder replaces the old one, g's cached value is + # cleared, g uses v5 instead of v4, and "g" evaluates to + # (1 + 2) * (3 + 5) = 24. + factory, operator, (v1, v2, v3, _, v5), _ = _make_factory_with_function( + make_args + ) + equation = factory.make_equation("g") + argument_builder = factory.register_argument("v4", v5) + actual_builder = factory.builders["v4"] + expected_builder = argument_builder + assert actual_builder == expected_builder + actual_literal = argument_builder.literal + expected_literal = v5 + assert actual_literal == expected_literal + actual_cached_value = operator._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_args = operator.args + expected_args = [v1, v2, v3, v5] + assert actual_args == expected_args + actual_value = equation() + expected_value = 24 + assert actual_value == expected_value + + +# Registering a new function under the name "g" replaces g inside equations +# that already use it. +@pytest.mark.parametrize( + "input_replacement, expected_value", + [ + # C1: g is replaced by the Python function 0.5 * v1. + # Expected: "g" evaluates to 0.5. + ("python_function", 0.5), + # C2: g is replaced by the equation "v1 + v2". + # Expected: "g" evaluates to 3. + ("equation", 3), + ], +) +def test_register_function_replaces_existing( + make_args, input_replacement, expected_value +): + factory, _, (v1, v2, _, _, _), _ = _make_factory_with_function(make_args) + equation = factory.make_equation("g") + if input_replacement == "python_function": + function_builder = factory.register_function( + "g", lambda v1: 0.5 * v1, ["v1"] + ) + expected_args = [v1] + else: + replacement = factory.make_equation("v1 + v2") + function_builder = factory.register_function( + "g", replacement, replacement.argdict.keys() + ) + expected_args = [v1, v2] + new_operator = function_builder.literal + actual_root = equation.root + expected_root = new_operator + assert actual_root == expected_root + actual_args = new_operator.args + assert actual_args == expected_args + actual_value = equation() + assert actual_value == expected_value + + +# The factory creates the arguments of an equation string in the order they +# first appear. +@pytest.mark.parametrize( + "input_equation, expected_arg_names", + [ + # C1: A scalar equation with four arguments. + # Expected: A, x, B, C. + ("A*sin(0.5*x)+divide(B,C)", ["A", "x", "B", "C"]), + # C2: A Gaussian that uses the constant e. + # Expected: x, sigma, without e. + ("sqrt(e**(-0.5*(x/sigma)**2))", ["x", "sigma"]), + ], +) +def test_make_equation_arg_order(input_equation, expected_arg_names): + factory = builder.EquationFactory() + equation = factory.make_equation(input_equation) + actual_arg_names = [arg.name for arg in equation.args] + assert actual_arg_names == expected_arg_names - def g2(v1): - return 0.5 * v1 +def test_make_equation_scalar(): + # C1: "A*sin(0.5*x)+divide(B,C)" with A=1, x=pi, B=4 and C=2. + # Expected: 1*sin(pi/2) + 4/2 = 3. factory = builder.EquationFactory() - v1, v2, v3, v4, v5 = make_args(5) + equation = factory.make_equation("A*sin(0.5*x)+divide(B,C)") + equation.A.set_value(1) + equation.x.set_value(numpy.pi) + equation.B.set_value(4.0) + equation.C.set_value(2.0) + actual_value = equation() + expected_value = 3 + assert actual_value == expected_value + + +def test_make_equation_array(): + # C1: A Gaussian is evaluated on an array of x values. + # Expected: The Gaussian at each x. + factory = builder.EquationFactory() + equation = factory.make_equation("sqrt(e**(-0.5*(x/sigma)**2))") + x = numpy.arange(0, 1, 0.05) + equation.x.set_value(x) + equation.sigma.set_value(0.1) + actual_values = equation() + expected_values = _gaussian(x, 0.1) + assert numpy.allclose(actual_values, expected_values) - factory.registerArgument("v1", v1) - factory.registerArgument("v2", v2) - factory.registerArgument("v3", v3) - factory.registerArgument("v4", v4) - b = factory.register_function("g", g1, ["v1", "v2", "v3", "v4"]) - - # Now associate args with the wrapped function - op = b.literal - assert op.operation == g1 - assert v1 in op.args - assert v2 in op.args - assert v3 in op.args - assert v4 in op.args - assert round(abs(21 - op.value), 7) == 0 - - eq1 = factory.makeEquation("g") - assert eq1.root is op - assert round(abs(21 - eq1()), 7) == 0 - - # Swap out an argument by registering it under a taken name - b = factory.registerArgument("v4", v5) - assert factory.builders["v4"] is b - assert b.literal is v5 - assert op._value is None - assert op.args == [v1, v2, v3, v5] - assert round(abs(24 - eq1()), 7) == 0 - - # Now swap out the function - b = factory.register_function("g", g2, ["v1"]) - op = b.literal - assert op.operation == g2 - assert v1 in op.args - assert eq1.root is op - assert round(abs(0.5 - op.value), 7) == 0 - assert round(abs(0.5 - eq1()), 7) == 0 - - # Make an equation - eqeq = factory.makeEquation("v1 + v2") - # Register this "g" - b = factory.registerFunction("g", eqeq, eqeq.argdict.keys()) - op = b.literal - assert v1 in op.args - assert v2 in op.args - assert eq1.root is op - assert round(abs(3 - op.value), 7) == 0 - assert round(abs(3 - eq1()), 7) == 0 - assert noObserversInGlobalBuilders - return - - -def testParseEquation(noObserversInGlobalBuilders): +def test_make_equation_with_registered_constant(): + # C1: x is registered as a constant before the Gaussian is built. + # Expected: sigma is the only argument, and the Gaussian uses the + # constant x. factory = builder.EquationFactory() - - # Scalar equation - eq = factory.makeEquation("A*sin(0.5*x)+divide(B,C)") - A = 1 - x = numpy.pi - B = 4.0 - C = 2.0 - eq.A.set_value(A) - eq.x.set_value(x) - eq.B.set_value(B) - eq.C.set_value(C) - assert numpy.array_equal(eq(), f_equation(A, x, B, C)) - - # Make sure that the arguments of eq are listed in the order in which - # they appear in the equations. - assert eq.args == [eq.A, eq.x, eq.B, eq.C] - - # Vector equation - eq = factory.makeEquation("sqrt(e**(-0.5*(x/sigma)**2))") x = numpy.arange(0, 1, 0.05) - sigma = 0.1 - eq.x.set_value(x) - eq.sigma.set_value(sigma) - assert numpy.allclose(eq(), gaussian_test(x, sigma)) - assert eq.args == [eq.x, eq.sigma] - - # Equation with constants - factory.registerConstant("x", x) - eq = factory.makeEquation("sqrt(e**(-0.5*(x/sigma)**2))") - assert "sigma" in eq.argdict - assert "x" not in eq.argdict - assert numpy.allclose(eq(sigma=sigma), gaussian_test(x, sigma)) - assert eq.args == [eq.sigma] - - # Equation with user-defined functions - factory.register_function("myfunc", eq, ["sigma"]) - eq2 = factory.makeEquation("c*myfunc(sigma)") - assert numpy.allclose(eq2(c=2, sigma=sigma), 2 * gaussian_test(x, sigma)) - assert "sigma" in eq2.argdict - assert "c" in eq2.argdict - assert eq2.args == [eq2.c, eq2.sigma] - assert noObserversInGlobalBuilders - return - - -def test_parse_constant(): - """Verify parsing of constant numeric expressions.""" + factory.register_constant("x", x) + equation = factory.make_equation("sqrt(e**(-0.5*(x/sigma)**2))") + actual_arg_names = [arg.name for arg in equation.args] + expected_arg_names = ["sigma"] + assert actual_arg_names == expected_arg_names + actual_values = equation(sigma=0.1) + expected_values = _gaussian(x, 0.1) + assert numpy.allclose(actual_values, expected_values) + + +def test_make_equation_with_registered_equation(): + # C1: A Gaussian equation is registered as the function myfunc(sigma), + # then used in "c*myfunc(sigma)". + # Expected: The arguments are c and sigma, and the value is c times the + # Gaussian. factory = builder.EquationFactory() - eq = factory.makeEquation("3.12 + 2") - assert isinstance(eq, builder.Equation) - assert set() == factory.equations - assert 5.12 == eq() + x = numpy.arange(0, 1, 0.05) + factory.register_constant("x", x) + gaussian = factory.make_equation("sqrt(e**(-0.5*(x/sigma)**2))") + factory.register_function("myfunc", gaussian, ["sigma"]) + equation = factory.make_equation("c*myfunc(sigma)") + actual_arg_names = [arg.name for arg in equation.args] + expected_arg_names = ["c", "sigma"] + assert actual_arg_names == expected_arg_names + actual_values = equation(c=2, sigma=0.1) + expected_values = 2 * _gaussian(x, 0.1) + assert numpy.allclose(actual_values, expected_values) + + +def test_make_equation_constant_expression(): + # C1: An equation string with only numbers, "3.12 + 2". + # Expected: An Equation with the value 5.12, which the factory does not + # keep, and which takes no arguments when called. + factory = builder.EquationFactory() + equation = factory.make_equation("3.12 + 2") + actual_type = type(equation) + expected_type = builder.Equation + assert actual_type == expected_type + actual_kept_equations = factory.equations + expected_kept_equations = set() + assert actual_kept_equations == expected_kept_equations + actual_value = equation() + expected_value = 5.12 + assert actual_value == expected_value with pytest.raises(ValueError): - eq(3) - return - - -def testBuildEquation(noObserversInGlobalBuilders): + equation(3) + + +# Global builders live for the whole session. Building equations must not +# leave observers on them, or those equations would never be freed. +@pytest.mark.parametrize( + "input_equation", + [ + # C1: An equation that uses numpy functions. + # Expected: No global builder has observers afterwards. + "A*sin(0.5*x)+divide(B,C)", + # C2: An equation that uses the constant e. + # Expected: No global builder has observers afterwards. + "sqrt(e**(-0.5*(x/sigma)**2))", + # C3: An equation with only numbers. + # Expected: No global builder has observers afterwards. + "3.12 + 2", + ], +) +def test_make_equation_leaves_no_global_observers( + find_builders_with_observers, input_equation +): + factory = builder.EquationFactory() + factory.make_equation(input_equation) + actual_builders = find_builders_with_observers() + expected_builders = [] + assert actual_builders == expected_builders - from numpy import array_equal - # simple equation - sin = builder.getBuilder("sin") +def test_build_equation_from_builders(): + # C1: The equation A*sin(a*x) is written with builder objects, with + # a=1, A=2 and x an array. + # Expected: Its arguments are A and a, and its value is 2*sin(x). + sin = builder.get_builder("sin") a = builder.ArgumentBuilder(name="a", value=1) A = builder.ArgumentBuilder(name="A", value=2) x = numpy.arange(0, numpy.pi, 0.1) - - beq = A * sin(a * x) - eq = beq.get_equation() - - assert "a" in eq.argdict - assert "A" in eq.argdict - assert array_equal(eq(), 2 * numpy.sin(x)) - - assert eq.args == [eq.A, eq.a] - - # Check the number of arguments - with pytest.raises(ValueError): - sin() - - # custom function - def _f(a, b): + equation = (A * sin(a * x)).get_equation() + actual_arg_names = [arg.name for arg in equation.args] + expected_arg_names = ["A", "a"] + assert actual_arg_names == expected_arg_names + actual_values = equation() + expected_values = 2 * numpy.sin(x) + assert numpy.array_equal(actual_values, expected_values) + + +def test_build_equation_with_wrapped_function(): + # C1: A Python function f(a, b) = (a - b) / (a + b) is wrapped and used + # in sin(f(a, b)) with a=2 and b=1. + # Expected: sin(f(2, 1)). + def f(a, b): return (a - b) * 1.0 / (a + b) - f = builder.wrapFunction("f", _f, 2, 1) + wrapped_f = builder.wrap_function("f", f, 2, 1) + sin = builder.get_builder("sin") a = builder.ArgumentBuilder(name="a", value=2) b = builder.ArgumentBuilder(name="b", value=1) - - beq = sin(f(a, b)) - eq = beq.get_equation() - assert eq() == numpy.sin(_f(2, 1)) - - # complex function - sqrt = builder.getBuilder("sqrt") - e = numpy.e - _x = numpy.arange(0, 1, 0.05) - x = builder.ArgumentBuilder(name="x", value=_x, const=True) + equation = sin(wrapped_f(a, b)).get_equation() + actual_value = equation() + expected_value = numpy.sin(f(2, 1)) + assert actual_value == expected_value + + +def test_build_equation_with_constant_argument(): + # C1: A Gaussian is written with builders, with x a constant array. + # Expected: The Gaussian at each x. + sqrt = builder.get_builder("sqrt") + x_values = numpy.arange(0, 1, 0.05) + x = builder.ArgumentBuilder(name="x", value=x_values, const=True) sigma = builder.ArgumentBuilder(name="sigma", value=0.1) - beq = sqrt(e ** (-0.5 * (x / sigma) ** 2)) - eq = beq.get_equation() - assert numpy.allclose(eq(), numpy.sqrt(numpy.exp(-0.5 * (_x / 0.1) ** 2))) - - # Equation with Equation + equation = sqrt(numpy.e ** (-0.5 * (x / sigma) ** 2)).get_equation() + actual_values = equation() + expected_values = _gaussian(x_values, 0.1) + assert numpy.allclose(actual_values, expected_values) + + +# A builder for a numpy function checks how many arguments it is given. +@pytest.mark.parametrize( + "input_arg_count", + [ + # C1: sin is called with no arguments. + # Expected: A ValueError is raised. + 0, + # C2: sin is called with two arguments. + # Expected: A ValueError is raised. + 2, + ], +) +def test_builder_wrong_arg_count_bad(input_arg_count): + sin = builder.get_builder("sin") + args = [builder.ArgumentBuilder(value=1)] * input_arg_count + with pytest.raises(ValueError): + sin(*args) + + +# The equation A + B, with A=2 and B=4, is wrapped as the operator E. +@pytest.mark.parametrize( + "input_expression, expected_value", + [ + # C1: E is used as a value, in 2*E. + # Expected: 2 * (2 + 4) = 12. + ("2*E", 12), + # C2: E is called with the new arguments C=5 and D=6, in E(C, D) + 1. + # Expected: (5 + 6) + 1 = 12. + ("E(C, D) + 1", 12), + # C3: E is called with its own A and the new D=6, in 3*E(A, D) - 1. + # Expected: 3 * (2 + 6) - 1 = 23. + ("3*E(A, D) - 1", 23), + ], +) +def test_build_equation_with_wrapped_equation( + input_expression, expected_value +): A = builder.ArgumentBuilder(name="A", value=2) B = builder.ArgumentBuilder(name="B", value=4) - beq = A + B - eq = beq.get_equation() - E = builder.wrapOperator("eq", eq) - eq2 = (2 * E).get_equation() - # Make sure these evaluate to the same thing - assert eq.args == [A.literal, B.literal] - assert 2 * eq() == eq2() - # Pass new arguments to the equation C = builder.ArgumentBuilder(name="C", value=5) D = builder.ArgumentBuilder(name="D", value=6) - eq3 = (E(C, D) + 1).get_equation() - assert 12 == eq3() - # Pass old and new arguments to the equation - # If things work right, A has been given the value of C in the last - # evaluation (5) - eq4 = (3 * E(A, D) - 1).getEquation() - assert 32 == eq4() - # Try to pass the wrong number of arguments - with pytest.raises(ValueError): - E(A) - with pytest.raises(ValueError): - E(A, B, C) - assert noObserversInGlobalBuilders - return - + E = builder.wrap_operator("eq", (A + B).get_equation()) + names = {"A": A, "B": B, "C": C, "D": D, "E": E} + equation = eval(input_expression, {}, names).get_equation() + actual_value = equation() + assert actual_value == expected_value -def f_equation(a, x, b, c): - return a * numpy.sin(0.5 * x) + numpy.divide(b, c) - -def gaussian_test(x, sigma): - return numpy.sqrt(numpy.exp(-0.5 * (x / sigma) ** 2)) +def test_build_equation_wrapped_equation_copies_argument_values(): + # C1: The equation A + B, with A=2 and B=4, is wrapped as E and + # evaluated as E(C, D) with C=5 and D=6. + # Expected: The values of C and D are copied into A and B. + A = builder.ArgumentBuilder(name="A", value=2) + B = builder.ArgumentBuilder(name="B", value=4) + C = builder.ArgumentBuilder(name="C", value=5) + D = builder.ArgumentBuilder(name="D", value=6) + E = builder.wrap_operator("eq", (A + B).get_equation()) + E(C, D).get_equation()() + actual_values = (A.literal.value, B.literal.value) + expected_values = (5, 6) + assert actual_values == expected_values + + +# A wrapped equation must be called with as many arguments as it has. +@pytest.mark.parametrize( + "input_arg_names", + [ + # C1: E(A + B) is called with one argument. + # Expected: A ValueError is raised. + ["A"], + # C2: E(A + B) is called with three arguments. + # Expected: A ValueError is raised. + ["A", "B", "C"], + ], +) +def test_build_equation_wrapped_equation_wrong_arg_count_bad(input_arg_names): + arguments = { + name: builder.ArgumentBuilder(name=name, value=1) + for name in ("A", "B", "C") + } + E = builder.wrap_operator( + "eq", (arguments["A"] + arguments["B"]).get_equation() + ) + with pytest.raises(ValueError): + E(*[arguments[name] for name in input_arg_names]) diff --git a/tests/test_constraint.py b/tests/test_constraint.py index ac95bc85..00def3c5 100644 --- a/tests/test_constraint.py +++ b/tests/test_constraint.py @@ -31,8 +31,8 @@ def test_constrain_parameter(self): factory = EquationFactory() - factory.registerArgument("p1", p1) - factory.registerArgument("p2", p2) + factory.register_argument("p1", p1) + factory.register_argument("p2", p2) c = Constraint() # Constrain p1 = 2*p2 @@ -76,8 +76,8 @@ def test_constrain_deprecated(self): factory = EquationFactory() - factory.registerArgument("p1", p1) - factory.registerArgument("p2", p2) + factory.register_argument("p1", p1) + factory.register_argument("p2", p2) c = Constraint() # Constrain p1 = 2*p2 diff --git a/tests/test_contribution.py b/tests/test_contribution.py index eb63cd10..4958c12a 100644 --- a/tests/test_contribution.py +++ b/tests/test_contribution.py @@ -212,7 +212,7 @@ def _fsquare(x): return x**2 -def testResidual(noObserversInGlobalBuilders): +def testResidual(find_builders_with_observers): """Test the residual, which requires all other methods.""" gen = ProfileGenerator("test") profile = Profile() @@ -291,11 +291,11 @@ def testResidual(noObserversInGlobalBuilders): fc1.set_residual_equation("chiv") fc1.set_equation("A * x") fc1.set_residual_equation("chiv") - assert noObserversInGlobalBuilders + assert find_builders_with_observers() == [] return -def test_setEquation(noObserversInGlobalBuilders): +def test_setEquation(find_builders_with_observers): """Check replacement of removed parameters.""" fc = FitContribution("test") fc.setEquation("x + 5") @@ -305,11 +305,11 @@ def test_setEquation(noObserversInGlobalBuilders): x = arange(0, 10, 0.5) fc.new_parameter("x", x) assert np.array_equal(5 + x, fc.evaluate()) - assert noObserversInGlobalBuilders + assert find_builders_with_observers() == [] return -def test_set_equation(noObserversInGlobalBuilders): +def test_set_equation(find_builders_with_observers): """Check replacement of removed parameters.""" fc = FitContribution("test") fc.set_equation("x + 5") @@ -319,27 +319,27 @@ def test_set_equation(noObserversInGlobalBuilders): x = arange(0, 10, 0.5) fc.new_parameter("x", x) assert np.array_equal(5 + x, fc.evaluate()) - assert noObserversInGlobalBuilders + assert find_builders_with_observers() == [] return -def test_getEquation(noObserversInGlobalBuilders): +def test_getEquation(find_builders_with_observers): """Check getting the current profile simulation formula.""" fc = FitContribution("test") assert "" == fc.get_equation() fc.set_equation("A * sin(x + 5)") assert "(A * sin((x + 5)))" == fc.getEquation() - assert noObserversInGlobalBuilders + assert find_builders_with_observers() == [] return -def test_get_equation(noObserversInGlobalBuilders): +def test_get_equation(find_builders_with_observers): """Check getting the current profile simulation formula.""" fc = FitContribution("test") assert "" == fc.get_equation() fc.set_equation("A * sin(x + 5)") assert "(A * sin((x + 5)))" == fc.get_equation() - assert noObserversInGlobalBuilders + assert find_builders_with_observers() == [] return diff --git a/tests/test_equation.py b/tests/test_equation.py index 90b3dd75..9789fc2a 100644 --- a/tests/test_equation.py +++ b/tests/test_equation.py @@ -12,192 +12,239 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Tests for refinableobj module.""" +"""Tests for the equationmod module.""" import pytest import diffpy.srfit.equation.literals as literals from diffpy.srfit.equation import Equation - -def testSimpleFunction(make_args, noObserversInGlobalBuilders): - """Test a simple function.""" - # Make some variables - v1, v2, v3, v4, c = make_args(5) - c.name = "c" - c.const = True - - # Make some operations - mult = literals.MultiplicationOperator() - root = mult2 = literals.MultiplicationOperator() - plus = literals.AdditionOperator() - minus = literals.SubtractionOperator() - - # Create the equation c*(v1+v3)*(v4-v2) - plus.addLiteral(v1) - plus.addLiteral(v3) - minus.addLiteral(v4) - minus.addLiteral(v2) - mult.addLiteral(plus) - mult.addLiteral(minus) - mult2.addLiteral(mult) - mult2.addLiteral(c) - - # Set the values of the variables. - # The equation should evaluate to 2.5*(1+3)*(4-2) = 20 - v1.set_value(1) - v2.set_value(2) - v3.set_value(3) - v4.set_value(4) - c.set_value(2.5) - - # Make an equation and test - eq = Equation("eq", mult2) - - assert eq._value is None - args = eq.args - assert v1 in args - assert v2 in args - assert v3 in args - assert v4 in args - assert c not in args - assert root is eq.root - - assert v1 is eq.v1 - assert v2 is eq.v2 - assert v3 is eq.v3 - assert v4 is eq.v4 - - assert 20 == eq() # 20 = 2.5*(1+3)*(4-2) - assert 20 == eq.get_value() # same as above - assert 20 == eq.value # same as above - assert 25 == eq(v1=2) # 25 = 2.5*(2+3)*(4-2) - assert 50 == eq(v2=0) # 50 = 2.5*(2+3)*(4-0) - assert 30 == eq(v3=1) # 30 = 2.5*(2+1)*(4-0) - assert 0 == eq(v4=0) # 20 = 2.5*(2+1)*(0-0) - - # Try some swapping - eq.swap(v4, v1) - assert eq._value is None - assert 15 == eq() # 15 = 2.5*(2+1)*(2-0) - args = eq.args - assert v4 not in args - - # Try to create a dependency loop - with pytest.raises(ValueError): - eq.swap(v1, eq.root) - - with pytest.raises(ValueError): - eq.swap(v1, plus) - - with pytest.raises(ValueError): - eq.swap(v1, minus) - - with pytest.raises(ValueError): - eq.swap(v1, mult) - - with pytest.raises(ValueError): - eq.swap(v1, root) - - # Swap the root - eq.swap(eq.root, v1) - assert eq._value is None - assert v1.value, eq() - - assert noObserversInGlobalBuilders - return +# Every test builds the equation c*(v1+v3)*(v4-v2) with v1=1, v2=2, v3=3, +# v4=4 and the constant c=2.5, so it evaluates to 2.5*(1+3)*(4-2) = 20. +# +# Every test runs for both kinds of root, given as input_root_kind: +# - "operator": the operator tree is the equation's root. +# - "equation": the operator tree is wrapped in its own Equation, which is +# then the root (an embedded equation). +ROOT_KINDS = ["operator", "equation"] -def testEmbeddedEquation(make_args, noObserversInGlobalBuilders): - """Test a simple function.""" - # Make some variables +def _build_equation(make_args, root_kind): v1, v2, v3, v4, c = make_args(5) c.name = "c" c.const = True - - # Make some operations - mult = literals.MultiplicationOperator() - mult2 = literals.MultiplicationOperator() + c.set_value(2.5) plus = literals.AdditionOperator() + plus.add_literal(v1) + plus.add_literal(v3) minus = literals.SubtractionOperator() - - # Create the equation c*(v1+v3)*(v4-v2) - plus.addLiteral(v1) - plus.addLiteral(v3) - minus.addLiteral(v4) - minus.addLiteral(v2) - mult.addLiteral(plus) - mult.addLiteral(minus) - mult2.addLiteral(mult) - mult2.addLiteral(c) - - # Set the values of the variables. - # The equation should evaluate to 2.5*(1+3)*(4-2) = 20 - v1.set_value(1) - v2.set_value(2) - v3.set_value(3) - v4.set_value(4) - c.set_value(2.5) - - # Make an equation and test - root = Equation("root", mult2) - eq = Equation("eq", root) - - assert eq._value is None - args = eq.args - assert v1 in args - assert v2 in args - assert v3 in args - assert v4 in args - assert c not in args - assert root is eq.root - - assert v1 is eq.v1 - assert v2 is eq.v2 - assert v3 is eq.v3 - assert v4 is eq.v4 - - # Make sure the right messages get sent - v1.value = 0 - assert root._value is None - assert eq._value is None - v1.value = 1 - - assert 20 == eq() # 20 = 2.5*(1+3)*(4-2) - assert 20 == eq.get_value() # same as above - assert 20 == eq.value # same as above - assert 25 == eq(v1=2) # 25 = 2.5*(2+3)*(4-2) - assert 50 == eq(v2=0) # 50 = 2.5*(2+3)*(4-0) - assert 30 == eq(v3=1) # 30 = 2.5*(2+1)*(4-0) - assert 0 == eq(v4=0) # 20 = 2.5*(2+1)*(0-0) - - # Try some swapping. - eq.swap(v4, v1) - assert eq._value is None - assert 15 == eq() # 15 = 2.5*(2+1)*(2-0) - args = eq.args - assert v4 not in args - - # Try to create a dependency loop - with pytest.raises(ValueError): - eq.swap(v1, eq.root) - - with pytest.raises(ValueError): - eq.swap(v1, plus) - - with pytest.raises(ValueError): - eq.swap(v1, minus) - - with pytest.raises(ValueError): - eq.swap(v1, mult) - + minus.add_literal(v4) + minus.add_literal(v2) + mult = literals.MultiplicationOperator() + mult.add_literal(plus) + mult.add_literal(minus) + top = literals.MultiplicationOperator() + top.add_literal(mult) + top.add_literal(c) + root = top if root_kind == "operator" else Equation("root", top) + equation = Equation("eq", root) + parts = { + "v1": v1, + "v2": v2, + "v3": v3, + "v4": v4, + "c": c, + "plus": plus, + "minus": minus, + "mult": mult, + "root": root, + } + return equation, parts + + +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +def test_equation_args(make_args, input_root_kind): + # C1: The equation is built. + # Expected: Its arguments are v1 to v4, but not the constant c. + equation, _ = _build_equation(make_args, input_root_kind) + actual_arg_names = sorted(arg.name for arg in equation.args) + expected_arg_names = ["v1", "v2", "v3", "v4"] + assert actual_arg_names == expected_arg_names + + +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +def test_equation_root_and_attributes(make_args, input_root_kind): + # C1: The equation is built. + # Expected: Its root is the given root, and each argument can be read + # as an attribute of the same name. + equation, parts = _build_equation(make_args, input_root_kind) + actual_root = equation.root + expected_root = parts["root"] + assert actual_root is expected_root + actual_attributes = [equation.v1, equation.v2, equation.v3, equation.v4] + expected_attributes = [parts["v1"], parts["v2"], parts["v3"], parts["v4"]] + assert actual_attributes == expected_attributes + + +# The value can be read in three equivalent ways. +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +@pytest.mark.parametrize( + "input_read", + [ + # C1: The equation is called. + # Expected: 20. + "call", + # C2: get_value is called. + # Expected: 20. + "get_value", + # C3: The value attribute is read. + # Expected: 20. + "value", + ], +) +def test_equation_value(make_args, input_root_kind, input_read): + equation, _ = _build_equation(make_args, input_root_kind) + reads = { + "call": equation, + "get_value": equation.get_value, + "value": lambda: equation.value, + } + actual_value = reads[input_read]() + expected_value = 20 + assert actual_value == expected_value + + +# Calling the equation with a keyword sets that argument before evaluating. +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +@pytest.mark.parametrize( + "input_name, input_value, expected_value", + [ + # C1: v1 is set to 2. + # Expected: 2.5*(2+3)*(4-2) = 25, and v1 keeps the value 2. + ("v1", 2, 25), + # C2: v2 is set to 0. + # Expected: 2.5*(1+3)*(4-0) = 40, and v2 keeps the value 0. + ("v2", 0, 40), + # C3: v3 is set to 1. + # Expected: 2.5*(1+1)*(4-2) = 10, and v3 keeps the value 1. + ("v3", 1, 10), + # C4: v4 is set to 0. + # Expected: 2.5*(1+3)*(0-2) = -20, and v4 keeps the value 0. + ("v4", 0, -20), + ], +) +def test_equation_call_with_keyword( + make_args, input_root_kind, input_name, input_value, expected_value +): + equation, parts = _build_equation(make_args, input_root_kind) + actual_value = equation(**{input_name: input_value}) + assert actual_value == expected_value + actual_argument_value = parts[input_name].value + expected_argument_value = input_value + assert actual_argument_value == expected_argument_value + + +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +def test_equation_value_cleared_when_argument_changes( + make_args, input_root_kind +): + # C1: The equation is evaluated, then v1 is changed. + # Expected: The cached values of both the root and the equation are + # cleared. + equation, parts = _build_equation(make_args, input_root_kind) + equation() + parts["v1"].value = 0 + actual_cached_values = (parts["root"]._value, equation._value) + expected_cached_values = (None, None) + assert actual_cached_values == expected_cached_values + + +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +def test_equation_swap_argument(make_args, input_root_kind): + # C1: The equation is evaluated, then v4 is swapped for v1. + # Expected: The cached value is cleared, v4 is no longer an argument, + # and the equation evaluates to 2.5*(1+3)*(1-2) = -10. + equation, parts = _build_equation(make_args, input_root_kind) + equation() + equation.swap(parts["v4"], parts["v1"]) + actual_cached_value = equation._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_arg_names = sorted(arg.name for arg in equation.args) + expected_arg_names = ["v1", "v2", "v3"] + assert actual_arg_names == expected_arg_names + actual_value = equation() + expected_value = -10 + assert actual_value == expected_value + + +# Swapping an argument for a literal that contains that argument would make +# the equation depend on itself. +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +@pytest.mark.parametrize( + "input_old_literal, input_new_literal", + [ + # C1: v1 is swapped for the root. + # Expected: A ValueError is raised. + ("v1", "root"), + # C2: v1 is swapped for the multiplication (v1+v3)*(v4-v2). + # Expected: A ValueError is raised. + ("v1", "mult"), + # C3: v1 is swapped for the addition v1+v3. + # Expected: A ValueError is raised. + ("v1", "plus"), + # C4: v4 is swapped for the subtraction v4-v2. + # Expected: A ValueError is raised. + ("v4", "minus"), + ], +) +def test_equation_swap_cycle_bad( + make_args, input_root_kind, input_old_literal, input_new_literal +): + equation, parts = _build_equation(make_args, input_root_kind) with pytest.raises(ValueError): - eq.swap(v1, root) - - # Swap the root - eq.swap(eq.root, v1) - assert eq._value is None - assert v1.value == eq() - - assert noObserversInGlobalBuilders - return + equation.swap(parts[input_old_literal], parts[input_new_literal]) + + +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +def test_equation_swap_root(make_args, input_root_kind): + # C1: The equation is evaluated, then its root is swapped for v1. + # Expected: The cached value is cleared, and the equation evaluates to + # the value of v1. + equation, parts = _build_equation(make_args, input_root_kind) + equation() + equation.swap(equation.root, parts["v1"]) + actual_cached_value = equation._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_value = equation() + expected_value = parts["v1"].value + assert actual_value == expected_value + + +@pytest.mark.parametrize("input_root_kind", ROOT_KINDS) +def test_equation_set_root(make_args, input_root_kind): + # C1: The equation is evaluated, then its root is set to plus = v1+v3. + # Expected: The cached value is cleared, the arguments are v1 and v3, + # and the equation evaluates to 1+3 = 4. + equation, parts = _build_equation(make_args, input_root_kind) + equation() + equation.set_root(parts["plus"]) + actual_cached_value = equation._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_arg_names = [arg.name for arg in equation.args] + expected_arg_names = ["v1", "v3"] + assert actual_arg_names == expected_arg_names + actual_value = equation() + expected_value = 4 + assert actual_value == expected_value + + +def test_equation_add_literal_bad(make_args): + # C1: A literal is added to an equation. + # Expected: A RuntimeError is raised, because an equation takes its + # literals from its root. + equation, parts = _build_equation(make_args, "operator") + with pytest.raises(RuntimeError): + equation.add_literal(parts["v1"]) diff --git a/tests/test_literals.py b/tests/test_literals.py index cfe3adb4..59f93715 100644 --- a/tests/test_literals.py +++ b/tests/test_literals.py @@ -14,9 +14,6 @@ ############################################################################## """Tests for the diffpy.srfit.equation.literals module.""" -import re -import unittest - import numpy import pytest @@ -24,263 +21,263 @@ import diffpy.srfit.equation.literals.abcs as abcs from diffpy.srfit.equation.equationmod import Equation -# ---------------------------------------------------------------------------- - - -class TestArgument(unittest.TestCase): - - def testInit(self): - """Test that everything initializes as expected.""" - a = literals.Argument() - self.assertEqual(None, a._value) - self.assertTrue(False is a.const) - self.assertTrue(None is a.name) - return - - def testIdentity(self): - """Make sure an Argument is an Argument.""" - a = literals.Argument() - self.assertTrue(issubclass(literals.Argument, abcs.ArgumentABC)) - self.assertTrue(isinstance(a, abcs.ArgumentABC)) - return - - def testValue(self): - """Test value setting.""" - a = literals.Argument() - - self.assertEqual(None, a.get_value()) - - # Test setting value - a.set_value(3.14) - self.assertAlmostEqual(3.14, a._value) - - a.set_value(3.14) - self.assertAlmostEqual(3.14, a.value) - self.assertAlmostEqual(3.14, a.get_value()) - return - - -# ---------------------------------------------------------------------------- - - -class TestCustomOperator(unittest.TestCase): - - def setUp(self): - self.op = literals.makeOperator( - name="add", symbol="+", operation=numpy.add, nin=2, nout=1 - ) - return - - def testInit(self): - """Test that everything initializes as expected.""" - op = self.op - self.assertEqual("+", op.symbol) - self.assertEqual(numpy.add, op.operation) - self.assertEqual(2, op.nin) - self.assertEqual(1, op.nout) - self.assertEqual(None, op._value) - self.assertEqual([], op.args) - return - - def testIdentity(self): - """Make sure an Argument is an Argument.""" - op = self.op - self.assertTrue(issubclass(literals.Operator, abcs.OperatorABC)) - self.assertTrue(isinstance(op, abcs.OperatorABC)) - return - - def testValue(self): - """Test value.""" - # Test addition and operations - op = self.op - a = literals.Argument(value=0) - b = literals.Argument(value=0) - - op.addLiteral(a) - op.addLiteral(b) - - self.assertAlmostEqual(0, op.value) - - # Test update from the nodes - a.set_value(4) - self.assertTrue(op._value is None) - self.assertAlmostEqual(4, op.value) - self.assertAlmostEqual(4, op.get_value()) - - b.value = 2 - self.assertTrue(op._value is None) - self.assertAlmostEqual(6, op.value) - - return - - def testAddLiteral(self): - """Test adding a literal to an operator node.""" - op = self.op - - self.assertRaises(TypeError, op.get_value) - op._value = 1 - self.assertEqual(op.get_value(), 1) - - # Test addition and operations - a = literals.Argument(name="a", value=0) - b = literals.Argument(name="b", value=0) - - op.addLiteral(a) - self.assertRaises(TypeError, op.get_value) - - op.addLiteral(b) - self.assertAlmostEqual(0, op.value) - - a.set_value(1) - b.set_value(2) - self.assertAlmostEqual(3, op.value) - - a.set_value(None) - # Test for self-references - - # Try to add self - op1 = literals.makeOperator( - name="add", symbol="+", operation=numpy.add, nin=2, nout=1 - ) - op1.addLiteral(a) - self.assertRaises(ValueError, op1.addLiteral, op1) - - # Try to add argument that contains self - op2 = literals.makeOperator( - name="sub", symbol="-", operation=numpy.subtract, nin=2, nout=1 - ) - op2.addLiteral(op1) - self.assertRaises(ValueError, op1.addLiteral, op2) - - return - - -# ---------------------------------------------------------------------------- - -class TestConvolutionOperator(unittest.TestCase): - - def testValue(self): - """Make sure the convolution operator is working properly.""" - exp = numpy.exp - - x = numpy.linspace(0, 10, 1000) - - mu1 = 4.5 - sig1 = 0.1 - mu2 = 2.5 - sig2 = 0.4 - - g1 = exp(-0.5 * ((x - mu1) / sig1) ** 2) - a1 = literals.Argument(name="g1", value=g1) - g2 = exp(-0.5 * ((x - mu2) / sig2) ** 2) - a2 = literals.Argument(name="g2", value=g2) - - op = literals.ConvolutionOperator() - op.addLiteral(a1) - op.addLiteral(a2) - - g3c = op.value - - mu3 = mu1 - sig3 = (sig1**2 + sig2**2) ** 0.5 - g3 = exp(-0.5 * ((x - mu3) / sig3) ** 2) - g3 *= sum(g1) / sum(g3) - - self.assertAlmostEqual(sum(g3c), sum(g3)) - self.assertAlmostEqual(0, sum((g3 - g3c) ** 2)) - return - - -# ---------------------------------------------------------------------------- - - -class TestArrayOperator(unittest.TestCase): - - def test_value(self): - """Check ArrayOperator.value.""" - x = literals.Argument("x", 1.0) - y = literals.Argument("y", 2.0) - z = literals.Argument("z", 3.0) - # check empty array - op = literals.ArrayOperator() - self.assertEqual(0, len(op.value)) - self.assertTrue(isinstance(op.value, numpy.ndarray)) - # check behavior with 2 arguments - op.addLiteral(x) - self.assertTrue(numpy.array_equal([1], op.value)) - op.addLiteral(y) - op.addLiteral(z) - self.assertTrue(numpy.array_equal([1, 2, 3], op.value)) - z.value = 7 - self.assertTrue(numpy.array_equal([1, 2, 7], op.value)) - return - - -# ---------------------------------------------------------------------------- -# Literal.getValue is deprecated in favor of Literal.get_value. Every Literal -# in the hierarchy must keep accepting the old name, warn with a message that -# names the replacement, and dispatch to the subclass implementation of -# get_value rather than to Literal's own NotImplementedError stub. - - -# C1: Argument holds the value directly. -# Expected: getValue warns and returns Argument.get_value. -def test_argument_get_value_deprecated(): - expected_msg = ( - "'diffpy.srfit.equation.literals.Literal.getValue' is deprecated " - "and will be removed in version 4.0.0. Please use " - "'diffpy.srfit.equation.literals.Literal.get_value' instead." +def _make_add_operator(): + return literals.make_operator( + name="add", symbol="+", operation=numpy.add, nin=2, nout=1 ) - expected_value = 3.5 - literal = literals.Argument(name="a", value=expected_value) - with pytest.warns(DeprecationWarning, match=re.escape(expected_msg)): - actual_value = literal.getValue() - assert actual_value == expected_value - - -# C2: Operator computes the value from its own literals. -# Expected: getValue warns and returns Operator.get_value. -def test_operator_get_value_deprecated(): - expected_msg = ( - "'diffpy.srfit.equation.literals.Literal.getValue' is deprecated " - "and will be removed in version 4.0.0. Please use " - "'diffpy.srfit.equation.literals.Literal.get_value' instead." +def _make_sum(literal_kind): + """Return a literal whose value is 1.5 + 2.0 = 3.5.""" + if literal_kind == "argument": + return literals.Argument(name="a", value=3.5) + operator = literals.AdditionOperator() + operator.add_literal(literals.Argument(name="a", value=1.5)) + operator.add_literal(literals.Argument(name="b", value=2.0)) + if literal_kind == "operator": + return operator + return Equation(name="eq", root=operator) + + +def test_argument_init(): + # C1: An Argument is created without any arguments. + # Expected: It has no value and no name, and it is not constant. + argument = literals.Argument() + actual_attributes = (argument._value, argument.const, argument.name) + expected_attributes = (None, False, None) + assert actual_attributes == expected_attributes + + +def test_operator_init(): + # C1: An add operator is made with make_operator. + # Expected: It stores the given symbol, operation and argument counts, + # and has no value and no literals yet. + operator = _make_add_operator() + actual_attributes = ( + operator.symbol, + operator.operation, + operator.nin, + operator.nout, + operator._value, + operator.args, + ) + expected_attributes = ("+", numpy.add, 2, 1, None, []) + assert actual_attributes == expected_attributes + + +# Each literal class is registered with its abstract base class. +@pytest.mark.parametrize( + "input_class, input_literal, expected_abc", + [ + # C1: An Argument. + # Expected: Argument and its instances count as ArgumentABC. + (literals.Argument, literals.Argument(), abcs.ArgumentABC), + # C2: An Operator. + # Expected: Operator and its instances count as OperatorABC. + (literals.Operator, _make_add_operator(), abcs.OperatorABC), + ], +) +def test_literal_abc(input_class, input_literal, expected_abc): + actual_is_abc = ( + issubclass(input_class, expected_abc), + isinstance(input_literal, expected_abc), ) + expected_is_abc = (True, True) + assert actual_is_abc == expected_is_abc + + +# get_value and value return the last value given to set_value. +@pytest.mark.parametrize( + "input_values, expected_value", + [ + # C1: No value is set. + # Expected: None. + ([], None), + # C2: One value is set. + # Expected: That value. + ([3.14], 3.14), + # C3: Two values are set one after the other. + # Expected: The second value. + ([1.0, 3.14], 3.14), + ], +) +def test_argument_value(input_values, expected_value): + argument = literals.Argument() + for value in input_values: + argument.set_value(value) + actual_values = (argument.get_value(), argument.value) + expected_values = (expected_value, expected_value) + assert actual_values == expected_values + + +# get_value of every literal kind returns that literal's own value. +@pytest.mark.parametrize( + "input_literal_kind", + [ + # C1: An Argument holds the value itself. + # Expected: 3.5. + "argument", + # C2: An Operator computes the value from its literals. + # Expected: 3.5. + "operator", + # C3: An Equation evaluates the operator at its root. + # Expected: 3.5. + "equation", + ], +) +def test_get_value(input_literal_kind): + literal = _make_sum(input_literal_kind) + actual_value = literal.get_value() expected_value = 3.5 - operator = literals.AdditionOperator() - operator.addLiteral(literals.Argument(name="a", value=1.5)) - operator.addLiteral(literals.Argument(name="b", value=2.0)) - - with pytest.warns(DeprecationWarning, match=re.escape(expected_msg)): - actual_value = operator.getValue() - assert actual_value == expected_value -# C3: Equation evaluates the operator tree at its root. -# Expected: getValue warns and returns Equation.get_value. -def test_equation_get_value_deprecated(): - expected_msg = ( - "'diffpy.srfit.equation.literals.Literal.getValue' is deprecated " - "and will be removed in version 4.0.0. Please use " - "'diffpy.srfit.equation.literals.Literal.get_value' instead." - ) - expected_value = 3.5 - operator = literals.AdditionOperator() - operator.addLiteral(literals.Argument(name="a", value=1.5)) - operator.addLiteral(literals.Argument(name="b", value=2.0)) - equation = Equation(name="eq", root=operator) +# An add operator over a=0 and b=0 caches its value. Changing an argument +# clears the cache, and the next value is the new sum. +@pytest.mark.parametrize( + "input_change, expected_value", + [ + # C1: a is changed to 4 with set_value. + # Expected: The cache is cleared and the value is 4. + ("set_value", 4), + # C2: b is changed to 2 by assigning to value. + # Expected: The cache is cleared and the value is 2. + ("assign_value", 2), + ], +) +def test_operator_value_after_argument_change(input_change, expected_value): + operator = _make_add_operator() + a = literals.Argument(value=0) + b = literals.Argument(value=0) + operator.add_literal(a) + operator.add_literal(b) + actual_initial_value = operator.value + expected_initial_value = 0 + assert actual_initial_value == expected_initial_value + if input_change == "set_value": + a.set_value(4) + else: + b.value = 2 + actual_cached_value = operator._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_value = operator.value + assert actual_value == expected_value - with pytest.warns(DeprecationWarning, match=re.escape(expected_msg)): - actual_value = equation.getValue() +# An operator that has fewer literals than it needs has no value. +@pytest.mark.parametrize( + "input_literal_count", + [ + # C1: The add operator has no literals. + # Expected: get_value raises a TypeError. + 0, + # C2: The add operator has one of its two literals. + # Expected: get_value raises a TypeError. + 1, + ], +) +def test_operator_get_value_missing_literals_bad(input_literal_count): + operator = _make_add_operator() + for _ in range(input_literal_count): + operator.add_literal(literals.Argument(value=1)) + with pytest.raises(TypeError): + operator.get_value() + + +def test_operator_get_value_returns_cached_value(): + # C1: An operator without literals already has a cached value. + # Expected: get_value returns the cached value. + operator = _make_add_operator() + operator._value = 1 + actual_value = operator.get_value() + expected_value = 1 assert actual_value == expected_value -# ---------------------------------------------------------------------------- - -if __name__ == "__main__": - unittest.main() +# An operator may not contain itself, directly or through another operator. +@pytest.mark.parametrize( + "input_cycle", + [ + # C1: The add operator is added to itself. + # Expected: A ValueError is raised. + "self", + # C2: A subtract operator that contains the add operator is added + # to the add operator. + # Expected: A ValueError is raised. + "containing_operator", + ], +) +def test_operator_add_literal_cycle_bad(input_cycle): + add = _make_add_operator() + add.add_literal(literals.Argument(name="a", value=1)) + subtract = literals.make_operator( + name="sub", symbol="-", operation=numpy.subtract, nin=2, nout=1 + ) + subtract.add_literal(add) + literal_to_add = {"self": add, "containing_operator": subtract} + with pytest.raises(ValueError): + add.add_literal(literal_to_add[input_cycle]) + + +def test_convolution_operator(): + # C1: Two Gaussians, centred at 4.5 and 2.5 with widths 0.1 and 0.4, + # are convolved. + # Expected: A Gaussian centred at the first one's position, with width + # sqrt(0.1**2 + 0.4**2), scaled to the first one's area. + x = numpy.linspace(0, 10, 1000) + g1 = numpy.exp(-0.5 * ((x - 4.5) / 0.1) ** 2) + g2 = numpy.exp(-0.5 * ((x - 2.5) / 0.4) ** 2) + operator = literals.ConvolutionOperator() + operator.add_literal(literals.Argument(name="g1", value=g1)) + operator.add_literal(literals.Argument(name="g2", value=g2)) + actual_convolution = operator.value + width = (0.1**2 + 0.4**2) ** 0.5 + expected_convolution = numpy.exp(-0.5 * ((x - 4.5) / width) ** 2) + expected_convolution *= sum(g1) / sum(expected_convolution) + assert sum(actual_convolution) == pytest.approx( + sum(expected_convolution), abs=1e-7 + ) + actual_squared_difference = sum( + (expected_convolution - actual_convolution) ** 2 + ) + assert actual_squared_difference == pytest.approx(0, abs=1e-7) + + +# ArrayOperator collects the values of its literals x=1, y=2 and z=3 into +# an array. +@pytest.mark.parametrize( + "input_literal_count, input_new_z_value, expected_array", + [ + # C1: No literals. + # Expected: An empty array. + (0, None, []), + # C2: Only x. + # Expected: [1]. + (1, None, [1]), + # C3: x, y and z. + # Expected: [1, 2, 3]. + (3, None, [1, 2, 3]), + # C4: x, y and z, then z is changed to 7. + # Expected: The array follows the change, [1, 2, 7]. + (3, 7, [1, 2, 7]), + ], +) +def test_array_operator( + input_literal_count, input_new_z_value, expected_array +): + arguments = [ + literals.Argument("x", 1.0), + literals.Argument("y", 2.0), + literals.Argument("z", 3.0), + ] + operator = literals.ArrayOperator() + for argument in arguments[:input_literal_count]: + operator.add_literal(argument) + if input_new_z_value is not None: + operator.get_value() + arguments[2].value = input_new_z_value + actual_array = operator.value + assert isinstance(actual_array, numpy.ndarray) + assert actual_array.tolist() == expected_array diff --git a/tests/test_recipeorganizer.py b/tests/test_recipeorganizer.py index 36b4288a..94c227c0 100644 --- a/tests/test_recipeorganizer.py +++ b/tests/test_recipeorganizer.py @@ -45,8 +45,8 @@ def test_get_equation_from_string(self): factory = EquationFactory() - factory.registerArgument("p1", p1) - factory.registerArgument("p2", p2) + factory.register_argument("p1", p1) + factory.register_argument("p2", p2) # Check usage where all parameters are registered with the factory eq = get_equation_from_string("p1+p2", factory) @@ -338,8 +338,8 @@ def test_add_restraint(self): p1 = Parameter("p1", 1) p2 = Parameter("p2", 2) p3 = Parameter("p3", 3) - self.m._eqfactory.registerArgument("p1", p1) - self.m._eqfactory.registerArgument("p2", p2) + self.m._eqfactory.register_argument("p1", p1) + self.m._eqfactory.register_argument("p2", p2) self.assertEqual(0, len(self.m._restraints)) r = self.m.add_soft_bounds("p1+p2", upper_bound=10) diff --git a/tests/test_restraint.py b/tests/test_restraint.py index 93a4daff..57d5dbc1 100644 --- a/tests/test_restraint.py +++ b/tests/test_restraint.py @@ -31,8 +31,8 @@ def testRestraint(self): factory = EquationFactory() - factory.registerArgument("p1", p1) - factory.registerArgument("p2", p2) + factory.register_argument("p1", p1) + factory.register_argument("p2", p2) # Restrain 1 < p1 + p2 < 5 eq = get_equation_from_string("p1 + p2", factory) diff --git a/tests/test_speed.py b/tests/test_speed.py index a7d43b4a..ebb17560 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -47,18 +47,18 @@ def make_lazy_equation(make_args): mult2 = literals.MultiplicationOperator() # Create the equation ((v1+v2)*(v3-v4))**v5 * v6^v7 - plus.addLiteral(v1) - plus.addLiteral(v2) - minus.addLiteral(v3) - minus.addLiteral(v4) - mult.addLiteral(plus) - mult.addLiteral(minus) - pow.addLiteral(mult) - pow.addLiteral(v5) - exp.addLiteral(v6) - exp.addLiteral(v7) - mult2.addLiteral(pow) - mult2.addLiteral(exp) + plus.add_literal(v1) + plus.add_literal(v2) + minus.add_literal(v3) + minus.add_literal(v4) + mult.add_literal(plus) + mult.add_literal(minus) + pow.add_literal(mult) + pow.add_literal(v5) + exp.add_literal(v6) + exp.add_literal(v7) + mult2.add_literal(pow) + mult2.add_literal(exp) v2.set_value(x) v3.set_value(50 * x) @@ -138,8 +138,8 @@ def test_speed2(mutate=2): 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) + factory.register_constant("x", x) + eq = factory.make_equation(eqstr) eq.qsig.set_value(qsig) eq.sigma1.set_value(sigma) eq.sigma2.set_value(sigma) @@ -215,8 +215,8 @@ def test_speed3(mutate=2): 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) + factory.register_constant("x", x) + eq = factory.make_equation(eqstr) eq.qsig.set_value(qsig) eq.sigma1.set_value(sigma) eq.sigma2.set_value(sigma) @@ -300,8 +300,8 @@ def test_speed4(mutate=2): 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) + factory.register_constant("x", x) + eq = factory.make_equation(eqstr) from numpy import polyval from sympy import lambdify, var @@ -358,8 +358,8 @@ def test_weighted(mutate=2): 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) + factory.register_constant("x", x) + eq = factory.make_equation(eqstr) eq.b1.set_value(0) eq.b2.set_value(1) @@ -427,8 +427,8 @@ def test_profile(): 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) + factory.register_constant("x", x) + eq = factory.make_equation(eqstr) eq.b1.set_value(0) eq.b2.set_value(1) diff --git a/tests/test_visitors.py b/tests/test_visitors.py index 52abaf63..ff262577 100644 --- a/tests/test_visitors.py +++ b/tests/test_visitors.py @@ -12,209 +12,246 @@ # See LICENSE_DANSE.txt for license information. # ############################################################################## -"""Tests for refinableobj module.""" - -import unittest +"""Tests for the visitors module.""" +import numpy import pytest import diffpy.srfit.equation.literals as literals import diffpy.srfit.equation.visitors as visitors - - -class TestValidator: - @pytest.fixture(autouse=True) - def setup(self, make_args): - self.make_args = make_args - - def testSimpleFunction(self): - """Test a simple function.""" - # Make some variables - v1, v2, v3, v4 = self.make_args(4) - - # Make some operations - mult = literals.MultiplicationOperator() - plus = literals.AdditionOperator() - minus = literals.SubtractionOperator() - - # Let's hobble the plus operator - plus.name = None - plus.symbol = None - plus.operation = None - - # Partially define the equation (v1+v3)*(v4-v2). Let's only give one - # variable to the '-' operation. - plus.addLiteral(v1) - plus.addLiteral(v3) - minus.addLiteral(v4) - mult.addLiteral(plus) - mult.addLiteral(minus) - - # Now validate - validator = visitors.Validator() - mult.identify(validator) - assert 4 == len(validator.errors) - - # Fix the equation - minus.addLiteral(v3) - validator.reset() - mult.identify(validator) - assert 3 == len(validator.errors) - - # Fix the name of plus - plus.name = "add" - validator.reset() - mult.identify(validator) - assert 2 == len(validator.errors) - - # Fix the symbol of plus - plus.symbol = "+" - validator.reset() - mult.identify(validator) - assert 1 == len(validator.errors) - - # Fix the operation of plus - import numpy - - plus.operation = numpy.add - validator.reset() - mult.identify(validator) - assert 0 == len(validator.errors) - - # Add another literal to minus - minus.addLiteral(v1) - validator.reset() - mult.identify(validator) - assert 1 == len(validator.errors) - - return - - -class TestArgFinder: - @pytest.fixture(autouse=True) - def setup(self, make_args): - self.make_args = make_args - - def testSimpleFunction(self): - """Test a simple function.""" - # Make some variables - v1, v2, v3, v4 = self.make_args(4) - - # Make some operations - mult = literals.MultiplicationOperator() - plus = literals.AdditionOperator() - minus = literals.SubtractionOperator() - - # Create the equation (v1+v3)*(v4-v2) - plus.addLiteral(v1) - plus.addLiteral(v3) - minus.addLiteral(v4) - minus.addLiteral(v2) - mult.addLiteral(plus) - mult.addLiteral(minus) - - # Set the values of the variables. - # The equation should evaluate to (1+3)*(4-2) = 8 - v1.set_value(1) - v2.set_value(2) - v3.set_value(3) - v4.set_value(4) - - # now get the args - args = visitors.getArgs(mult) - assert 4 == len(args) - assert v1 in args - assert v2 in args - assert v3 in args - assert v4 in args - - return - - def testArg(self): - """Test just an Argument equation.""" - # Make some variables - v1 = self.make_args(1)[0] - - args = visitors.getArgs(v1) - - assert 1 == len(args) - assert args[0] == v1 - return - - -class TestSwapper: - @pytest.fixture(autouse=True) - def setup(self, make_args): - self.make_args = make_args - - def testSimpleFunction(self): - """Test a simple function.""" - # Make some variables - v1, v2, v3, v4, v5 = self.make_args(5) - - # Make some operations - mult = literals.MultiplicationOperator() - plus = literals.AdditionOperator() - minus = literals.SubtractionOperator() - - # Create the equation (v1+v3)*(v4-v2) - plus.addLiteral(v1) - plus.addLiteral(v3) - minus.addLiteral(v4) - minus.addLiteral(v2) - mult.addLiteral(plus) - mult.addLiteral(minus) - - # Set the values of the variables. - # The equation should evaluate to (1+3)*(4-2) = 8 - v1.set_value(1) - v2.set_value(2) - v3.set_value(3) - v4.set_value(4) - v5.set_value(5) - - # Evaluate - assert 8 == mult.value - - # Now swap an argument - visitors.swap(mult, v2, v5) - - # Check that the operator value is invalidated - assert mult._value is None - assert not v2.has_observer(minus._flush) - assert v5.has_observer(minus._flush) - - # now get the args - args = visitors.getArgs(mult) - assert 4 == len(args) - assert v1 in args - assert v2 not in args - assert v3 in args - assert v4 in args - assert v5 in args - - # Re-evaluate (1+3)*(4-5) = -4 - assert -4 == mult.value - - # Swap out the "-" operator - plus2 = literals.AdditionOperator() - visitors.swap(mult, minus, plus2) - assert mult._value is None - assert not minus.has_observer(mult._flush) - assert plus2.has_observer(mult._flush) - - # plus2 has no arguments yet. Verify this. - with pytest.raises(TypeError): - mult.get_value() - # Add the arguments to plus2. - plus2.addLiteral(v4) - plus2.addLiteral(v5) - - # Re-evaluate (1+3)*(4+5) = 36 - assert 36 == mult.value - - return - - -if __name__ == "__main__": - unittest.main() +from diffpy.srfit.equation import Equation +from diffpy.srfit.equation.visitors.visitor import Visitor + + +def _build_tree(make_args): + """Build (v1+v3)*(v4-v2) with v1=1, v2=2, v3=3 and v4=4, which is 8. + + Also return v5=5, which is not part of the tree. + """ + v1, v2, v3, v4, v5 = make_args(5) + plus = literals.AdditionOperator() + plus.add_literal(v1) + plus.add_literal(v3) + minus = literals.SubtractionOperator() + minus.add_literal(v4) + minus.add_literal(v2) + mult = literals.MultiplicationOperator() + mult.add_literal(plus) + mult.add_literal(minus) + parts = { + "v1": v1, + "v2": v2, + "v3": v3, + "v4": v4, + "v5": v5, + "plus": plus, + "minus": minus, + } + return mult, parts + + +# The Validator counts the problems in (v1+v3)*(v4-...). Each problem is an +# operator with the wrong number of literals, or an operator missing its +# name, symbol or operation. +@pytest.mark.parametrize( + "input_minus_literals, input_plus_missing, expected_error_count", + [ + # C1: minus has one of its two literals, and plus has no name, + # symbol or operation. + # Expected: 4 errors. + (["v4"], ["name", "symbol", "operation"], 4), + # C2: minus is complete, and plus has no name, symbol or operation. + # Expected: 3 errors. + (["v4", "v3"], ["name", "symbol", "operation"], 3), + # C3: minus is complete, and plus has no symbol or operation. + # Expected: 2 errors. + (["v4", "v3"], ["symbol", "operation"], 2), + # C4: minus is complete, and plus has no operation. + # Expected: 1 error. + (["v4", "v3"], ["operation"], 1), + # C5: Every operator is complete. + # Expected: No errors. + (["v4", "v3"], [], 0), + # C6: minus has three literals but takes two. + # Expected: 1 error. + (["v4", "v3", "v1"], [], 1), + ], +) +def test_validator( + make_args, input_minus_literals, input_plus_missing, expected_error_count +): + v1, v2, v3, v4 = make_args(4) + arguments = {"v1": v1, "v2": v2, "v3": v3, "v4": v4} + plus = literals.AdditionOperator() + plus.add_literal(v1) + plus.add_literal(v3) + for attribute in input_plus_missing: + setattr(plus, attribute, None) + minus = literals.SubtractionOperator() + for name in input_minus_literals: + minus.add_literal(arguments[name]) + mult = literals.MultiplicationOperator() + mult.add_literal(plus) + mult.add_literal(minus) + validator = visitors.Validator() + mult.identify(validator) + actual_error_count = len(validator.errors) + assert actual_error_count == expected_error_count + + +def test_validator_reset(make_args): + # C1: A Validator checks a broken tree, is reset, and checks a valid + # tree. + # Expected: The errors from the first check are cleared. + mult, parts = _build_tree(make_args) + parts["plus"].operation = None + validator = visitors.Validator() + mult.identify(validator) + parts["plus"].operation = numpy.add + validator.reset() + mult.identify(validator) + actual_errors = validator.errors + expected_errors = [] + assert actual_errors == expected_errors + + +# get_args returns every Argument in a literal. +@pytest.mark.parametrize( + "input_literal, expected_arg_names", + [ + # C1: The tree (v1+v3)*(v4-v2). + # Expected: v1, v2, v3 and v4. + ("tree", ["v1", "v2", "v3", "v4"]), + # C2: The single argument v1. + # Expected: v1 only. + ("v1", ["v1"]), + ], +) +def test_get_args(make_args, input_literal, expected_arg_names): + mult, parts = _build_tree(make_args) + literal = mult if input_literal == "tree" else parts[input_literal] + actual_arg_names = sorted(arg.name for arg in visitors.get_args(literal)) + assert actual_arg_names == expected_arg_names + + +def test_swap_argument(make_args): + # C1: The tree is evaluated, then v2 is swapped for v5. + # Expected: The cached value is cleared, minus now observes v5 instead + # of v2, the arguments are v1, v3, v4 and v5, and the value is + # (1+3)*(4-5) = -4. + mult, parts = _build_tree(make_args) + v2, v5, minus = parts["v2"], parts["v5"], parts["minus"] + mult.get_value() + visitors.swap(mult, v2, v5) + actual_cached_value = mult._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_observed = ( + v2.has_observer(minus._flush), + v5.has_observer(minus._flush), + ) + expected_observed = (False, True) + assert actual_observed == expected_observed + actual_arg_names = sorted(arg.name for arg in visitors.get_args(mult)) + expected_arg_names = ["v1", "v3", "v4", "v5"] + assert actual_arg_names == expected_arg_names + actual_value = mult.value + expected_value = -4 + assert actual_value == expected_value + + +def test_swap_operator(make_args): + # C1: The tree is evaluated, then minus is swapped for an empty add + # operator, which is then given v4 and v5. + # Expected: The cached value is cleared, mult now observes the new + # operator instead of minus, there is no value until the new operator + # has its literals, and then the value is (1+3)*(4+5) = 36. + mult, parts = _build_tree(make_args) + minus = parts["minus"] + mult.get_value() + plus2 = literals.AdditionOperator() + visitors.swap(mult, minus, plus2) + actual_cached_value = mult._value + expected_cached_value = None + assert actual_cached_value == expected_cached_value + actual_observed = ( + minus.has_observer(mult._flush), + plus2.has_observer(mult._flush), + ) + expected_observed = (False, True) + assert actual_observed == expected_observed + with pytest.raises(TypeError): + mult.get_value() + plus2.add_literal(parts["v4"]) + plus2.add_literal(parts["v5"]) + actual_value = mult.value + expected_value = 36 + assert actual_value == expected_value + + +# get_expression writes a literal as an equation string. Embedded +# equations whose names match eqskip are written by name. +@pytest.mark.parametrize( + "input_eqskip, expected_expression", + [ + # C1: No eqskip is given. + # Expected: The embedded equation "inner" is written out in full. + (None, "(((v1 + v3) * (v4 - v2)) + v5)"), + # C2: eqskip matches the name of the embedded equation. + # Expected: The embedded equation is written as "inner". + ("inner", "(inner + v5)"), + # C3: eqskip does not match the name of the embedded equation. + # Expected: The embedded equation is written out in full. + ("other", "(((v1 + v3) * (v4 - v2)) + v5)"), + ], +) +def test_get_expression(make_args, input_eqskip, expected_expression): + mult, parts = _build_tree(make_args) + top = literals.AdditionOperator() + top.add_literal(Equation("inner", mult)) + top.add_literal(parts["v5"]) + actual_expression = visitors.get_expression(top, eqskip=input_eqskip) + assert actual_expression == expected_expression + + +def test_pretty_print(make_args, capsys): + # C1: The tree (v1+v3)*(v4-v2) is printed. + # Expected: Its expression is printed on one line. + mult, _ = _build_tree(make_args) + visitors.pretty_print(mult) + actual_output = capsys.readouterr().out + expected_output = "((v1 + v3) * (v4 - v2))\n" + assert actual_output == expected_output + + +# The Visitor base class leaves arguments and operators to its subclasses, +# and treats an equation as an operator. +@pytest.mark.parametrize( + "input_method, expected_error", + [ + # C1: on_argument is called on the base class. + # Expected: A NotImplementedError is raised. + ("on_argument", NotImplementedError), + # C2: on_operator is called on the base class. + # Expected: A NotImplementedError is raised. + ("on_operator", NotImplementedError), + ], +) +def test_visitor_base_bad(make_args, input_method, expected_error): + visitor = Visitor() + with pytest.raises(expected_error): + getattr(visitor, input_method)(make_args(1)[0]) + + +def test_visitor_on_equation_uses_on_operator(make_args): + # C1: on_equation is called on a visitor that only defines + # on_operator. + # Expected: The equation is passed to on_operator. + class OperatorRecorder(Visitor): + def on_operator(self, op): + return op + + mult, _ = _build_tree(make_args) + equation = Equation("eq", mult) + actual_visited = OperatorRecorder().on_equation(equation) + assert actual_visited is equation