Collect summands that occur multiple times into a single summand

    Also filters out zero-summands.

    Example:
        >>> A, B, C = (OperatorSymbol(s, hs=0) for s in ('A', 'B', 'C'))
        >>> collect_summands(
        ...     OperatorPlus, (A, B, C, ZeroOperator, 2 * A, B, -C) , {})
        ((3 * A^(0), 2 * B^(0)), {})
        >>> collect_summands(OperatorPlus, (A, -A), {})
        ZeroOperator
        >>> collect_summands(OperatorPlus, (B, A, -B), {})
        A^(0)

def collect_summands(cls, ops, kwargs):
    """Collect summands that occur multiple times into a single summand

    Also filters out zero-summands.

    Example:
        >>> A, B, C = (OperatorSymbol(s, hs=0) for s in ('A', 'B', 'C'))
        >>> collect_summands(
        ...     OperatorPlus, (A, B, C, ZeroOperator, 2 * A, B, -C) , {})
        ((3 * A^(0), 2 * B^(0)), {})
        >>> collect_summands(OperatorPlus, (A, -A), {})
        ZeroOperator
        >>> collect_summands(OperatorPlus, (B, A, -B), {})
        A^(0)
    """
    from qnet.algebra.core.abstract_quantum_algebra import (
        ScalarTimesQuantumExpression)
    coeff_map = OrderedDict()
    for op in ops:
        if isinstance(op, ScalarTimesQuantumExpression):
            coeff, term = op.coeff, op.term
        else:
            coeff, term = 1, op
        if term in coeff_map:
            coeff_map[term] += coeff
        else:
            coeff_map[term] = coeff
    fops = []
    for (term, coeff) in coeff_map.items():
        op = coeff * term
        if not op.is_zero:
            fops.append(op)
    if len(fops) == 0:
        return cls._zero
    elif len(fops) == 1:
        return fops[0]
    else:
        return tuple(fops), kwargs