图表换正式数据
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
import csv, json, math
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Arc, Circle, FancyArrowPatch, Polygon, Rectangle, Wedge
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIG = ROOT / "docs" / "figures"
|
||||
FIG.mkdir(parents=True, exist_ok=True)
|
||||
INK = "#234C70"
|
||||
GRAY = "#676767"
|
||||
LIGHT = "#9DB7CA"
|
||||
PALE = "#D9E4EC"
|
||||
GOLD = "#B58B3A"
|
||||
GREEN = "#4E7A55"
|
||||
plt.rcParams.update({"font.sans-serif": ["DejaVu Sans"], "axes.unicode_minus": False,
|
||||
"figure.dpi": 120, "savefig.dpi": 300, "axes.edgecolor": GRAY,
|
||||
"axes.labelcolor": "#222222", "text.color": "#222222"})
|
||||
|
||||
def save(fig, name):
|
||||
fig.savefig(FIG / name, bbox_inches="tight", facecolor="white")
|
||||
plt.close(fig)
|
||||
|
||||
def load_formal():
|
||||
rows = []
|
||||
path = ROOT / "docs" / "paper" / "data" / "formal-results.csv"
|
||||
with path.open(encoding="utf-8-sig") as f:
|
||||
for row in csv.DictReader(f):
|
||||
for key in ("problem_no", "formal_index", "cleared_jammer_count",
|
||||
"measure_accepted_count", "channel_switch_count",
|
||||
"clear_failure_count"):
|
||||
row[key] = int(row[key])
|
||||
for key in ("virtual_time_s", "program_run_duration_s",
|
||||
"inferred_movement_time_s"):
|
||||
row[key] = float(row[key])
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
def fig_formal_total():
|
||||
rows = load_formal()
|
||||
labels = ["Q%d-%d" % (r["problem_no"], r["formal_index"]) for r in rows]
|
||||
total = [r["virtual_time_s"] for r in rows]
|
||||
runtime = [r["program_run_duration_s"] for r in rows]
|
||||
colors = [INK, LIGHT, PALE, GOLD, "#C98963", GREEN]
|
||||
fig, axes = plt.subplots(1, 2, figsize=(10.5, 3.6))
|
||||
for ax, vals, title, unit in ((axes[0], total, "Total virtual time", "s"),
|
||||
(axes[1], runtime, "Program runtime", "s")):
|
||||
bars = ax.barh(labels[::-1], vals[::-1], color=colors[::-1])
|
||||
ax.set_xlabel(title + " / " + unit)
|
||||
ax.grid(axis="x", color="#E5E5E5", linewidth=0.7)
|
||||
ax.set_axisbelow(True)
|
||||
for bar, val in zip(bars, vals[::-1]):
|
||||
ax.text(bar.get_width() + max(vals) * 0.015,
|
||||
bar.get_y() + bar.get_height() / 2,
|
||||
"%.3f" % val, va="center", fontsize=8)
|
||||
ax.margins(x=0.18)
|
||||
fig.suptitle("Direct metrics of three formal tests", fontsize=11)
|
||||
save(fig, "formal-total-time.png")
|
||||
|
||||
|
||||
def fig_formal_time_bars():
|
||||
rows = load_formal()
|
||||
labels = ["Q%d-%d" % (r["problem_no"], r["formal_index"]) for r in rows]
|
||||
move = np.array([r["inferred_movement_time_s"] for r in rows])
|
||||
measure = np.array([5.0 * r["measure_accepted_count"] for r in rows])
|
||||
switch = np.array([1.0 * r["channel_switch_count"] for r in rows])
|
||||
clear = np.array([5.0 * r["cleared_jammer_count"] + 3.0 * r["clear_failure_count"] for r in rows])
|
||||
fig, ax = plt.subplots(figsize=(9.0, 3.8))
|
||||
ax.barh(labels, move, color=INK, label="Move")
|
||||
ax.barh(labels, measure, left=move, color=LIGHT, label="Measure")
|
||||
ax.barh(labels, switch, left=move + measure, color=GRAY, label="Switch")
|
||||
ax.barh(labels, clear, left=move + measure + switch, color=GOLD, label="Clear")
|
||||
ax.set_xlabel("Cumulative virtual time / s")
|
||||
ax.grid(axis="x", color="#E5E5E5", linewidth=0.7)
|
||||
ax.set_axisbelow(True)
|
||||
ax.legend(loc="lower right", frameon=False, ncol=4)
|
||||
save(fig, "formal-time-bars.png")
|
||||
|
||||
def box(ax, x, y, w, h, text, fc=PALE, ec=INK):
|
||||
ax.add_patch(Rectangle((x, y), w, h, facecolor=fc, edgecolor=ec, linewidth=1.0))
|
||||
ax.text(x + w/2, y + h/2, text, ha="center", va="center", fontsize=8, wrap=True)
|
||||
|
||||
|
||||
def arrow(ax, p, q, color=GRAY):
|
||||
ax.add_patch(FancyArrowPatch(p, q, arrowstyle="-|>", mutation_scale=10,
|
||||
linewidth=1.0, color=color))
|
||||
|
||||
|
||||
def fig_flow_overall():
|
||||
fig, ax = plt.subplots(figsize=(8.0, 4.8))
|
||||
ax.axis("off")
|
||||
box(ax, 0.5, 4.0, 7.0, 0.8, "L0 Problem abstraction: area, motion, timing, channels", fc="#EFEFEF")
|
||||
box(ax, 0.5, 3.0, 2.1, 0.7, "L1 Evidence\ncandidate sets")
|
||||
box(ax, 2.9, 3.0, 2.1, 0.7, "L1 Decision\ntask tables")
|
||||
box(ax, 5.3, 3.0, 2.1, 0.7, "L1 Execution\nactions")
|
||||
box(ax, 0.5, 2.0, 1.6, 0.7, "L2 Q1\ngeometry", fc="#E8EEF4")
|
||||
box(ax, 2.3, 2.0, 1.6, 0.7, "L2 Q2\nactive measure", fc="#E8EEF4")
|
||||
box(ax, 4.1, 2.0, 1.6, 0.7, "L2 Q3\nomni loop", fc="#E8EEF4")
|
||||
box(ax, 5.9, 2.0, 1.6, 0.7, "L2 Q4\nmixed loop", fc="#E8EEF4")
|
||||
box(ax, 1.4, 0.8, 5.2, 0.7, "L3 Discovery -> Localisation -> Clearance", fc="#E6F0E7", ec=GREEN)
|
||||
for px in (1.55, 3.95, 6.35):
|
||||
arrow(ax, (px, 4.0), (px, 3.7))
|
||||
for px in (1.3, 3.1, 4.9, 6.7):
|
||||
arrow(ax, (px, 2.7), (3.9, 1.5))
|
||||
ax.set_xlim(0, 8); ax.set_ylim(0, 5.2)
|
||||
save(fig, "flow-overall-architecture.png")
|
||||
|
||||
|
||||
def fig_framework():
|
||||
fig, ax = plt.subplots(figsize=(5.8, 4.2))
|
||||
ax.axis("off")
|
||||
box(ax, 0.3, 2.8, 2.0, 0.7, "Observation\nbearing / near / no-signal")
|
||||
box(ax, 3.1, 2.8, 2.0, 0.7, "Candidate update\ngeometry / grid / heading")
|
||||
box(ax, 3.1, 1.4, 2.0, 0.7, "Task selection\ncover / locate / clear")
|
||||
box(ax, 0.3, 1.4, 2.0, 0.7, "Action execution\nmove and timing")
|
||||
arrow(ax, (2.3, 3.15), (3.1, 3.15)); arrow(ax, (4.1, 2.8), (4.1, 2.1))
|
||||
arrow(ax, (3.1, 1.75), (2.3, 1.75)); arrow(ax, (1.3, 2.1), (1.3, 2.8))
|
||||
box(ax, 3.1, 0.3, 2.0, 0.6, "Exit / report", fc="#EFEFEF")
|
||||
arrow(ax, (4.1, 1.4), (4.1, 0.9))
|
||||
ax.set_xlim(0, 5.6); ax.set_ylim(0, 4.0)
|
||||
save(fig, "framework-decision-loop.png")
|
||||
|
||||
|
||||
def fig_state_machine():
|
||||
fig, ax = plt.subplots(figsize=(7.0, 2.2))
|
||||
ax.axis("off")
|
||||
xs = [(0.1, "IDLE"), (1.8, "ENTER\n5 s countdown"), (3.8, "OPEN\ninterface"),
|
||||
(5.8, "CLOSED\nexit / timeout")]
|
||||
for x, t in xs:
|
||||
box(ax, x, 0.6, 1.5, 0.8, t)
|
||||
for x in (1.6, 3.6, 5.6):
|
||||
arrow(ax, (x, 1.0), (x + 0.2, 1.0))
|
||||
ax.set_xlim(0, 7.4); ax.set_ylim(0, 2.0)
|
||||
save(fig, "steps-state-machine.png")
|
||||
|
||||
|
||||
def fig_timing_attachment():
|
||||
steps = ["step 1", "step 2", "step 3", "step 4"]
|
||||
move = np.array([100.0, 0.0, 80.0, 0.0])
|
||||
action = np.array([5.0, 5.0, 3.0, 5.0])
|
||||
switch = np.array([0.0, 1.0, 0.0, 0.0])
|
||||
fig, ax = plt.subplots(figsize=(6.0, 3.2))
|
||||
ax.bar(steps, move, color=INK, label="Move")
|
||||
ax.bar(steps, action, bottom=move, color=LIGHT, label="Measure / clear")
|
||||
ax.bar(steps, switch, bottom=move + action, color=GRAY, label="Switch")
|
||||
for i, total in enumerate(move + action + switch):
|
||||
ax.text(i, total + 2, "%.0f" % total, ha="center", fontsize=8)
|
||||
ax.set_ylabel("Virtual time / s")
|
||||
ax.legend(frameon=False)
|
||||
save(fig, "timing-attachment-example-105-111-194-199.png")
|
||||
|
||||
def clip_halfplane(poly, a, b, c):
|
||||
if len(poly) == 0:
|
||||
return poly
|
||||
out = []
|
||||
for i in range(len(poly)):
|
||||
p = poly[i]; q = poly[(i + 1) % len(poly)]
|
||||
fp = a * p[0] + b * p[1] - c
|
||||
fq = a * q[0] + b * q[1] - c
|
||||
if fp <= 1e-9:
|
||||
out.append(p)
|
||||
if (fp < -1e-9 and fq > 1e-9) or (fp > 1e-9 and fq < -1e-9):
|
||||
t = fp / (fp - fq)
|
||||
out.append(p + t * (q - p))
|
||||
return np.array(out, dtype=float)
|
||||
|
||||
|
||||
def wedge_halfplanes(sx, sy, theta_deg, delta_deg):
|
||||
theta = math.radians(theta_deg); delta = math.radians(delta_deg)
|
||||
a1 = math.sin(theta - delta); b1 = -math.cos(theta - delta)
|
||||
c1 = a1 * sx + b1 * sy
|
||||
a2 = -math.sin(theta + delta); b2 = math.cos(theta + delta)
|
||||
c2 = a2 * sx + b2 * sy
|
||||
a3 = -math.cos(theta); b3 = -math.sin(theta)
|
||||
c3 = a3 * sx + b3 * sy
|
||||
return [(a1, b1, c1), (a2, b2, c2), (a3, b3, c3)]
|
||||
|
||||
|
||||
def intersection_polygon(meas, bound=2500.0):
|
||||
poly = np.array([[-bound, -bound], [bound, -bound], [bound, bound], [-bound, bound]], dtype=float)
|
||||
for m in meas:
|
||||
for a, b, c in wedge_halfplanes(m["x"], m["y"], m["bearing_deg"], m["half_angle_deg"]):
|
||||
poly = clip_halfplane(poly, a, b, c)
|
||||
if len(poly) == 0:
|
||||
return poly
|
||||
return poly
|
||||
|
||||
|
||||
def pair_circle(p, q):
|
||||
c = (p + q) / 2.0
|
||||
r = np.linalg.norm(p - q) / 2.0
|
||||
return c, r
|
||||
|
||||
|
||||
def contains_all(c, r, pts, tol=1e-6):
|
||||
return all(np.linalg.norm(p - c) <= r + tol for p in pts)
|
||||
|
||||
|
||||
def circumcircle(p, q, r):
|
||||
ax, ay = p; bx, by = q; cx, cy = r
|
||||
d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
|
||||
if abs(d) < 1e-12:
|
||||
return None
|
||||
ux = ((ax*ax + ay*ay) * (by - cy) + (bx*bx + by*by) * (cy - ay) + (cx*cx + cy*cy) * (ay - by)) / d
|
||||
uy = ((ax*ax + ay*ay) * (cx - bx) + (bx*bx + by*by) * (ax - cx) + (cx*cx + cy*cy) * (bx - ax)) / d
|
||||
c = np.array([ux, uy])
|
||||
return c, np.linalg.norm(c - p)
|
||||
|
||||
|
||||
def min_enclosing_circle(pts):
|
||||
pts = np.asarray(pts, dtype=float)
|
||||
best_c = pts[0]; best_r = 0.0
|
||||
for i in range(len(pts)):
|
||||
for j in range(i + 1, len(pts)):
|
||||
c, r = pair_circle(pts[i], pts[j])
|
||||
if contains_all(c, r, pts) and r < best_r or (best_r == 0.0 and contains_all(c, r, pts)):
|
||||
best_c, best_r = c, r
|
||||
for i in range(len(pts)):
|
||||
for j in range(i + 1, len(pts)):
|
||||
for k in range(j + 1, len(pts)):
|
||||
cc = circumcircle(pts[i], pts[j], pts[k])
|
||||
if cc is None:
|
||||
continue
|
||||
c, r = cc
|
||||
if contains_all(c, r, pts) and r < best_r:
|
||||
best_c, best_r = c, r
|
||||
return best_c, best_r
|
||||
|
||||
|
||||
def fig_q1_counterexample():
|
||||
A = np.array([0.0, 0.0]); B = np.array([40.0, 0.0]); C = np.array([20.0, 20.0 * math.sqrt(3)])
|
||||
fig, ax = plt.subplots(figsize=(4.6, 4.2))
|
||||
ax.add_patch(Polygon([A, B, C], closed=True, fill=False, edgecolor=INK, linewidth=1.4))
|
||||
dc, dr = pair_circle(A, B); ax.add_patch(Circle(dc, dr, fill=False, edgecolor=GOLD, linestyle="--", linewidth=1.2))
|
||||
mc, mr = min_enclosing_circle(np.array([A, B, C])); ax.add_patch(Circle(mc, mr, fill=False, edgecolor=GREEN, linewidth=1.4))
|
||||
ax.scatter([A[0], B[0], C[0]], [A[1], B[1], C[1]], color=INK, s=12)
|
||||
ax.text(C[0] + 0.8, C[1] + 0.6, "C", fontsize=9)
|
||||
ax.text(-1.2, -2.0, "A", fontsize=9); ax.text(40.0, -2.0, "B", fontsize=9)
|
||||
ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE"); ax.set_xlim(-8, 52); ax.set_ylim(-8, 42)
|
||||
ax.set_xlabel("x / m"); ax.set_ylabel("y / m")
|
||||
ax.legend(["triangle", "diameter circle (r=20)", "minimum enclosing circle"], frameon=False, fontsize=7, loc="upper left")
|
||||
save(fig, "geo-q1-diameter-circle-counterexample.png")
|
||||
|
||||
|
||||
def fig_q1_meetup():
|
||||
data_path = ROOT / "examples" / "q1" / "triangle.json"
|
||||
meas = json.loads(data_path.read_text(encoding="utf-8"))["measurements"]
|
||||
poly = intersection_polygon(meas, bound=2500.0)
|
||||
if len(poly) < 3:
|
||||
poly = np.array([[0.0, 0.0], [40.0, 0.0], [40.0, 30.0], [0.0, 30.0]])
|
||||
fig, axes = plt.subplots(1, 2, figsize=(8.2, 4.0))
|
||||
ax = axes[0]
|
||||
for m in meas:
|
||||
x, y = m["x"], m["y"]; th = math.radians(m["bearing_deg"]); d = math.radians(m["half_angle_deg"])
|
||||
for sgn in (-1.0, 1.0):
|
||||
ang = th + sgn * d
|
||||
ray = np.array([x, y]) + 3600.0 * np.array([math.cos(ang), math.sin(ang)])
|
||||
ax.plot([x, ray[0]], [y, ray[1]], color=LIGHT, linewidth=0.8)
|
||||
ax.scatter([x], [y], color=GRAY, s=14)
|
||||
ax.text(x + 40, y + 40, "S", fontsize=8)
|
||||
ax.add_patch(Polygon(poly, closed=True, facecolor=PALE, edgecolor=INK, linewidth=1.4))
|
||||
ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE")
|
||||
ax.set_xlim(-1200, 1000); ax.set_ylim(-1000, 1100)
|
||||
ax.set_xlabel("x / m"); ax.set_ylabel("y / m")
|
||||
ax.set_title("Wedge intersection", fontsize=9)
|
||||
ax2 = axes[1]
|
||||
ax2.add_patch(Polygon(poly, closed=True, facecolor=PALE, edgecolor=INK, linewidth=1.4))
|
||||
c, r = min_enclosing_circle(poly)
|
||||
d = max(np.linalg.norm(poly[i] - poly[j]) for i in range(len(poly)) for j in range(i + 1, len(poly)))
|
||||
dc, dr = pair_circle(poly[0], poly[np.argmax(np.linalg.norm(poly - poly[0], axis=1))])
|
||||
ax2.add_patch(Circle(dc, dr, fill=False, edgecolor=GOLD, linestyle="--", linewidth=1.1))
|
||||
ax2.add_patch(Circle(c, r, fill=False, edgecolor=GREEN, linewidth=1.3))
|
||||
ax2.scatter(poly[:, 0], poly[:, 1], color=INK, s=10)
|
||||
lo = poly.min(axis=0) - 4; hi = poly.max(axis=0) + 4
|
||||
ax2.set_xlim(lo[0], hi[0]); ax2.set_ylim(lo[1], hi[1])
|
||||
ax2.set_aspect("equal"); ax2.grid(True, color="#EEEEEE")
|
||||
ax2.set_title("Zoom: diameter vs. enclosing circle", fontsize=9)
|
||||
ax2.set_xlabel("x / m"); ax2.set_ylabel("y / m")
|
||||
save(fig, "geo-q1-convex-intersection-zoom.png")
|
||||
|
||||
def fig_q2_safe_disk():
|
||||
delta = math.radians(1.0); rp = 1500.0; a = rp / (2.0 * math.cos(delta)); safe = 1000.0 - a
|
||||
fig, axes = plt.subplots(1, 2, figsize=(8.0, 3.6))
|
||||
ax = axes[0]
|
||||
ax.plot([0, rp * math.cos(delta)], [0, rp * math.sin(delta)], color=GRAY, linewidth=0.8)
|
||||
ax.plot([0, rp * math.cos(delta)], [0, -rp * math.sin(delta)], color=GRAY, linewidth=0.8)
|
||||
ax.add_patch(Circle((a, 0), a, fill=False, edgecolor=LIGHT, linestyle="--", linewidth=1.2))
|
||||
ax.add_patch(Circle((a, 0), safe, facecolor=PALE, edgecolor=INK, linewidth=1.2, alpha=0.8))
|
||||
ax.scatter([0], [0], color=GRAY, s=12); ax.scatter([a], [0], color=INK, s=12)
|
||||
ax.text(a + 20, 20, "c", fontsize=9); ax.text(10, 15, "s", fontsize=9)
|
||||
ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE")
|
||||
ax.set_xlim(-80, rp + 80); ax.set_ylim(-320, 320)
|
||||
ax.set_xlabel("x / m"); ax.set_ylabel("y / m"); ax.set_title("Truncated wedge and safe disk", fontsize=9)
|
||||
ax = axes[1]
|
||||
ax.plot([0, 0], [0, 1], color=GRAY, linewidth=2)
|
||||
ax.plot([0, 1], [0, 0], color=INK, linewidth=4)
|
||||
ax.text(0.5, -0.08, "a = 1500/(2cos delta)", ha="center", fontsize=8)
|
||||
ax.text(0.5, 0.55, "safe radius = 1000 - a", ha="center", fontsize=8)
|
||||
ax.axis("off")
|
||||
save(fig, "geo-q2-guaranteed-reception-disk.png")
|
||||
|
||||
|
||||
def fig_q3_coverage():
|
||||
a = 1159.7; R = 1800.0
|
||||
fig, ax = plt.subplots(figsize=(5.2, 5.0))
|
||||
ax.add_patch(Circle((0, 0), R, fill=False, edgecolor=INK, linewidth=1.4))
|
||||
stations = [(0.0, 0.0)] + [(a * math.cos(k * math.pi / 3.0), a * math.sin(k * math.pi / 3.0)) for k in range(6)]
|
||||
for x, y in stations:
|
||||
ax.add_patch(Circle((x, y), 1000.0, facecolor=PALE, edgecolor=LIGHT, linewidth=0.7, alpha=0.35))
|
||||
ax.scatter([x], [y], color=INK, s=16)
|
||||
ax.add_patch(Circle((0, 0), a, fill=False, edgecolor=GRAY, linestyle="--", linewidth=1.0))
|
||||
ax.text(0, 0, "O", fontsize=9, ha="right", va="top"); ax.text(a, 0, "a", fontsize=9, ha="center", va="bottom")
|
||||
ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE"); ax.set_xlim(-2000, 2000); ax.set_ylim(-2000, 2000)
|
||||
ax.set_xlabel("x / m"); ax.set_ylabel("y / m"); ax.set_title("Seven-point coverage", fontsize=9)
|
||||
save(fig, "geo-q3-seven-point-coverage.png")
|
||||
|
||||
|
||||
def fig_q4_plan():
|
||||
data = json.loads((ROOT / "src" / "mcm26b" / "guarantee" / "plans" / "q4_mincover_v2.json").read_text(encoding="utf-8"))
|
||||
pts = np.array(data["points"], dtype=float)
|
||||
fig, ax = plt.subplots(figsize=(5.4, 5.0))
|
||||
ax.add_patch(Circle((0, 0), 1800.0, facecolor=PALE, edgecolor=INK, linewidth=1.1, alpha=0.45))
|
||||
ax.scatter(pts[:, 0], pts[:, 1], color=INK, s=18, label="31 stations")
|
||||
ax.scatter([0], [0], color=GOLD, marker="*", s=50, label="origin")
|
||||
ax.arrow(1200, 1200, 450, 0, head_width=70, head_length=100, color=GREEN, length_includes_head=True)
|
||||
ax.add_patch(Wedge((1200, 1200), 650, -90, 90, facecolor=GREEN, alpha=0.10, edgecolor=GREEN, linewidth=0.8))
|
||||
ax.text(1250, 1250, "forward half-disk", fontsize=8)
|
||||
ax.set_aspect("equal"); ax.grid(True, color="#EEEEEE"); ax.set_xlim(-2800, 2800); ax.set_ylim(-2800, 2800)
|
||||
ax.set_xlabel("x / m"); ax.set_ylabel("y / m"); ax.legend(frameon=False, fontsize=8, loc="lower right")
|
||||
save(fig, "geo-q4-minimum-cover-31-points.png")
|
||||
|
||||
|
||||
def main():
|
||||
fig_formal_total()
|
||||
fig_formal_time_bars()
|
||||
fig_flow_overall()
|
||||
fig_framework()
|
||||
fig_state_machine()
|
||||
fig_timing_attachment()
|
||||
fig_q1_counterexample()
|
||||
fig_q1_meetup()
|
||||
fig_q2_safe_disk()
|
||||
fig_q3_coverage()
|
||||
fig_q4_plan()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user