from mcp_calculator.ops.calculus import analyze_function, calculus, solve_ode from mcp_calculator.ops.context import MathError from tests.support import MathTestCase class CalculusTest(MathTestCase): def test_derivative(self): self.assertEqual(calculus("derivative", "x^3")["result"], "3*x**2") def test_higher_derivative(self): self.assertEqual(calculus("derivative", "x^5", order=3)["result"], "60*x**2") def test_mixed_partial(self): self.assertEqual(calculus("derivative", "x^2*y^3", variable="x, y")["result"], "6*x*y**2") def test_indefinite_integral(self): r = calculus("integral", "x*e^x") self.assertEqual(r["result"], "(x - 1)*exp(x)") self.assertIn("constant", r["note"]) def test_definite_integral(self): self.assertEqual(calculus("integral", "e^(-x^2)", lower="-oo", upper="oo")["result"], "sqrt(pi)") def test_numeric_integral_fallback(self): r = calculus("integral", "sin(sin(x))", lower="0", upper="1") self.assertTrue(r["method"].startswith("numeric")) self.assertTrue(r["numeric"].startswith("0.4306061031")) def test_integral_needs_both_bounds(self): with self.assertRaises(MathError): calculus("integral", "x", lower="0") def test_limit(self): self.assertEqual(calculus("limit", "sin(x)/x", point="0")["result"], "1") self.assertEqual(calculus("limit", "(1 + 1/n)^n", variable="n", point="oo")["result"], "E") def test_one_sided_limit(self): self.assertEqual(calculus("limit", "1/x", point="0", direction="+")["result"], "oo") self.assertEqual(calculus("limit", "1/x", point="0", direction="-")["result"], "-oo") def test_series(self): self.assertEqual(calculus("series", "exp(x)", terms=4)["polynomial"], "x**3/6 + x**2/2 + x + 1") def test_infinite_sum(self): r = calculus("sum", "1/n^2", variable="n", lower="1", upper="oo") self.assertEqual(r["result"], "pi**2/6") self.assertIs(r["converges"], True) def test_finite_sum_and_product(self): self.assertEqual(calculus("sum", "k", variable="k", lower="1", upper="100")["result"], "5050") self.assertEqual(calculus("product", "k", variable="k", lower="1", upper="5")["result"], "120") def test_gradient_hessian_jacobian(self): self.assertEqual(calculus("gradient", "x^2*y", variable="x,y")["result"], ["2*x*y", "x**2"]) self.assertEqual(calculus("hessian", "x^2*y", variable="x,y")["result"], [["2*y", "2*x"], ["2*x", "0"]]) self.assertEqual(calculus("jacobian", "x*y, x + y", variable="x,y")["result"], [["y", "x"], ["1", "1"]]) class AnalyzeFunctionTest(MathTestCase): def test_cubic(self): r = analyze_function("x^3 - 3x") self.assertEqual({z["x"] for z in r["zeros"]}, {"0", "-sqrt(3)", "sqrt(3)"}) self.assertEqual({(e["x"], e["type"]) for e in r["extrema"]}, {("-1", "maximum"), ("1", "minimum")}) self.assertEqual(r["inflection_points"][0]["x"], "0") self.assertTrue(r["symmetry"].startswith("odd")) self.assertEqual(r["limit_at_plus_infinity"], "oo") def test_rational_function(self): r = analyze_function("1/(x^2 - 1)") self.assertEqual(r["vertical_asymptotes"], ["-1", "1"]) self.assertTrue(r["symmetry"].startswith("even")) self.assertEqual(r["limit_at_plus_infinity"], "0") def test_saddle_point(self): self.assertTrue(analyze_function("x^3")["extrema"][0]["type"].startswith("saddle")) def test_rejects_parameters(self): with self.assertRaisesRegex(MathError, "parameter"): analyze_function("a*x^2") class OdeTest(MathTestCase): def test_general_solution(self): r = solve_ode("y'' + y = 0") self.assertEqual(r["solutions"], ["C1*sin(x) + C2*cos(x)"]) self.assertIn("C1", r["note"]) def test_initial_value_problem(self): self.assertEqual(solve_ode("y' = 2y", initial_conditions={"y(0)": 3})["solutions"], ["3*exp(2*x)"]) def test_second_order_ivp(self): self.assertEqual(solve_ode("y'' + y = 0", initial_conditions={"y(0)": 0, "y'(0)": 1})["solutions"], ["sin(x)"]) def test_custom_names(self): r = solve_ode("N' = -k*N", function="N", variable="t", initial_conditions={"N(0)": "N0"}) self.assertEqual(r["solutions"], ["N0*exp(-k*t)"]) def test_requires_derivative(self): with self.assertRaisesRegex(MathError, "no derivative"): solve_ode("y = x") def test_bad_initial_condition_key(self): with self.assertRaisesRegex(MathError, "initial condition"): solve_ode("y' = y", initial_conditions={"z(0)": 1})