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)
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)
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))
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__
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)
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