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