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"
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_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
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_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
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})
def test_symbol_array(): a = SymbolArray(3, "foo") assert a.period == (1, 1, 1) assert a.nop is False assert a.relative_step_size_to(a) == (1, 1, 1) assert a.relative_step_size_to(SymbolArray(2, "bar")) is None assert a[0, 0, 0] == LinExp(("foo", 0, 0, 0)) assert a[1, 2, 3] == LinExp(("foo", 1, 2, 3))
def test_subsampling(self): a = SymbolArray(3, "v") s = SubsampledArray(a, (1, 2, 3), (0, 10, 20)) assert s[0, 0, 0] == LinExp(("v", 0, 10, 20)) assert s[1, 0, 0] == LinExp(("v", 1, 10, 20)) assert s[0, 1, 0] == LinExp(("v", 0, 12, 20)) assert s[0, 0, 1] == LinExp(("v", 0, 10, 23)) assert s[2, 2, 2] == LinExp(("v", 2, 14, 26))
def synthesis_filter_bounds(expression): """ Find the lower- and upper-bound reachable in a :py:class:`~vc2_bit_widths.linexp.LinExp` describing a synthesis filter. The filter expression must contain only affine error symbols (:py:class:`~vc2_bit_widths.linexp.AAError`) and symbols of the form ``((_, level, orient), x, y)`` representing transform coefficients. Parameters ========== expression : :py:class:`~vc2_bit_widths.linexp.LinExp` Returns ======= (lower_bound, upper_bound) : :py:class:`~vc2_bit_widths.linexp.LinExp` Lower and upper bounds for the signal level in terms of the symbols of the form ``LinExp("signal_LEVEL_ORIENT_min")`` and ``LinExp("signal_LEVEL_ORIENT_max")``, representing the minimum and maximum signal levels for transform coefficients in level ``LEVEL`` and orientation ``ORIENT`` respectively. """ # Replace all transform coefficients with affine error terms scaled to # appropriate ranges expression = LinExp(expression) lower_bound = affine_lower_bound( expression.subs({ sym: ("coeff_{}_{}_min" if coeff > 0 else "coeff_{}_{}_max").format( sym[0][1], sym[0][2], ) for sym, coeff in expression if sym is not None and not isinstance(sym, AAError) })) upper_bound = affine_upper_bound( expression.subs({ sym: ("coeff_{}_{}_min" if coeff < 0 else "coeff_{}_{}_max").format( sym[0][1], sym[0][2], ) for sym, coeff in expression if sym is not None and not isinstance(sym, AAError) })) return (lower_bound, upper_bound)
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_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
def test_find_analysis_filter_bounds(): expr = (1 * LinExp(("pixel", 0, 0)) + 2 * LinExp( ("pixel", 1, 0)) + -4 * LinExp( ("pixel", 2, 0)) + 10 * LinExp.new_affine_error_symbol() + 1) lower_bound, upper_bound = analysis_filter_bounds(expr) assert lower_bound == 3 * LinExp("signal_min") + -4 * LinExp( "signal_max") - 10 + 1 assert upper_bound == -4 * LinExp("signal_min") + 3 * LinExp( "signal_max") + 10 + 1
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 })
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_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 serialise_signal_bounds(signal_bounds): """ Convert a dictionary of analysis or synthesis signal bounds expressions into a JSON-serialisable form. See :py:func:`serialise_linexp` for details of the ``"lower_bound"`` and ``"upper_bound"`` fields. For example:: >>> before = { ... (1, "LH", 2, 3): ( ... LinExp("foo")/2 + 1, ... LinExp("bar")/4 + 2, ... ), ... ... ... } >>> serialise_signal_bounds(before) [ { "level": 1, "array_name": "LH", "phase": [2, 3], "lower_bound": [ {"symbol": "foo", "numer": "1", "denom": "2"}, {"symbol": None, "numer": "1", "denom": "1"}, ], "upper_bound": [ {"symbol": "bar", "numer": "1", "denom": "4"}, {"symbol": None, "numer": "2", "denom": "1"}, ], }, ... ] """ return serialise_intermediate_value_dictionary( OrderedDict(( key, { "lower_bound": serialise_linexp(LinExp(lower_bound)), "upper_bound": serialise_linexp(LinExp(upper_bound)), }, ) for key, (lower_bound, upper_bound) in signal_bounds.items()))
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
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
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_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_evaluate_synthesis_filter_bounds(dc_band_name): expr = (LinExp((("coeff", 0, dc_band_name), 1, 2)) + LinExp( (("coeff", 2, "HH"), 3, 4)) + 10000) lower_bound_exp, upper_bound_exp = synthesis_filter_bounds(expr) coeff_bounds = { (0, dc_band_name): (-100, 200), (2, "HH"): (-1000, 2000), } lower_bound, upper_bound = evaluate_synthesis_filter_bounds( lower_bound_exp, upper_bound_exp, coeff_bounds, ) assert isinstance(lower_bound, int) assert isinstance(upper_bound, int) assert lower_bound == 10000 - 100 - 1000 assert upper_bound == 10000 + 200 + 2000
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 }))
def test_serialise_signal_bounds(): before = OrderedDict([ ((1, "LH", 2, 3), ( LinExp("foo")/2, LinExp("bar")/4, )), ((2, "HL", 3, 2), ( LinExp("qux")/8, LinExp("quo")/16, )), ]) after = serialise_signal_bounds(before) after = json_roundtrip(after) assert after == [ { "level": 1, "array_name": "LH", "phase": [2, 3], "lower_bound": [ {"symbol": "foo", "numer": "1", "denom": "2"}, ], "upper_bound": [ {"symbol": "bar", "numer": "1", "denom": "4"}, ], }, { "level": 2, "array_name": "HL", "phase": [3, 2], "lower_bound": [ {"symbol": "qux", "numer": "1", "denom": "8"}, ], "upper_bound": [ {"symbol": "quo", "numer": "1", "denom": "16"}, ], }, ] assert deserialise_signal_bounds(after) == before
def test_serialise_linexp(): before = LinExp("foo")/2 + 1 after = serialise_linexp(before) after = json_roundtrip(after) exp = [ {"symbol": "foo", "numer": "1", "denom": "2"}, {"symbol": None, "numer": "1", "denom": "1"}, ] assert after == exp or after == exp[::-1] assert deserialise_linexp(after) == before
def test_find_synthesis_filter_bounds(): coeff_arrays = make_symbol_coeff_arrays(1, 0) expr = (1 * coeff_arrays[0]["LL"][0, 0] + 2 * coeff_arrays[0]["LL"][1, 0] + -4 * coeff_arrays[0]["LL"][2, 0] + 10 * coeff_arrays[1]["HH"][0, 0] + 20 * coeff_arrays[1]["HH"][1, 0] + -40 * coeff_arrays[1]["HH"][2, 0] + 100 * LinExp.new_affine_error_symbol() + 1) lower_bound, upper_bound = synthesis_filter_bounds(expr) assert lower_bound == (3 * LinExp("coeff_0_LL_min") + -4 * LinExp("coeff_0_LL_max") + 30 * LinExp("coeff_1_HH_min") + -40 * LinExp("coeff_1_HH_max") + -100 + 1) assert upper_bound == (-4 * LinExp("coeff_0_LL_min") + 3 * LinExp("coeff_0_LL_max") + -40 * LinExp("coeff_1_HH_min") + 30 * LinExp("coeff_1_HH_max") + +100 + 1)
def test_evaluate_analysis_filter_bounds(): expr = LinExp(("pixel", 0, 0)) + 1000 lower_bound_exp, upper_bound_exp = analysis_filter_bounds(expr) lower_bound, upper_bound = evaluate_analysis_filter_bounds( lower_bound_exp, upper_bound_exp, 10, ) assert isinstance(lower_bound, int) assert isinstance(upper_bound, int) assert lower_bound == 1000 - 512 assert upper_bound == 1000 + 511
def get_maximising_inputs(expression): """ Find the symbol value assignment which maximises the provided expression. Parameters ========== expression : :py:class:`~vc2_bit_widths.linexp.LinExp` The expression whose value is to be maximised. Returns ======= maximising_symbol_assignment : {sym: +1 or -1, ...} A dictionary giving the polarity of a maximising assignment to each non-:py:class:`~vc2_bit_widths.affine_arithmetic.Error` term in the expression. """ return { sym: +1 if coeff > 0 else -1 for sym, coeff in LinExp(expression) if sym is not None and not isinstance(sym, AAError) }
def get(self, keys): # What phase does the requested value lie on phase_offset = tuple( k % p for k, p in zip(keys, self._array.period) ) # How many periods along each axis is the requested value period_number = tuple( k // p for k, p in zip(keys, self._array.period) ) base_value = self._array[phase_offset] replacements = {} for sym in base_value.symbols(): if sym is None or isinstance(sym, AAError): continue prefix = sym[0] component_keys = sym[1:] component_array = self._prefix_to_symbol_array[prefix] new_keys = tuple( (p*s) + o for p, s, o in zip( period_number, self._get_scale_factors(component_array), component_keys, ) ) replacements[(prefix, ) + component_keys] = (prefix, ) + new_keys # Could use LinExp.subs but this direct implementation is substantially # faster. return LinExp({ replacements.get(sym, sym): coeff for sym, coeff in base_value })
def analysis_filter_bounds(expression): """ Find the lower- and upper-bound reachable in a :py:class:`~vc2_bit_widths.linexp.LinExp` describing an analysis filter. The filter expression must consist of only affine error symbols (:py:class:`~vc2_bit_widths.linexp.AAError`) and symbols of the form ``(_, x, y)`` representing pixel values in an input picture. Parameters ========== expression : :py:class:`~vc2_bit_widths.linexp.LinExp` Returns ======= lower_bound : :py:class:`~vc2_bit_widths.linexp.LinExp` upper_bound : :py:class:`~vc2_bit_widths.linexp.LinExp` Algebraic expressions for the lower and upper bounds for the signal level. These expressions are given in terms of the symbols ``LinExp("signal_min")`` and ``LinExp("signal_max")``, which represent the minimum and maximum picture signal levels respectively. """ signal_min = LinExp("signal_min") signal_max = LinExp("signal_max") expression = LinExp(expression) lower_bound = affine_lower_bound( expression.subs({ sym: signal_min if coeff > 0 else signal_max for sym, coeff in expression if sym is not None and not isinstance(sym, AAError) })) upper_bound = affine_upper_bound( expression.subs({ sym: signal_min if coeff < 0 else signal_max for sym, coeff in expression if sym is not None and not isinstance(sym, AAError) })) return (lower_bound, upper_bound)