docs: def simplify(self):
          """
          Return a new simplified expression in canonical form from this
          expression.
  
          For simplification of AND and OR fthe ollowing rules are used
          recursively bottom up:
           - Associativity (output does not contain same operations nested)
           - Annihilation
           - Idempotence
           - Identity
           - Complementation
           - Elimination
           - Absorption
           - Commutativity (output is always sorted)
  
          Other boolean objects are also in their canonical form.
          """
          # TODO: Refactor DualBase.simplify into different "sub-evals".
  
          # If self is already canonical do nothing.
          if self.iscanonical:
              return self
  
          # Otherwise bring arguments into canonical form.
          args = [arg.simplify() for arg in self.args]
  
          # Create new instance of own class with canonical args.
          # TODO: Only create new class if some args changed.
          expr = self.__class__(*args)
  
          # Literalize before doing anything, this also applies De Morgan's Law
          expr = expr.literalize()
  
          # Associativity:
          #     (A & B) & C = A & (B & C) = A & B & C
          #     (A | B) | C = A | (B | C) = A | B | C
          expr = expr.flatten()
  
          # Annihilation: A & 0 = 0, A | 1 = 1
          if self.annihilator in expr.args:
              return self.annihilator
  
          # Idempotence: A & A = A, A | A = A
          # this boils down to removing duplicates
          args = []
          for arg in expr.args:
              if arg not in args:
                  args.append(arg)
          if len(args) == 1:
              return args[0]
  
          # Identity: A & 1 = A, A | 0 = A
          if self.identity in args:
              args.remove(self.identity)
              if len(args) == 1:
                  return args[0]
  
          # Complementation: A & ~A = 0, A | ~A = 1
          for arg in args:
              if self.NOT(arg) in args:
                  return self.annihilator
  
          # Elimination: (A & B) | (A & ~B) = A, (A | B) & (A | ~B) = A
          i = 0
          while i < len(args) - 1:
              j = i + 1
              ai = args[i]
              if not isinstance(ai, self.dual):
                  i += 1
                  continue
              while j < len(args):
                  aj = args[j]
                  if not isinstance(aj, self.dual) or len(ai.args) != len(aj.args):
                      j += 1
                      continue
  
                  # Find terms where only one arg is different.
                  negated = None
                  for arg in ai.args:
                      # FIXME: what does this pass Do?
                      if arg in aj.args:
                          pass
                      elif self.NOT(arg).cancel() in aj.args:
                          if negated is None:
                              negated = arg
                          else:
                              negated = None
                              break
                      else:
                          negated = None
                          break
  
                  # If the different arg is a negation simplify the expr.
                  if negated is not None:
                      # Cancel out one of the two terms.
                      del args[j]
                      aiargs = list(ai.args)
                      aiargs.remove(negated)
                      if len(aiargs) == 1:
                          args[i] = aiargs[0]
                      else:
                          args[i] = self.dual(*aiargs)
  
                      if len(args) == 1:
                          return args[0]
                      else:
                          # Now the other simplifications have to be redone.
                          return self.__class__(*args).simplify()
                  j += 1
              i += 1
  
          # Absorption: A & (A | B) = A, A | (A & B) = A
          # Negative absorption: A & (~A | B) = A & B, A | (~A & B) = A | B
          args = self.absorb(args)
          if len(args) == 1:
              return args[0]
  
          # Commutativity: A & B = B & A, A | B = B | A
          args.sort()
  
          # Create new (now canonical) expression.
          expr = self.__class__(*args)
          expr.iscanonical = True
          return expr