예제 #1
0
    def test_hash(self):
        # Hashses should be order insensitive
        assert hash(LinExp(OrderedDict([
            ("a", 123),
            ("b", 321),
            ("c", 111),
        ]))) == hash(
            LinExp(OrderedDict([
                ("c", 111),
                ("b", 321),
                ("a", 123),
            ])))

        # Hashes of LinExp constants should match the raw constant
        assert hash(LinExp(123)) == hash(123)
        assert hash(LinExp(Fraction(1, 3))) == hash(Fraction(1, 3))

        # Hashes of symbols should match the raw symbols
        assert hash(LinExp("a")) == hash("a")

        # Hashes should produce different values for different contents... Note
        # that this test is not strictly guaranteed to pass in theory, though
        # it practice it is almost certain to do so. Should it start failing,
        # think very hard!!
        assert hash(LinExp({"a": 123, "b": 321})) != hash(LinExp({"a": 123}))
예제 #2
0
def affine_error_with_range(lower, upper):
    """
    Create an affine arithmetic expression defining the specified
    range.
    """
    mean = Fraction(lower + upper, 2)
    half_range = Fraction(upper - lower, 2)
    
    return (half_range * LinExp.new_affine_error_symbol()) + mean
예제 #3
0
    def test_shifting(self):
        a = SymbolArray(3, "foo")
        sa = RightShiftedArray(a, 3)

        v = a[1, 2, 3]

        sv = sa[1, 2, 3]

        assert affine_lower_bound(sv) == v / 8 - Fraction(1, 2)
        assert affine_upper_bound(sv) == v / 8 + Fraction(1, 2)
예제 #4
0
 def _div_operator(cls, a, b):
     try:
         a = cls(a)
         b = cls(b)
     except TypeError:
         return NotImplemented
     
     # Division is only supported in the following cases:
     # * Both sides are constants
     # * where RHS = constant
     # * where LHS = constant*RHS (and the result is constant)
     # In all other cases, the result will become non-linear
     if b.is_constant:
         constant = b.constant
         
         # NB: Convert integers into Fraction (rational) numbers so that the
         # reciprocal operation in the next step continues to produce exact
         # results.
         if isinstance(constant, Integral):
             constant = Fraction(constant)
         
         return a * (1 / constant)
     else:
         # Check if the LHS is a multiple of the RHS (special case: or that
         # LHS is zero)
         if a._coeffs and (set(a._coeffs) != set(b._coeffs)):
             return NotImplemented
         
         # NB: Because we know the RHS is not a constant (and therefore not
         # zero) we know there will be at least one symbol and so this loop
         # will run at least once.
         factor = None
         for symbol in b.symbols():
             lcoeff = a[symbol]
             rcoeff = b[symbol]
             
             # Use Fractions (if possible) to ensure exact results
             if isinstance(lcoeff, Integral):
                 lcoeff = Fraction(lcoeff)
             if isinstance(rcoeff, Integral):
                 rcoeff = Fraction(rcoeff)
             
             this_factor = lcoeff/rcoeff
             if factor is not None and this_factor != factor:
                 return NotImplemented
             factor = this_factor
         
         return cls(factor)
예제 #5
0
 def relative_step_size_to(self, other):
     if other is self:
         return (Fraction(1), ) * self.ndim
     
     even_step_size = self._array_even.relative_step_size_to(other)
     odd_step_size = self._array_odd.relative_step_size_to(other)
     
     if even_step_size is None and odd_step_size is None:
         return None
     
     if even_step_size is not None and not odd_step_size is None:
         raise ValueError(
             "Cannot find relative step size of {} in {} "
             "as it is interleaved with itself.".format(other, self)
         )
     
     if even_step_size is not None:
         relative_step_size = even_step_size
     else:
         relative_step_size = odd_step_size
     
     return tuple(
         s/2 if d == self._interleave_dimension else s
         for d, s in enumerate(relative_step_size)
     )
예제 #6
0
    def test_integration(self):
        # A simple test showing most of the moving parts used in a way they
        # might be in a video filter...

        a = LinExp("a")
        b = LinExp("b")

        ab = a + b
        assert str(ab) == "a + b"

        ab3 = 3 * ab
        assert str(ab3) == "3*a + 3*b"

        ab2 = ab3 * Fraction(2, 3)
        assert str(ab2) == "2*a + 2*b"

        b2_1 = 2 * b - 1
        assert str(b2_1) == "2*b + -1"

        a2_1 = ab2 - b2_1
        assert str(a2_1) == "2*a + 1"

        a_05 = a2_1 / 2
        assert str(a_05) == "a + 1/2"

        two_and_a_half = a_05.subs({"a": 2})
        assert str(two_and_a_half) == "5/2"

        three = (3 * a_05) / a_05
        assert str(three) == "3"
예제 #7
0
def deserialise_linexp(json):
    """
    Inverse of :py:func:`serialise_linexp`.
    """
    return LinExp({
        d["symbol"]: Fraction(int(d["numer"]), int(d["denom"]))
        for d in json
    })
예제 #8
0
 def relative_step_size_to(self, other):
     if other is self:
         return (Fraction(1), ) * self.ndim
     
     relative_step_size = self._input_array.relative_step_size_to(other)
     if relative_step_size is None:
         return None
     
     return tuple(
         step_size * prev_relative_step_size
         for step_size, prev_relative_step_size in zip(
             self._steps,
             relative_step_size,
         )
     )
예제 #9
0
def test_round_away_from_zero():
    assert round_away_from_zero(Fraction(0, 1)) == 0

    assert round_away_from_zero(Fraction(20, 10)) == 2
    assert round_away_from_zero(Fraction(21, 10)) == 3

    assert round_away_from_zero(Fraction(-20, 10)) == -2
    assert round_away_from_zero(Fraction(-21, 10)) == -3

    assert type(round_away_from_zero(Fraction(0, 1))) is int
예제 #10
0
 def relative_step_size_to(self, other):
     if other is self:
         return (Fraction(1), ) * self.ndim
     else:
         return self._input_array.relative_step_size_to(other)
예제 #11
0
 def relative_step_size_to(self, other):
     if other is self:
         return (Fraction(1), ) * self.ndim
     else:
         return None
예제 #12
0
 def __rfloordiv__(self, other):
     return (
         self._div_operator(other, self) +
         Fraction(1, 2)*type(self).new_affine_error_symbol() -
         Fraction(1, 2)
     )
예제 #13
0
class TestLinExp(object):
    def test_constructor_default(self):
        assert LinExp()._coeffs == {}

    @pytest.mark.parametrize("value", [0, 0.0, Fraction(0)])
    def test_constructor_zero(self, value):
        assert LinExp(value)._coeffs == {}

    @pytest.mark.parametrize("value", [123, 1.23, Fraction(1, 10)])
    def test_constructor_number(self, value):
        assert LinExp(value)._coeffs == {None: value}

    @pytest.mark.parametrize("symbol", ["a", (1, 2, 3)])
    def test_constructor_symbol(self, symbol):
        assert LinExp(symbol)._coeffs == {symbol: 1}

    def test_constructor_dictionary(self):
        # Zero coefficients should be removed
        assert LinExp({
            "a": 100,
            "b": 200,
            "c": 0,
            None: 123
        })._coeffs == {
            "a": 100,
            "b": 200,
            None: 123,
        }

    def test_constructor_linexp(self):
        v1 = LinExp({"a": 1, "b": 2})

        v2 = LinExp(v1)

        # Should pass through original object without creating a new value
        assert v1 is v2

    def test_new_affine_error_symbol(self):
        e1 = LinExp.new_affine_error_symbol()
        e2 = LinExp.new_affine_error_symbol()

        assert e1.is_symbol
        assert isinstance(e1.symbol, AAError)
        assert e2.is_symbol
        assert isinstance(e2.symbol, AAError)

        assert e1 != e2

    @pytest.mark.parametrize("value,exp_symbols", [
        (0, set([])),
        (123, set([None])),
        ({
            "a": 123
        }, set(["a"])),
        ({
            "a": 123,
            "b": 321,
            None: 111
        }, set(["a", "b", None])),
    ])
    def test_symbols(self, value, exp_symbols):
        assert set(LinExp(value).symbols()) == exp_symbols

    @pytest.mark.parametrize("value,exp_items", [
        (0, set([])),
        (123, set([(None, 123)])),
        ({
            "a": 123
        }, set([("a", 123)])),
        ({
            "a": 123,
            "b": 321,
            None: 111
        }, set([("a", 123), ("b", 321), (None, 111)])),
    ])
    def test_iter(self, value, exp_items):
        assert set(iter(LinExp(value))) == exp_items

    def test_getitem(self):
        v = LinExp({"a": 123, "b": 321, None: 111})

        assert v["a"] == 123
        assert v["b"] == 321
        assert v[None] == 111
        assert v["xxx"] == 0

    def test_contains(self):
        v = LinExp({"a": 123, "b": 321, None: 111})

        assert ("a" in v) is True
        assert ("b" in v) is True
        assert (None in v) is True
        assert ("xxx" in v) is False

    @pytest.mark.parametrize("value,exp_is_constant", [
        (0, True),
        (123, True),
        ({
            None: 123
        }, True),
        ({
            "a": 123
        }, False),
        ({
            "a": 123,
            None: 321
        }, False),
    ])
    def test_is_constant(self, value, exp_is_constant):
        assert LinExp(value).is_constant is exp_is_constant

    @pytest.mark.parametrize("value,exp_is_symbol", [
        (0, False),
        (123, False),
        ({
            None: 1
        }, False),
        ({
            None: 123
        }, False),
        ({
            "a": 1
        }, True),
        ({
            "a": 123
        }, False),
        ({
            "a": 1,
            None: 321
        }, False),
    ])
    def test_is_symbol(self, value, exp_is_symbol):
        assert LinExp(value).is_symbol is exp_is_symbol

    def test_symbol(self):
        assert LinExp((1, 2, 3)).symbol == (1, 2, 3)

        with pytest.raises(TypeError):
            LinExp({(1, 2, 3): 2}).symbol

    @pytest.mark.parametrize("value,exp_value", [
        (0, 0),
        (123, 123),
    ])
    def test_constant(self, value, exp_value):
        assert LinExp(value).constant == exp_value

    @pytest.mark.parametrize("value", [
        {
            "a": 123
        },
        {
            "a": 123,
            None: 321
        },
    ])
    def test_constant_when_not_constant(self, value):
        with pytest.raises(TypeError):
            LinExp(value).constant

    def test_number_type_casts(self):
        v = LinExp(1.5)

        assert isinstance(complex(v), complex)
        assert complex(v) == 1.5 + 0j

        assert isinstance(float(v), float)
        assert float(v) == 1.5

        assert isinstance(int(v), int)
        assert int(v) == 1

    @pytest.mark.parametrize("value,exp_bool", [
        (0, False),
        ({
            None: 123
        }, True),
        ({
            "a": 123
        }, True),
        ({
            "a": 123,
            None: 321
        }, True),
    ])
    def test_bool(self, value, exp_bool):
        assert bool(LinExp(value)) is exp_bool

    @pytest.mark.parametrize(
        "value,exp_strs",
        [
            # Constants
            (0, ["LinExp(0)"]),
            (123, ["LinExp(123)"]),
            (Fraction(1, 10), ["LinExp(Fraction(1, 10))"]),
            # Symbols with weight 1
            ("a", ["LinExp('a')"]),
            # Anything else
            ({
                "a": 123
            }, ["LinExp({'a': 123})"]),
            (
                {
                    "a": 123,
                    "b": 321
                },
                [
                    "LinExp({'a': 123, 'b': 321})",
                    "LinExp({'b': 321, 'a': 123})",
                ],
            ),
        ])
    def test_repr(self, value, exp_strs):
        assert repr(LinExp(value)) in exp_strs

    @pytest.mark.parametrize(
        "value,exp_strs",
        [
            # Constants
            (0, ["0"]),
            (123, ["123"]),
            (Fraction(1, 10), ["1/10"]),
            # Symbol with weight 1
            ("a", ["a"]),
            # Weighted symbol
            ({
                "a": 123
            }, ["123*a"]),
            # Orderable symbols
            (
                {
                    "a": 123,
                    "c": Fraction(1, 10),
                    "b": 321
                },
                [
                    "123*a + 321*b + (1/10)*c",
                ],
            ),
            (
                {
                    "a": 123,
                    None: 42,
                    "c": Fraction(1, 10),
                    "b": 321
                },
                [
                    "123*a + 321*b + (1/10)*c + 42",
                ],
            ),
            # Unorderable symbols
            ({
                "a": 123,
                "b": 321,
                (1, 2, 3): Fraction(1, 10)
            },
             list(" + ".join(parts) for parts in permutations([
                 "123*a",
                 "321*b",
                 "(1/10)*(1, 2, 3)",
             ]))),
            ({
                "a": 123,
                None: 42,
                "b": 321,
                (1, 2, 3): Fraction(1, 10)
            },
             list("{} + 42".format(" + ".join(parts))
                  for parts in permutations([
                      "123*a",
                      "321*b",
                      "(1/10)*(1, 2, 3)",
                  ]))),
        ])
    def test_str(self, value, exp_strs):
        assert str(LinExp(value)) in exp_strs

    def test_hash(self):
        # Hashses should be order insensitive
        assert hash(LinExp(OrderedDict([
            ("a", 123),
            ("b", 321),
            ("c", 111),
        ]))) == hash(
            LinExp(OrderedDict([
                ("c", 111),
                ("b", 321),
                ("a", 123),
            ])))

        # Hashes of LinExp constants should match the raw constant
        assert hash(LinExp(123)) == hash(123)
        assert hash(LinExp(Fraction(1, 3))) == hash(Fraction(1, 3))

        # Hashes of symbols should match the raw symbols
        assert hash(LinExp("a")) == hash("a")

        # Hashes should produce different values for different contents... Note
        # that this test is not strictly guaranteed to pass in theory, though
        # it practice it is almost certain to do so. Should it start failing,
        # think very hard!!
        assert hash(LinExp({"a": 123, "b": 321})) != hash(LinExp({"a": 123}))

    def test_eq_constants(self):
        assert LinExp(123) == LinExp(123)
        assert LinExp(123) == 123
        assert 123 == LinExp(123)

        assert LinExp(123) != LinExp(321)
        assert LinExp(123) != 321
        assert 321 != LinExp(123)

    def test_eq_expressions(self):
        assert LinExp({"a": 123}) == LinExp({"a": 123})
        assert LinExp({"a": 123}) != LinExp({"b": 123})
        assert LinExp({"a": 123}) != LinExp({"a": 321})

    def test_lt_constants(self):
        assert LinExp(1) < LinExp(2)
        assert not (LinExp(1) < LinExp(1))
        assert not (LinExp(2) < LinExp(1))

        assert 1 < LinExp(2)
        assert not (1 < LinExp(1))
        assert not (2 < LinExp(1))

        assert LinExp(1) < 2
        assert not (LinExp(1) < 1)
        assert not (LinExp(2) < 1)

    def test_lt_free_symbols_cancel(self):
        assert LinExp({"a": 123, None: 1}) < LinExp({"a": 123, None: 2})
        assert not (LinExp({"a": 123, None: 1}) < LinExp({"a": 123, None: 1}))
        assert not (LinExp({"a": 123, None: 2}) < LinExp({"a": 123, None: 1}))

    def test_lt_free_symbols_dont_cancel(self):
        with pytest.raises(TypeError):
            LinExp({"a": 1}) < LinExp({"a": 2})
        with pytest.raises(TypeError):
            123 < LinExp({"a": 2})
        with pytest.raises(TypeError):
            LinExp({"a": 1}) < 123

    @pytest.mark.parametrize(
        "a,b,exp_result",
        [
            # Constants
            (LinExp(10), LinExp(3), LinExp(7)),
            (LinExp(10), 3, LinExp(7)),
            (10, LinExp(3), LinExp(7)),
            # Symbols should be summed too
            (
                LinExp({
                    "a": 10,
                    "b": 100,
                    None: 1000
                }),
                LinExp({
                    "a": 7,
                    "b": 70,
                    None: 700
                }),
                LinExp({
                    "a": 3,
                    "b": 30,
                    None: 300
                }),
            ),
            # Both sides may differ in which symbols they have
            (
                LinExp({
                    "a": 10,
                    "b": 100,
                    None: 10000
                }),
                LinExp({
                    "b": 70,
                    "c": 700,
                    None: 7000
                }),
                LinExp({
                    "a": 10,
                    "b": 30,
                    "c": -700,
                    None: 3000
                }),
            ),
            # One side cannot be cast (due to non-hashable type)
            (123, ["fail"], NotImplemented),
            (["fail"], 123, NotImplemented),
        ])
    def test_pairwise_operator(self, a, b, exp_result):
        assert LinExp._pairwise_operator(a, b, operator.sub) == exp_result

    def test_add(self):
        assert (LinExp({
            "a": 10,
            "b": 20
        }) + LinExp({
            "b": 30,
            "c": 40
        }) == LinExp({
            "a": 10,
            "b": 50,
            "c": 40
        }))
        # radd
        assert (10 + LinExp({
            "a": 20,
            "b": 30
        }) == LinExp({
            None: 10,
            "a": 20,
            "b": 30
        }))

    def test_sub(self):
        assert (LinExp({
            "a": 10,
            "b": 20
        }) - LinExp({
            "b": 30,
            "c": 40
        }) == LinExp({
            "a": 10,
            "b": -10,
            "c": -40
        }))
        # rsub
        assert (10 - LinExp({
            "a": 20,
            "b": 30
        }) == LinExp({
            None: 10,
            "a": -20,
            "b": -30
        }))

    @pytest.mark.parametrize(
        "a,b,exp_result",
        [
            # Both sides constant
            (LinExp(2), LinExp(3), LinExp(6)),
            (LinExp(2), 3, LinExp(6)),
            (2, LinExp(3), LinExp(6)),
            # One side constant
            (LinExp({
                "a": 10,
                None: 100
            }), LinExp(3), LinExp({
                "a": 30,
                None: 300
            })),
            (LinExp({
                "a": 10,
                None: 100
            }), 3, LinExp({
                "a": 30,
                None: 300
            })),
            (LinExp(3), LinExp({
                "a": 10,
                None: 100
            }), LinExp({
                "a": 30,
                None: 300
            })),
            (3, LinExp({
                "a": 10,
                None: 100
            }), LinExp({
                "a": 30,
                None: 300
            })),
            # One side cannot be cast (due to non-hashable type)
            (123, ["fail"], NotImplemented),
            (["fail"], 123, NotImplemented),
            # Neither side is constant
            (LinExp({
                "a": 10,
                None: 100
            }), LinExp({
                "a": 10,
                None: 100
            }), NotImplemented),
        ])
    def test_mul_operator(self, a, b, exp_result):
        assert LinExp._mul_operator(a, b) == exp_result

    def test_mul(self):
        assert LinExp({
            "a": 10,
            None: 100
        }) * LinExp(3) == LinExp({
            "a": 30,
            None: 300
        })
        # rmul
        assert 3 * LinExp({"a": 10, None: 100}) == LinExp({"a": 30, None: 300})

    @pytest.mark.parametrize(
        "a,b,exp_result",
        [
            # Both sides constant
            (LinExp(2), LinExp(3), LinExp(Fraction(2, 3))),
            (LinExp(2), 3, LinExp(Fraction(2, 3))),
            (2, LinExp(3), LinExp(Fraction(2, 3))),
            # RHS is a constant
            (LinExp({"a": 2}), LinExp(3), LinExp({"a": Fraction(2, 3)})),
            (LinExp({
                None: 1,
                "a": 2,
                "b": 3
            }), LinExp(3),
             LinExp({
                 None: Fraction(1, 3),
                 "a": Fraction(2, 3),
                 "b": 1,
             })),
            # LHS and RHS are multiples of eachother
            (
                LinExp({
                    None: 1,
                    "a": 2,
                    "b": 3
                }),
                LinExp({
                    None: -10,
                    "a": -20,
                    "b": -30
                }),
                LinExp(Fraction(-1, 10)),
            ),
            # LHS is zero
            (
                LinExp(0),
                LinExp({
                    None: -10,
                    "a": -20,
                    "b": -30
                }),
                LinExp(0),
            ),
            # LHS and RHS are not multiples of eachother
            (
                LinExp({
                    None: 2,
                    "a": 2,
                    "b": 3
                }),
                LinExp({
                    None: -10,
                    "a": -20,
                    "b": -30
                }),
                NotImplemented,
            ),
            # Different symbols on each side
            (LinExp(3), LinExp({"a": 2}), NotImplemented),
            (LinExp({
                "a": 10,
                None: 100
            }), LinExp({
                "b": 10,
                None: 100
            }), NotImplemented),
            # One side cannot be cast (due to non-hashable type)
            (123, ["fail"], NotImplemented),
            (["fail"], 123, NotImplemented),
        ])
    def test_div_operator(self, a, b, exp_result):
        assert LinExp._div_operator(a, b) == exp_result

    def test_div_operator_div_by_zero(self):
        with pytest.raises(ZeroDivisionError):
            LinExp._div_operator(LinExp(123), LinExp(0))
        with pytest.raises(ZeroDivisionError):
            LinExp._div_operator(LinExp({"a": 123}), LinExp(0))

    def test_div(self):
        assert LinExp({
            "a": 10,
            None: 100
        }) / LinExp(2) == LinExp({
            "a": 5,
            None: 50
        })
        # rrealdiv/rdiv
        assert 0 / LinExp({"a": 10, None: 100}) == LinExp(0)

    def test_floordiv(self):
        a = LinExp("a")
        a_over_3 = a // 3
        assert affine_lower_bound(a_over_3) == (a / 3) - 1
        assert affine_upper_bound(a_over_3) == a / 3

    @pytest.mark.parametrize(
        "a,b,exp_result",
        [
            # Both sides constant
            (LinExp(2), LinExp(3), LinExp(16)),
            (2, LinExp(3), LinExp(16)),
            (LinExp(2), 3, LinExp(16)),
            # LHS side contains a symbol
            (LinExp("a"), LinExp(3), LinExp("a") * 8),
            # RHS side contains a symbol
            (LinExp(3), LinExp("a"), NotImplemented),
            # One side cannot be cast (due to non-hashable type)
            (123, ["fail"], NotImplemented),
            (["fail"], 123, NotImplemented),
        ])
    def test_lshift_operator(self, a, b, exp_result):
        assert LinExp._lshift_operator(a, b) == exp_result

    @pytest.mark.parametrize(
        "a,b,exp_result",
        [
            # Both sides constant
            (LinExp(123), LinExp(3), LinExp(123) // 8),
            (123, LinExp(3), LinExp(123) // 8),
            (LinExp(123), 3, LinExp(123) // 8),
            # LHS side contains a symbol
            (LinExp("a"), LinExp(3), LinExp("a") // 8),
            # RHS side contains a symbol
            (LinExp(3), LinExp("a"), NotImplemented),
            # One side cannot be cast (due to non-hashable type)
            (123, ["fail"], NotImplemented),
            (["fail"], 123, NotImplemented),
        ])
    def test_rshift_operator(self, a, b, exp_result):
        actual_result = LinExp._rshift_operator(a, b)

        # Make error symbols match
        if exp_result is not NotImplemented:
            actual_error = actual_result - strip_affine_errors(actual_result)
            exp_error = exp_result - strip_affine_errors(exp_result)
            actual_result = actual_result.subs(
                {next(actual_error.symbols()): next(exp_error.symbols())})

        assert actual_result == exp_result

    @pytest.mark.parametrize(
        "a,b,exp_result",
        [
            # Both sides constant
            (LinExp(2), LinExp(3), LinExp(8)),
            (2, LinExp(3), LinExp(8)),
            (LinExp(2), 3, LinExp(8)),
            # Either side contains a symbol
            (LinExp("a"), LinExp(3), NotImplemented),
            (LinExp(3), LinExp("a"), NotImplemented),
            # One side cannot be cast (due to non-hashable type)
            (123, ["fail"], NotImplemented),
            (["fail"], 123, NotImplemented),
        ])
    def test_pow_operator(self, a, b, exp_result):
        assert LinExp._pow_operator(a, b) == exp_result

    def test_pow(self):
        assert LinExp(2)**LinExp(3) == LinExp(8)

        # Modulo never supported
        with pytest.raises(TypeError):
            LinExp(2)**LinExp(3) % 5

        # rpow
        assert 2**LinExp(3) == LinExp(8)

    def test_neg(self):
        assert -LinExp() == LinExp()
        assert -LinExp(3) == LinExp(-3)
        assert -LinExp({"a": 1, "b": -2}) == LinExp({"a": -1, "b": 2})

    def test_pos(self):
        assert +LinExp() == LinExp()
        assert +LinExp(3) == LinExp(3)
        assert +LinExp({"a": 1, "b": -2}) == LinExp({"a": 1, "b": -2})

    @pytest.mark.parametrize(
        "before,substitutions,exp_after",
        [
            # Do nothing to nothing
            (LinExp(), {}, LinExp()),
            # Do something to nothing
            (LinExp(), {
                "a": "b"
            }, LinExp()),
            # Do nothing to something
            (LinExp({"a": 123}), {}, LinExp({"a": 123})),
            # Zero out a value
            (LinExp({
                "a": 123,
                "b": 321
            }), {
                "a": 0
            }, LinExp({"b": 321})),
            # Replace symbol with number
            (LinExp({
                "a": 10,
                "b": 100
            }), {
                "a": 5
            }, LinExp({
                "b": 100,
                None: 50
            })),
            # Replace symbol with expression
            (LinExp({
                "a": 10,
                "b": 100
            }), {
                "a": LinExp({
                    "b": 20,
                    "c": 30
                })
            }, LinExp({
                "b": 100 + (20 * 10),
                "c": 30 * 10
            })),
            # Replace symbol with symbol
            (LinExp({
                "a": 10,
                "b": 100
            }), {
                "a": "c"
            }, LinExp({
                "c": 10,
                "b": 100
            })),
            # Replace symbol with existing symbol
            (LinExp({
                "a": 10,
                "b": 100,
                "c": 1000
            }), {
                "a": "c"
            }, LinExp({
                "c": 1010,
                "b": 100
            })),
            # Replace several symbols with same (existing) symbol
            (LinExp({
                "a": 10,
                "b": 100,
                "c": 1000
            }), {
                "a": "c",
                "b": "c"
            }, LinExp({"c": 1110})),
            # Swap symbols (ensure simultaneous replacement)
            (LinExp({
                "a": 10,
                "b": 20
            }), {
                "a": "b",
                "b": "a"
            }, LinExp({
                "a": 20,
                "b": 10
            })),
            # 'Overwrite' symbol (again, ensuring simultaneous action of different
            # mutations)
            (LinExp({
                "a": 10,
                "b": 20
            }), {
                "a": "b",
                "b": 0
            }, LinExp({"b": 10})),
        ])
    def test_subs(self, before, substitutions, exp_after):
        assert before.subs(substitutions) == exp_after

    def test_integration(self):
        # A simple test showing most of the moving parts used in a way they
        # might be in a video filter...

        a = LinExp("a")
        b = LinExp("b")

        ab = a + b
        assert str(ab) == "a + b"

        ab3 = 3 * ab
        assert str(ab3) == "3*a + 3*b"

        ab2 = ab3 * Fraction(2, 3)
        assert str(ab2) == "2*a + 2*b"

        b2_1 = 2 * b - 1
        assert str(b2_1) == "2*b + -1"

        a2_1 = ab2 - b2_1
        assert str(a2_1) == "2*a + 1"

        a_05 = a2_1 / 2
        assert str(a_05) == "a + 1/2"

        two_and_a_half = a_05.subs({"a": 2})
        assert str(two_and_a_half) == "5/2"

        three = (3 * a_05) / a_05
        assert str(three) == "3"