118 lines
4.8 KiB
Python
118 lines
4.8 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 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
|