# -*- coding: utf-8 -*- """Ti-2 配方优化求解器集成 单元测试(Issue #79)。 覆盖: - 求解器配置(grid_steps/max_combinations 合法性); - 候选取值生成(bounds 等分/integer 去重/choices 枚举); - 网格求解(全局最优、可行性、target 达成、无可行解降级); - 坐标下降(规模超限降级、收敛); - solve 统一入口(自动选策略、静态校验失败、空问题); - Solution 序列化; - 加载 #78 模板后端到端求解。 """ import os import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import _bootstrap # noqa: E402 挂载 recipe_optim 包 from recipe_optim.problem import ( # noqa: E402 ConstraintKind, ConstraintSpec, DecisionVariable, DomainKind, ObjectiveSpec, ObjectiveTerm, OptimizationProblem, Sense, load_problem, ) from recipe_optim.solver import ( # noqa: E402 SolverConfig, SolverError, Solution, candidate_values, solve, ) CONFIG_PATH = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "recipe_optim.template.yaml", ) def _toy_problem() -> OptimizationProblem: """minimize -(x+y),x∈[0,4] 5 点,y∈[0,4] 5 点 → 网格 25 组合。""" return OptimizationProblem( variables=[ DecisionVariable("x", DomainKind.BOUNDS, bounds=(0.0, 4.0)), DecisionVariable("y", DomainKind.BOUNDS, bounds=(0.0, 4.0)), ], objective=ObjectiveSpec(Sense.MAXIMIZE, terms=[ObjectiveTerm("x", 1.0), ObjectiveTerm("y", 1.0)]), ) class TestSolverConfig(unittest.TestCase): def test_defaults(self): c = SolverConfig() self.assertGreaterEqual(c.grid_steps, 2) self.assertGreaterEqual(c.max_combinations, 1) def test_invalid_grid_steps(self): with self.assertRaises(SolverError): SolverConfig(grid_steps=1) def test_invalid_max_combinations(self): with self.assertRaises(SolverError): SolverConfig(max_combinations=0) class TestCandidateValues(unittest.TestCase): def test_bounds_grid(self): v = DecisionVariable("t", DomainKind.BOUNDS, bounds=(0.0, 4.0)) vals = candidate_values(v, SolverConfig(grid_steps=5)) self.assertEqual(vals[0], 0.0) self.assertEqual(vals[-1], 4.0) self.assertEqual(len(vals), 5) def test_integer_dedupe(self): v = DecisionVariable("n", DomainKind.BOUNDS, bounds=(0.0, 4.0), integer=True) vals = candidate_values(v, SolverConfig(grid_steps=5)) self.assertEqual(vals, [0, 1, 2, 3, 4]) def test_choices_enum(self): v = DecisionVariable("c", DomainKind.CHOICES, choices=["A", "B", "C"]) self.assertEqual(candidate_values(v, SolverConfig()), ["A", "B", "C"]) self.assertEqual(candidate_values(v, SolverConfig(enumerate_choices=False)), ["A"]) class TestSolveGrid(unittest.TestCase): def test_global_optimum_maximize(self): p = _toy_problem() sol = solve(p, SolverConfig(grid_steps=5)) self.assertTrue(sol.feasible) # 最优 x=y=4 → obj=8 self.assertAlmostEqual(sol.objective_value, 8.0) self.assertEqual(sol.assignment["x"], 4.0) self.assertEqual(sol.assignment["y"], 4.0) self.assertEqual(sol.strategy, "grid") def test_minimize(self): p = OptimizationProblem( variables=[DecisionVariable("x", DomainKind.BOUNDS, bounds=(0.0, 4.0))], objective=ObjectiveSpec(Sense.MINIMIZE, terms=[ObjectiveTerm("x", 1.0)]), ) sol = solve(p, SolverConfig(grid_steps=5)) self.assertTrue(sol.feasible) self.assertEqual(sol.assignment["x"], 0.0) self.assertAlmostEqual(sol.objective_value, 0.0) def test_target_met(self): p = OptimizationProblem( variables=[DecisionVariable("x", DomainKind.BOUNDS, bounds=(0.0, 10.0))], objective=ObjectiveSpec(Sense.MAXIMIZE, target_value=8.0, terms=[ObjectiveTerm("x", 1.0)]), ) sol = solve(p, SolverConfig(grid_steps=11)) self.assertTrue(sol.feasible) self.assertTrue(sol.target_met) # x=10 >= 8 def test_target_not_met(self): p = OptimizationProblem( variables=[DecisionVariable("x", DomainKind.BOUNDS, bounds=(0.0, 5.0))], objective=ObjectiveSpec(Sense.MAXIMIZE, target_value=99.0, terms=[ObjectiveTerm("x", 1.0)]), ) sol = solve(p, SolverConfig(grid_steps=6)) self.assertTrue(sol.feasible) self.assertFalse(sol.target_met) def test_no_feasible_solution(self): # box 收紧到与域不交 → 无可行 p = OptimizationProblem( variables=[DecisionVariable("x", DomainKind.BOUNDS, bounds=(0.0, 1.0))], constraints=[ConstraintSpec(ConstraintKind.BOX, variable="x", bounds=(5.0, 6.0))], objective=ObjectiveSpec(Sense.MAXIMIZE, terms=[ObjectiveTerm("x", 1.0)]), ) sol = solve(p, SolverConfig(grid_steps=3)) self.assertFalse(sol.feasible) self.assertIn("无可行解", sol.message) class TestCoordinateDescent(unittest.TestCase): def test_falls_back_when_grid_too_large(self): # 三个变量 × grid_steps=5 = 125;设 max_combinations=10 → 降级 p = OptimizationProblem( variables=[ DecisionVariable(f"v{i}", DomainKind.BOUNDS, bounds=(0.0, 4.0)) for i in range(3) ], objective=ObjectiveSpec(Sense.MAXIMIZE, terms=[ObjectiveTerm(f"v{i}", 1.0) for i in range(3)]), ) sol = solve(p, SolverConfig(grid_steps=5, max_combinations=10)) self.assertEqual(sol.strategy, "coordinate_descent") # 坐标下降应能爬到各维上界附近(贪心可收敛到此线性目标的全局最优) self.assertTrue(sol.feasible) self.assertAlmostEqual(sol.objective_value, 12.0, places=6) class TestSolveEntry(unittest.TestCase): def test_validate_failure_returns_infeasible(self): p = OptimizationProblem( variables=[DecisionVariable("x", DomainKind.BOUNDS, bounds=(0.0, 1.0))], objective=ObjectiveSpec(Sense.MAXIMIZE, terms=[ObjectiveTerm("ghost", 1.0)]), ) sol = solve(p, SolverConfig()) self.assertFalse(sol.feasible) self.assertEqual(sol.strategy, "validate") self.assertIn("校验失败", sol.message) def test_empty_problem_trivially_feasible(self): p = OptimizationProblem() sol = solve(p, SolverConfig()) self.assertTrue(sol.feasible) self.assertEqual(sol.strategy, "empty") def test_solution_to_dict(self): p = _toy_problem() sol = solve(p, SolverConfig(grid_steps=3)) d = sol.to_dict() self.assertIn("assignment", d) self.assertIn("objective_value", d) self.assertIn("evaluated", d) self.assertTrue(d["feasible"]) class TestEndToEndFromTemplate(unittest.TestCase): def test_solve_loaded_problem(self): p = load_problem(CONFIG_PATH) sol = solve(p, SolverConfig(grid_steps=7, max_combinations=200000)) # 模板含 4 变量;7^3 * 3 = 1029 组合 < 上限 → 网格 self.assertEqual(sol.strategy, "grid") self.assertTrue(sol.feasible) # 取值应满足 box 收紧(clf_temp∈[820,900])与 forbidden(非 C@910) self.assertGreaterEqual(sol.assignment["clf_temp"], 820 - 1e-6) self.assertLessEqual(sol.assignment["clf_temp"], 900 + 1e-6) self.assertFalse(sol.assignment["catalyst"] == "C" and sol.assignment.get("clf_temp") == 910) # evaluated>0 self.assertGreater(sol.evaluated, 0) if __name__ == "__main__": unittest.main()