补充真实 MOM 数据验证
This commit is contained in:
parent
9aec353c73
commit
818885951c
|
|
@ -1043,6 +1043,8 @@ def _record_rejected_candidate(
|
||||||
closed_loop: ClosedLoopProblem,
|
closed_loop: ClosedLoopProblem,
|
||||||
report: Any,
|
report: Any,
|
||||||
candidate_result: Mapping[str, Any],
|
candidate_result: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
engine_type: str = "CLOSED_LOOP",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Persist validation diagnostics without leaking candidate WO/VL artifacts."""
|
"""Persist validation diagnostics without leaking candidate WO/VL artifacts."""
|
||||||
|
|
||||||
|
|
@ -1053,12 +1055,21 @@ def _record_rejected_candidate(
|
||||||
version_id = next_id("flexScheduleVersion")
|
version_id = next_id("flexScheduleVersion")
|
||||||
version_no = f"FV{closed_loop.business_date.replace('-', '')}-{len(world['flexScheduleVersions']) + 1:03d}"
|
version_no = f"FV{closed_loop.business_date.replace('-', '')}-{len(world['flexScheduleVersions']) + 1:03d}"
|
||||||
violations = list(report.hardViolations)
|
violations = list(report.hardViolations)
|
||||||
|
normalized_engine = (
|
||||||
|
"OPTIMIZE"
|
||||||
|
if str(engine_type or "CLOSED_LOOP").strip().upper() == "OPTIMIZE"
|
||||||
|
else "CLOSED_LOOP"
|
||||||
|
)
|
||||||
version = {
|
version = {
|
||||||
"id": version_id,
|
"id": version_id,
|
||||||
"versionNo": version_no,
|
"versionNo": version_no,
|
||||||
"versionName": f"闭环排产校验失败 {closed_loop.business_date}",
|
"versionName": f"闭环排产校验失败 {closed_loop.business_date}",
|
||||||
"sortMode": "CLOSED_LOOP",
|
"sortMode": "CLOSED_LOOP",
|
||||||
"engineType": "CLOSED_LOOP",
|
"engineType": normalized_engine,
|
||||||
|
"solverId": candidate_result.get("solverId"),
|
||||||
|
"solverVersion": candidate_result.get("solverVersion"),
|
||||||
|
"algorithmId": candidate_result.get("algorithmId"),
|
||||||
|
"algorithmVersion": candidate_result.get("algorithmVersion"),
|
||||||
"status": "DRAFT",
|
"status": "DRAFT",
|
||||||
"solveStatus": "REJECTED",
|
"solveStatus": "REJECTED",
|
||||||
"planningProblemId": closed_loop.problem_id,
|
"planningProblemId": closed_loop.problem_id,
|
||||||
|
|
@ -1094,7 +1105,11 @@ def _record_rejected_candidate(
|
||||||
return {
|
return {
|
||||||
"versionId": version_id,
|
"versionId": version_id,
|
||||||
"versionNo": version_no,
|
"versionNo": version_no,
|
||||||
"engineType": "CLOSED_LOOP",
|
"engineType": normalized_engine,
|
||||||
|
"solverId": version.get("solverId"),
|
||||||
|
"solverVersion": version.get("solverVersion"),
|
||||||
|
"algorithmId": version.get("algorithmId"),
|
||||||
|
"algorithmVersion": version.get("algorithmVersion"),
|
||||||
"status": "DRAFT",
|
"status": "DRAFT",
|
||||||
"solveStatus": "REJECTED",
|
"solveStatus": "REJECTED",
|
||||||
"orderCount": version["orderCount"],
|
"orderCount": version["orderCount"],
|
||||||
|
|
@ -1226,7 +1241,14 @@ def run_closed_loop_candidate(
|
||||||
solved["projectedFlexOrders"] = projected
|
solved["projectedFlexOrders"] = projected
|
||||||
|
|
||||||
if not report.valid or solution.solveStatus != SolveStatus.FEASIBLE:
|
if not report.valid or solution.solveStatus != SolveStatus.FEASIBLE:
|
||||||
rejected = _record_rejected_candidate(world, next_id, closed_loop, report, solved)
|
rejected = _record_rejected_candidate(
|
||||||
|
world,
|
||||||
|
next_id,
|
||||||
|
closed_loop,
|
||||||
|
report,
|
||||||
|
solved,
|
||||||
|
engine_type=normalized_engine,
|
||||||
|
)
|
||||||
return {**rejected, **base, "projectedFlexOrders": projected}
|
return {**rejected, **base, "projectedFlexOrders": projected}
|
||||||
|
|
||||||
for key in (
|
for key in (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from server.aps_domain.closed_loop_runtime import run_closed_loop_candidate
|
from server.aps_domain.closed_loop_runtime import run_closed_loop_candidate
|
||||||
|
from server.aps_domain.kangni_intake import apply_site_payload_to_world, build_site_payload_from_data_dir
|
||||||
from server.engines import get_engine
|
from server.engines import get_engine
|
||||||
from server.engines.optimize_engine import normalize_dispatch_rule
|
from server.engines.optimize_engine import normalize_dispatch_rule
|
||||||
from server.engines.pool_engine import PoolEngine
|
from server.engines.pool_engine import PoolEngine
|
||||||
|
from server.state.seed import seed_world
|
||||||
|
|
||||||
from tests.golden.test_closed_loop_runtime import BUSINESS_DATE, _ready_world
|
from tests.golden.test_closed_loop_runtime import BUSINESS_DATE, _ready_world
|
||||||
|
|
||||||
|
|
@ -73,3 +81,39 @@ def test_optimize_blocker_keeps_engine_identity_and_zero_artifacts():
|
||||||
assert result["woCount"] == 0
|
assert result["woCount"] == 0
|
||||||
assert world["flexScheduleVersions"][-1]["engineType"] == "OPTIMIZE"
|
assert world["flexScheduleVersions"][-1]["engineType"] == "OPTIMIZE"
|
||||||
assert world["flexWorkOrders"] == []
|
assert world["flexWorkOrders"] == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not os.environ.get("APS_KANGNI_DATA_DIR"),
|
||||||
|
reason="set APS_KANGNI_DATA_DIR to run the external Kangni/MOM workbook test",
|
||||||
|
)
|
||||||
|
def test_real_kangni_workbooks_run_through_optimize_closed_loop_without_source_mutation():
|
||||||
|
data_dir = Path(os.environ["APS_KANGNI_DATA_DIR"])
|
||||||
|
files = sorted(data_dir.glob("*.xlsx"))
|
||||||
|
assert files, f"no .xlsx workbooks found in {data_dir}"
|
||||||
|
before = {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in files}
|
||||||
|
|
||||||
|
payload = build_site_payload_from_data_dir(data_dir, station_count=4)
|
||||||
|
world = seed_world()
|
||||||
|
intake = apply_site_payload_to_world(world, payload, clear_all=True)
|
||||||
|
result = run_closed_loop_candidate(
|
||||||
|
world,
|
||||||
|
_next_id_factory(),
|
||||||
|
business_date=BUSINESS_DATE,
|
||||||
|
engine_type="OPTIMIZE",
|
||||||
|
sort_mode="EDD",
|
||||||
|
strict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
after = {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in files}
|
||||||
|
assert before == after
|
||||||
|
assert intake["orderCount"] == 10
|
||||||
|
assert intake["routingRecordCount"] == 72
|
||||||
|
assert intake["bomCount"] == 823
|
||||||
|
assert result["engineType"] == "OPTIMIZE"
|
||||||
|
assert result["solveStatus"] == "REJECTED"
|
||||||
|
assert result["solverId"] == "optimize-dispatch"
|
||||||
|
assert result["algorithmId"] == "optimize.edd"
|
||||||
|
assert result["vlCount"] == 0
|
||||||
|
assert result["woCount"] == 0
|
||||||
|
assert result["planning"]["summary"]["blockerCounts"]["SUPPLY_SHORTAGE"] > 0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue