feat: account for defects and five-unit batches in delivery forecasts

This commit is contained in:
ar51
2026-10-09 11:20:49 +08:00
parent 512e434a2f
commit de6656dfe0
5 changed files with 137 additions and 9 deletions

View File

@@ -83,6 +83,8 @@ ALLOWED_VERSIONS = {
"170H": {"V3"},
}
CANDIDATE_BATCH_SIZE = 4
DEFECT_RATE_PERCENT = 5
PRODUCTION_BATCH_SIZE = 5
CANDIDATE_CONTINUATION_TTL_SECONDS = 6 * 60 * 60
RECEIVED_REACTION_TYPE = "OnIt"
@@ -180,6 +182,15 @@ class CandidateUnavailableError(RuntimeError):
pass
def production_quantity_for_demand(quantity: int) -> int:
"""Cover 5% defects, then round up to a batch of five without float error."""
if isinstance(quantity, bool) or not isinstance(quantity, int) or quantity <= 0:
raise InputError("需求数量必须为正整数。")
numerator = quantity * 100
denominator = (100 - DEFECT_RATE_PERCENT) * PRODUCTION_BATCH_SIZE
return ((numerator + denominator - 1) // denominator) * PRODUCTION_BATCH_SIZE
_candidate_continuations: dict[str, CandidateContinuation] = {}
_candidate_continuation_lock = threading.Lock()
@@ -819,11 +830,12 @@ def overall_judgment_lines(candidates: list[dict[str, Any]]) -> list[str]:
]
def candidate_report_lines(candidate: dict[str, Any], quantity: int) -> list[str]:
def candidate_report_lines(candidate: dict[str, Any], quantity: int, production_quantity: int) -> list[str]:
details = candidate["details"]
return [
f"{candidate['index']}. {candidate['model']}({candidate['type']})",
f"- 数量:{quantity}台",
f"- 需求数量:{quantity}台",
f"- 排产测算数量:{production_quantity}台",
f"- {details['prediction']}",
f"- 可用库存/覆盖情况:{details['inventory']}",
f"- 主导因素:{details['main_factor']}",
@@ -894,6 +906,9 @@ def render_record(
"",
f"**输入型号**:{request.model}",
f"**输入数量**:{request.quantity}台",
f"**排产测算数量(API查询数量)**:{production_quantity_for_demand(request.quantity)}台",
"**数量计算规则**:原始需求÷95%良率,再按5台的倍数向上取整。",
"**库存覆盖口径**:按排产测算数量判断。",
f"**生成时间**:{generation_time:%Y-%m-%d %H:%M:%S %z}",
f"**规则源实际路径**:`{RULE_SOURCE_DISPLAY}`",
f"**接口依据**:`{API_SOURCE_DISPLAY}`",
@@ -969,6 +984,7 @@ def write_record(
def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
if candidate_offset < 0:
raise InputError("候选批次位置无效,请重新发起型号和数量查询。")
production_quantity = production_quantity_for_demand(request.quantity)
all_candidate_specs = generate_candidate_specs(request.model)
candidate_specs = all_candidate_specs[
@@ -982,12 +998,12 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
baseline: dict[str, Any] | None = None
if is_initial_batch:
try:
baseline_payload = run_controlled_query(request)
baseline_payload = run_controlled_query(RequestInput(request.model, production_quantity))
except CandidateUnavailableError as exc:
raise RuntimeError(f"原型号未生成候选结果。\n原因:{exc}") from exc
baseline = lead_time_details(
baseline_payload,
request.quantity,
production_quantity,
)
if not baseline["has_lead_time"]:
raise RuntimeError(
@@ -1012,12 +1028,12 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
def query_candidate(spec: dict[str, str]) -> tuple[dict[str, str], dict[str, Any] | None]:
try:
payload = run_controlled_query(RequestInput(spec["model"], request.quantity))
payload = run_controlled_query(RequestInput(spec["model"], production_quantity))
except CandidateUnavailableError as exc:
return {**spec, "unavailable_reason": str(exc)}, None
details = lead_time_details(
payload,
request.quantity,
production_quantity,
)
if not details["has_lead_time"]:
return {**spec, "unavailable_reason": details["unavailable_reason"]}, None
@@ -1088,6 +1104,8 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
),
"",
f"输入需求:{request.model},{request.quantity}台",
f"排产测算数量:{production_quantity}台(不良率{DEFECT_RATE_PERCENT}%,按{PRODUCTION_BATCH_SIZE}台的倍数向上取整)",
"库存覆盖口径:按排产测算数量判断",
f"数据时间戳:{data_timestamp}",
"状态:试运行版输出 / 内部候选",
"判断等级:AM516算法预测参考,需人工复核",
@@ -1098,7 +1116,7 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
"",
]
for candidate_item in candidates:
report_lines.extend(candidate_report_lines(candidate_item, request.quantity))
report_lines.extend(candidate_report_lines(candidate_item, request.quantity, production_quantity))
report_lines.append("不可用候选:")
if unavailable:
for item in unavailable: