aps-agent/server/shipyard_synthetic/master_data.py

581 lines
22 KiB
Python

from __future__ import annotations
from datetime import timedelta
from typing import Any
from .config import DATASET_TYPE, GeneratorConfig
from .models import DatasetBundle, stable_id
from .registry import TABLE_SPECS
_MASTER_TABLES = frozenset(
{
"organizations",
"workshops",
"workcenters",
"resource-groups",
"resources",
"calendars",
"shifts",
"skills",
"teams",
"employees",
"suppliers",
"warehouses",
"locations",
}
)
_WORKSHOPS = (
("HULL", "船体结构车间"),
("STEEL", "钢材预处理车间"),
("BLOCK", "分段制造车间"),
("OUTFIT", "舾装车间"),
("PIPE", "管系车间"),
("ELECTRICAL", "电气车间"),
("PAINT", "涂装车间"),
("DOCK", "船坞搭载车间"),
("QUAY", "码头调试车间"),
("LOGISTICS", "物流集配车间"),
("QUALITY", "质量检验中心"),
("COMMISSION", "系统调试中心"),
)
_RESOURCE_TYPES = (
("BUILDING", "厂房", "AREA", "m2", "HULL"),
("WORKSHOP", "车间", "AREA", "m2", "BLOCK"),
("WORKSTATION", "工位", "COUNT", "station", "OUTFIT"),
("JIG", "胎架", "COUNT", "set", "BLOCK"),
("PLATFORM", "装配平台", "AREA", "m2", "BLOCK"),
("BLOCK_YARD", "分段堆场", "AREA", "m2", "LOGISTICS"),
("DOCK", "船坞", "COUNT", "vessel", "DOCK"),
("SLIPWAY", "船台", "COUNT", "vessel", "DOCK"),
("BERTH", "码头泊位", "LENGTH", "m", "QUAY"),
("GANTRY_CRANE", "龙门吊", "WEIGHT", "t", "DOCK"),
("PORTAL_CRANE", "门座吊", "WEIGHT", "t", "QUAY"),
("MOBILE_CRANE", "汽车吊", "WEIGHT", "t", "LOGISTICS"),
("TRANSPORTER", "平板运输车", "WEIGHT", "t", "LOGISTICS"),
("CNC_CUTTER", "数控切割机", "HOURS", "h/day", "STEEL"),
("PLATE_BENDER", "弯板机", "HOURS", "h/day", "STEEL"),
("PIPE_BENDER", "弯管机", "HOURS", "h/day", "PIPE"),
("WELDER", "焊机", "HOURS", "h/day", "HULL"),
("SHOT_BLASTER", "抛丸设备", "AREA", "m2/day", "PAINT"),
("PAINT_BOOTH", "涂装房", "AREA", "m2/day", "PAINT"),
("NDT_EQUIPMENT", "探伤设备", "HOURS", "h/day", "QUALITY"),
("PRESSURE_TEST", "压力试验设备", "HOURS", "h/day", "QUALITY"),
("COMMISSION_EQUIPMENT", "调试设备", "HOURS", "h/day", "COMMISSION"),
("SHORE_POWER", "岸电设施", "POWER", "kW", "QUAY"),
)
_SKILL_NAMES = (
"船体装配",
"手工焊",
"自动焊",
"特种钢焊接",
"管系安装",
"管系焊接",
"电气安装",
"电气调试",
"涂装",
"起重指挥",
"起重机操作",
"平板车操作",
"无损检测",
"精度测量",
"密性试验",
"压力试验",
"主机调试",
"自动化调试",
"船舶试航",
)
_SUPPLIER_TYPES = (
"STEEL",
"MAIN_ENGINE",
"GENERATOR",
"PUMP_VALVE",
"ELECTRICAL",
"CABLE",
"NAVIGATION",
"DECK_MACHINERY",
"FIRE_SAFETY",
"LIFE_SAVING",
"PAINT",
"INSULATION",
"CAST_FORGE",
"OUTSOURCE_PIPE",
"OUTSOURCE_GALVANIZING",
"OUTSOURCE_MACHINING",
"OUTSOURCE_SURFACE",
)
_WAREHOUSE_TYPES = (
("STEEL_YARD", "钢材堆场"),
("PIPE", "管材仓库"),
("EQUIPMENT", "设备仓库"),
("ELECTRICAL", "电气仓库"),
("HAZMAT", "油漆危险品仓库"),
("CONSTANT_TEMP", "焊材恒温库"),
("OUTFITTING", "舾装件仓库"),
("CONSOLIDATION", "集配与委外收发仓库"),
)
_LOCATION_TYPES = (
("STEEL_YARD", "钢材堆场", 1),
("PIPE_STORAGE", "管材仓库", 2),
("EQUIPMENT_STORAGE", "设备仓库", 3),
("ELECTRICAL_STORAGE", "电气仓库", 4),
("PAINT_HAZMAT", "油漆危险品仓库", 5),
("WELDING_CONSUMABLE", "焊材恒温库", 6),
("OUTFITTING_STORAGE", "舾装件仓库", 7),
("SECTION_KITTING", "分段配套区", 8),
("PALLET_KITTING", "托盘集配区", 8),
("INSPECTION_PENDING", "待检区", 3),
("NONCONFORMING", "不合格品区", 3),
("OUTSOURCE_TRANSFER", "委外收发区", 8),
)
def _set_owned_rows(
bundle: DatasetBundle,
table_name: str,
rows: list[dict[str, Any]],
*,
order_by: tuple[str, ...],
) -> None:
if table_name not in _MASTER_TABLES or table_name not in TABLE_SPECS:
raise ValueError(f"table is outside W69-MASTER ownership: {table_name}")
spec = TABLE_SPECS[table_name]
missing = [
(index, field)
for index, row in enumerate(rows)
for field in spec.required
if field not in row
]
if missing:
index, field = missing[0]
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 _calendar_id(code: str) -> str:
return stable_id("calendar", code, prefix="CAL")
def _workshop_id(code: str) -> str:
return stable_id("workshop", code, prefix="WKS")
def _workcenter_id(code: str) -> str:
return stable_id("workcenter", code, prefix="WKC")
def _resource_group_id(resource_type: str) -> str:
return stable_id("resource-group", f"RG-SYN-{resource_type}", prefix="RGP")
def generate_master(bundle: DatasetBundle, config: GeneratorConfig) -> DatasetBundle:
"""Populate deterministic organization, resource, workforce and supplier tables."""
base_date = config.planning_base_date
horizon_end = config.planning_horizon_end
organization_code = "ORG-BH-SYN-001"
organization_id = stable_id("organization", organization_code, prefix="ORG")
organizations = [
{
"organizationId": organization_id,
"organizationCode": organization_code,
"name": "北海造船 APS 模拟组织-SYN",
"scenarioCode": "BEIHAI_SHIPYARD_APS",
"datasetType": DATASET_TYPE,
"timezone": config.timezone,
"status": "ACTIVE",
}
]
workshops: list[dict[str, Any]] = []
workcenters: list[dict[str, Any]] = []
center_ids_by_workshop: dict[str, list[str]] = {}
for workshop_index, (workshop_key, workshop_name) in enumerate(_WORKSHOPS, start=1):
workshop_code = f"WS-SYN-{workshop_index:02d}"
workshop_id = _workshop_id(workshop_code)
workshops.append(
{
"workshopId": workshop_id,
"workshopCode": workshop_code,
"organizationId": organization_id,
"name": f"{workshop_name}-SYN",
"workshopType": workshop_key,
"status": "ACTIVE",
}
)
center_ids_by_workshop[workshop_key] = []
for center_number in range(1, 3):
center_code = f"WC-SYN-{workshop_index:02d}-{center_number:02d}"
center_id = _workcenter_id(center_code)
center_ids_by_workshop[workshop_key].append(center_id)
workcenters.append(
{
"workcenterId": center_id,
"workcenterCode": center_code,
"workshopId": workshop_id,
"name": f"{workshop_name}{center_number}号工作中心-SYN",
"capacityMode": "FINITE",
"status": "ACTIVE",
}
)
calendar_defs = (
("CAL-SYN-DAY", "常日班日历-SYN", "SHIFT", [1, 2, 3, 4, 5]),
("CAL-SYN-TWO", "两班制日历-SYN", "SHIFT", [1, 2, 3, 4, 5, 6]),
("CAL-SYN-THREE", "三班制日历-SYN", "SHIFT", [1, 2, 3, 4, 5, 6]),
("CAL-SYN-OT", "周末加班日历-SYN", "OVERTIME", [6, 7]),
("CAL-SYN-HOLIDAY", "法定节假日历-SYN", "BLACKOUT", []),
("CAL-SYN-MAINT", "设备维护日历-SYN", "MAINTENANCE", []),
("CAL-SYN-WEATHER", "极端天气停工日历-SYN", "WEATHER", []),
("CAL-SYN-SUPPLIER", "供应商产能日历-SYN", "SUPPLIER", [1, 2, 3, 4, 5]),
)
calendars = [
{
"calendarId": _calendar_id(code),
"calendarCode": code,
"name": name,
"calendarType": calendar_type,
"timezone": config.timezone,
"effectiveFrom": base_date.isoformat(),
"effectiveTo": horizon_end.isoformat(),
"workingDays": working_days,
"blackoutDates": (
[
(base_date + timedelta(days=45)).isoformat(),
(base_date + timedelta(days=225)).isoformat(),
]
if calendar_type == "MAINTENANCE"
else []
),
"status": "ACTIVE",
}
for code, name, calendar_type, working_days in calendar_defs
]
shifts: list[dict[str, Any]] = []
shift_defs = (
("CAL-SYN-DAY", (("DAY", "08:00", "17:00"),)),
("CAL-SYN-TWO", (("A", "06:00", "14:00"), ("B", "14:00", "22:00"))),
(
"CAL-SYN-THREE",
(
("A", "06:00", "14:00"),
("B", "14:00", "22:00"),
("C", "22:00", "06:00"),
),
),
("CAL-SYN-OT", (("OT", "08:00", "16:00"),)),
)
for calendar_code, definitions in shift_defs:
for shift_code, start_time, end_time in definitions:
code = f"SHIFT-SYN-{calendar_code.removeprefix('CAL-SYN-')}-{shift_code}"
shifts.append(
{
"shiftId": stable_id("shift", code, prefix="SFT"),
"shiftCode": code,
"calendarId": _calendar_id(calendar_code),
"name": f"{shift_code}班-SYN",
"startTime": start_time,
"endTime": end_time,
"breakMinutes": 60 if shift_code == "DAY" else 30,
"status": "ACTIVE",
}
)
resource_groups = [
{
"resourceGroupId": _resource_group_id(resource_type),
"resourceGroupCode": f"RG-SYN-{resource_type}",
"name": f"{resource_name}资源组-SYN",
"resourceType": resource_type,
"capacityType": capacity_type,
"unit": unit,
"status": "ACTIVE",
}
for resource_type, resource_name, capacity_type, unit, _ in _RESOURCE_TYPES
]
resources: list[dict[str, Any]] = []
lift_types = {"GANTRY_CRANE", "PORTAL_CRANE", "MOBILE_CRANE", "TRANSPORTER"}
for index in range(1, config.profile.equipment_resource_count + 1):
type_index = (index - 1) % len(_RESOURCE_TYPES)
resource_type, resource_name, capacity_type, unit, workshop_key = (
_RESOURCE_TYPES[type_index]
)
center_ids = center_ids_by_workshop[workshop_key]
workcenter_id = center_ids[
((index - 1) // len(_RESOURCE_TYPES)) % len(center_ids)
]
resource_code = f"RES-SYN-{index:03d}"
exclusive = capacity_type in {"COUNT", "WEIGHT", "LENGTH", "POWER"}
base_capacity = 1.0 if exclusive else float(8 + index % 3)
if resource_type == "BERTH":
base_capacity = float(260 + (index % 4) * 40)
elif resource_type in {"GANTRY_CRANE", "PORTAL_CRANE", "MOBILE_CRANE"}:
base_capacity = float(80 + (index % 8) * 70)
elif resource_type == "TRANSPORTER":
base_capacity = float(120 + (index % 6) * 80)
elif resource_type == "SHORE_POWER":
base_capacity = float(2500 + (index % 5) * 750)
resources.append(
{
"resourceId": stable_id("resource", resource_code, prefix="RES"),
"resourceCode": resource_code,
"resourceName": f"{resource_name}-SYN-{index:03d}",
"name": f"{resource_name}-SYN-{index:03d}",
"resourceType": resource_type,
"workcenterId": workcenter_id,
"resourceGroupId": _resource_group_id(resource_type),
"resourceGroup": f"RG-SYN-{resource_type}",
"capacityType": capacity_type,
"capacity": base_capacity,
"dailyCapacity": base_capacity,
"unit": unit,
"exclusive": exclusive,
"shiftCalendarId": _calendar_id(
"CAL-SYN-THREE" if type_index % 3 == 0 else "CAL-SYN-TWO"
),
"maintenanceCalendarId": _calendar_id("CAL-SYN-MAINT"),
"maintenanceBlackoutDates": [
(base_date + timedelta(days=45)).isoformat(),
(base_date + timedelta(days=225)).isoformat(),
],
"efficiency": round(0.82 + (index % 12) * 0.01, 2),
"utilizationUpperLimit": 0.9,
"minimumLoad": 0.0,
"maximumLoad": base_capacity,
"capabilityTags": [resource_type, "FINITE_CAPACITY", "SYNTHETIC"],
"maximumWeight": base_capacity if resource_type in lift_types else None,
"maximumLength": 360.0 if resource_type in {"DOCK", "BERTH"} else 45.0,
"maximumWidth": 72.0 if resource_type == "DOCK" else 28.0,
"maximumHeight": 75.0 if resource_type == "GANTRY_CRANE" else 18.0,
"location": f"场区-SYN-{(index - 1) % 12 + 1:02d}",
"transportZone": f"TZ-SYN-{(index - 1) % 6 + 1:02d}",
"compatibleOperationCodes": [resource_type],
"status": "ACTIVE",
}
)
skills = [
{
"skillCode": f"SK-SYN-{index:03d}",
"name": f"{name}-SYN",
"qualificationRequired": index in {4, 10, 11, 12, 13, 15, 16, 19},
"qualificationType": "SPECIAL_OPERATION"
if index in {4, 10, 11, 12, 13, 15, 16, 19}
else "INTERNAL",
"status": "ACTIVE",
}
for index, name in enumerate(_SKILL_NAMES, start=1)
]
teams: list[dict[str, Any]] = []
employees: list[dict[str, Any]] = []
employee_index = 0
for index in range(1, config.profile.team_count + 1):
team_code = f"TEAM-SYN-{index:03d}"
team_id = stable_id("team", team_code, prefix="TEAM")
skill_index = (index - 1) % len(_SKILL_NAMES) + 1
skill_codes = [
f"SK-SYN-{skill_index:03d}",
f"SK-SYN-{skill_index % len(_SKILL_NAMES) + 1:03d}",
]
crew_size = 6 + (index % 7)
workshop_position = (skill_index - 1) % len(_WORKSHOPS) + 1
shift_calendar_id = _calendar_id(
"CAL-SYN-TWO" if index % 3 else "CAL-SYN-THREE"
)
teams.append(
{
"teamId": team_id,
"teamCode": team_code,
"name": f"{_SKILL_NAMES[skill_index - 1]}班组-SYN-{index:03d}",
"workshopId": _workshop_id(f"WS-SYN-{workshop_position:02d}"),
"crewSize": crew_size,
"skillCodes": skill_codes,
"shiftCalendarId": shift_calendar_id,
"crossProjectSupport": list(config.project_codes),
"overtimeLimitHoursPerWeek": 12,
"continuousWorkLimitHours": 10,
"learningCurve": round(0.92 + (index % 5) * 0.015, 3),
"sisterShipEfficiencyFactor": 1.08 if index % 4 == 0 else 1.03,
"status": "ACTIVE",
}
)
for crew_position in range(1, crew_size + 1):
employee_index += 1
employee_code = f"EMP-SYN-{employee_index:04d}"
qualification_required = any(
int(code.rsplit("-", 1)[-1]) in {4, 10, 11, 12, 13, 15, 16, 19}
for code in skill_codes
)
employees.append(
{
"employeeId": stable_id("employee", employee_code, prefix="EMP"),
"employeeCode": employee_code,
"teamId": team_id,
"name": f"{_SKILL_NAMES[skill_index - 1]}工-模拟{employee_index:04d}",
"skillCodes": skill_codes,
"qualificationCode": f"CERT-SYN-{skill_index:03d}-{employee_index:04d}"
if qualification_required
else None,
"qualificationValidTo": (
base_date + timedelta(days=730 + crew_position)
).isoformat(),
"availableShiftCalendarId": shift_calendar_id,
"status": "ACTIVE",
}
)
suppliers: list[dict[str, Any]] = []
outsource_supplier_types = tuple(
supplier_type
for supplier_type in _SUPPLIER_TYPES
if supplier_type.startswith("OUTSOURCE_")
)
for index in range(1, config.profile.supplier_count + 1):
supplier_code = f"SUP-SYN-{index:03d}"
if config.profile.supplier_count < 14 and index <= len(
outsource_supplier_types
):
supplier_type = outsource_supplier_types[index - 1]
else:
supplier_type = _SUPPLIER_TYPES[(index - 1) % len(_SUPPLIER_TYPES)]
is_outsource_supplier = supplier_type.startswith("OUTSOURCE_")
capability_codes = (
[supplier_type.removeprefix("OUTSOURCE_")] if is_outsource_supplier else []
)
transport_outbound_days = 1 if is_outsource_supplier else 1 + index % 4
transport_inbound_days = 1 if is_outsource_supplier else 1 + (index + 1) % 4
blackout_dates = sorted(
{
(base_date + timedelta(days=31 + index)).isoformat(),
(base_date + timedelta(days=181 + index * 2)).isoformat(),
}
)
suppliers.append(
{
"supplierId": stable_id("supplier", supplier_code, prefix="SUP"),
"supplierCode": supplier_code,
"name": f"供应商-SYN-{index:03d}",
"supplierName": f"供应商-SYN-{index:03d}",
"supplierType": supplier_type,
"suppliedMaterialGroups": [supplier_type],
"outsourceCapabilityCodes": capability_codes,
"outsourceOperationCodes": list(capability_codes),
"standardLeadTimeDays": 12 + (index * 7) % 90,
"transportLeadTimeDays": transport_outbound_days
+ transport_inbound_days,
"transportOutboundDays": transport_outbound_days,
"transportInboundDays": transport_inbound_days,
"inspectionLeadTimeDays": (
1 if is_outsource_supplier else 1 + index % 5
),
"monthlyCapacity": (
600.0 + (index % 10) * 175.0
if is_outsource_supplier
else 5_000_000.0 + (index % 10) * 250_000.0
),
"monthlyCapacityUnit": (
"OPERATION" if is_outsource_supplier else "PCS"
),
"blackoutDates": blackout_dates,
"onTimeDeliveryRate": round(0.82 + (index % 15) * 0.01, 2),
"qualityPassRate": round(0.9 + (index % 9) * 0.01, 2),
"riskLevel": "HIGH"
if index % 13 == 0
else "MEDIUM"
if index % 5 == 0
else "LOW",
"region": f"模拟区域-SYN-{(index - 1) % 8 + 1:02d}",
"approved": True if is_outsource_supplier else index % 13 != 0,
"blackoutCalendar": _calendar_id("CAL-SYN-HOLIDAY"),
"minimumOrderValue": float(10000 + (index % 6) * 5000),
"capacityCalendar": _calendar_id("CAL-SYN-SUPPLIER"),
"status": "ACTIVE",
}
)
warehouses: list[dict[str, Any]] = []
warehouse_ids: dict[int, str] = {}
for index, (warehouse_type, warehouse_name) in enumerate(_WAREHOUSE_TYPES, start=1):
warehouse_code = f"WH-SYN-{index:03d}"
warehouse_id = stable_id("warehouse", warehouse_code, prefix="WH")
warehouse_ids[index] = warehouse_id
warehouses.append(
{
"warehouseId": warehouse_id,
"warehouseCode": warehouse_code,
"name": f"{warehouse_name}-SYN",
"warehouseType": warehouse_type,
"organizationId": organization_id,
"status": "ACTIVE",
}
)
locations = []
for index, (location_type, location_name, warehouse_index) in enumerate(
_LOCATION_TYPES, start=1
):
location_code = f"LOC-SYN-{index:03d}"
quality_statuses = ["AVAILABLE", "ALLOCATED", "RESERVED"]
if location_type == "INSPECTION_PENDING":
quality_statuses = ["QUALITY_HOLD"]
elif location_type == "NONCONFORMING":
quality_statuses = ["BLOCKED"]
locations.append(
{
"locationId": stable_id("location", location_code, prefix="LOC"),
"locationCode": location_code,
"warehouseId": warehouse_ids[warehouse_index],
"locationType": location_type,
"name": f"{location_name}-SYN",
"qualityStatusAllowed": quality_statuses,
"status": "ACTIVE",
}
)
table_rows = {
"organizations": organizations,
"workshops": workshops,
"workcenters": workcenters,
"resource-groups": resource_groups,
"resources": resources,
"calendars": calendars,
"shifts": shifts,
"skills": skills,
"teams": teams,
"employees": employees,
"suppliers": suppliers,
"warehouses": warehouses,
"locations": locations,
}
order_fields = {
"organizations": ("organizationCode",),
"workshops": ("workshopCode",),
"workcenters": ("workcenterCode",),
"resource-groups": ("resourceGroupCode",),
"resources": ("resourceCode",),
"calendars": ("calendarCode",),
"shifts": ("shiftCode",),
"skills": ("skillCode",),
"teams": ("teamCode",),
"employees": ("employeeCode",),
"suppliers": ("supplierCode",),
"warehouses": ("warehouseCode",),
"locations": ("locationCode",),
}
for table_name, rows in table_rows.items():
_set_owned_rows(bundle, table_name, rows, order_by=order_fields[table_name])
return bundle