Files
iAOP/templates/ti-cl4/recipe-optim/tests/test_solver.py
T

208 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()