# ============================================================ # MD-06 换型矩阵黄金测试 # ============================================================ from __future__ import annotations from server.aps_domain.changeover import ( build_changeover_view, extra_setup_minutes, lookup_setup_minutes, product_family, ) from server.engines.base import EngineParams from server.engines.rule_engine import RuleEngine from server.state.seed import seed_world def test_seed_families_and_matrix(): world = seed_world() a = next(m for m in world["materials"] if m["code"] == "CTRL-A") c = next(m for m in world["materials"] if m["code"] == "CTRL-C") assert product_family(a) == "CTRL-STD" assert product_family(c) == "CTRL-HF" assert lookup_setup_minutes(world, "CTRL-STD", "CTRL-HF") == 45 assert lookup_setup_minutes(world, "CTRL-STD", "CTRL-STD") == 0 assert extra_setup_minutes(world, None, "CTRL-HF") == 0 view = build_changeover_view(world) assert len(view["rows"]) >= 2 assert any(f["code"] == "CTRL-STD" for f in view["families"]) def test_missing_pair_uses_default_cross(): world = seed_world() world["changeoverMatrix"] = [] assert lookup_setup_minutes(world, "CTRL-STD", "CTRL-HF") == 30 def test_rule_engine_applies_changeover_on_family_switch(): world = seed_world() # 强制 STD 与 HF 只能进同一条线,保证出现跨族换型 world["lineProducts"] = [ {"id": 1, "lineId": 1, "productId": 1, "standardCapacity": 1000, "priority": 1, "setupTime": 30}, {"id": 2, "lineId": 1, "productId": 2, "standardCapacity": 800, "priority": 1, "setupTime": 30}, {"id": 3, "lineId": 1, "productId": 3, "standardCapacity": 600, "priority": 1, "setupTime": 30}, ] engine = RuleEngine("RULE") counter = {"n": 1000} def next_id(_k: str) -> int: counter["n"] += 1 return counter["n"] params = EngineParams( strategyTemplate="FIFO", name="changeover-test", constraints={"materialKit": False, "changeover": True, "capacity": True, "dueDate": True, "equipment": True, "personnel": True, "tooling": True, "freeze": True}, ) result = engine.solve(world, params, next_id) assert result.versionId version = next(v for v in world["scheduleVersions"] if v["id"] == result.versionId) assert version.get("totalChangeoverMin", 0) > 0 pos = [p for p in world["productionOrders"] if p.get("schedulingVersionId") == result.versionId] assert any((p.get("changeoverMin") or 0) > 0 for p in pos) def test_changeover_constraint_off_skips_matrix(): world = seed_world() engine = RuleEngine("RULE") counter = {"n": 2000} def next_id(_k: str) -> int: counter["n"] += 1 return counter["n"] params = EngineParams( strategyTemplate="FIFO", name="no-changeover", constraints={"materialKit": False, "changeover": False, "capacity": True, "dueDate": True, "equipment": True, "personnel": True, "tooling": True, "freeze": True}, ) result = engine.solve(world, params, next_id) version = next(v for v in world["scheduleVersions"] if v["id"] == result.versionId) assert version.get("totalChangeoverMin", 0) == 0 def test_changeover_min_beats_fifo_on_shared_line(): """SC-07:同线多族时,CHANGEOVER_MIN 累计换型不应高于 FIFO。""" from server.engines.base import EngineParams from server.engines.rule_engine import RuleEngine def _run(strategy: str) -> float: world = seed_world() world["lineProducts"] = [ {"id": 1, "lineId": 1, "productId": 1, "standardCapacity": 1000, "priority": 1, "setupTime": 30}, {"id": 2, "lineId": 1, "productId": 2, "standardCapacity": 800, "priority": 1, "setupTime": 30}, {"id": 3, "lineId": 1, "productId": 3, "standardCapacity": 600, "priority": 1, "setupTime": 30}, ] counter = {"n": 3000} def next_id(_k: str) -> int: counter["n"] += 1 return counter["n"] params = EngineParams( strategyTemplate=strategy, name=f"sc07-{strategy}", constraints={"materialKit": False, "changeover": True, "capacity": True, "dueDate": True, "equipment": True, "personnel": True, "tooling": True, "freeze": True}, ) result = RuleEngine("RULE").solve(world, params, next_id) ver = next(v for v in world["scheduleVersions"] if v["id"] == result.versionId) return float(ver.get("totalChangeoverMin") or 0) fifo_co = _run("FIFO") min_co = _run("CHANGEOVER_MIN") assert min_co <= fifo_co # 同线多族场景下 FIFO 通常会跨族;最小化应显著压低(允许相等) assert min_co < fifo_co or fifo_co == 0