# coding=utf-8
# This file is part of QNET.
#
# QNET is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# QNET is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QNET. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2012-2013, Nikolas Tezak
#
###########################################################################
r"""
Abstract Algebra
================
The abstract algebra package provides a basic interface
for defining custom Algebras.
See :ref:`abstract_algebra` for more details.
"""
from __future__ import division
from abc import ABCMeta, abstractmethod
from functools import reduce
from types import MethodType
import six
if six.PY3:
basestring = str
long = int
def _trace(fn):
"""
Function decorator to receive debugging information about function calls and return values.
:param fn: Function whose calls to _trace
:type fn: FunctionType
:return: Decorated function
:rtype: FunctionType
"""
### uncomment for debugging
# def _tfn(*args, **kwargs):
# print("[", "-" * 40)
# ret = fn(*args, **kwargs)
# print("{}({},{}) called".format(fn.__name__, ", ".join(repr(a) for a in args),
# ", ".join(str(k) + "=" + repr(v) for k, v in kwargs.items())))
# print("-->", repr(ret))
# print("-" * 40, "]")
# return ret
# return _tfn
return fn
# define our own exceptions/errors
[docs]class AlgebraException(Exception):
"""
Base class for all errors concerning the mathematical
definitions and rules of an algebra.
"""
pass
[docs]class AlgebraError(AlgebraException):
"""
Base class for all errors concerning the mathematical
definitions and rules of an algebra.
"""
pass
[docs]class CannotSimplify(AlgebraException):
"""
Raised when an expression cannot be further simplified
"""
pass
[docs]class WrongSignatureError(AlgebraError):
"""
Raise when an operation is instantiated
with operands of the wrong signature.
"""
pass
[docs]@six.add_metaclass(ABCMeta)
class Expression(object):
"""
Basic class defining the basic methods any Expression object should implement.
"""
[docs] def substitute(self, var_map):
"""
Substitute all_symbols for other expressions.
:param var_map: Dictionary with entries of the form ``{symbol: substitution}``
:type var_map: dict
"""
return self._substitute(var_map)
def _substitute(self, var_map):
if self in var_map:
return var_map[self]
return self
[docs] def tex(self):
"""
Return a string containing a TeX-representation of self.
Note that this needs to be wrapped by '$' characters for 'inline' LaTeX use.
"""
return self._tex()
@abstractmethod
def _tex(self):
return str(self)
def _repr_latex_(self):
"""
For compatibility with the IPython notebook, generate TeX expression and surround it with $'s.
"""
return "${}$".format(self.tex())
[docs] def all_symbols(self):
"""
:return: The set of all_symbols contained within the expression.
:rtype: set
"""
return self._all_symbols()
@abstractmethod
def _all_symbols(self):
raise NotImplementedError(self.__class__.__name__)
@abstractmethod
def __hash__(self):
"""
Provide a hashing mechanism for self.
"""
raise NotImplementedError(self.__class__.__name__)
def __eq__(self, other):
"""
Implements a very strict definition of ``self == other``.
This should be overloaded where appropriate.
"""
if self is other:
return True
return False
def __ne__(self, other):
"""
If it is well-defined (i.e. boolean), simply return
the negation of ``self.__eq__(other)``
Otherwise return NotImplemented.
"""
eq = self.__eq__(other)
if type(eq) is bool:
return not eq
return NotImplemented
[docs]def substitute(expr, var_map):
"""
(Safe) substitute, substitute objects for all symbols.
:param expr: The expression in which to perform the substitution
:param var_map: The substitution dictionary. See :py:meth:`qnet.algebra.abstract_algebra.substitute` documentation
:type var_map: dict
"""
try:
return expr.substitute(var_map)
except AttributeError:
if expr in var_map:
return var_map[expr]
return expr
[docs]def tex(obj):
"""
:param obj: Object to represent in LaTeX.
:return: Return a LaTeX string-representation of obj.
:rtype: str
"""
try:
return obj.tex()
except AttributeError:
return r"{{\rm {!s}}}".format(obj)
#
#def mathematica(obj):
# """
# Return a Mathematica string-representation of obj
# """
# if isinstance(obj, str):
# return identifier_to_mathematica(obj)
# if is_number(obj):
# return format_number_for_mathematica(obj)
# if isinstance(obj, SympyBasic):
# return capitalize_sympy_functions_for_mathematica(
# re.compile(r'([A-Za-z0-9]+)\(([^\)]+)\)').sub(
# r"\1[\2]", identifier_to_mathematica(str(obj)).replace("**","^")))
# try:
# return obj.mathematica()
# except AttributeError:
# return str(obj)
#
#def capitalize_sympy_functions_for_mathematica(string):
# words = ("cos", "sin", "exp", "sqrt", "conjugate", "cosh", "sinh")
# return creduce(lambda a, b: a.replace(b, b[0].upper() + b[1:]), words, string)
#
#
#def free_of(expr, *all_symbols):
# """
# Safe free_of
# """
# try:
# return expr.free_of(*all_symbols)
# except AttributeError:
# return True
#
#def all_symbols(expr):
# """
# Safe all_symbols
# """
# try:
# return expr.all_symbols()
# except AttributeError:
# try:
# return expr.atoms()
# except AttributeError:
# return set(())
#def format_number_for_tex(num):
# if num == 0: #also True for 0., 0j
# return "0"
# if isinstance(num, complex):
# if num.real == 0:
# if num.imag == 1:
# return "i"
# if num.imag == -1:
# return "(-i)"
# if num.imag < 0:
# return "(-%si)" % format_number_for_tex(-num.imag)
# return "%si" % format_number_for_tex(num.imag)
# if num.imag == 0:
# return format_number_for_tex(num.real)
# return "(%s + %si)" % (format_number_for_tex(num.real), format_number_for_tex(num.imag))
# if num < 0:
# return "(%g)" % num
# return "%g" % num
#
#def format_number_for_mathematica(num):
# if num == 0: #also True for 0., 0j
# return "0"
# if isinstance(num, complex):
# if num.imag == 0:
# return format_number_for_tex(num.real)
# return "Complex[%g,%g]" % (num.real, num.imag)
#
# return "%g" % num
#
#
#
#
#greek_letter_strings = ["alpha", "beta", "gamma", "delta", "epsilon", "varepsilon", \
# "zeta", "eta", "theta", "vartheta", "iota", "kappa", \
# "lambda", "mu", "nu", "xi", "pi", "varpi", "rho", \
# "varrho", "sigma", "varsigma", "tau", "upsilon", "phi", \
# "varphi", "chi", "psi", "omega", \
# "Gamma", "Delta", "Theta", "Lambda", "Xi", \
# "Pi", "Sigma", "Upsilon", "Phi", "Psi", "Omega"]
#greekToLatex = {"alpha":"Alpha", "beta":"Beta", "gamma":"Gamma", "delta":"Delta", "epsilon":"Epsilon", "varepsilon":"Epsilon", \
# "zeta":"Zeta", "eta":"Eta", "theta":"Theta", "vartheta":"Theta", "iota":"Iota", "kappa":"Kappa", \
# "lambda":"Lambda", "mu":"Mu", "nu":"Nu", "xi":"Xi", "pi":"Pi", "varpi":"Pi", "rho":"Rho", \
# "varrho":"Rho", "sigma":"Sigma", "varsigma":"Sigma", "tau":"Tau", "upsilon":"Upsilon", "phi": "Phi", \
# "varphi":"Phi", "chi":"Chi", "psi":"Psi", "omega":"Omega", \
# "Gamma":"CapitalGamma", "Delta":"CapitalDelta", "Theta":"CapitalTheta", "Lambda":"CapitalLambda", "Xi":"CapitalXi", \
# "Pi":"CapitalPi", "Sigma":"CapitalSigma", "Upsilon":"CapitalUpsilon", "Phi":"CapitalPhi", "Psi":"CapitalPsi", "Omega":"CapitalOmega"
# }
#
#import re
#def identifier_to_tex(identifier):
# """
# If an identifier contains a greek symbol name as a separate word,
# (e.g. 'my_alpha_1' contains 'alpha' as a separate word, but 'alphaman' doesn't)
# add a backslash in front.
# """
# identifier = creduce(lambda a,b: "{%s_%s}" % (b, a), ["{%s}" % part for part in reversed(identifier.split("__"))])
# p = re.compile(r'([^\\A-Za-z]?)(%s)\b' % "|".join(greek_letter_strings))
# return p.sub(r'\1{\\\2}', identifier)
#
#
#
#
#
#def identifier_to_mathematica(identifier):
# """
# If an identifier contains a greek symbol name as a separate word,
# (e.g. 'my_alpha_1' contains 'alpha' as a separate word, but 'alphaman' doesn't)
# add a backslash in front.
# """
# identifier = creduce(lambda a,b: "Subscript[%s,%s]" % (b, a), reversed(identifier.split("__")))
# p = re.compile(r'\b(%s)\b' % "|".join(greek_letter_strings))
# repl = lambda m: r"\[" + greekToLatex[m.group(1)] + "]"
# return p.sub(repl, identifier)
[docs]class KeyTuple(tuple):
def __lt__(self, other):
# print("<", self, other)
if isinstance(other, (long, basestring)):
return False
if isinstance(other, KeyTuple):
return super(KeyTuple, self).__lt__(other)
raise AlgebraException("Cannot compare: {}".format(other))
def __gt__(self, other):
# print(">", self, other)
if isinstance(other, (long, basestring)):
return True
if isinstance(other, KeyTuple):
return super(KeyTuple, self).__gt__(other)
raise AlgebraException("Cannot compare: {}".format(other))
[docs]def set_union(*sets):
"""
Similar to ``sum()``, but for sets. Generate the union of an arbitrary number of set arguments.
:param sets: Sets to for the union of.
:type sets: set
:return: Union set.
:rtype: set
"""
return reduce(lambda a, b: a.union(b), sets, set(()))
[docs]def all_symbols(expr):
"""
Return all all_symbols featured within an expression.
:param expr: The expression to find all_symbols in.
:return: A set of all_symbols within expr.
:rtype: set
"""
try:
return expr.all_symbols()
except AttributeError:
return set(())
[docs]class Operation(Expression):
"""
Abstract base class for all operations,
where the operands themselves are also expressions.
"""
# hash str, is generated on demand (lazily)
__hash = None
def __init__(self, *operands):
"""
Create a symbolic operation with the given operands.::
Operation(*operands)
:param operands: The operands of the expression.
:type operands: object or as defined in the Operation's signature
"""
self._operands = operands
@property
def operands(self):
"""
:return: The operands of the operation.
:rtype: tuple
"""
return self._operands
def _all_symbols(self):
return set_union(*[all_symbols(op) for op in self.operands])
def _substitute(self, var_map):
if self in var_map:
return var_map[self]
return self.__class__.create(*map(lambda o: substitute(o, var_map), self.operands))
def _tex(self):
return r"{{\rm {}}}\left({{}}\right)".format(self.__class__.__name__, ", ".join(tex(o) for o in self.operands))
def __repr__(self):
return "{}({})".format(self.__class__.__name__, ", ".join(map(repr, self.operands)))
# def mathematica(self):
# return "%s[%s]" % (self.__class__.__name__, ", ".join(map(mathematica, self.operands)))
def __eq__(self, other):
# print(type(self), type(other), type(self) == type(other))
return type(self) == type(other) and self.operands == other.operands
def __hash__(self):
if not self.__hash:
self.__hash = hash((self.__class__, self.operands))
return self.__hash
[docs] @classmethod
def create(cls, *operands):
"""
Instead of directly instantiating an instance of any subclass of Operation,
it is advised to call the ``create()`` classmethod instead.
This method takes the same arguments as the constructor, but can preprocess them and even return an object
of a different type based on the operands.
:param operands: The operands for the operation.
"""
return cls(*operands)
[docs] @classmethod
def order_key(cls, obj):
"""
Provide a default ordering mechanism for achieving canonical ordering of expressions sequences.
:param obj: The object to create a key for.
"""
try:
return obj._order_key()
except AttributeError:
return str(obj)
def _order_key(self):
return KeyTuple((self.__class__.__name__,) + tuple(map(Operation.order_key, self.operands)))
mathematica = lambda s: s
########################################################################################################################
########################### WILDCARDS AND PATTERN MATCHING FUNCTIONS ###################################################
########################################################################################################################
inf = float('inf')
[docs]@_trace
def match_range(pattern):
"""
Compute how many objects/operands a given pattern can minimally and maximally match.
:param pattern: The pattern object
:return: min_number, max_number
:rtype: tuple
:raise: ValueError, if unknown pattern mode for Wildcard object
"""
if isinstance(pattern, Wildcard):
if pattern.mode == Wildcard.single:
return 1, 1
if pattern.mode == Wildcard.one_or_more:
return 1, inf
if pattern.mode == Wildcard.zero_or_more:
return 0, inf
raise ValueError()
if isinstance(pattern, PatternTuple):
if len(pattern):
a0, b0 = match_range(pattern[0])
a1, b1 = match_range(pattern[1:])
# a1, b1 = match_range(PatternTuple(pattern[1:]))
return a0 + a1, b0 + b1
return 0, 0
return 1, 1
[docs]class OperandsTuple(tuple):
"""
Specialized tuple to store expression operands for the purpose of matching them to patterns.
"""
def __getitem__(self, item):
if isinstance(item, slice):
#noinspection PyTypeChecker
return OperandsTuple(super(OperandsTuple, self).__getitem__(item))
return super(OperandsTuple, self).__getitem__(item)
def __getslice__(self, i, j):
return OperandsTuple(super(OperandsTuple, self).__getslice__(i, j))
[docs]class PatternTuple(tuple):
"""
Specialized tuple to store expression pattern operands.
"""
def __getitem__(self, item):
if isinstance(item, slice):
#noinspection PyTypeChecker
return PatternTuple(super(PatternTuple, self).__getitem__(item))
return super(PatternTuple, self).__getitem__(item)
def __getslice__(self, i, j):
return PatternTuple(super(PatternTuple, self).__getslice__(i, j))
[docs]class NamedPattern(Operation):
"""
Create a named (sub-)pattern for later use in processing elements of a matched expression.::
NamedPattern(name, pattern)
:param name: Pattern identifier
:type name: str
:param pattern: Pattern expression
:type pattern: Expression, PatternTuple
"""
def __init__(self, name, pattern):
super(NamedPattern, self).__init__(name, pattern)
def _flatten(seq):
"""
Helper method to _flatten out PatternTuple and OperandTuple elements within a sequence.
:param seq: Sequence of objects, some of which may be PatternTuple or OperandTuple objects.
:type seq: sequence
:return: A flattened list.
:rtype: list
"""
sres = []
for s in seq:
if isinstance(s, (PatternTuple, OperandsTuple)):
sres += list(s)
else:
sres.append(s)
return sres
[docs]@_trace
def update_pattern(expr, match_obj):
"""
Replace all wildcards in the pattern expression with their matched values as specified in a Match object.
:param expr: Pattern expression
:type expr: Expression or PatternTuple
:param match_obj: Match object
:type match_obj: Match
:return: Expression with replaced wildcards
:rtype: Expression or PatternTuple
"""
if isinstance(expr, Wildcard):
if expr.name in match_obj:
return match_obj[expr.name]
elif isinstance(expr, (PatternTuple, OperandsTuple)):
return expr.__class__(_flatten([update_pattern(o, match_obj) for o in expr]))
elif isinstance(expr, Operation):
return expr.__class__(*_flatten([update_pattern(o, match_obj) for o in expr.operands]))
return expr
[docs]@_trace
def match(pattern, expr):
"""
Match a pattern against an expression and return a Match object if successful or False, if not.
Works recursively.
:param pattern: Pattern expression
:type pattern: Expression or PatternTuple
:param expr: Expression to match against the pattern.
:type expr: Expression or OperandsTuple
:return: Match object or False
:rtype: Match or False
"""
if pattern is expr:
return Match()
a, b = match_range(pattern)
if isinstance(expr, OperandsTuple):
l = len(expr)
else:
l = 1
if not a <= l <= b:
return False
if isinstance(pattern, PatternTuple):
if not len(pattern):
assert l == 0
return Match()
p0 = pattern[0]
prest = pattern[1:]
if isinstance(expr, OperandsTuple):
if isinstance(p0, Wildcard):
if p0.mode != Wildcard.single:
a0, b0 = match_range(p0)
for k in range(a0, min(l, b0) + 1):
o0 = expr[:k]
orest = expr[k:]
m0 = match(p0, o0)
if m0:
if len(m0):
mrest = match(update_pattern(prest, m0), orest)
else:
mrest = match(prest, orest)
if mrest:
return m0 + mrest
return False
else:
m0 = match(p0, expr[0])
if m0:
orest = expr[1:]
if len(m0):
mrest = match(update_pattern(prest, m0), orest)
else:
mrest = match(prest, orest)
# print(m0, update_pattern(prest, m0), mrest)
if mrest:
return m0 + mrest
return False
else:
# noinspection PyTypeChecker
m0 = match(p0, expr[0])
if m0:
orest = expr[1:]
if len(m0):
mrest = match(update_pattern(prest, m0), orest)
else:
mrest = match(prest, orest)
# print(m0, update_pattern(prest, m0), mrest)
if mrest:
return m0 + mrest
return False
elif isinstance(pattern, Wildcard):
if pattern.mode == Wildcard.single:
if isinstance(expr, OperandsTuple):
assert len(expr) == 1
#noinspection PyRedeclaration
expr = expr[0]
if pattern.head and not isinstance(expr, pattern.head):
return False
if pattern.condition and not pattern.condition(expr):
return False
if pattern.name:
return Match({pattern.name: expr})
return Match()
else:
if not isinstance(expr, OperandsTuple):
expr = OperandsTuple((expr,))
if pattern.head and not all(isinstance(e, pattern.head) for e in expr):
return False
if pattern.condition and not all(pattern.condition(e) for e in expr):
return False
if pattern.name:
return Match({pattern.name: expr})
return Match()
elif isinstance(pattern, NamedPattern):
name, p = pattern.operands
m = match(p, expr)
if m:
return m + Match({name: expr})
return False
elif isinstance(pattern, Operation):
if isinstance(expr, Operation) and type(pattern) is type(expr):
return match(PatternTuple(pattern.operands), OperandsTuple(expr.operands))
else:
return Match() if pattern == expr else False
[docs]class Wildcard(Expression):
"""
Basic wildcard expression that can match a single expression or in the context of
matching the operands of an Operation object one may match one_or_more or zero_or_more operands
with the same wildcards. If the wildcard has a name, a successful match leads to a Match object in which the
object that matched the wildcard is stored under that name. One can also restrict the type of the matched Expression
by providing a head argument and the condition argument allows for passing a function that performs additional tests
on a potential match.
"""
single = 1
"""Value of :py:attr:`Wildcard.mode` for matching single operands/objects"""
one_or_more = 2
"""Value of :py:attr:`Wildcard.mode` for matching one or more operands/objects"""
zero_or_more = 3
"""Value of :py:attr:`Wildcard.mode` for matching zero or more operands/objects"""
name = ""
"""name/identifier of the wildcard (default = "")."""
mode = single
"""mode of the wildcard, i.e. how many operands it can match (default = ``single``)."""
head = None
"""head/type of the matched object (default = ``None``, corresponding to no restriction)."""
condition = None
"""extra condition for a successful match (default = ``None``, corresponding to no restriction)."""
_hash = None
#noinspection PyRedeclaration
def __init__(self, name="", mode=single, head=None, condition=None):
"""
:param name: Wildcard name, (default = "")
:type name: str
:param mode: The matching mode, i.e. how many objects/operands can the wildcard match.
:type mode: One of :py:attr:`Wildcard.single`, :py:attr:`Wildcard.one_or_more`, :py:attr:`Wildcard.zero_or_more`
:param head: Restriction of the type of the matched expression
:type head: tuple or type or None
:param condition: An additional function that returns True if match should be accepted.
:type condition: FunctionType or None
"""
self.name = name
self.mode = mode
self.head = head
self.condition = condition
def __str__(self):
if isinstance(self.head, tuple):
head_string = "({})".format("|".join(h.__name__ for h in self.head))
elif self.head is not None:
head_string = self.head.__name__
else:
head_string = ""
return "{}{}{}{}".format(self.name,
"_" * self.mode,
head_string,
self.condition.__name__ if self.condition else "")
def __repr__(self):
if isinstance(self.head, tuple):
head_string = "({})".format(", ".join(h.__name__ for h in self.head))
elif self.head is not None:
head_string = self.head.__name__
else:
head_string = "None"
return "Wildcard({}, {}, {}, {})".format(repr(self.name),
"_" * self.mode,
head_string,
self.condition.__name__ if self.condition else "None")
def __eq__(self, other):
return (self.__class__ == other.__class__
and self.name == other.name
and self.head == other.head
and self.mode == other.mode
and self.condition == other.condition)
def __hash__(self):
if not self._hash:
self._hash = hash((self.name, self.mode, self.head, self.condition))
return self._hash
def _tex(self):
return r"{\rm " + self.name + ("\_" * self.mode) + self.head.__name__ + (
"?{}".format(self.condition.__name__) if hasattr(self.condition, "__name__") else "") + "}"
def _all_symbols(self):
return set(())
[docs]class Match(dict):
"""
Subclass of dict that overloads the + operator
to create a new dictionary combining the entries.
It fails when there are duplicate keys.
"""
def __add__(self, other):
if not len(self):
return other
if not len(other):
return self
# make sure the sets of keys are disjoint
overlap = set(self.keys()) & set(other.keys())
if not len(overlap) == 0:
raise ValueError()
ret = Match(self)
ret.update(other)
return ret
# def __repr__(self):
# return "Match({})".format(dict.__repr__(self))
#
# def __str__(self):
# return "Match({})".format(dict.__str__(self))
# noinspection PyMethodMayBeStatic
def __bool__(self):
return True
__nonzero__ = __bool__
import re
name_mode_pattern = re.compile(r"^([A-Za-z]?[A-Za-z0-9]*)(_{0,3})$")
[docs]def wc(name_mode="_", head=None, condition=None):
"""
Helper function to create a Wildcard object.
:param name_mode: Combined name and mode (cf :py:class:`Wildcard`) argument.
* ``"A"`` -> ``name="A", mode = Wildcard.single``
* ``"A_"`` -> ``name="A", mode = Wildcard.single``
* ``"B__"`` -> ``name="B", mode = Wildcard.one_or_more``
* ``"B___"`` -> ``name="C", mode = Wildcard.zero_or_more``
:type name_mode: str
:param head: See Wildcard doc
:type head: tuple or ClassType or None
:param condition: See Wildcard doc
:type condition: FunctionType or None
:return: A Wildcard object
:rtype: Wildcard
"""
m = name_mode_pattern.match(name_mode)
if not m:
raise ValueError()
name, modelength = m.groups()
mode = len(modelength) or Wildcard.single
if not 1 <= mode <= 3:
raise ValueError()
return Wildcard(name, mode=mode, head=head, condition=condition)
########################################################################################################################
########################### CLASS DECORATORS TO ACHIEVE OPERAND PREPROCESSING ##########################################
########################################################################################################################
[docs]def make_classmethod(method, cls):
"""
Make a bound classmethod from an unbound method taking an additional first argument ``cls``
:param method: The unbound method
:type method: FunctionType
:param cls: The class to bind it to
:type cls: type
:return: Bound class method
:rtype: MethodType
"""
return MethodType(method, cls, type(cls))
[docs]def preprocess_create_with(method):
"""
This factory method allows for adding argument pre-processing decorators to a class's ``create`` classmethod.
:param method: A decorating create classmethod ``f()`` with signature:
``f(decorated_class, decorated_method, cls, *args)``
:type method: FunctionType
:return: A class decorator function that decorates the 'create' classmethod of the decorated class.
:rtype: FunctionType
"""
# noinspection PyDocstring
def decorator(dcls):
if six.PY2:
clsmtd = getattr(dcls, "create").im_func
else:
clsmtd = getattr(dcls, "create").__func__
# noinspection PyDocstring
def dclsmtd(cls, *args):
return method(dcls, clsmtd, cls, *args)
dclsmtd.method = method
dclsmtd.decorated = clsmtd
dclsmtd.dcls = dcls
dclsmtd.__doc__ = (str(clsmtd.__doc__)
+ "\n-- {}.create() preprocessed by {} --\n".format(dcls.__name__, method.__name__)
+ str(method.__doc__))
# store a list of all applied decorators as an attribute of the new create method's im_func.
dclsmtd.decorators = (method,) + getattr(clsmtd, "decorators", ())
dclsmtd.__name__ = "create"
# noinspection PyTypeChecker
# nmtd = make_classmethod(dclsmtd, dcls)
nmtd = classmethod(dclsmtd)
setattr(dcls, "create", nmtd)
return dcls
# Copy docstring from method to class decorator
decorator.__doc__ = """
{}
Automatically generated class decorator based on the method ``qnet.algebra.abstract_algebra.{}()`` using
:py:func:`preprocess_create_with`.
""".format(method.__doc__, method.__name__)
return decorator
#noinspection PyUnusedLocal,PyDocstring
def _assoc(dcls, clsmtd, cls, *ops):
"""
Associatively expand out nested arguments of the flat class.
>>> @assoc
>>> class Plus(Operation):
>>> pass
>>> Plus.create(1,Plus(2,3))
Plus(1,2,3)
"""
nops = sum(((o,) if not isinstance(o, cls) else o.operands for o in ops), ())
return clsmtd(cls, *nops)
# noinspection PyTypeChecker
assoc = preprocess_create_with(_assoc)
#noinspection PyUnusedLocal,PyDocstring
def _idem(dcls, clsmtd, cls, *ops):
"""
Remove duplicate arguments and order them via the cls's order_key key object/function.
E.g.
>>> @idem
>>> class Set(Operation):
>>> pass
>>> Set.create(1,2,3,1,3)
Set(1,2,3)
"""
return clsmtd(cls, *sorted(set(ops), key=cls.order_key))
# noinspection PyTypeChecker
idem = preprocess_create_with(_idem)
#noinspection PyUnusedLocal,PyDocstring
def _orderby(dcls, clsmtd, cls, *ops):
"""
Re-order arguments via the class's ``order_key`` key object/function.
Use this for commutative operations:
E.g.
>>> @orderby
>>> class Times(Operation):
>>> pass
>>> Times.create(2,1)
Times(1,2)
"""
try:
return clsmtd(cls, *sorted(ops, key=cls.order_key))
except TypeError as te:
print(list(map(cls.order_key,ops)))
raise te
# noinspection PyTypeChecker
orderby = preprocess_create_with(_orderby)
unequals = lambda x: (lambda y: x != y)
#noinspection PyUnusedLocal,PyDocstring
def _filter_neutral(dcls, clsmtd, cls, *ops):
"""
Remove occurrences of a neutral element from the argument/operand list, if that list has at least two elements.
To use this, one must also specify a neutral element,
which can be anything that allows for an equality check with each argument.
E.g.
>>> @filter_neutral
>>> class X(Operation):
>>> neutral_element = 1
>>> X.create(2,1,3,1)
X(2,3)
"""
c_n = cls.neutral_element
if not len(ops):
return c_n
fops = tuple(filter(unequals(c_n), ops))
if len(fops) > 1:
return clsmtd(cls, *fops)
elif len(fops) == 1:
# the remaining operand is the single non-trivial one
return fops[0]
else:
# the original list of operands consists only of neutral elements
return ops[0]
# noinspection PyTypeChecker
filter_neutral = preprocess_create_with(_filter_neutral)
CLS = object()
DCLS = object()
[docs]def extended_isinstance(obj, class_info, dcls, cls):
"""
Like isinstance but with two extra arguments to allow for placeholder objects ``(DCLS, CLS)``
to stand for the class objects passed as extra arguments ``dcls``, ``cls``.
This allows one to specify a self-referential
`signature` class attribute to allow for recursive Operation signatures.
E.g.
>>> @check_signature
>>> class X(Operation):
>>> signature = str, X
will yield an exception, because X within the class body refers to a class object that has not been defined yet.
Instead, one can do
>>> @check_signature
>>> class X(Operation):
>>> signature = str, CLS
to refer to the class of the object being instantiated (could be a subclass of ``X``), or
>>> @check_signature
>>> class X(Operation):
>>> signature = str, DCLS
to always refer to ``X`` itself and not a subclass.
:param obj: The instance
:type obj: object
:param class_info: A type, ``DCLS``, ``CLS``, or a tuple of these
:type class_info: type or tuple of type-objects
:param dcls: The (super-)class that the signature is defined for.
:type dcls: type
:param cls: The concrete (sub-)class whose instance is being initialized.
:type cls: type
"""
if isinstance(class_info, tuple):
return any(extended_isinstance(obj, cli, dcls, cls) for cli in class_info)
if class_info is CLS:
#noinspection PyRedeclaration
class_info = cls
elif class_info is DCLS:
#noinspection PyUnusedLocal
class_info = dcls
return isinstance(obj, class_info)
#noinspection PyDocstring
def _check_signature(dcls, clsmtd, cls, *ops):
"""
Check that the operands passed to the create classmethod of an Operation type conform to certain types.
For each allowed argument/operand, provide a tuple of types (or one of ``CLS, DCLS``, see extended_isinstance docs).
E.g.
>>> @check_signature
>>> class X(Operation):
>>> signature = str, (int, str)
>>>
>>> X.create("1", 2)
X("1", 2)
>>> X.create("1", "2")
X("1", "2")
The following all raise :py:class:`WrongSignatureError` exception.
>>> X.create("1")
>>> X.create(1, "1")
>>> X.create("1", 2, 3)
"""
sgn = cls.signature
if not len(ops) == len(sgn):
raise WrongSignatureError()
if not all(extended_isinstance(o, s, dcls, cls) for o, s in zip(ops, sgn)):
raise WrongSignatureError("class: {}, operands: {}".format(str(cls), str(ops)))
return clsmtd(cls, *ops)
check_signature = preprocess_create_with(_check_signature)
#noinspection PyDocstring
def _check_signature_assoc(dcls, clsmtd, cls, *ops):
"""
Like :py:func:`check_signature` but for :py:func:`assoc`-decorated Operations. In this case the signature need only contain a single entry.
>>> @assoc
>>> @check_signature
>>> class X(Operation):
>>> signature = str
>>> X.create("hello", "you")
X("hello", "you")
The following then raises a :class:`WrongSignatureError` because the third argument is no string
>>> X.create("hello", "you", 2)
"""
sgn = cls.signature[0]
if not all(extended_isinstance(o, sgn, dcls, cls) for o in ops):
print(sgn, dcls, cls, ops)
raise WrongSignatureError()
return clsmtd(cls, *ops)
# noinspection PyTypeChecker
check_signature_assoc = preprocess_create_with(_check_signature_assoc)
# noinspection PyDocstring
# noinspection PyUnusedLocal
def _match_replace(dcls, clsmtd, cls, *ops):
"""
Match and replace a full operand specification to a function that provides a replacement for the whole expression
or raises a :py:class:`CannotSimplify` exception.
E.g.
First define wildcards:
>>> A = wc("A")
>>> A_float = wc("A", head = float)
Then an operation:
>>> @match_replace
>>> class Invert(Operation):
>>> _rules = []
Then some _rules:
>>> Invert._rules += [
>>> ((Invert(A),), lambda A: A),
>>> ((A_float,), lambda A: 1./A),
>>> ]
Check rule application:
>>> Invert.create("hallo") # matches no rule
Invert("hallo")
>>> Invert.create(Invert("hallo")) # matches first rule
"hallo"
>>> Invert.create(.2) # matches second rule
5.
A pattern can also have the same wildcard appear twice:
>>> @match_replace
>>> class X(Operation):
>>> _rules = [
>>> ((A, A), lambda A: A),
>>> ]
>>> X.create(1,2)
X(1,2)
>>> X.create(1,1)
1
"""
for patterns, replacement in cls._rules:
m = match(PatternTuple(patterns), OperandsTuple(ops))
if m:
try:
return replacement(**m)
except CannotSimplify:
continue
return clsmtd(cls, *ops)
# noinspection PyTypeChecker
match_replace = preprocess_create_with(_match_replace)
#noinspection PyDocstring
# noinspection PyUnusedLocal
def _match_replace_binary(dcls, clsmtd, cls, *ops):
"""
Similar to :py:func:`match_replace`, but for arbitrary length operations, such that each two pairs of subsequent operands are matched pairwise.
>>> A = wc("A")
>>> @match_replace_binary
>>> class FilterDupes(Operation):
>>> _rules = [
>>> ((A,A), lambda A: A),
>>> ]
>>> FilterDupes.create(1,2,3,4) # No subsequent duplicates present
FilterDupes(1,2,3,4)
>>> FilterDupes.create(1,2,2,3,4) # Some duplicates
FilterDupes(1,2,3,4)
Note that this only works for *subsequent* duplicate entries:
>>> FilterDupes.create(1,2,3,2,4) # Some duplicates, but not subsequent
FilterDupes(1,2,3,2,4)
"""
_rules = cls._binary_rules
# print("entering: ", ops)
j = 1
while j < len(ops):
first, second = ops[j - 1], ops[j]
m = False
r = False
for patterns, replacement in _rules:
m = match(PatternTuple(patterns), OperandsTuple((first, second)))
if m:
try:
r = replacement(**m)
break
except CannotSimplify:
continue
if not(r is False):
# if Operation is also "assoc", then expand out the operands of a binary-simplified result
if _assoc in getattr(cls.create.im_func
if six.PY2
else cls.create.__func__,
"decorators", ()) and isinstance(r, cls):
ops = ops[:j - 1] + r.operands + ops[j + 1:]
else:
ops = ops[:j - 1] + (r,) + ops[j + 1:]
if j > 1:
j -= 1
else:
j += 1
# print("exiting: ", ops)
return clsmtd(cls, *ops)
match_replace_binary = preprocess_create_with(_match_replace_binary)
# Store all singletons in a dict
_singletons = {}
def _get_singleton(name):
"""Retrieve singletons by name."""
return _singletons[name]
[docs]def singleton(cls):
"""
Singleton class decorator. Turns a class object into a unique instance.
:param cls: Class to decorate
:type cls: type
:return: The singleton instance of that class
:rtype: cls
"""
# noinspection PyDocstring
class S(cls):
__instance = None
def __hash__(self):
return hash(cls)
# noinspection PyMethodMayBeStatic
def _symbols(self):
return set(())
def __repr__(self):
return cls.__name__
def __call__(self):
return self.__instance
def __reduce__(self):
"""
This magic method ensures that singletons can be pickled.
See also https://docs.python.org/3.1/library/pickle.html#pickle.object.__reduce__
"""
return (_get_singleton, (cls.__name__,))
S.__name__ = cls.__name__
S.__instance = s = S()
_singletons[S.__name__] = s
return s
[docs]def prod(sequence, neutral=1):
"""
Analog of the builtin `sum()` method.
:param sequence: Sequence of objects that support being multiplied to each other.
:type sequence: Any object that implements __mul__()
:param neutral: The initial return value, which is also returned for zero-length sequence arguments.
:type neutral: Any object that implements __mul__()
:return: The product of the elements of `sequence`
"""
return reduce(lambda a, b: a * b, sequence, neutral)