from __future__ import annotations from datetime import date, timedelta from typing import Any from .config import GeneratorConfig from .models import DatasetBundle, stable_id from .registry import TABLE_SPECS _PROJECT_TABLES = frozenset( { "contracts", "ship-projects", "milestones", "wbs", "blocks", "zones", "work-packages", } ) _PROJECT_SPECS = ( { "code": "BH-SYN-2601", "shipType": "210000 DWT 散货船", "startOffset": 0, "deliveryOffset": 348, "priority": 8, "shipOwner": "船东-SYN-A", "status": "IN_PROGRESS", "feature": "大型散货船并行建造模拟场景", }, { "code": "BH-SYN-2602", "shipType": "210000 DWT 散货船", "startOffset": 44, "deliveryOffset": 424, "priority": 7, "shipOwner": "船东-SYN-A", "status": "PLANNED", "feature": "BH-SYN-2601 姊妹船,允许复用工时、工艺和学习数据", "sisterProjectCode": "BH-SYN-2601", }, { "code": "BH-SYN-2603", "shipType": "大型集装箱船", "startOffset": 19, "deliveryOffset": 465, "priority": 9, "shipOwner": "船东-SYN-B", "status": "IN_PROGRESS", "feature": "舾装、电气和管系密集型工作包", }, { "code": "BH-SYN-2604", "shipType": "汽车运输船 PCTC", "startOffset": 61, "deliveryOffset": 483, "priority": 6, "shipOwner": "船东-SYN-C", "status": "PLANNED", "feature": "多层车辆甲板和复杂通风系统", }, ) _MILESTONES = ( ("CONTRACT_EFFECTIVE", "合同生效", -120, "商务管理部", 10), ("BASIC_DESIGN", "基本设计完成", -75, "设计部", 7), ("DETAIL_DESIGN", "详细设计完成", -45, "设计部", 7), ("PRODUCTION_DESIGN", "生产设计完成", -20, "生产设计部", 5), ("FIRST_STEEL", "首批钢材到厂", -7, "采购供应部", 3), ("STEEL_CUT", "开工/钢板切割", 0, "钢材预处理车间", 5), ("KEEL_LAY", "龙骨铺设", 60, "船体结构部", 5), ("MAIN_ENGINE_ARRIVAL", "主机到厂", "DELIVERY-190", "采购供应部", 7), ("SECTION_COMPLETE", "分段制造完成", "DELIVERY-165", "分段制造部", 7), ("DOCK_ERECTION_START", "船坞搭载开始", "DELIVERY-155", "船坞搭载部", 5), ("HULL_FORMED", "船体成型", "DELIVERY-110", "船坞搭载部", 7), ("LAUNCH", "下水", "DELIVERY-87", "船坞搭载部", 7), ("QUAY_OUTFITTING", "码头舾装", "DELIVERY-80", "舾装部", 5), ("MOORING_TRIAL", "系泊试验", "DELIVERY-48", "系统调试部", 5), ("SEA_TRIAL", "海上试航", "DELIVERY-31", "试航部", 5), ("CLASS_ACCEPTANCE", "船级社验收", "DELIVERY-14", "质量管理部", 5), ("OWNER_ACCEPTANCE", "船东验收", "DELIVERY-7", "项目管理部", 3), ("DELIVERY", "交付", "DELIVERY", "项目管理部", 0), ) _DISCIPLINES = ( ("HULL", "船体结构", "船体结构部"), ("MACHINERY", "机装", "机装部"), ("PIPE", "管系", "管系部"), ("ELECTRICAL", "电气", "电气部"), ("HVAC", "通风", "通风部"), ("INTERIOR", "内装", "内装部"), ("PAINT", "涂装", "涂装部"), ("DECK_MACHINERY", "甲板机械", "甲板机械部"), ("POWER", "动力系统", "动力系统部"), ("AUTOMATION", "自动化系统", "自动化部"), ("SAFETY", "安全系统", "安全管理部"), ("COMMISSION", "调试与试验", "系统调试部"), ) _ZONE_TYPES = ( ("BOW", "艏部区域"), ("STERN", "艉部区域"), ("CARGO", "货舱区域"), ("ENGINE_ROOM", "机舱区域"), ("SUPERSTRUCTURE", "上层建筑"), ("DECK", "甲板区域"), ("DOUBLE_BOTTOM", "双层底区域"), ("SIDE", "舷侧区域"), ("STEERING_GEAR", "舵机舱区域"), ("BRIDGE", "驾驶室区域"), ) def _set_owned_rows( bundle: DatasetBundle, table_name: str, rows: list[dict[str, Any]], *, order_by: tuple[str, ...], ) -> None: if table_name not in _PROJECT_TABLES or table_name not in TABLE_SPECS: raise ValueError(f"table is outside W69-PROJECT 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 _distribute(total: int, groups: int) -> list[int]: quotient, remainder = divmod(total, groups) return [quotient + (1 if index < remainder else 0) for index in range(groups)] def _project_id(project_code: str) -> str: return stable_id("project", project_code, prefix="PRJ") def _wbs_id(wbs_code: str) -> str: return stable_id("wbs", wbs_code, prefix="WBS") def _planned_window( project_start: date, delivery_date: date, ordinal: int, total: int, *, duration_days: int, ) -> tuple[str, str]: span = max(1, (delivery_date - project_start).days) usable = max(1, span - duration_days) offset = min(usable, (max(0, ordinal) * usable) // max(1, total)) start = project_start + timedelta(days=offset) end = min(delivery_date, start + timedelta(days=duration_days)) return start.isoformat(), end.isoformat() def _wbs_row( *, wbs_code: str, project_id: str, wbs_type: str, name: str, parent_wbs_id: str | None, planned_start: str, planned_end: str, department: str, discipline: str | None, zone: str | None, weight_t: float, status: str = "PLANNED", ) -> dict[str, Any]: return { "wbsId": _wbs_id(wbs_code), "wbsCode": wbs_code, "projectId": project_id, "parentWbsId": parent_wbs_id, "wbsType": wbs_type, "name": name, "plannedStart": planned_start, "plannedEnd": planned_end, "responsibilityDepartment": department, "discipline": discipline, "zone": zone, "weightT": round(weight_t, 3), "progressPct": 0.0, "status": status, } def generate_projects(bundle: DatasetBundle, config: GeneratorConfig) -> DatasetBundle: """Populate deterministic contracts, projects, milestones and complete WBS hierarchy.""" selected_specs = _PROJECT_SPECS[: config.profile.project_count] base_date = config.planning_base_date ship_projects: list[dict[str, Any]] = [] contracts: list[dict[str, Any]] = [] milestones: list[dict[str, Any]] = [] wbs_rows: list[dict[str, Any]] = [] zones: list[dict[str, Any]] = [] blocks: list[dict[str, Any]] = [] work_packages: list[dict[str, Any]] = [] project_contexts: list[dict[str, Any]] = [] for spec in selected_specs: project_code = str(spec["code"]) project_id = _project_id(project_code) project_start = base_date + timedelta(days=int(spec["startOffset"])) delivery_date = base_date + timedelta(days=int(spec["deliveryOffset"])) launch_date = delivery_date - timedelta(days=87) sea_trial_date = delivery_date - timedelta(days=31) sister_code = spec.get("sisterProjectCode") ship_projects.append( { "projectId": project_id, "projectCode": project_code, "name": f"{project_code} 船舶项目-SYN", "shipType": spec["shipType"], "status": spec["status"], "plannedStart": project_start.isoformat(), "launchTarget": launch_date.isoformat(), "seaTrialTarget": sea_trial_date.isoformat(), "deliveryTarget": delivery_date.isoformat(), "priority": spec["priority"], "shipOwner": spec["shipOwner"], "sisterProjectId": _project_id(str(sister_code)) if sister_code else None, "sisterProjectCode": sister_code, "reuseLearningAllowed": bool(sister_code), "scenarioFeature": spec["feature"], "dateSource": "ATTACHMENT" if project_code == "BH-SYN-2601" else "DERIVED_SYNTHETIC", "datasetType": "SYNTHETIC", } ) contract_code = f"CTR-{project_code}" contracts.append( { "contractId": stable_id("contract", contract_code, prefix="CTR"), "contractCode": contract_code, "projectId": project_id, "shipOwner": spec["shipOwner"], "effectiveDate": (project_start - timedelta(days=120)).isoformat(), "deliveryDate": delivery_date.isoformat(), "currency": "CNY", "contractStatus": "EFFECTIVE", "synthetic": True, } ) root_code = f"{project_code}-WBS-ROOT" root_wbs_id = _wbs_id(root_code) wbs_rows.append( _wbs_row( wbs_code=root_code, project_id=project_id, wbs_type="PROJECT", name=f"{project_code} 船舶项目-SYN", parent_wbs_id=None, planned_start=project_start.isoformat(), planned_end=delivery_date.isoformat(), department="项目管理部", discipline=None, zone=None, weight_t=0.0, status=str(spec["status"]), ) ) discipline_wbs_ids: list[str] = [] for discipline_index, ( discipline_code, discipline_name, department, ) in enumerate(_DISCIPLINES, start=1): wbs_code = f"{project_code}-DISC-{discipline_index:02d}" discipline_wbs_ids.append(_wbs_id(wbs_code)) wbs_rows.append( _wbs_row( wbs_code=wbs_code, project_id=project_id, wbs_type="DISCIPLINE", name=f"{discipline_name}-SYN", parent_wbs_id=root_wbs_id, planned_start=project_start.isoformat(), planned_end=delivery_date.isoformat(), department=department, discipline=discipline_code, zone=None, weight_t=0.0, ) ) zone_contexts: list[dict[str, Any]] = [] for zone_index, (zone_type, zone_name) in enumerate(_ZONE_TYPES, start=1): zone_code = f"{project_code}-ZONE-{zone_index:02d}" zone_id = stable_id("zone", zone_code, prefix="ZONE") discipline_index = (zone_index - 1) % len(_DISCIPLINES) discipline_code, _, department = _DISCIPLINES[discipline_index] zone_wbs_code = f"{project_code}-WBS-ZONE-{zone_index:02d}" zone_wbs_id = _wbs_id(zone_wbs_code) zones.append( { "zoneId": zone_id, "zoneCode": zone_code, "projectId": project_id, "zoneType": zone_type, "name": f"{zone_name}-SYN", "wbsId": zone_wbs_id, "status": "ACTIVE", } ) wbs_rows.append( _wbs_row( wbs_code=zone_wbs_code, project_id=project_id, wbs_type="ZONE", name=f"{zone_name}-SYN", parent_wbs_id=discipline_wbs_ids[discipline_index], planned_start=project_start.isoformat(), planned_end=delivery_date.isoformat(), department=department, discipline=discipline_code, zone=zone_type, weight_t=0.0, ) ) zone_contexts.append( { "zoneId": zone_id, "zoneCode": zone_code, "zoneType": zone_type, "zoneName": zone_name, "wbsId": zone_wbs_id, "discipline": discipline_code, "department": department, } ) project_contexts.append( { "projectCode": project_code, "projectId": project_id, "projectStart": project_start, "deliveryDate": delivery_date, "rootWbsId": root_wbs_id, "zones": zone_contexts, } ) previous_milestone_id: str | None = None previous_date: date | None = None for milestone_index, (code, name, timing, department, buffer_days) in enumerate( _MILESTONES, start=1 ): if timing == "DELIVERY": planned_date = delivery_date elif isinstance(timing, str) and timing.startswith("DELIVERY-"): planned_date = delivery_date - timedelta(days=int(timing.split("-")[1])) else: planned_date = project_start + timedelta(days=int(timing)) milestone_code = f"{project_code}-MS-{milestone_index:02d}-{code}" milestone_id = stable_id("milestone", milestone_code, prefix="MS") lag_days = ( 0 if previous_date is None else (planned_date - previous_date).days ) milestones.append( { "milestoneId": milestone_id, "projectId": project_id, "milestoneCode": milestone_code, "milestoneType": code, "name": f"{name}-SYN", "plannedDate": planned_date.isoformat(), "predecessorMilestoneId": previous_milestone_id, "relationType": "FS", "lagDays": lag_days, "leadDays": 0, "bufferDays": buffer_days, "responsibilityDepartment": department, "sequence": milestone_index, "status": "PLANNED", } ) previous_milestone_id = milestone_id previous_date = planned_date grand_counts = _distribute(config.profile.grand_block_count, len(project_contexts)) section_counts = _distribute(config.profile.section_count, len(project_contexts)) package_counts = _distribute( config.profile.work_package_count, len(project_contexts) ) task_counts = _distribute(config.profile.wbs_task_count, len(project_contexts)) for project_index, context in enumerate(project_contexts): project_code = context["projectCode"] project_id = context["projectId"] project_start = context["projectStart"] delivery_date = context["deliveryDate"] zone_contexts = context["zones"] grand_contexts: list[dict[str, Any]] = [] for local_index in range(1, grand_counts[project_index] + 1): zone = zone_contexts[(local_index - 1) % len(zone_contexts)] block_code = f"{project_code}-GB-{local_index:03d}" block_id = stable_id("block", block_code, prefix="BLK") wbs_code = f"{project_code}-WBS-GB-{local_index:03d}" wbs_id = _wbs_id(wbs_code) planned_start, planned_end = _planned_window( project_start, delivery_date, local_index - 1, grand_counts[project_index], duration_days=100, ) weight = 720.0 + (local_index % 11) * 42.5 blocks.append( { "blockId": block_id, "blockCode": block_code, "projectId": project_id, "parentBlockId": None, "blockType": "GRAND_BLOCK", "name": f"总段-SYN-{local_index:03d}", "zoneId": zone["zoneId"], "wbsId": wbs_id, "weightT": weight, "lengthM": 32.0 + local_index % 9, "widthM": 18.0 + local_index % 6, "heightM": 14.0 + local_index % 5, "status": "PLANNED", } ) wbs_rows.append( _wbs_row( wbs_code=wbs_code, project_id=project_id, wbs_type="GRAND_BLOCK", name=f"总段-SYN-{local_index:03d}", parent_wbs_id=zone["wbsId"], planned_start=planned_start, planned_end=planned_end, department=zone["department"], discipline=zone["discipline"], zone=zone["zoneType"], weight_t=weight, ) ) grand_contexts.append( { "blockId": block_id, "wbsId": wbs_id, "zone": zone, "weightT": weight, } ) section_contexts: list[dict[str, Any]] = [] for local_index in range(1, section_counts[project_index] + 1): parent = grand_contexts[(local_index - 1) % len(grand_contexts)] zone = parent["zone"] block_code = f"{project_code}-SEC-{local_index:03d}" block_id = stable_id("block", block_code, prefix="BLK") wbs_code = f"{project_code}-WBS-SEC-{local_index:03d}" wbs_id = _wbs_id(wbs_code) planned_start, planned_end = _planned_window( project_start, delivery_date, local_index - 1, section_counts[project_index], duration_days=55, ) weight = 95.0 + (local_index % 17) * 8.5 blocks.append( { "blockId": block_id, "blockCode": block_code, "projectId": project_id, "parentBlockId": parent["blockId"], "blockType": "SECTION", "name": f"分段-SYN-{local_index:03d}", "zoneId": zone["zoneId"], "wbsId": wbs_id, "weightT": weight, "lengthM": 12.0 + local_index % 11, "widthM": 8.0 + local_index % 7, "heightM": 6.0 + local_index % 5, "status": "PLANNED", } ) wbs_rows.append( _wbs_row( wbs_code=wbs_code, project_id=project_id, wbs_type="SECTION", name=f"分段-SYN-{local_index:03d}", parent_wbs_id=parent["wbsId"], planned_start=planned_start, planned_end=planned_end, department=zone["department"], discipline=zone["discipline"], zone=zone["zoneType"], weight_t=weight, ) ) section_contexts.append( { "blockId": block_id, "wbsId": wbs_id, "zone": zone, "weightT": weight, } ) package_contexts: list[dict[str, Any]] = [] for local_index in range(1, package_counts[project_index] + 1): section = section_contexts[(local_index - 1) % len(section_contexts)] zone = section["zone"] package_code = f"{project_code}-WP-{local_index:04d}" package_id = stable_id("work-package", package_code, prefix="WP") wbs_code = f"{project_code}-WBS-WP-{local_index:04d}" wbs_id = _wbs_id(wbs_code) planned_start, planned_end = _planned_window( project_start, delivery_date, local_index - 1, package_counts[project_index], duration_days=24, ) wbs_rows.append( _wbs_row( wbs_code=wbs_code, project_id=project_id, wbs_type="WORK_PACKAGE", name=f"区域托盘工作包-SYN-{local_index:04d}", parent_wbs_id=section["wbsId"], planned_start=planned_start, planned_end=planned_end, department=zone["department"], discipline=zone["discipline"], zone=zone["zoneType"], weight_t=round(section["weightT"] / 4.0, 3), ) ) work_packages.append( { "workPackageId": package_id, "workPackageCode": package_code, "projectId": project_id, "wbsId": wbs_id, "blockId": section["blockId"], "zoneId": zone["zoneId"], "packageType": "PALLET" if local_index % 2 else "AREA", "discipline": zone["discipline"], "plannedStart": planned_start, "plannedEnd": planned_end, "needDate": planned_end, "status": "PLANNED", } ) package_contexts.append( { "workPackageId": package_id, "wbsId": wbs_id, "zone": zone, "blockId": section["blockId"], } ) for local_index in range(1, task_counts[project_index] + 1): package = package_contexts[(local_index - 1) % len(package_contexts)] zone = package["zone"] task_code = f"{project_code}-TASK-{local_index:04d}" planned_start, planned_end = _planned_window( project_start, delivery_date, local_index - 1, task_counts[project_index], duration_days=8, ) wbs_rows.append( _wbs_row( wbs_code=task_code, project_id=project_id, wbs_type="TASK", name=f"生产任务-SYN-{local_index:04d}", parent_wbs_id=package["wbsId"], planned_start=planned_start, planned_end=planned_end, department=zone["department"], discipline=zone["discipline"], zone=zone["zoneType"], weight_t=round(0.5 + (local_index % 25) * 0.18, 3), ) ) table_rows = { "contracts": contracts, "ship-projects": ship_projects, "milestones": milestones, "wbs": wbs_rows, "blocks": blocks, "zones": zones, "work-packages": work_packages, } order_fields = { "contracts": ("contractCode",), "ship-projects": ("projectCode",), "milestones": ("projectId", "sequence"), "wbs": ("wbsCode",), "blocks": ("blockCode",), "zones": ("zoneCode",), "work-packages": ("workPackageCode",), } for table_name, rows in table_rows.items(): _set_owned_rows(bundle, table_name, rows, order_by=order_fields[table_name]) return bundle