392 lines
19 KiB
Python
392 lines
19 KiB
Python
"""Incrementally seed GHC-06 topology data without rebuilding earlier missions.
|
|
|
|
Usage:
|
|
python topology_api/seed/seed_ghc06.py
|
|
"""
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from topology_api.db import write_cursor
|
|
|
|
|
|
UTC = timezone.utc
|
|
|
|
|
|
def ts(value):
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(UTC)
|
|
|
|
|
|
def seed():
|
|
with write_cursor() as cur:
|
|
cur.execute("SELECT id FROM bases WHERE code = 'guanghan'")
|
|
base = cur.fetchone()
|
|
if not base:
|
|
raise RuntimeError("Base 'guanghan' is missing; seed GHC-01 through GHC-05 first")
|
|
base_id = base["id"]
|
|
|
|
cur.execute("""
|
|
INSERT INTO missions (id, base_id, code, title, occurrence_status,
|
|
description, source_ref)
|
|
VALUES (gen_random_uuid(), %(base)s, 'GHC-06',
|
|
'广寒基地建设任务-06', 'occurred',
|
|
'第二次乘组轮换与广寒 04 北部扩展区建设',
|
|
'/home/mission/Guanghan_Program/GHC-06')
|
|
ON CONFLICT (base_id, code) DO UPDATE SET
|
|
title = EXCLUDED.title,
|
|
occurrence_status = EXCLUDED.occurrence_status,
|
|
description = EXCLUDED.description,
|
|
source_ref = EXCLUDED.source_ref,
|
|
updated_at = now()
|
|
RETURNING id
|
|
""", {"base": base_id})
|
|
mission_id = cur.fetchone()["id"]
|
|
|
|
event_specs = [
|
|
("landing", "2025-01-11T10:49:00Z", 0, "component_introduced",
|
|
"吴刚三号着陆广寒基地"),
|
|
("lander_connected", "2025-01-12T00:00:00Z", 0, "connection_established",
|
|
"吴刚三号通过柔性加压管道接入基地"),
|
|
("hub_installed", "2025-02-15T00:00:00Z", 0, "component_installed",
|
|
"十字安装模块安装至北部扩展位置"),
|
|
("greenhouse_installed", "2025-02-17T00:00:00Z", 0, "component_installed",
|
|
"温室二号与堆肥模块安装完成"),
|
|
("habitat_storage_installed", "2025-02-19T00:00:00Z", 0, "component_installed",
|
|
"存储模块与居住舱三号安装完成"),
|
|
("passage_installed", "2025-02-22T00:00:00Z", 0, "connection_established",
|
|
"温室模块与温室二号加压走道安装完成"),
|
|
("topology_reconfigured", "2025-02-22T00:00:00Z", 1, "connection_changed",
|
|
"吴刚二号改接实验舱远端接口"),
|
|
("reactor_35", "2025-02-27T00:00:00Z", 0, "state_changed",
|
|
"核反应堆功率提升至 35%"),
|
|
("lander_departed", "2025-03-11T00:00:00Z", 0, "component_retired",
|
|
"GHC-06 乘组搭乘吴刚三号撤离"),
|
|
("terminal", "2025-03-11T00:00:00Z", 1, "mission_checkpoint",
|
|
"GHC-06 任务结束状态"),
|
|
]
|
|
events = {}
|
|
event_times = {}
|
|
for key, effective_at, order, event_type, title in event_specs:
|
|
event_times[key] = ts(effective_at)
|
|
cur.execute("""
|
|
INSERT INTO mission_events
|
|
(id, mission_id, effective_at, event_order, event_type,
|
|
title, source_ref)
|
|
VALUES (gen_random_uuid(), %(mission)s, %(at)s, %(ord)s,
|
|
%(type)s, %(title)s, %(source)s)
|
|
ON CONFLICT (mission_id, effective_at, event_order) DO UPDATE SET
|
|
event_type = EXCLUDED.event_type,
|
|
title = EXCLUDED.title,
|
|
source_ref = EXCLUDED.source_ref
|
|
RETURNING id
|
|
""", {
|
|
"mission": mission_id, "at": event_times[key], "ord": order,
|
|
"type": event_type, "title": title,
|
|
"source": "/home/mission/Guanghan_Program/GHC-06",
|
|
})
|
|
events[key] = cur.fetchone()["id"]
|
|
|
|
component_specs = [
|
|
("wugang_3", "vehicle", "landing", "吴刚三号",
|
|
"Block 2 载人月面着陆器", {"block": 2, "role": "crewed_lander"}),
|
|
("expansion_hub", "node", "hub_installed", "十字安装模块",
|
|
"广寒 04 北部扩展区四向安装节点", {"dockingNodes": 4}),
|
|
("greenhouse_2", "module", "greenhouse_installed", "温室二号",
|
|
"第二座折叠式月面温室", {"foldingArms": 4}),
|
|
("compost_module", "equipment", "greenhouse_installed", "堆肥模块",
|
|
"温室二号有机废弃物与养分循环设备", {}),
|
|
("storage_module", "module", "habitat_storage_installed", "存储模块",
|
|
"北部扩展区温湿度受控存储设施", {}),
|
|
("habitat_3", "module", "habitat_storage_installed", "居住舱段三号",
|
|
"四铺位居住舱段,使基地容量提升至 14 人", {"capacity": 4}),
|
|
]
|
|
components = {}
|
|
for key, component_type, event_key, display_name, summary, metadata in component_specs:
|
|
cur.execute("""
|
|
INSERT INTO components
|
|
(id, base_id, component_key, component_type,
|
|
introduced_by_event_id, metadata)
|
|
VALUES (gen_random_uuid(), %(base)s, %(key)s, %(type)s,
|
|
%(event)s, %(metadata)s)
|
|
ON CONFLICT (base_id, component_key) DO UPDATE SET
|
|
component_type = EXCLUDED.component_type,
|
|
introduced_by_event_id = EXCLUDED.introduced_by_event_id,
|
|
metadata = EXCLUDED.metadata,
|
|
updated_at = now()
|
|
RETURNING id
|
|
""", {
|
|
"base": base_id, "key": key, "type": component_type,
|
|
"event": events[event_key], "metadata": json.dumps(metadata),
|
|
})
|
|
component_id = cur.fetchone()["id"]
|
|
components[key] = component_id
|
|
|
|
cur.execute("""
|
|
SELECT id FROM component_versions
|
|
WHERE component_id = %(component)s
|
|
AND started_by_event_id = %(event)s
|
|
""", {"component": component_id, "event": events[event_key]})
|
|
version = cur.fetchone()
|
|
values = {
|
|
"component": component_id, "name": display_name,
|
|
"summary": summary, "properties": json.dumps(metadata),
|
|
"event": events[event_key],
|
|
}
|
|
if version:
|
|
cur.execute("""
|
|
UPDATE component_versions SET
|
|
display_name = %(name)s, summary = %(summary)s,
|
|
properties = %(properties)s, updated_at = now()
|
|
WHERE id = %(id)s
|
|
""", {**values, "id": version["id"]})
|
|
else:
|
|
cur.execute("""
|
|
INSERT INTO component_versions
|
|
(id, component_id, display_name, summary, properties,
|
|
valid_from, started_by_event_id)
|
|
VALUES (gen_random_uuid(), %(component)s, %(name)s,
|
|
%(summary)s, %(properties)s,
|
|
(SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
%(event)s)
|
|
""", values)
|
|
|
|
cur.execute("""
|
|
UPDATE components SET retired_by_event_id = %(event)s, updated_at = now()
|
|
WHERE id = %(component)s
|
|
""", {"event": events["lander_departed"], "component": components["wugang_3"]})
|
|
cur.execute("""
|
|
UPDATE component_versions SET
|
|
valid_to = (SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
ended_by_event_id = %(event)s,
|
|
updated_at = now()
|
|
WHERE component_id = %(component)s AND valid_to IS NULL
|
|
""", {"event": events["lander_departed"], "component": components["wugang_3"]})
|
|
|
|
# Include existing components used as attachment roots.
|
|
cur.execute("""
|
|
SELECT component_key, id FROM components
|
|
WHERE base_id = %(base)s
|
|
AND component_key IN (
|
|
'greenhouse', 'lab_module', 'wugang_2', 'nuclear_reactor'
|
|
)
|
|
""", {"base": base_id})
|
|
components.update({row["component_key"]: row["id"] for row in cur.fetchall()})
|
|
for required in ("greenhouse", "lab_module", "wugang_2", "nuclear_reactor"):
|
|
if required not in components:
|
|
raise RuntimeError(f"Required component '{required}' is missing")
|
|
|
|
port_specs = [
|
|
("wugang_3", "dock", "加压对接口", "south", "docking"),
|
|
("expansion_hub", "north", "北部安装口", "north", "structural"),
|
|
("expansion_hub", "east", "东部安装口", "east", "structural"),
|
|
("expansion_hub", "south", "南部加压接口", "south", "pressurized"),
|
|
("expansion_hub", "west", "西部安装口", "west", "structural"),
|
|
("greenhouse_2", "core", "十字模块接口", "west", "structural"),
|
|
("greenhouse_2", "passage", "温室模块加压接口", "east", "pressurized"),
|
|
("greenhouse_2", "mount", "顶部设备接口", "north", "structural"),
|
|
("compost_module", "mount", "温室安装接口", "south", "structural"),
|
|
("storage_module", "core", "十字模块接口", "south", "structural"),
|
|
("habitat_3", "core", "十字模块接口", "east", "structural"),
|
|
("greenhouse", "distal", "远端对接口", "west", "pressurized"),
|
|
("lab_module", "distal", "远端加压接口", "east", "pressurized"),
|
|
("wugang_2", "dock", "加压对接口", "west", "docking"),
|
|
]
|
|
ports = {}
|
|
for component_key, port_key, name, direction, interface_type in port_specs:
|
|
cur.execute("""
|
|
INSERT INTO ports
|
|
(id, component_id, port_key, display_name, direction, interface_type)
|
|
VALUES (gen_random_uuid(), %(component)s, %(key)s, %(name)s,
|
|
%(direction)s, %(interface)s)
|
|
ON CONFLICT (component_id, port_key) DO UPDATE SET
|
|
display_name = EXCLUDED.display_name,
|
|
direction = EXCLUDED.direction,
|
|
interface_type = EXCLUDED.interface_type
|
|
RETURNING id
|
|
""", {
|
|
"component": components[component_key], "key": port_key,
|
|
"name": name, "direction": direction, "interface": interface_type,
|
|
})
|
|
ports[(component_key, port_key)] = cur.fetchone()["id"]
|
|
|
|
connection_specs = [
|
|
("expansion_hub", "north", "storage_module", "core", "structural_mount",
|
|
"habitat_storage_installed", None),
|
|
("expansion_hub", "east", "greenhouse_2", "core", "structural_mount",
|
|
"greenhouse_installed", None),
|
|
("expansion_hub", "west", "habitat_3", "core", "structural_mount",
|
|
"habitat_storage_installed", None),
|
|
("greenhouse_2", "mount", "compost_module", "mount", "structural_mount",
|
|
"greenhouse_installed", None),
|
|
("greenhouse", "distal", "greenhouse_2", "passage", "pressurized_passage",
|
|
"passage_installed", None),
|
|
("lab_module", "distal", "wugang_2", "dock", "pressurized_passage",
|
|
"topology_reconfigured", None),
|
|
]
|
|
|
|
# Remove the earlier inferred south connection; the GHC-06 terminal
|
|
# topology keeps the expansion hub's south port intentionally vacant.
|
|
cur.execute("""
|
|
DELETE FROM topology_connections tc
|
|
USING ports pa, ports pb
|
|
WHERE tc.port_a_id = pa.id AND tc.port_b_id = pb.id
|
|
AND tc.established_by_event_id = %(event)s
|
|
AND (
|
|
pa.component_id = %(hub)s OR pb.component_id = %(hub)s
|
|
)
|
|
AND (
|
|
pa.component_id NOT IN (%(storage)s, %(greenhouse)s, %(habitat)s, %(compost)s)
|
|
OR pb.component_id NOT IN (%(storage)s, %(greenhouse)s, %(habitat)s, %(compost)s)
|
|
)
|
|
""", {
|
|
"event": events["passage_installed"],
|
|
"hub": components["expansion_hub"],
|
|
"storage": components["storage_module"],
|
|
"greenhouse": components["greenhouse_2"],
|
|
"habitat": components["habitat_3"],
|
|
"compost": components["compost_module"],
|
|
})
|
|
|
|
# Close the old greenhouse-to-Wugang-2 passage when the lander moves
|
|
# to the laboratory cabin's distal interface.
|
|
cur.execute("""
|
|
UPDATE topology_connections tc SET
|
|
valid_to = %(at)s,
|
|
ended_by_event_id = %(event)s,
|
|
updated_at = now()
|
|
FROM ports pa, ports pb
|
|
WHERE tc.port_a_id = pa.id AND tc.port_b_id = pb.id
|
|
AND tc.valid_to IS NULL
|
|
AND ((pa.component_id = %(greenhouse)s AND pb.component_id = %(wugang)s)
|
|
OR (pa.component_id = %(wugang)s AND pb.component_id = %(greenhouse)s))
|
|
""", {
|
|
"at": event_times["topology_reconfigured"],
|
|
"event": events["topology_reconfigured"],
|
|
"greenhouse": components["greenhouse"],
|
|
"wugang": components["wugang_2"],
|
|
})
|
|
for comp_a, port_a, comp_b, port_b, kind, start_key, end_key in connection_specs:
|
|
port_a_id = ports[(comp_a, port_a)]
|
|
port_b_id = ports[(comp_b, port_b)]
|
|
cur.execute("""
|
|
SELECT id FROM topology_connections
|
|
WHERE base_id = %(base)s
|
|
AND port_a_id = %(port_a)s AND port_b_id = %(port_b)s
|
|
AND established_by_event_id = %(event)s
|
|
""", {
|
|
"base": base_id, "port_a": port_a_id, "port_b": port_b_id,
|
|
"event": events[start_key],
|
|
})
|
|
connection = cur.fetchone()
|
|
params = {
|
|
"base": base_id, "port_a": port_a_id, "port_b": port_b_id,
|
|
"kind": kind, "event": events[start_key],
|
|
"end_event": events[end_key] if end_key else None,
|
|
"end_at": event_times[end_key] if end_key else None,
|
|
}
|
|
if connection:
|
|
cur.execute("""
|
|
UPDATE topology_connections SET
|
|
connection_type = %(kind)s,
|
|
valid_to = %(end_at)s,
|
|
ended_by_event_id = %(end_event)s,
|
|
updated_at = now()
|
|
WHERE id = %(id)s
|
|
""", {**params, "id": connection["id"]})
|
|
else:
|
|
cur.execute("""
|
|
INSERT INTO topology_connections
|
|
(id, base_id, port_a_id, port_b_id, connection_type,
|
|
valid_from, valid_to, established_by_event_id, ended_by_event_id)
|
|
VALUES (
|
|
gen_random_uuid(), %(base)s, %(port_a)s, %(port_b)s, %(kind)s,
|
|
(SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
%(end_at)s,
|
|
%(event)s, %(end_event)s)
|
|
""", params)
|
|
|
|
# Lifecycle states for newly introduced components.
|
|
for key, _, event_key, _, _, _ in component_specs:
|
|
cur.execute("""
|
|
SELECT id FROM component_states
|
|
WHERE component_id = %(component)s AND state_kind = 'lifecycle'
|
|
AND started_by_event_id = %(event)s
|
|
""", {"component": components[key], "event": events[event_key]})
|
|
if not cur.fetchone():
|
|
cur.execute("""
|
|
INSERT INTO component_states
|
|
(id, component_id, state_kind, state_value, valid_from,
|
|
valid_to, started_by_event_id, ended_by_event_id)
|
|
VALUES (
|
|
gen_random_uuid(), %(component)s, 'lifecycle', 'installed',
|
|
(SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
%(end_at)s,
|
|
%(event)s, %(end_event)s)
|
|
""", {
|
|
"component": components[key], "event": events[event_key],
|
|
"end_event": events["lander_departed"] if key == "wugang_3" else None,
|
|
"end_at": event_times["lander_departed"] if key == "wugang_3" else None,
|
|
})
|
|
|
|
# Change the reactor state from 30% to 35% without creating overlaps.
|
|
cur.execute("""
|
|
UPDATE component_states SET
|
|
valid_to = (SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
ended_by_event_id = %(event)s,
|
|
updated_at = now()
|
|
WHERE component_id = %(component)s AND state_kind = 'operation'
|
|
AND valid_to IS NULL AND details @> '{"powerPercent": 30}'
|
|
""", {"component": components["nuclear_reactor"], "event": events["reactor_35"]})
|
|
cur.execute("""
|
|
SELECT id FROM component_states
|
|
WHERE component_id = %(component)s AND state_kind = 'operation'
|
|
AND started_by_event_id = %(event)s
|
|
""", {"component": components["nuclear_reactor"], "event": events["reactor_35"]})
|
|
if not cur.fetchone():
|
|
cur.execute("""
|
|
INSERT INTO component_states
|
|
(id, component_id, state_kind, state_value, details,
|
|
valid_from, started_by_event_id)
|
|
VALUES (
|
|
gen_random_uuid(), %(component)s, 'operation', 'operational',
|
|
'{"powerPercent": 35}',
|
|
(SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
%(event)s)
|
|
""", {"component": components["nuclear_reactor"], "event": events["reactor_35"]})
|
|
|
|
# The version carries the headline engineering property used by the
|
|
# card; create a new temporal version so older snapshots still show 30%.
|
|
cur.execute("""
|
|
UPDATE component_versions SET
|
|
valid_to = (SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
ended_by_event_id = %(event)s,
|
|
updated_at = now()
|
|
WHERE component_id = %(component)s AND valid_to IS NULL
|
|
AND started_by_event_id <> %(event)s
|
|
""", {"component": components["nuclear_reactor"], "event": events["reactor_35"]})
|
|
cur.execute("""
|
|
SELECT id FROM component_versions
|
|
WHERE component_id = %(component)s AND started_by_event_id = %(event)s
|
|
""", {"component": components["nuclear_reactor"], "event": events["reactor_35"]})
|
|
if not cur.fetchone():
|
|
cur.execute("""
|
|
INSERT INTO component_versions
|
|
(id, component_id, display_name, summary, properties,
|
|
valid_from, started_by_event_id)
|
|
VALUES (
|
|
gen_random_uuid(), %(component)s, '核反应堆',
|
|
'广寒基地主电力来源,GHC-06 后以百分之三十五功率运行',
|
|
'{"powerPercent": 35}',
|
|
(SELECT effective_at FROM mission_events WHERE id = %(event)s),
|
|
%(event)s)
|
|
""", {"component": components["nuclear_reactor"], "event": events["reactor_35"]})
|
|
|
|
print("GHC-06 topology seed complete")
|
|
print(f" mission: GHC-06")
|
|
print(f" events: {len(events)}")
|
|
print(f" new components: {len(component_specs)}")
|
|
print(f" connections: {len(connection_specs)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
seed()
|