Checkpoint CM95 PBF production code and docs for Linux migration

This commit is contained in:
kaotei
2026-09-11 18:10:56 +08:00
parent 0ca1f63320
commit b52b3447cf
116 changed files with 55495 additions and 2 deletions

View File

@@ -0,0 +1,304 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CM95 唐津 AOI Presence Diagnostic v2
只读诊断:
1) chart_cell bbox 是否覆盖唐津 z10/881/410;
2) chart_feature bbox 是否命中;
3) 实际 Point/Line/Area geometry 是否通过 MBRIntersects 命中;
4) Sounding point 是否命中;
5) 输出离唐津中心最近的 Cell / Feature,帮助判断 Dataset 1 是否根本没有唐津细节数据。
不修改数据库。
"""
from __future__ import annotations
import argparse
import getpass
import json
import math
from pathlib import Path
from datetime import datetime
import pymysql
from pymysql.cursors import DictCursor
LON_MIN = 129.7265625
LAT_MIN = 33.4314413355753
LON_MAX = 130.078125
LAT_MAX = 33.7243396617476
CENTER_LON = (LON_MIN + LON_MAX) / 2
CENTER_LAT = (LAT_MIN + LAT_MAX) / 2
def args():
p = argparse.ArgumentParser()
p.add_argument("--host", default="127.0.0.1")
p.add_argument("--port", type=int, default=3306)
p.add_argument("--user", default="root")
p.add_argument("--password")
p.add_argument("--database", default="cm93_chart")
p.add_argument("--dataset-id", type=int, default=1)
p.add_argument("--output", default="cm95_karatsu_presence_diagnostic_v2.json")
return p.parse_args()
def fetch(cur, sql, params=()):
cur.execute(sql, params)
return list(cur.fetchall())
def scalar(cur, sql, params=()):
cur.execute(sql, params)
r = cur.fetchone()
return None if not r else next(iter(r.values()))
def main():
a = args()
if a.password is None:
a.password = getpass.getpass("MySQL password: ")
conn = pymysql.connect(
host=a.host, port=a.port, user=a.user, password=a.password,
database=a.database, charset="utf8mb4",
cursorclass=DictCursor, autocommit=False,
connect_timeout=20, read_timeout=900,
)
report = {
"version": "v2",
"generated_at": datetime.now().isoformat(timespec="seconds"),
"dataset_id": a.dataset_id,
"target": {
"tile": "z10/881/410",
"bbox": [LON_MIN, LAT_MIN, LON_MAX, LAT_MAX],
"center": [CENTER_LON, CENTER_LAT],
},
"read_only": True,
}
poly_wkt = (
f"POLYGON(({LON_MIN} {LAT_MIN},"
f"{LON_MAX} {LAT_MIN},"
f"{LON_MAX} {LAT_MAX},"
f"{LON_MIN} {LAT_MAX},"
f"{LON_MIN} {LAT_MIN}))"
)
try:
with conn.cursor() as cur:
try:
cur.execute("SET SESSION TRANSACTION READ ONLY")
cur.execute("START TRANSACTION READ ONLY")
except Exception:
cur.execute("START TRANSACTION")
report["dataset"] = fetch(cur, """
SELECT id,name,source_type,source_root,parser_version,normalized_srid
FROM chart_dataset WHERE id=%s
""", (a.dataset_id,))
report["global_feature_bbox"] = fetch(cur, """
SELECT
MIN(bbox_min_lon) min_lon,
MIN(bbox_min_lat) min_lat,
MAX(bbox_max_lon) max_lon,
MAX(bbox_max_lat) max_lat,
SUM(bbox_min_lon IS NOT NULL) bbox_rows,
COUNT(*) total_rows
FROM chart_feature
WHERE dataset_id=%s
""", (a.dataset_id,))
report["global_cell_bbox"] = fetch(cur, """
SELECT
MIN(bbox_min_lon) min_lon,
MIN(bbox_min_lat) min_lat,
MAX(bbox_max_lon) max_lon,
MAX(bbox_max_lat) max_lat,
SUM(bbox_min_lon IS NOT NULL) bbox_rows,
COUNT(*) total_rows
FROM chart_cell
WHERE dataset_id=%s
""", (a.dataset_id,))
bbox_params = (
a.dataset_id,
LON_MIN, LON_MAX, LAT_MIN, LAT_MAX,
)
report["cell_bbox_overlap"] = fetch(cur, """
SELECT id,source_relative_path,scale_code,native_scale,
bbox_min_lon,bbox_min_lat,bbox_max_lon,bbox_max_lat,
feature_count
FROM chart_cell
WHERE dataset_id=%s
AND bbox_max_lon >= %s
AND bbox_min_lon <= %s
AND bbox_max_lat >= %s
AND bbox_min_lat <= %s
ORDER BY native_scale,source_relative_path
""", bbox_params)
report["feature_bbox_count"] = scalar(cur, """
SELECT COUNT(*)
FROM chart_feature
WHERE dataset_id=%s
AND bbox_max_lon >= %s
AND bbox_min_lon <= %s
AND bbox_max_lat >= %s
AND bbox_min_lat <= %s
""", bbox_params)
# Direct actual geometry checks. These bypass chart_feature bbox metadata.
direct = {}
for kind, table in [
("point", "chart_point_geometry"),
("line", "chart_line_geometry"),
("area", "chart_area_geometry"),
]:
try:
direct[kind] = scalar(cur, f"""
SELECT COUNT(*)
FROM {table} g
JOIN chart_feature f ON f.id=g.feature_id
WHERE f.dataset_id=%s
AND MBRIntersects(
g.geom,
ST_GeomFromText(%s,4326)
)
""", (a.dataset_id, poly_wkt))
except Exception as e:
direct[kind] = {"error": str(e)}
report["direct_geometry_mbr_counts"] = direct
try:
report["sounding_bbox_count"] = scalar(cur, """
SELECT COUNT(*)
FROM chart_sounding_point s
JOIN chart_feature f ON f.id=s.feature_id
WHERE f.dataset_id=%s
AND ST_X(s.geom) BETWEEN %s AND %s
AND ST_Y(s.geom) BETWEEN %s AND %s
""", (
a.dataset_id,
LON_MIN, LON_MAX, LAT_MIN, LAT_MAX,
))
except Exception as e:
report["sounding_bbox_count"] = {"error": str(e)}
# Nearest cell by bbox center, excluding pathological world-scale cells when possible.
report["nearest_cells"] = fetch(cur, """
SELECT id,source_relative_path,scale_code,native_scale,
bbox_min_lon,bbox_min_lat,bbox_max_lon,bbox_max_lat,
feature_count,
SQRT(
POW(((bbox_min_lon+bbox_max_lon)/2)-%s,2) +
POW(((bbox_min_lat+bbox_max_lat)/2)-%s,2)
) AS center_distance_deg
FROM chart_cell
WHERE dataset_id=%s
AND bbox_min_lon IS NOT NULL
AND bbox_min_lat IS NOT NULL
AND bbox_max_lon IS NOT NULL
AND bbox_max_lat IS NOT NULL
AND (bbox_max_lon-bbox_min_lon) < 20
AND (bbox_max_lat-bbox_min_lat) < 20
ORDER BY center_distance_deg ASC
LIMIT 30
""", (CENTER_LON, CENTER_LAT, a.dataset_id))
report["nearest_features"] = fetch(cur, """
SELECT f.id,f.cell_id,c.source_relative_path,
f.source_record_index,f.object_class_id,f.object_class,
f.geometry_type,f.scale_code,f.native_scale,
f.bbox_min_lon,f.bbox_min_lat,f.bbox_max_lon,f.bbox_max_lat,
SQRT(
POW(((f.bbox_min_lon+f.bbox_max_lon)/2)-%s,2) +
POW(((f.bbox_min_lat+f.bbox_max_lat)/2)-%s,2)
) AS center_distance_deg
FROM chart_feature f
JOIN chart_cell c ON c.id=f.cell_id
WHERE f.dataset_id=%s
AND f.bbox_min_lon IS NOT NULL
AND f.bbox_min_lat IS NOT NULL
AND f.bbox_max_lon IS NOT NULL
AND f.bbox_max_lat IS NOT NULL
ORDER BY center_distance_deg ASC
LIMIT 50
""", (CENTER_LON, CENTER_LAT, a.dataset_id))
# Coarse 2-degree spatial occupancy by feature bbox center.
report["coarse_feature_grid"] = fetch(cur, """
SELECT
FLOOR(((bbox_min_lon+bbox_max_lon)/2)/2)*2 AS lon_bin,
FLOOR(((bbox_min_lat+bbox_max_lat)/2)/2)*2 AS lat_bin,
COUNT(*) AS feature_count
FROM chart_feature
WHERE dataset_id=%s
AND bbox_min_lon IS NOT NULL
AND bbox_min_lat IS NOT NULL
AND bbox_max_lon IS NOT NULL
AND bbox_max_lat IS NOT NULL
AND ((bbox_min_lon+bbox_max_lon)/2) BETWEEN 120 AND 160
AND ((bbox_min_lat+bbox_max_lat)/2) BETWEEN 20 AND 50
GROUP BY lon_bin,lat_bin
ORDER BY lat_bin,lon_bin
""", (a.dataset_id,))
conn.rollback()
finally:
conn.close()
# Derive diagnosis.
fcnt = report.get("feature_bbox_count")
d = report.get("direct_geometry_mbr_counts", {})
direct_sum = 0
direct_errors = []
for k, v in d.items():
if isinstance(v, int):
direct_sum += v
else:
direct_errors.append((k, v))
if isinstance(fcnt, int) and fcnt > 0:
diagnosis = "AOI_PRESENT_BY_FEATURE_BBOX"
elif direct_sum > 0:
diagnosis = "AOI_PRESENT_IN_GEOMETRY_BUT_FEATURE_BBOX_QUERY_FAILED"
else:
diagnosis = "NO_CM95_GEOMETRY_FOUND_IN_KARATSU_AOI"
report["diagnosis"] = diagnosis
report["direct_geometry_total"] = direct_sum
report["direct_geometry_errors"] = direct_errors
out = Path(a.output).resolve()
out.write_text(json.dumps(report, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
print("CM95 唐津 AOI Presence Diagnostic v2")
print("=" * 72)
print("Target bbox :", [LON_MIN, LAT_MIN, LON_MAX, LAT_MAX])
print("Cell bbox overlap :", len(report["cell_bbox_overlap"]))
print("Feature bbox count:", report["feature_bbox_count"])
print("Direct geometry :", report["direct_geometry_mbr_counts"])
print("Soundings :", report["sounding_bbox_count"])
print("Diagnosis :", diagnosis)
print("")
print("Nearest cells:")
for r in report["nearest_cells"][:10]:
print(
f" {r['source_relative_path']} scale={r['native_scale']} "
f"bbox=({r['bbox_min_lon']},{r['bbox_min_lat']},"
f"{r['bbox_max_lon']},{r['bbox_max_lat']}) "
f"dist={float(r['center_distance_deg']):.3f}°"
)
print("")
print("Output:", out)
print("把 JSON 上传给 ChatGPT。")
if __name__ == "__main__":
main()