aps-agent/server/shipyard_synthetic/projects.py

626 lines
24 KiB
Python
Raw Normal View History

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