Пример #1
0
def test_interdependency_constrained():
    """
    Test a model with interdependent components, and with constraints which
    depend on the Model's output.
    This is done in the MatrixSymbol formalism, using a Tikhonov
    regularization as an example. In this, a matrix inverse has to be
    calculated and is used multiple times. Therefore we split that term of
    into a seperate component, so the inverse only has to be computed once
    per model call.

    See https://arxiv.org/abs/1901.05348 for a more detailed background.
    """
    N = Symbol('N', integer=True)
    M = MatrixSymbol('M', N, N)
    W = MatrixSymbol('W', N, N)
    I = MatrixSymbol('I', N, N)
    y = MatrixSymbol('y', N, 1)
    c = MatrixSymbol('c', N, 1)
    a, = parameters('a')
    z, = variables('z')
    i = Idx('i')

    model_dict = {W: Inverse(I + M / a**2), c: -W * y, z: sqrt(c.T * c)}
    # Sympy currently does not support derivatives of matrix expressions,
    # so we use CallableModel instead of Model.
    model = CallableModel(model_dict)

    # Generate data
    iden = np.eye(2)
    M_mat = np.array([[2, 1], [3, 4]])
    y_vec = np.array([[3], [5]])
    eval_model = model(I=iden, M=M_mat, y=y_vec, a=0.1)
    # Calculate the answers 'manually' so I know it was done properly
    W_manual = np.linalg.inv(iden + M_mat / 0.1**2)
    c_manual = -np.atleast_2d(W_manual.dot(y_vec))
    z_manual = np.atleast_1d(np.sqrt(c_manual.T.dot(c_manual)))

    assert y_vec.shape == (2, 1)
    assert M_mat.shape == (2, 2)
    assert iden.shape == (2, 2)
    assert W_manual.shape == (2, 2)
    assert c_manual.shape == (2, 1)
    assert z_manual.shape == (1, 1)
    assert W_manual == pytest.approx(eval_model.W)
    assert c_manual == pytest.approx(eval_model.c)
    assert z_manual == pytest.approx(eval_model.z)
    fit = Fit(model, z=z_manual, I=iden, M=M_mat, y=y_vec)
    fit_result = fit.execute()

    # See if a == 0.1 was reconstructed properly. Since only a**2 features
    # in the equations, we check for the absolute value. Setting a.min = 0.0
    # is not appreciated by the Minimizer, it seems.
    assert np.abs(fit_result.value(a)) == pytest.approx(0.1)
Пример #2
0
def test_MatrixSymbolModel():
    """
    Test a model which is defined by ModelSymbols, see #194
    """
    N = Symbol('N', integer=True)
    M = MatrixSymbol('M', N, N)
    W = MatrixSymbol('W', N, N)
    I = MatrixSymbol('I', N, N)
    y = MatrixSymbol('y', N, 1)
    c = MatrixSymbol('c', N, 1)
    a, b = parameters('a, b')
    z, x = variables('z, x')

    model_dict = {
        W: Inverse(I + M / a ** 2),
        c: - W * y,
        z: sqrt(c.T * c)
    }
    # TODO: This should be a Model in the future, but sympy is not yet
    # capable of computing Matrix derivatives at the time of writing.
    model = CallableModel(model_dict)

    assert model.params == [a]
    assert model.independent_vars == [I, M, y]
    assert model.dependent_vars == [z]
    assert model.interdependent_vars == [W, c]
    assert model.connectivity_mapping == {W: {I, M, a}, c: {W, y}, z: {c}}
    # Generate data
    iden = np.eye(2)
    M_mat = np.array([[2, 1], [3, 4]])
    y_vec = np.array([3, 5])

    eval_model = model(I=iden, M=M_mat, y=y_vec, a=0.1)
    W_manual = np.linalg.inv(iden + M_mat / 0.1 ** 2)
    c_manual = - W_manual.dot(y_vec)
    z_manual = np.atleast_1d(np.sqrt(c_manual.T.dot(c_manual)))
    assert eval_model.W == pytest.approx(W_manual)
    assert eval_model.c == pytest.approx(c_manual)
    assert eval_model.z == pytest.approx(z_manual)

    # Now try to retrieve the value of `a` from a fit
    a.value = 0.2
    fit = Fit(model, z=z_manual, I=iden, M=M_mat, y=y_vec)
    fit_result = fit.execute()
    eval_model = model(I=iden, M=M_mat, y=y_vec, **fit_result.params)
    assert 0.1 == pytest.approx(np.abs(fit_result.value(a)))
    assert eval_model.W == pytest.approx(W_manual)
    assert eval_model.c == pytest.approx(c_manual)
    assert eval_model.z == pytest.approx(z_manual)
Пример #3
0
def test_CallableNumericalModel():
    x, y, z = variables('x, y, z')
    a, b = parameters('a, b')

    model = CallableModel({y: a * x + b})
    numerical_model = CallableNumericalModel(
        {y: lambda x, a, b: a * x + b}, [x], [a, b]
    )
    assert model.__signature__ == numerical_model.__signature__

    xdata = np.linspace(0, 10)
    ydata = model(x=xdata, a=5.5, b=15.0).y + np.random.normal(0, 1)

    symbolic_answer = np.array(model(x=xdata, a=5.5, b=15.0))
    numerical_answer = np.array(numerical_model(x=xdata, a=5.5, b=15.0))

    assert numerical_answer == pytest.approx(symbolic_answer)

    faulty_model = CallableNumericalModel({y: lambda x, a, b: a * x + b},
                                          [], [a, b])
    assert not model.__signature__ == faulty_model.__signature__
    with pytest.raises(TypeError):
        # This is an incorrect signature, even though the lambda function is
        # correct. Should fail.
        faulty_model(xdata, 5.5, 15.0)

    # Faulty model whose components do not all accept all of the args
    faulty_model = CallableNumericalModel(
        {y: lambda x, a, b: a * x + b, z: lambda x, a: x**a}, [x], [a, b]
    )
    assert model.__signature__ == faulty_model.__signature__

    with pytest.raises(TypeError):
        # Lambda got an unexpected keyword 'b'
        faulty_model(xdata, 5.5, 15.0)

    # Faulty model with a wrongly named argument
    faulty_model = CallableNumericalModel(
        {y: lambda x, a, c=5: a * x + c}, [x], [a, b]
    )
    assert model.__signature__ == faulty_model.__signature__

    with pytest.raises(TypeError):
        # Lambda got an unexpected keyword 'b'
        faulty_model(xdata, 5.5, 15.0)

    # Correct version of the previous model
    numerical_model = CallableNumericalModel(
        {y: lambda x, a, b: a * x + b, z: lambda x, a: x ** a},
        connectivity_mapping={y: {a, b, x}, z: {x, a}}
    )
    # Correct version of the previous model
    mixed_model = CallableNumericalModel(
        {y: lambda x, a, b: a * x + b, z: x ** a}, [x],
        [a, b]
    )

    numberical_answer = np.array(numerical_model(x=xdata, a=5.5, b=15.0))
    mixed_answer = np.array(mixed_model(x=xdata, a=5.5, b=15.0))
    assert numberical_answer == pytest.approx(mixed_answer)

    zdata = mixed_model(x=xdata, a=5.5, b=15.0).z + np.random.normal(0, 1)

    # Check if the fits are the same
    fit = Fit(mixed_model, x=xdata, y=ydata, z=zdata)
    mixed_result = fit.execute()
    fit = Fit(numerical_model, x=xdata, y=ydata, z=zdata)
    numerical_result = fit.execute()
    for param in [a, b]:
        assert mixed_result.value(param) == pytest.approx(numerical_result.value(param))
        if mixed_result.stdev(param) is not None and numerical_result.stdev(param) is not None:
            assert mixed_result.stdev(param) == pytest.approx(numerical_result.stdev(param))
        else:
            assert  mixed_result.stdev(param) is None and numerical_result.stdev(param) is None
    assert mixed_result.r_squared == pytest.approx(numerical_result.r_squared)

    # Test if the constrained syntax is supported
    fit = Fit(numerical_model, x=xdata, y=ydata,
              z=zdata, constraints=[Eq(a, b)])
    constrained_result = fit.execute()
    assert constrained_result.value(a) == pytest.approx(constrained_result.value(b))
Пример #4
0
def test_interdependency():
    a, b = parameters('a, b')
    x, y, z = variables('x, y, z')
    model_dict = {
        y: a**3 * x + b**2,
        z: y**2 + a * b
    }
    callable_model = CallableModel(model_dict)
    assert callable_model.independent_vars == [x]
    assert callable_model.interdependent_vars == [y]
    assert callable_model.dependent_vars == [z]
    assert callable_model.params == [a, b]
    assert callable_model.connectivity_mapping == {y: {a, b, x}, z: {a, b, y}}
    assert callable_model(x=3, a=1, b=2) == pytest.approx(np.atleast_2d([7, 51]).T)
    for var, func in callable_model.vars_as_functions.items():
        # TODO comment on what this does
        str_con_map = set(x.name for x in callable_model.connectivity_mapping[var])
        str_args = set(str(x.__class__) if isinstance(x, Function) else x.name
                       for x in func.args)
        assert str_con_map == str_args

    jac_model = jacobian_from_model(callable_model)
    assert jac_model.params == [a, b]
    assert jac_model.dependent_vars == [D(z, a), D(z, b), z]
    assert jac_model.interdependent_vars == [D(y, a), D(y, b), y]
    assert jac_model.independent_vars == [x]
    for p1, p2 in zip_longest(jac_model.__signature__.parameters, [x, a, b]):
        assert str(p1) == str(p2)
    # The connectivity of jac_model should be that from it's own components
    # plus that of the model. The latter is needed to properly compute the
    # Hessian.
    jac_con_map = {D(y, a): {a, x},
                   D(y, b): {b},
                   D(z, a): {b, y, D(y, a)},
                   D(z, b): {a, y, D(y, b)},
                   y: {a, b, x}, z: {a, b, y}}
    assert jac_model.connectivity_mapping == jac_con_map
    jac_model_dict = {D(y, a): 3 * a**2 * x,
                      D(y, b): 2 * b,
                      D(z, a): b + 2 * y * D(y, a),
                      D(z, b): a + 2 * y * D(y, b),
                      y: callable_model[y], z: callable_model[z]}
    assert jac_model.model_dict == jac_model_dict
    for var, func in jac_model.vars_as_functions.items():
        str_con_map = set(x.name for x in jac_model.connectivity_mapping[var])
        str_args = set(str(x.__class__) if isinstance(x, Function) else x.name
                       for x in func.args)
        assert str_con_map == str_args

    hess_model = hessian_from_model(callable_model)
    # Result according to Mathematica
    hess_as_dict = {
        D(y, (a, 2)): 6 * a * x,
        D(y, a, b): 0,
        D(y, b, a): 0,
        D(y, (b, 2)): 2,
        D(z, (a, 2)): 2 * D(y, a)**2 + 2 * y * D(y, (a, 2)),
        D(z, a, b): 1 + 2 * D(y, b) * D(y, a) + 2 * y * D(y, a, b),
        D(z, b, a): 1 + 2 * D(y, b) * D(y, a) + 2 * y * D(y, a, b),
        D(z, (b, 2)): 2 * D(y, b)**2 + 2 * y * D(y, (b, 2)),
        D(y, a): 3 * a ** 2 * x,
        D(y, b): 2 * b,
        D(z, a): b + 2 * y * D(y, a),
        D(z, b): a + 2 * y * D(y, b),
        y: callable_model[y], z: callable_model[z]
    }
    assert dict(hess_model) == hess_as_dict

    assert hess_model.params == [a, b]
    assert hess_model.dependent_vars == [D(z, (a, 2)), D(z, a, b), D(z, (b, 2)), D(z, b, a), D(z, a), D(z, b), z]
    assert hess_model.interdependent_vars == [D(y, (a, 2)), D(y, a), D(y, b), y]
    assert hess_model.independent_vars == [x]

    model = Model(model_dict)
    assert model(x=3, a=1, b=2) == pytest.approx(np.atleast_2d([7, 51]).T)
    assert model.eval_jacobian(x=3, a=1, b=2) == pytest.approx(np.array([[[9], [4]], [[128], [57]]]))
    assert model.eval_hessian(x=3, a=1, b=2) == pytest.approx(np.array([[[[18], [0]], [[0], [2]]],[[[414], [73]], [[73], [60]]]]))

    assert model.__signature__ == model.jacobian_model.__signature__
    assert model.__signature__ == model.hessian_model.__signature__
Пример #5
0
    def test_constrained_dependent_on_matrixmodel(self):
        """
        Similar to test_constrained_dependent_on_model, but now using
        MatrixSymbols. This is much more powerful, since now the constraint can
        really be written down as a symbolical one as well.
        """
        A, mu, sig = parameters('A, mu, sig')
        M = symbols('M', integer=True)  # Number of measurements

        # Create vectors for all the quantities
        x = MatrixSymbol('x', M, 1)
        dx = MatrixSymbol('dx', M, 1)
        y = MatrixSymbol('y', M, 1)
        I = MatrixSymbol('I', M, 1)  # 'identity' vector
        Y = MatrixSymbol('Y', 1, 1)
        B = MatrixSymbol('B', M, 1)
        i = Idx('i', M)

        # Looks overly complicated, but it's just a simple Gaussian
        model = CallableModel(
            {y: A * sympy.exp(- HadamardProduct(B, B) / (2 * sig**2))
                    /sympy.sqrt(2*sympy.pi*sig**2),
             B: (x - mu * I)}
        )
        self.assertEqual(model.independent_vars, [I, x])
        self.assertEqual(model.dependent_vars, [y])
        self.assertEqual(model.interdependent_vars, [B])
        self.assertEqual(model.params, [A, mu, sig])

        # Generate data, sample from a N(1.2, 2) distribution. Has to be 2D.
        np.random.seed(2)
        xdata = np.random.normal(1.2, 2, size=10000)
        ydata, xedges = np.histogram(xdata, bins=int(np.sqrt(len(xdata))), density=True)
        xcentres = np.atleast_2d((xedges[1:] + xedges[:-1]) / 2).T
        xdiff = np.atleast_2d((xedges[1:] - xedges[:-1])).T
        ydata = np.atleast_2d(ydata).T
        Idata = np.ones_like(xcentres)

        self.assertEqual(xcentres.shape, (int(np.sqrt(len(xdata))), 1))
        self.assertEqual(xdiff.shape, (int(np.sqrt(len(xdata))), 1))
        self.assertEqual(ydata.shape, (int(np.sqrt(len(xdata))), 1))

        fit = Fit(model, x=xcentres, y=ydata, I=Idata)
        unconstr_result = fit.execute()

        constraint = CallableModel({Y: Sum(y[i, 0] * dx[i, 0], i) - 1})
        with self.assertRaises(ModelError):
            fit = Fit(model, x=xcentres, y=ydata, dx=xdiff, M=len(xcentres),
                      I=Idata, constraints=[constraint])

        constraint = CallableModel.as_constraint(
            {Y: Sum(y[i, 0] * dx[i, 0], i) - 1},
            model=model,
            constraint_type=Eq
        )
        self.assertEqual(constraint.independent_vars, [I, M, dx, x])
        self.assertEqual(constraint.dependent_vars, [Y])
        self.assertEqual(constraint.interdependent_vars, [B, y])
        self.assertEqual(constraint.params, [A, mu, sig])
        self.assertEqual(constraint.constraint_type, Eq)

        # Provide the extra data needed for the constraints as well
        fit = Fit(model, x=xcentres, y=ydata, dx=xdiff, M=len(xcentres),
                  I=Idata, constraints=[constraint])

        # After treatment, our constraint should have `y` & `b` dependencies
        self.assertEqual(fit.constraints[0].independent_vars, [I, M, dx, x])
        self.assertEqual(fit.constraints[0].dependent_vars, [Y])
        self.assertEqual(fit.constraints[0].interdependent_vars, [B, y])
        self.assertEqual(fit.constraints[0].params, [A, mu, sig])
        self.assertEqual(fit.constraints[0].constraint_type, Eq)

        self.assertEqual(set(k for k, v in fit.data.items() if v is not None),
                         {x, y, dx, M, I, fit.model.sigmas[y]})
        # These belong to internal variables
        self.assertEqual(set(k for k, v in fit.data.items() if v is None),
                         {B, constraint.sigmas[Y], Y})

        constr_result = fit.execute()
        # The constraint should not be met for the unconstrained fit
        self.assertNotAlmostEqual(
            fit.minimizer.wrapped_constraints[0]['fun'](
                **unconstr_result.params
            )[0], 0, 3
        )
        # And at high precision with constraint
        self.assertAlmostEqual(
            fit.minimizer.wrapped_constraints[0]['fun'](
                **constr_result.params
            )[0], 0, 8
        )

        # Constraining will negatively effect the R^2 value, but...
        self.assertLess(constr_result.r_squared, unconstr_result.r_squared)
        # both should be pretty good
        self.assertGreater(constr_result.r_squared, 0.99)
Пример #6
0
def test_constrained_dependent_on_matrixmodel():
    """
    Similar to test_constrained_dependent_on_model, but now using
    MatrixSymbols. This is much more powerful, since now the constraint can
    really be written down as a symbolical one as well.
    """
    A, mu, sig = parameters('A, mu, sig')
    M = symbols('M', integer=True)  # Number of measurements

    # Create vectors for all the quantities
    x = MatrixSymbol('x', M, 1)
    dx = MatrixSymbol('dx', M, 1)
    y = MatrixSymbol('y', M, 1)
    I = MatrixSymbol('I', M, 1)  # 'identity' vector
    Y = MatrixSymbol('Y', 1, 1)
    B = MatrixSymbol('B', M, 1)
    i = Idx('i', M)

    # Looks overly complicated, but it's just a simple Gaussian
    model = CallableModel({
        y:
        A * sympy.exp(-HadamardProduct(B, B) / (2 * sig**2)) /
        sympy.sqrt(2 * sympy.pi * sig**2),
        B: (x - mu * I)
    })
    assert model.independent_vars == [I, x]
    assert model.dependent_vars == [y]
    assert model.interdependent_vars == [B]
    assert model.params == [A, mu, sig]

    # Generate data, sample from a N(1.2, 2) distribution. Has to be 2D.
    np.random.seed(2)
    # TODO: sample points on a Guassian and add appropriate noise.
    xdata = np.random.normal(1.2, 2, size=10000)
    ydata, xedges = np.histogram(xdata,
                                 bins=int(np.sqrt(len(xdata))),
                                 density=True)
    xcentres = np.atleast_2d((xedges[1:] + xedges[:-1]) / 2).T
    xdiff = np.atleast_2d((xedges[1:] - xedges[:-1])).T
    ydata = np.atleast_2d(ydata).T
    Idata = np.ones_like(xcentres)

    assert xcentres.shape == (int(np.sqrt(len(xdata))), 1)
    assert xdiff.shape == (int(np.sqrt(len(xdata))), 1)
    assert ydata.shape == (int(np.sqrt(len(xdata))), 1)

    fit = Fit(model, x=xcentres, y=ydata, I=Idata)
    unconstr_result = fit.execute()

    constraint = CallableModel({Y: Sum(y[i, 0] * dx[i, 0], i) - 1})

    with pytest.raises(ModelError):
        fit = Fit(model,
                  x=xcentres,
                  y=ydata,
                  dx=xdiff,
                  M=len(xcentres),
                  I=Idata,
                  constraints=[constraint])

    constraint = CallableModel.as_constraint(
        {Y: Sum(y[i, 0] * dx[i, 0], i) - 1}, model=model, constraint_type=Eq)
    assert constraint.independent_vars == [I, M, dx, x]
    assert constraint.dependent_vars == [Y]
    assert constraint.interdependent_vars == [B, y]
    assert constraint.params == [A, mu, sig]
    assert constraint.constraint_type == Eq

    # Provide the extra data needed for the constraints as well
    fit = Fit(model,
              x=xcentres,
              y=ydata,
              dx=xdiff,
              M=len(xcentres),
              I=Idata,
              constraints=[constraint])

    # After treatment, our constraint should have `y` & `b` dependencies
    assert fit.constraints[0].independent_vars == [I, M, dx, x]
    assert fit.constraints[0].dependent_vars == [Y]
    assert fit.constraints[0].interdependent_vars == [B, y]
    assert fit.constraints[0].params == [A, mu, sig]
    assert fit.constraints[0].constraint_type == Eq
    assert isinstance(fit.objective, LeastSquares)
    assert isinstance(fit.minimizer.constraints[0], MinimizeModel)

    assert {k
            for k, v in fit.data.items()
            if v is not None} == {x, y, dx, M, I, fit.model.sigmas[y]}
    # These belong to internal variables
    assert {k
            for k, v in fit.data.items()
            if v is None} == {constraint.sigmas[Y], Y}

    constr_result = fit.execute()
    # The constraint should not be met for the unconstrained fit
    assert not fit.minimizer.wrapped_constraints[0]['fun'](
        **unconstr_result.params)[0] == pytest.approx(0, 1e-3)
    # And at high precision with constraint
    # TODO Change after resolve bug at pytest
    assert fit.minimizer.wrapped_constraints[0]['fun'](
        **constr_result.params)[0] == pytest.approx(0, abs=1e-8)

    # Constraining will negatively effect the R^2 value, but...
    assert constr_result.r_squared < unconstr_result.r_squared
    # both should be pretty good
    assert constr_result.r_squared > 0.99