aps-agent/server/shipyard_synthetic/engineering.py

477 lines
19 KiB
Python

from __future__ import annotations
from datetime import timedelta
from typing import Any
from .config import GeneratorConfig
from .models import DatasetBundle, stable_id
from .registry import TABLE_SPECS
_ENGINEERING_TABLES = frozenset(
{
"drawings",
"ebom",
"pbom",
"mbom",
"routings",
"routing-operations",
"engineering-releases",
}
)
_ROUTING_TEMPLATES = (
(
"STEEL_PREP",
"钢材预处理",
(
("钢材预处理", "SHOT_BLASTER", 3, False, False, False),
("钢板矫平", "PLATE_BENDER", 1, False, False, False),
("喷码标识", "WORKSTATION", 1, False, False, False),
("数控套料", "WORKSTATION", 1, False, False, False),
("数控切割", "CNC_CUTTER", 3, False, False, False),
("坡口加工", "CNC_CUTTER", 2, False, False, False),
("尺寸检查", "NDT_EQUIPMENT", 14, False, True, False),
("转运入库", "TRANSPORTER", 12, False, False, True),
),
),
(
"PART_FAB",
"零件制造",
(
("零件下料", "CNC_CUTTER", 3, False, False, False),
("零件成形", "PLATE_BENDER", 1, False, False, False),
("小组立", "JIG", 1, False, False, False),
("焊接", "WELDER", 2, False, False, False),
("矫正", "WORKSTATION", 1, False, False, False),
("打磨", "WORKSTATION", 1, False, False, False),
("检验", "NDT_EQUIPMENT", 13, False, True, False),
("配送", "TRANSPORTER", 12, False, False, True),
),
),
(
"SECTION_ASSEMBLY",
"分段制造",
(
("胎架准备", "JIG", 1, False, False, False),
("拼板", "PLATFORM", 1, False, False, False),
("自动焊", "WELDER", 3, False, False, False),
("构架安装", "JIG", 1, False, False, False),
("分段总组", "PLATFORM", 1, False, False, False),
("精度测量", "WORKSTATION", 14, False, True, False),
("分段涂装", "PAINT_BOOTH", 9, False, False, True),
("完工检验", "NDT_EQUIPMENT", 13, False, True, False),
),
),
(
"GRAND_BLOCK",
"总段合拢",
(
("场地准备", "BLOCK_YARD", 1, False, False, False),
("总段定位", "GANTRY_CRANE", 10, False, False, True),
("大接缝焊接", "WELDER", 4, False, False, False),
("精度测量", "WORKSTATION", 14, False, True, False),
("管路连接", "WORKSTATION", 5, False, False, False),
("电缆连接", "WORKSTATION", 7, False, False, False),
("密性试验", "PRESSURE_TEST", 15, False, True, False),
("总段检验", "NDT_EQUIPMENT", 13, False, True, False),
),
),
(
"DOCK_ERECTION",
"船坞搭载",
(
("总段运输", "TRANSPORTER", 12, False, False, True),
("吊装准备", "DOCK", 10, False, False, True),
("龙门吊占用", "GANTRY_CRANE", 11, False, False, True),
("总段搭载", "DOCK", 1, False, False, True),
("定位", "WORKSTATION", 14, False, False, False),
("焊接", "WELDER", 4, False, False, False),
("精度检验", "NDT_EQUIPMENT", 13, False, True, False),
("舾装恢复", "WORKSTATION", 1, False, False, False),
),
),
(
"PIPE",
"管系",
(
("管子下料", "WORKSTATION", 5, False, False, False),
("弯管", "PIPE_BENDER", 5, False, False, False),
("装配", "WORKSTATION", 5, False, False, False),
("焊接", "WELDER", 6, False, False, False),
("酸洗镀锌", "WORKSTATION", 6, True, False, False),
("压力试验", "PRESSURE_TEST", 16, False, True, False),
("托盘集配", "WORKSTATION", 5, False, False, False),
("船上安装", "WORKSTATION", 5, False, False, False),
),
),
(
"ELECTRICAL",
"电气",
(
("电缆托架安装", "WORKSTATION", 7, False, False, False),
("电缆敷设", "WORKSTATION", 7, False, False, False),
("设备安装", "WORKSTATION", 7, False, False, False),
("接线", "WORKSTATION", 7, False, False, False),
("绝缘测试", "COMMISSION_EQUIPMENT", 8, False, True, False),
("通电测试", "SHORE_POWER", 8, False, True, False),
("系统联调", "COMMISSION_EQUIPMENT", 18, False, False, False),
("完工检验", "NDT_EQUIPMENT", 13, False, True, False),
),
),
(
"PAINT",
"涂装",
(
("表面处理", "SHOT_BLASTER", 9, False, False, True),
("清洁", "PAINT_BOOTH", 9, False, False, True),
("底漆", "PAINT_BOOTH", 9, False, False, True),
("中间漆", "PAINT_BOOTH", 9, False, False, True),
("面漆", "PAINT_BOOTH", 9, False, False, True),
("固化", "PAINT_BOOTH", 9, False, False, True),
("膜厚检查", "NDT_EQUIPMENT", 13, False, True, False),
("缺陷修补", "PAINT_BOOTH", 9, False, False, True),
),
),
(
"OUTFITTING",
"舾装",
(
("托盘准备", "WORKSTATION", 1, False, False, False),
("设备定位", "WORKSTATION", 1, False, False, False),
("设备安装", "WORKSTATION", 1, False, False, False),
("管路接驳", "WORKSTATION", 5, False, False, False),
("电缆接驳", "WORKSTATION", 7, False, False, False),
("内装封板", "WORKSTATION", 1, False, False, False),
("功能检查", "COMMISSION_EQUIPMENT", 18, False, True, False),
("区域移交", "WORKSTATION", 14, False, True, False),
),
),
(
"COMMISSION",
"调试与试航",
(
("单机调试", "COMMISSION_EQUIPMENT", 17, False, False, False),
("系统调试", "COMMISSION_EQUIPMENT", 18, False, False, False),
("系泊试验", "SHORE_POWER", 18, False, True, False),
("倾斜试验", "BERTH", 19, False, True, True),
("航行试验", "BERTH", 19, False, False, True),
("性能测试", "COMMISSION_EQUIPMENT", 19, False, True, True),
("故障整改", "WORKSTATION", 18, False, False, False),
("船东船级社验收", "BERTH", 19, False, True, True),
),
),
)
def _set_owned_rows(
bundle: DatasetBundle,
table_name: str,
rows: list[dict[str, Any]],
*,
order_by: tuple[str, ...],
) -> None:
if table_name not in _ENGINEERING_TABLES or table_name not in TABLE_SPECS:
raise ValueError(f"table is outside W69-ENGINEERING ownership: {table_name}")
spec = TABLE_SPECS[table_name]
for index, row in enumerate(rows):
for field in spec.required:
if field not in row:
raise ValueError(
f"{table_name}[{index}] missing required field {field}"
)
primary_keys = [row[spec.primary_key] for row in rows]
if len(primary_keys) != len(set(primary_keys)):
raise ValueError(f"duplicate primary key in {table_name}")
bundle.set_rows(
table_name,
sorted(rows, key=lambda row: tuple(str(row.get(key, "")) for key in order_by)),
)
def _material_identity(index: int) -> tuple[str, str]:
material_code = f"MAT-SYN-{index:05d}"
return material_code, stable_id("material", material_code, prefix="MAT")
def _routing_identity(index: int) -> tuple[str, str]:
routing_code = f"RT-SYN-{index:04d}"
return routing_code, stable_id("routing", routing_code, prefix="RTG")
def _resource_group_id(resource_type: str) -> str:
return stable_id("resource-group", f"RG-SYN-{resource_type}", prefix="RGP")
def _project_rows(bundle: DatasetBundle) -> list[dict[str, Any]]:
rows = sorted(bundle.rows("ship-projects"), key=lambda row: str(row["projectCode"]))
if not rows:
raise ValueError("generate_engineering requires generate_projects to run first")
return rows
def generate_engineering(
bundle: DatasetBundle, config: GeneratorConfig
) -> DatasetBundle:
"""Populate deterministic drawings, three BOM views and routing templates."""
projects = _project_rows(bundle)
work_packages = sorted(
bundle.rows("work-packages"), key=lambda row: str(row["workPackageCode"])
)
if not work_packages:
raise ValueError("generate_engineering requires work-packages")
base_date = config.planning_base_date
releases: list[dict[str, Any]] = []
for project in projects:
for sequence, (release_type, offset) in enumerate(
(("BASIC_DESIGN", -75), ("DETAIL_DESIGN", -45), ("PRODUCTION_DESIGN", -20)),
start=1,
):
release_code = f"{project['projectCode']}-REL-{sequence:02d}"
releases.append(
{
"releaseId": stable_id(
"engineering-release", release_code, prefix="REL"
),
"releaseCode": release_code,
"projectId": project["projectId"],
"releaseType": release_type,
"status": "RELEASED",
"effectiveDate": (
date_from_iso(str(project["plannedStart"]))
+ timedelta(days=offset)
).isoformat(),
"revision": "A",
"sourceType": "SYNTHETIC",
}
)
drawings: list[dict[str, Any]] = []
drawing_types = ("STRUCTURE", "OUTFITTING", "PIPE", "ELECTRICAL", "HVAC")
for index, package in enumerate(work_packages, start=1):
drawing_no = f"DWG-SYN-{index:05d}"
drawings.append(
{
"drawingId": stable_id("drawing", drawing_no, prefix="DWG"),
"drawingNo": drawing_no,
"projectId": package["projectId"],
"workPackageId": package["workPackageId"],
"drawingType": drawing_types[(index - 1) % len(drawing_types)],
"releaseStatus": "RELEASED",
"revision": chr(65 + (index % 3)),
"effectiveFrom": (
base_date - timedelta(days=45) + timedelta(days=index % 60)
).isoformat(),
"status": "ACTIVE",
"sourceType": "SYNTHETIC",
}
)
routings: list[dict[str, Any]] = []
routing_operations: list[dict[str, Any]] = []
for routing_index in range(1, config.profile.routing_count + 1):
routing_code, routing_id = _routing_identity(routing_index)
product_group, product_group_name, operations = _ROUTING_TEMPLATES[
(routing_index - 1) % len(_ROUTING_TEMPLATES)
]
material_code, material_id = _material_identity(routing_index)
routings.append(
{
"routingId": routing_id,
"routingCode": routing_code,
"name": f"{product_group_name}工艺路线-SYN-{routing_index:04d}",
"productGroup": product_group,
"outputMaterialId": material_id,
"outputMaterialCode": material_code,
"version": "1.0",
"validFrom": base_date.isoformat(),
"validTo": config.planning_horizon_end.isoformat(),
"status": "ACTIVE",
"sourceType": "SYNTHETIC_TEMPLATE",
}
)
predecessor_id: str | None = None
for operation_index, (
operation_name,
resource_type,
skill_index,
outsourcing_allowed,
hold_point,
weather_sensitive,
) in enumerate(operations, start=1):
sequence = operation_index * 10
operation_code = f"{routing_code}-OP-{sequence:03d}"
operation_id = stable_id("routing-operation", operation_code, prefix="ROP")
standard_hours = round(
4.0 + ((routing_index * 7 + operation_index * 3) % 28) * 0.75,
2,
)
routing_operations.append(
{
"routingOperationId": operation_id,
"operationId": operation_id,
"operationCode": operation_code,
"operationName": f"{operation_name}-SYN",
"routingId": routing_id,
"sequence": sequence,
"sequenceNo": sequence,
"predecessorOperationIds": [predecessor_id]
if predecessor_id
else [],
"relationType": "FS",
"lagHours": 0.0,
"durationType": "STANDARD_HOURS",
"standardHours": standard_hours,
"setupHours": round(0.5 + operation_index % 3 * 0.25, 2),
"teardownHours": 0.5,
"crewSize": 4 + (operation_index + routing_index) % 9,
"resourceType": resource_type,
"requiredSkillCodes": [f"SK-SYN-{skill_index:03d}"],
"primaryResourceGroup": _resource_group_id(resource_type),
"alternativeResourceGroups": [],
"toolRequirement": f"TOOL-SYN-{resource_type}",
"materialRequirementIds": [],
"drawingRequirement": True,
"inspectionRequirement": hold_point,
"holdPoint": hold_point,
"outsourcingAllowed": outsourcing_allowed,
"splitAllowed": operation_index in {3, 4, 6},
"overlapAllowed": operation_index in {2, 3},
"minimumTransferBatch": 1,
"maximumParallelism": 2 if operation_index in {3, 4} else 1,
"weatherSensitive": weather_sensitive,
"temperatureRange": [5, 35] if weather_sensitive else None,
"humidityLimit": 85 if weather_sensitive else None,
"status": "ACTIVE",
}
)
predecessor_id = operation_id
total_bom = config.profile.bom_relation_count
ebom_count = total_bom * 2 // 5
pbom_count = (total_bom - ebom_count) // 2
mbom_count = total_bom - ebom_count - pbom_count
material_count = config.profile.material_count
ebom: list[dict[str, Any]] = []
for index in range(1, ebom_count + 1):
project = projects[(index - 1) % len(projects)]
material_index = ((index * 37 + 11) % material_count) + 1
parent_index = ((material_index + 97 + index % 31 - 1) % material_count) + 1
if parent_index == material_index:
parent_index = parent_index % material_count + 1
material_code, material_id = _material_identity(material_index)
parent_code, parent_id = _material_identity(parent_index)
line_code = f"EBOM-SYN-{index:06d}"
ebom.append(
{
"bomLineId": stable_id("ebom-line", line_code, prefix="EBL"),
"bomLineCode": line_code,
"projectId": project["projectId"],
"bomType": "EBOM",
"parentMaterialId": parent_id,
"parentMaterialCode": parent_code,
"materialId": material_id,
"materialCode": material_code,
"quantity": round(1.0 + (index % 19) * 0.25, 3),
"unit": "PCS",
"bomLevel": 1 + index % 5,
"scrapRate": round((index % 5) * 0.005, 3),
"revision": "A",
"effectiveFrom": base_date.isoformat(),
"status": "ACTIVE",
"sourceType": "SYNTHETIC",
}
)
pbom: list[dict[str, Any]] = []
for index in range(1, pbom_count + 1):
package = work_packages[(index - 1) % len(work_packages)]
material_index = ((index * 43 + 17) % material_count) + 1
material_code, material_id = _material_identity(material_index)
line_code = f"PBOM-SYN-{index:06d}"
pbom.append(
{
"bomLineId": stable_id("pbom-line", line_code, prefix="PBL"),
"bomLineCode": line_code,
"projectId": package["projectId"],
"workPackageId": package["workPackageId"],
"materialId": material_id,
"materialCode": material_code,
"quantity": round(1.0 + (index % 23) * 0.2, 3),
"unit": "PCS",
"bomType": "PBOM",
"allocationMode": "PALLET" if index % 2 else "AREA",
"scrapRate": round((index % 4) * 0.006, 3),
"revision": "A",
"effectiveFrom": base_date.isoformat(),
"status": "ACTIVE",
"sourceType": "SYNTHETIC",
}
)
mbom: list[dict[str, Any]] = []
for index in range(1, mbom_count + 1):
production_order_index = (index - 1) % config.profile.production_order_count + 1
production_order_code = f"PO-SYN-{production_order_index:05d}"
project = projects[(production_order_index - 1) % len(projects)]
material_index = ((index * 47 + 23) % material_count) + 1
material_code, material_id = _material_identity(material_index)
routing_index = (production_order_index - 1) % config.profile.routing_count + 1
routing_code, routing_id = _routing_identity(routing_index)
output_code, output_id = _material_identity(routing_index)
line_code = f"MBOM-SYN-{index:06d}"
mbom.append(
{
"bomLineId": stable_id("mbom-line", line_code, prefix="MBL"),
"bomLineCode": line_code,
"projectId": project["projectId"],
"productionOrderId": stable_id(
"production-order", production_order_code, prefix="PO"
),
"productionOrderCode": production_order_code,
"routingId": routing_id,
"routingCode": routing_code,
"outputMaterialId": output_id,
"outputMaterialCode": output_code,
"materialId": material_id,
"materialCode": material_code,
"quantity": round(1.0 + (index % 29) * 0.15, 3),
"unit": "PCS",
"bomType": "MBOM",
"scrapRate": round((index % 6) * 0.004, 3),
"revision": "A",
"effectiveFrom": base_date.isoformat(),
"status": "ACTIVE",
"sourceType": "SYNTHETIC",
}
)
table_rows = {
"drawings": drawings,
"engineering-releases": releases,
"routings": routings,
"routing-operations": routing_operations,
"ebom": ebom,
"pbom": pbom,
"mbom": mbom,
}
order_fields = {
"drawings": ("drawingNo",),
"engineering-releases": ("releaseCode",),
"routings": ("routingCode",),
"routing-operations": ("operationCode",),
"ebom": ("bomLineCode",),
"pbom": ("bomLineCode",),
"mbom": ("bomLineCode",),
}
for table_name, rows in table_rows.items():
_set_owned_rows(bundle, table_name, rows, order_by=order_fields[table_name])
return bundle
def date_from_iso(value: str):
from datetime import date
return date.fromisoformat(value)