class TestSize(object): @given(reals()) def test_infinite_lower_bound_greater_than_everything(self, f): b = Interval(None, 0) assert almostgte(b.size(), f) @given(reals()) def test_infinite_upper_bound_greater_than_everything(self, f): b = Interval(0, None) assert almostgte(b.size(), f) @given(reals()) def test_infinite_bounds_greater_than_everything(self, f): b = Interval(None, None) assert almostgte(b.size(), f) @given(intervals(allow_infinity=False, upper_bound=1e10), reals(allow_infinity=False, min_value=1.0, max_value=1e20)) def test_finite_bounds(self, a, f): b = a * f assert almostlte(a.size(), b.size()) @given(intervals(allow_infinity=False), reals(allow_infinity=False, min_value=0.0, max_value=1.0)) def test_finite_bounds_1(self, a, f): b = a * f assert almostgte(a.size(), b.size())
class TestPowConstantBase(object): def _result_with_base_expo(self, visitor, base, expo, mono_expo, bounds_expo): rule = PowerRule() mono = ComponentMap() mono[base] = M.Constant mono[expo] = mono_expo bounds = ComponentMap() bounds[base] = I(base, base) bounds[expo] = bounds_expo expr = base ** expo assume(isinstance(expr, PowExpression)) matched, result = visitor.visit_expression(expr, mono, bounds) assert matched return result @pytest.mark.parametrize( 'mono_expo,bounds_expo', itertools.product( [M.Nondecreasing, M.Nonincreasing, M.Unknown], [I(None, 0), I(0, None), I(None, None)] ) ) @given( base=reals(max_value=-0.01, allow_infinity=False), expo=expressions(), ) def test_negative_base(self, visitor, base, expo, mono_expo, bounds_expo): mono = self._result_with_base_expo(visitor, base, expo, mono_expo, bounds_expo) assert mono == M.Unknown @pytest.mark.parametrize('mono_expo,bounds_expo,expected', [ (M.Nondecreasing, I(None, 0), M.Nondecreasing), (M.Nondecreasing, I(0, None), M.Unknown), (M.Nonincreasing, I(0, None), M.Nondecreasing), (M.Nonincreasing, I(None, 0), M.Unknown), ]) @given( base=reals(min_value=0.01, max_value=0.999), expo=expressions(), ) def test_base_between_0_and_1(self, visitor, base, expo, mono_expo, bounds_expo, expected): mono = self._result_with_base_expo(visitor, base, expo, mono_expo, bounds_expo) assert mono == expected @pytest.mark.parametrize('mono_expo,bounds_expo,expected', [ (M.Nondecreasing, I(0, None), M.Nondecreasing), (M.Nondecreasing, I(None, 0), M.Unknown), (M.Nonincreasing, I(None, 0), M.Nondecreasing), (M.Nonincreasing, I(0, None), M.Unknown), ]) @given( base=reals(min_value=1.01, allow_infinity=False), expo=expressions(), ) def test_base_gt_1(self, visitor, base, expo, mono_expo, bounds_expo, expected): mono = self._result_with_base_expo(visitor, base, expo, mono_expo, bounds_expo) assert mono == expected
class TestQuadratic: def _rule_result(self, A): n = A.shape[0] var = [PE(ET.Variable) for _ in range(n)] terms = [] for i in range(n): for j in range(i + 1): terms.append(BilinearTerm(var[i], var[j], A[i, j])) expr = PE(ET.Quadratic, terms=terms, children=var) rule = QuadraticRule() return rule.apply(expr, None) @given(coefs=st.lists(reals(min_value=0, allow_infinity=False), min_size=1)) def test_sum_of_squares_is_convex_with_positive_coefficients(self, coefs): assume(any([c > 0 and not np.isclose(c, 0) for c in coefs])) A = np.eye(len(coefs)) * coefs cvx = self._rule_result(A) assert cvx == C.Convex @given(coefs=st.lists(reals(max_value=0, allow_infinity=False), min_size=1)) def test_sum_of_squares_is_concave_with_negative_coefficients(self, coefs): assume(any([c < 0 and not np.isclose(c, 0) for c in coefs])) A = np.eye(len(coefs)) * coefs cvx = self._rule_result(A) assert cvx == C.Concave @given(neg_coefs=st.lists(reals(max_value=0.0, allow_infinity=False), min_size=1), pos_coefs=st.lists(reals(min_value=0.0, allow_infinity=False), min_size=1)) def test_sum_of_squares_is_unknown_otherwise(self, neg_coefs, pos_coefs): assume(any([n < 0 and not np.isclose(n, 0) for n in neg_coefs])) assume(any([p > 0 and not np.isclose(p, 0) for p in pos_coefs])) coefs = neg_coefs + pos_coefs A = np.eye(len(coefs)) * coefs cvx = self._rule_result(A) assert cvx == C.Unknown @given(st.integers(min_value=1, max_value=100)) def test_positive_definite_is_convex(self, n): B = np.random.randn(n, n) A = np.eye(n) * n + 0.5 * (B + B.T) cvx = self._rule_result(A) assert cvx == C.Convex @given(st.integers(min_value=1, max_value=100)) def test_negative_definite_is_concave(self, n): B = np.random.randn(n, n) A = -np.eye(n) * n - 0.5 * (B + B.T) cvx = self._rule_result(A) assert cvx == C.Concave
def intervals(draw, allow_infinity=True, lower_bound=None, upper_bound=None): if lower_bound is None and allow_infinity: lower = draw( st.one_of(st.none(), reals(max_value=upper_bound, allow_infinity=False))) else: lower = draw( reals(min_value=lower_bound, max_value=upper_bound, allow_infinity=False)) if upper_bound is None and allow_infinity: upper = draw( st.one_of(st.none(), reals(min_value=lower, allow_infinity=False))) else: upper = draw( reals( min_value=lower, max_value=upper_bound, allow_infinity=False, )) return Interval(lower, upper)
class TestContains(object): @given(intervals()) def test_bound_contains_itself(self, a): assert a in a @given(intervals(allow_infinity=False)) def test_bound_contains_midpoint(self, a): assume((a.upper_bound - a.lower_bound) != inf) m = a.lower_bound + (a.upper_bound - a.lower_bound) / 2.0 assert m in a @given(reals()) def test_infinity_bound_contains_everything(self, n): a = Interval(None, None) assert n in a
# We test with denominator/denominator linear, constant and variable. class FractionalContext: def __init__(self, bounds=None): if bounds is None: bounds = {} self._b = bounds def bounds(self, expr): return self._b[expr] @pytest.mark.skip('Not update') # (a_1 x) / x is linear (constant) @given(coef=reals(allow_infinity=False)) def test_linear_over_variable(coef): rule = FractionalRule() x = PE(ET.Variable) num = PE(ET.Linear, [x], coefficients=[coef], constant_term=0.0) ctx = FractionalContext({ x: I(0, None), }) result = rule.apply(PE(ET.Division, [num, x]), ctx) assert result == Convexity.Linear @pytest.mark.skip('Not update') # (a_1 x + b_1) / x @given(coef=reals(allow_infinity=False), const=reals(allow_infinity=False)) def test_linear_with_constant_over_variable(coef, const):
class TestPowConstantExponent(object): def _rule_result(self, visitor, base, cvx_base, mono_base, bounds_base, expo): convexity = ComponentMap() convexity[base] = cvx_base convexity[expo] = C.Linear mono = ComponentMap() mono[base] = mono_base mono[expo] = M.Constant bounds = ComponentMap() bounds[base] = bounds_base bounds[expo] = I(expo, expo) expr = PowExpression([base, expo]) matched, result = visitor.visit_expression(expr, convexity, mono, bounds) assert matched return result @pytest.mark.parametrize( 'cvx_base,mono_base,bounds_base', itertools.product( [C.Convex, C.Concave, C.Linear, C.Unknown], [M.Nondecreasing, M.Nonincreasing, M.Unknown], [I(None, 0), I(0, None), I(None, None)], )) @given(base=expressions()) def test_exponent_equals_0(self, visitor, base, cvx_base, mono_base, bounds_base): cvx = self._rule_result(visitor, base, cvx_base, mono_base, bounds_base, 0.0) assert cvx == C.Linear @pytest.mark.parametrize( 'cvx_base,mono_base,bounds_base', itertools.product( [C.Convex, C.Concave, C.Linear, C.Unknown], [M.Nondecreasing, M.Nonincreasing, M.Unknown], [I(None, 0), I(0, None), I(None, None)], )) @given(base=expressions()) def test_exponent_equals_1(self, visitor, base, cvx_base, mono_base, bounds_base): cvx = self._rule_result(visitor, base, cvx_base, mono_base, bounds_base, 1.0) assert cvx == cvx_base @pytest.mark.parametrize('cvx_base,mono_base,bounds_base,expected', [ (C.Linear, M.Nondecreasing, I(None, None), C.Convex), (C.Convex, M.Unknown, I(0, None), C.Convex), (C.Convex, M.Unknown, I(None, 0), C.Unknown), (C.Concave, M.Unknown, I(0, None), C.Unknown), (C.Concave, M.Unknown, I(None, 0), C.Convex), ]) @given( base=expressions(), expo=st.integers(min_value=1), ) def test_positive_even_integer(self, visitor, base, expo, cvx_base, mono_base, bounds_base, expected): cvx = self._rule_result(visitor, base, cvx_base, mono_base, bounds_base, 2 * expo) assert cvx == expected @pytest.mark.parametrize('cvx_base,mono_base,bounds_base,expected', [ (C.Convex, M.Unknown, I(None, 0), C.Convex), (C.Convex, M.Unknown, I(0, None), C.Concave), (C.Concave, M.Unknown, I(0, None), C.Convex), (C.Concave, M.Unknown, I(None, 0), C.Concave), ]) @given( base=expressions(), expo=st.integers(min_value=1), ) def test_negative_even_integer(self, visitor, base, expo, cvx_base, mono_base, bounds_base, expected): cvx = self._rule_result(visitor, base, cvx_base, mono_base, bounds_base, -2 * expo) assert cvx == expected @pytest.mark.parametrize('cvx_base,mono_base,bounds_base,expected', [ (C.Convex, M.Unknown, I(0, None), C.Convex), (C.Convex, M.Unknown, I(None, 0), C.Unknown), (C.Concave, M.Unknown, I(None, 0), C.Concave), (C.Concave, M.Unknown, I(0, None), C.Unknown), ]) @given( base=expressions(), expo=st.integers(min_value=1), ) def test_positive_odd_integer(self, visitor, base, expo, cvx_base, mono_base, bounds_base, expected): cvx = self._rule_result(visitor, base, cvx_base, mono_base, bounds_base, 2 * expo + 1) assert cvx == expected @pytest.mark.parametrize('cvx_base,mono_base,bounds_base,expected', [ (C.Concave, M.Unknown, I(0, None), C.Convex), (C.Concave, M.Unknown, I(None, 0), C.Unknown), (C.Convex, M.Unknown, I(None, 0), C.Concave), (C.Convex, M.Unknown, I(0, None), C.Unknown), ]) @given( base=expressions(), expo=st.integers(min_value=1), ) def test_negative_odd_integer(self, visitor, base, expo, cvx_base, mono_base, bounds_base, expected): cvx = self._rule_result(visitor, base, cvx_base, mono_base, bounds_base, -2 * expo + 1) assert cvx == expected @given( base=expressions(), expo=reals(min_value=1, allow_infinity=False), ) def test_positive_gt_1_non_integer_negative_base(self, visitor, base, expo): expo = expo + 1e-6 assume(expo != int(expo)) cvx = self._rule_result(visitor, base, C.Convex, M.Unknown, I(None, -1), expo) assert cvx == C.Unknown @given( base=expressions(), expo=reals(min_value=1, allow_infinity=False), ) def test_positive_gt_1_non_integer(self, visitor, base, expo): expo = expo + 1e-5 # make it positive assume(expo != int(expo)) cvx = self._rule_result(visitor, base, C.Convex, M.Unknown, I(0, None), expo) assert cvx == C.Convex @pytest.mark.parametrize('cvx,expected', [(C.Convex, C.Concave), (C.Concave, C.Convex)]) @given( base=expressions(), expo=reals(max_value=0, allow_infinity=False), ) def test_positive_lt_0_non_integer(self, visitor, base, expo, cvx, expected): expo = expo - 1e-5 # make it negative assume(not almosteq(expo, int(expo))) cvx = self._rule_result(visitor, base, cvx, M.Unknown, I(0, None), expo) assert cvx == expected @given( base=expressions(), expo=reals(min_value=0, max_value=1, allow_infinity=False), ) def test_positive_0_1_non_integer(self, visitor, base, expo): assume(not almosteq(expo, int(expo))) cvx = self._rule_result(visitor, base, C.Concave, M.Unknown, I(0, None), expo) assert cvx == C.Concave
class TestPowConstantBase: def _rule_result(self, visitor, base, expo, cvx_expo, mono_expo, bounds_expo): convexity = ComponentMap() convexity[expo] = cvx_expo convexity[base] = C.Linear mono = ComponentMap() mono[expo] = mono_expo mono[base] = M.Constant bounds = ComponentMap() bounds[base] = I(base, base) bounds[expo] = bounds_expo expr = PowExpression([base, expo]) matched, result = visitor.visit_expression(expr, convexity, mono, bounds) assert matched return result @pytest.mark.parametrize( 'cvx_expo,mono_expo,bounds_expo', itertools.product( [C.Convex, C.Concave, C.Linear, C.Unknown], [M.Nondecreasing, M.Nonincreasing, M.Unknown], [I(None, 0), I(0, None), I(None, None)], )) @given( base=reals(max_value=-0.01, allow_infinity=False), expo=expressions(), ) def test_negative_base(self, visitor, base, expo, cvx_expo, mono_expo, bounds_expo): cvx = self._rule_result(visitor, base, expo, cvx_expo, mono_expo, bounds_expo) assert cvx == C.Unknown @pytest.mark.parametrize( 'cvx_expo,mono_expo,bounds_expo', itertools.product( [C.Convex, C.Concave, C.Linear, C.Unknown], [M.Nondecreasing, M.Nonincreasing, M.Unknown], [I(None, 0), I(0, None), I(None, None)], )) @given( base=reals(min_value=0.001, max_value=0.999), expo=expressions(), ) def test_base_between_0_and_1(self, visitor, base, expo, cvx_expo, mono_expo, bounds_expo): if cvx_expo == C.Concave or cvx_expo == C.Linear: expected = C.Convex else: expected = C.Unknown cvx = self._rule_result(visitor, base, expo, cvx_expo, mono_expo, bounds_expo) assert cvx == expected @pytest.mark.parametrize( 'cvx_expo,mono_expo,bounds_expo', itertools.product( [C.Convex, C.Concave, C.Linear, C.Unknown], [M.Nondecreasing, M.Nonincreasing, M.Unknown], [I(None, 0), I(0, None), I(None, None)], )) @given( base=reals(min_value=1, allow_infinity=False), expo=expressions(), ) def test_base_gt_1(self, visitor, base, expo, cvx_expo, mono_expo, bounds_expo): if cvx_expo == C.Convex or cvx_expo == C.Linear: expected = C.Convex else: expected = C.Unknown cvx = self._rule_result(visitor, base, expo, cvx_expo, mono_expo, bounds_expo) assert cvx == expected
class TestProduct: def _rule_result(self, visitor, f, g, cvx_f, cvx_g, mono_f, mono_g, bounds_f, bounds_g): convexity = ComponentMap() convexity[f] = cvx_f convexity[g] = cvx_g mono = ComponentMap() mono[f] = mono_f mono[g] = mono_g bounds = ComponentMap() bounds[f] = bounds_f bounds[g] = bounds_g expr = f * g assume(isinstance(expr, ProductExpression)) matched, result = visitor.visit_expression(expr, convexity, mono, bounds) assert matched return result @pytest.mark.parametrize('cvx_f,bounds_g,expected', [ (C.Convex, I(0, None), C.Convex), (C.Convex, I(None, 0), C.Concave), (C.Concave, I(0, None), C.Concave), (C.Concave, I(None, 0), C.Convex), (C.Linear, I(0, None), C.Linear), (C.Linear, I(None, 0), C.Linear), (C.Unknown, I(0, None), C.Unknown), (C.Unknown, I(None, 0), C.Unknown), ]) @given(f=expressions(), g=constants()) def test_product_with_constant(self, visitor, f, g, cvx_f, bounds_g, expected): assume(f.is_expression_type() and not isinstance(f, MonomialTermExpression)) cvx_g = C.Linear # g is constant mono_f = M.Unknown mono_g = M.Constant bounds_f = I(None, None) assert self._rule_result(visitor, f, g, cvx_f, cvx_g, mono_f, mono_g, bounds_f, bounds_g) == expected assert self._rule_result(visitor, g, f, cvx_g, cvx_f, mono_g, mono_f, bounds_g, bounds_f) == expected @pytest.mark.parametrize('cvx_f,bounds_f,expected', [ (C.Linear, I(None, None), C.Convex), (C.Linear, I(0, None), C.Convex), (C.Linear, I(None, 0), C.Convex), (C.Convex, I(None, None), C.Unknown), (C.Convex, I(0, None), C.Convex), (C.Convex, I(None, 0), C.Unknown), (C.Concave, I(None, None), C.Unknown), (C.Concave, I(0, None), C.Unknown), (C.Concave, I(None, 0), C.Convex), ]) @given(f=expressions()) def test_product_with_itself(self, visitor, f, cvx_f, bounds_f, expected): convexity = ComponentMap() convexity[f] = cvx_f bounds = ComponentMap() bounds[f] = bounds_f expr = f * f matched, result = visitor.visit_expression(expr, convexity, None, bounds) assert matched assert result == expected @given(var=variables(), coef=reals()) def test_product_with_itself_with_coeff(self, visitor, var, coef): if coef > 0: expected = C.Convex else: expected = C.Concave rule = ProductRule() g = coef * var assume(isinstance(g, ProductExpression)) matched, result = visitor.visit_expression(ProductExpression([var, g]), None, None, None) assert result == expected matched, result = visitor.visit_expression(ProductExpression([g, var]), None, None, None) assert result == expected @given(var=variables(), vars_with_coef=st.lists(st.tuples( variables(), constants(), ), )) def test_product_linear_by_var(self, visitor, var, vars_with_coef): rule = ProductRule() mono = ComponentMap() lin = sum(v * c for v, c in vars_with_coef) mono[var] = M.Nondecreasing mono[lin] = M.Nondecreasing matched, result = visitor.visit_expression( ProductExpression([var, lin]), None, mono, None) assert result == C.Unknown matched, result = visitor.visit_expression( ProductExpression([lin, var]), None, mono, None) assert result == C.Unknown
assert matched assert not result.is_polynomial() @given(expressions(), expressions(), polynomial_degrees()) def test_power_with_non_polynomial_base(visitor, base, expo, expo_poly): poly = ComponentMap() poly[base] = PolynomialDegree(None) poly[expo] = expo_poly expr = base**expo matched, result = visitor.visit_expression(expr, poly) assert matched assert not result.is_polynomial() @given(variables(), reals(min_value=1.1)) def test_power_constant_power_constant(visitor, v, c): expr = v**c poly = ComponentMap() poly[v] = PolynomialDegree(0) poly[c] = PolynomialDegree(0) matched, result = visitor.visit_expression(expr, poly) assert matched assert result.is_polynomial() and result.degree == 0 @given(variables(), reals(allow_infinity=False)) def test_power_non_constant(visitor, v, c): assume(c != int(c)) expr = v**c poly = ComponentMap()
from hypothesis.strategies import integers, lists from pyomo.core.kernel.component_map import ComponentMap from suspect.fbbt.propagation.rules import * from suspect.fbbt.propagation.visitor import BoundsPropagationVisitor from suspect.interval import Interval from suspect.pyomo.expressions import * from tests.strategies import expressions, reals, intervals @pytest.fixture def visitor(): return BoundsPropagationVisitor() @given(reals(), reals()) def test_variable_bound(visitor, a, b): lb = min(a, b) ub = max(a, b) var = pe.Var(bounds=(lb, ub)) var.construct() matched, result = visitor.visit_expression(var, None) assert matched assert result == Interval(lb, ub) @given(reals()) def test_constant_bound(visitor, c): const = NumericConstant(c) matched, result = visitor.visit_expression(const, None) assert matched
class TestPowConstantExponent(object): def _result_with_base_expo(self, visitor, base, mono_base, bounds_base, expo): mono = ComponentMap() mono[base] = mono_base mono[expo] = M.Constant bounds = ComponentMap() bounds[base] = bounds_base bounds[expo] = I(expo, expo) expr = PowExpression([base, expo]) matched, result = visitor.visit_expression(expr, mono, bounds) assert matched return result @pytest.mark.parametrize( 'mono_base,bounds_base', itertools.product([M.Nonincreasing, M.Nondecreasing], [I(None, 0), I(0, None), I(None, None)]) ) @given(base=expressions()) def test_exponent_equals_1(self, visitor, base, mono_base, bounds_base): mono = self._result_with_base_expo(visitor, base, mono_base, bounds_base, 1.0) assert mono == mono_base @pytest.mark.parametrize( 'mono_base,bounds_base', itertools.product([M.Nonincreasing, M.Nondecreasing], [I(None, 0), I(0, None), I(None, None)]) ) @given(base=expressions()) def test_exponent_equals_0(self, visitor, base, mono_base, bounds_base): mono = self._result_with_base_expo(visitor, base, mono_base, bounds_base, 0.0) assert mono == M.Constant @pytest.mark.parametrize('mono_base,bounds_base,expected', [ (M.Nondecreasing, I(0, None), M.Nondecreasing), (M.Nonincreasing, I(None, 0), M.Nondecreasing), (M.Nondecreasing, I(None, 0), M.Nonincreasing), (M.Nonincreasing, I(0, None), M.Nonincreasing), ]) @given( base=expressions(), expo=st.integers(min_value=1, max_value=1000), ) def test_positive_even_integer(self, visitor, base, expo, mono_base, bounds_base, expected): mono = self._result_with_base_expo(visitor, base, mono_base, bounds_base, 2*expo) assert mono == expected @pytest.mark.parametrize('mono_base,bounds_base,expected', [ (M.Nondecreasing, I(0, None), M.Nonincreasing), (M.Nonincreasing, I(None, 0), M.Nonincreasing), (M.Nondecreasing, I(None, 0), M.Nondecreasing), (M.Nonincreasing, I(0, None), M.Nondecreasing), ]) @given( base=expressions(), expo=st.integers(min_value=1, max_value=1000) ) def test_negative_even_integer(self, visitor, base, expo, mono_base, bounds_base, expected): mono = self._result_with_base_expo(visitor, base, mono_base, bounds_base, -2*expo) assert mono == expected @pytest.mark.parametrize('mono_base,expected', [ (M.Nondecreasing, M.Nondecreasing), (M.Nonincreasing, M.Nonincreasing), ]) @given( base=expressions(), expo=st.integers(min_value=1, max_value=1000) ) def test_positive_odd_integer(self, visitor, base, expo, mono_base, expected): mono = self._result_with_base_expo(visitor, base, mono_base, I(None, None), 2*expo+1) assert mono == expected @pytest.mark.parametrize('mono_base,expected', [ (M.Nondecreasing, M.Nonincreasing), (M.Nonincreasing, M.Nondecreasing), ]) @given( base=expressions(), expo=st.integers(min_value=1, max_value=1000) ) def test_negative_odd_integer(self, visitor, base, expo, mono_base, expected): mono = self._result_with_base_expo( visitor, base, mono_base, I(None, None), -2*expo+1 ) assert mono == expected @pytest.mark.parametrize('mono_base', [M.Nondecreasing, M.Nondecreasing]) @given( base=expressions(), expo=reals(allow_infinity=False, min_value=-1e5, max_value=1e5), ) def test_noninteger_negative_base(self, visitor, base, expo, mono_base): assume(not almosteq(expo, 0)) assume(not almosteq(expo, int(expo))) mono = self._result_with_base_expo( visitor, base, mono_base, I(None, 0), expo ) assert mono == M.Unknown @pytest.mark.parametrize('mono_base,expected', [ (M.Nondecreasing, M.Nondecreasing), (M.Nonincreasing, M.Nonincreasing) ]) @given( base=expressions(), expo=reals(allow_infinity=False, min_value=1e-5, max_value=1e-5) ) def test_positive_noninteger(self, visitor, base, expo, mono_base, expected): mono = self._result_with_base_expo(visitor, base, mono_base, I(0, None), expo) assert mono == expected @pytest.mark.parametrize('mono_base,expected', [ (M.Nonincreasing, M.Nondecreasing), (M.Nondecreasing, M.Nonincreasing) ]) @given( base=expressions(), expo=reals(allow_infinity=False, min_value=-1e5, max_value=-1e-5) ) def test_negative_noninteger(self, visitor, base, expo, mono_base, expected): mono = self._result_with_base_expo(visitor, base, mono_base, I(0, None), expo) assert mono == expected