184 lines
7.5 KiB
Python
184 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the IGF illustration figure (fig_igf.pdf).
|
|
|
|
Implements the paper's equations verbatim on the synthetic site of sitegeom.py:
|
|
|
|
eq:compact_kernel K(d; rho, m) = (1 - d/rho)^m for d < rho, else 0
|
|
eq:source_field U_S(z) = alpha_S * sum_{xi in S} K(|z-xi|; rho_S, m_S) ds_xi
|
|
eq:field_park U_park = U_e - U_p + U_x
|
|
eq:field_exit U_exit = U_e + U_p - U_x
|
|
eq:field_bounds U_g^max = 2 alpha_g rho_g / (m_g + 1)
|
|
U_e^max = alpha_e (2 rho_e / lbar_e + 1)
|
|
eq:field_normalize Uhat = (U + U_g^max) / (U_e^max + 2 U_g^max), clipped [0,1]
|
|
|
|
For S_e the paper takes ds_xi == 1 (vertex counting); S_p and S_x are
|
|
equal-arc-length samples carrying their true line element. Both are reproduced.
|
|
|
|
Construction illustration on a synthetic site -- not an experimental result.
|
|
"""
|
|
import numpy as np
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.patches import Polygon as MplPolygon
|
|
from matplotlib.path import Path
|
|
|
|
import sitegeom as sg
|
|
|
|
# Field parameters; alpha_e is set so that eq:alpha_dominance holds.
|
|
RHO_E, M_E, ALPHA_E = 8.0, 3.0, 1.00
|
|
RHO_G, M_G, ALPHA_G = 9.0, 3.0, 0.28
|
|
LBAR_E = 1.0 # mean boundary-vertex spacing, enters U_e^max
|
|
DS_G = 0.5 # arc-length element of the guidance-line samples
|
|
DELTA = 0.25 # field grid resolution
|
|
|
|
|
|
def kernel(d, rho, m):
|
|
out = np.zeros_like(d)
|
|
ins = d < rho
|
|
out[ins] = (1.0 - d[ins] / rho) ** m
|
|
return out
|
|
|
|
|
|
def source_field(Z, S, alpha, rho, m, ds):
|
|
U = np.zeros(Z.shape[0])
|
|
for i in range(0, S.shape[0], 256):
|
|
blk = S[i:i + 256]
|
|
d = np.linalg.norm(Z[:, None, :] - blk[None, :, :], axis=2)
|
|
U += (kernel(d, rho, m) * ds).sum(axis=1)
|
|
return alpha * U
|
|
|
|
|
|
def resample_closed(poly, step):
|
|
pts = np.vstack([poly, poly[:1]])
|
|
seg = np.diff(pts, axis=0)
|
|
L = np.hypot(seg[:, 0], seg[:, 1])
|
|
cum = np.concatenate([[0.0], np.cumsum(L)])
|
|
s = np.arange(0.0, cum[-1], step)
|
|
out = np.empty((s.size, 2))
|
|
for i, si in enumerate(s):
|
|
k = min(np.searchsorted(cum, si, side="right") - 1, len(seg) - 1)
|
|
t = (si - cum[k]) / L[k]
|
|
out[i] = pts[k] + t * seg[k]
|
|
return out
|
|
|
|
|
|
def main():
|
|
g = sg.build_centerline()
|
|
center, park_line, exit_line = g["center"], g["park_line"], g["exit_line"]
|
|
|
|
# ---- source sets -------------------------------------------------------
|
|
S_e = resample_closed(sg.SITE, LBAR_E)
|
|
S_p = sg.resample(park_line, DS_G)
|
|
S_x = sg.resample(exit_line, DS_G)
|
|
|
|
# ---- grid --------------------------------------------------------------
|
|
x0, x1 = sg.SITE[:, 0].min() - 1.0, sg.SITE[:, 0].max() + 1.0
|
|
y0, y1 = sg.SITE[:, 1].min() - 1.0, sg.SITE[:, 1].max() + 1.0
|
|
xs = np.arange(x0, x1, DELTA)
|
|
ys = np.arange(y0, y1, DELTA)
|
|
XX, YY = np.meshgrid(xs, ys)
|
|
Z = np.column_stack([XX.ravel(), YY.ravel()])
|
|
|
|
U_e = source_field(Z, S_e, ALPHA_E, RHO_E, M_E, 1.0)
|
|
U_p = source_field(Z, S_p, ALPHA_G, RHO_G, M_G, DS_G)
|
|
U_x = source_field(Z, S_x, ALPHA_G, RHO_G, M_G, DS_G)
|
|
|
|
Ug_max = 2.0 * ALPHA_G * RHO_G / (M_G + 1.0)
|
|
Ue_max = ALPHA_E * (2.0 * RHO_E / LBAR_E + 1.0)
|
|
assert Ue_max > Ug_max, "eq:alpha_dominance violated"
|
|
|
|
def norm(U):
|
|
return np.clip((U + Ug_max) / (Ue_max + 2.0 * Ug_max), 0.0, 1.0)
|
|
|
|
Up_raw, Ux_raw = U_e - U_p + U_x, U_e + U_p - U_x
|
|
U_park = norm(Up_raw).reshape(XX.shape)
|
|
U_exit = norm(Ux_raw).reshape(XX.shape)
|
|
|
|
inside = Path(sg.SITE).contains_points(Z).reshape(XX.shape)
|
|
U_parkm = np.ma.array(U_park, mask=~inside)
|
|
U_exitm = np.ma.array(U_exit, mask=~inside)
|
|
|
|
# ---- numeric checks reported to stdout ---------------------------------
|
|
print("U_g^max = %.4f U_e^max = %.4f" % (Ug_max, Ue_max))
|
|
print("Uhat_park in [%.4f, %.4f]" % (U_parkm.min(), U_parkm.max()))
|
|
print("Uhat_exit in [%.4f, %.4f]" % (U_exitm.min(), U_exitm.max()))
|
|
print("mirror residual |(U_park+U_exit) - 2 U_e| = %.2e"
|
|
% np.abs(Up_raw + Ux_raw - 2 * U_e).max())
|
|
# the park field must be lower on the park line than on the exit line
|
|
def sample(F, P):
|
|
ii = np.clip(((P[:, 0] - x0) / DELTA).astype(int), 0, len(xs) - 1)
|
|
jj = np.clip(((P[:, 1] - y0) / DELTA).astype(int), 0, len(ys) - 1)
|
|
return F[jj, ii]
|
|
print("mean Uhat_park on Lambda_park = %.4f, on Lambda_exit = %.4f"
|
|
% (sample(U_park, park_line).mean(), sample(U_park, exit_line).mean()))
|
|
print("mean Uhat_exit on Lambda_park = %.4f, on Lambda_exit = %.4f"
|
|
% (sample(U_exit, park_line).mean(), sample(U_exit, exit_line).mean()))
|
|
|
|
# A common display ceiling for both panels: the analytic bound U_e^max is a
|
|
# worst-case estimate, so the realized field only reaches ~0.34. Both
|
|
# panels share one scale, which is what makes the mirror relation visible;
|
|
# the colorbar is annotated with the true values, not rescaled to [0,1].
|
|
vmax = float(max(U_parkm.max(), U_exitm.max()))
|
|
|
|
# ---- plot --------------------------------------------------------------
|
|
plt.rcParams.update({
|
|
"font.family": "serif",
|
|
"font.serif": ["Nimbus Roman", "Times New Roman", "DejaVu Serif"],
|
|
"font.size": 7,
|
|
"mathtext.fontset": "stix",
|
|
"axes.linewidth": 0.5,
|
|
"pdf.fonttype": 42,
|
|
})
|
|
fig, axes = plt.subplots(1, 3, figsize=(7.16, 1.75),
|
|
constrained_layout=True)
|
|
|
|
ax = axes[0]
|
|
ax.add_patch(MplPolygon(sg.SITE, closed=True, facecolor="white",
|
|
edgecolor="#585858", lw=0.7, zorder=1))
|
|
ax.plot(center[:, 0], center[:, 1], color="#703A94", lw=0.9,
|
|
ls=(0, (2.2, 1.2)), label=r"$\Lambda_{\mathrm{center}}$", zorder=3)
|
|
ax.plot(park_line[:, 0], park_line[:, 1], color="#00549F", lw=1.0,
|
|
label=r"$\Lambda_{\mathrm{park}}$", zorder=3)
|
|
ax.plot(exit_line[:, 0], exit_line[:, 1], color="#C44E0A", lw=1.0,
|
|
label=r"$\Lambda_{\mathrm{exit}}$", zorder=3)
|
|
ax.plot(*sg.LOAD_POSE, marker="o", ms=2.8, color="#222222", zorder=4)
|
|
ax.annotate(r"$\boldsymbol{\eta}_{\mathrm{load}}$", sg.LOAD_POSE,
|
|
textcoords="offset points", xytext=(2.5, 2.5), fontsize=6)
|
|
ax.legend(loc="lower right", fontsize=5.2, frameon=False,
|
|
handlelength=1.4, borderaxespad=0.1, labelspacing=0.2)
|
|
ax.set_title(r"(a) boundary and guidance sources", fontsize=6.6, pad=2)
|
|
|
|
for ax, U, name in ((axes[1], U_parkm, r"(b) $\hat U_{\mathrm{park}}$"),
|
|
(axes[2], U_exitm, r"(c) $\hat U_{\mathrm{exit}}$")):
|
|
im = ax.pcolormesh(XX, YY, U, cmap="viridis", vmin=0.0, vmax=vmax,
|
|
shading="auto", rasterized=True)
|
|
ax.contour(XX, YY, U.filled(np.nan),
|
|
levels=np.linspace(0.06, vmax * 0.92, 5),
|
|
colors="white", linewidths=0.25, alpha=0.7)
|
|
ax.add_patch(MplPolygon(sg.SITE, closed=True, facecolor="none",
|
|
edgecolor="#585858", lw=0.7))
|
|
ax.set_title(name, fontsize=6.6, pad=2)
|
|
|
|
cb = fig.colorbar(im, ax=axes[2], fraction=0.046, pad=0.02)
|
|
cb.set_ticks([0.0, vmax / 2.0, vmax])
|
|
cb.set_ticklabels(["0", "%.2f" % (vmax / 2.0), "%.2f" % vmax])
|
|
cb.ax.tick_params(labelsize=5.4, width=0.4, length=1.8)
|
|
cb.outline.set_linewidth(0.4)
|
|
|
|
for ax in axes:
|
|
ax.set_aspect("equal")
|
|
ax.set_xlim(x0, x1)
|
|
ax.set_ylim(y0, y1)
|
|
ax.set_xticks([])
|
|
ax.set_yticks([])
|
|
for sp in ax.spines.values():
|
|
sp.set_visible(False)
|
|
|
|
fig.savefig("fig_igf.pdf", dpi=600)
|
|
print("wrote fig_igf.pdf")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|