You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1070 lines
37 KiB
1070 lines
37 KiB
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Compute port power from time-modulated modal fields sampled at triangle quadrature points.
|
|
|
|
Inputs (in working dir):
|
|
- computeDGTD.log : contains PERFORMING INFORMATION + PORT line (see regex below)
|
|
- port_inc_field.csv : columns:
|
|
exc_face_id, local_face_idx, quad_idx,
|
|
x1,y1,z1, x2,y2,z2, x3,y3,z3, # triangle vertices (meters)
|
|
nx,ny,nz, # QUADRATURE-POINT coordinates (meters)
|
|
Et_x,Et_y,Et_z, # modal tangential E at quad (shape only)
|
|
Ht_x,Ht_y,Ht_z # modal tangential H at quad (shape only)
|
|
|
|
Outputs:
|
|
- port_power_spectrum.csv/.png : per-frequency incident power (W) vs f
|
|
- port_power_time_trace.csv : instantaneous P(t) (W) and time-axis
|
|
- console print of <P(t)> and sum P_k consistency check
|
|
|
|
Notes:
|
|
- If you want TEM-consistent amplitudes, set Hmag = Emag / Zc instead of Hmag = Emag.
|
|
- Ensure quad_idx ordering matches your 6/9-pt rule tables (it does in this script).
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import re
|
|
import numpy as np
|
|
import pandas as pd
|
|
import matplotlib.pyplot as plt
|
|
import fnmatch
|
|
import os
|
|
import os
|
|
os.environ["MPLBACKEND"] = "Agg" # must be set before importing pyplot
|
|
|
|
import matplotlib
|
|
matplotlib.use("Agg", force=True) # belt-and-suspenders
|
|
import matplotlib.pyplot as plt
|
|
|
|
import imageio.v2 as imageio
|
|
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (enables 3D)
|
|
from matplotlib import cm
|
|
|
|
|
|
|
|
# =========================
|
|
# Config / paths
|
|
# =========================
|
|
LOG_PATH = Path("computeDGTD.log")
|
|
CSV_PATH = Path("port_inc_field.csv")
|
|
PROBE_FOLDER = Path("./PROBES")
|
|
SAVE_PREFIX = "port"
|
|
|
|
# =========================
|
|
# Constants
|
|
# =========================
|
|
Pi = np.pi
|
|
c0 = 299792458.0 # m/s
|
|
|
|
# =========================
|
|
# Parse computeDGTD.log
|
|
# =========================
|
|
def extract_selected_log_values(filepath: Path):
|
|
targets = {
|
|
"Final Time(sec)": None,
|
|
"dt_sample": None,
|
|
"tsPerSampling": None
|
|
}
|
|
with filepath.open("r") as f:
|
|
lines = f.readlines()
|
|
|
|
in_block = False
|
|
for line in lines:
|
|
if "==========================================" in line and "PERFORMING INFORMATION" in line:
|
|
in_block = True
|
|
continue
|
|
if in_block and "==========================================" in line:
|
|
break
|
|
|
|
for key in targets:
|
|
if key in line:
|
|
m = re.search(r'=\s*([\d.eE+-]+)', line)
|
|
if m:
|
|
val = float(m.group(1))
|
|
if key == "tsPerSampling":
|
|
targets[key] = int(val)
|
|
else:
|
|
targets[key] = val
|
|
|
|
return {
|
|
"FinalTime": targets["Final Time(sec)"],
|
|
"dt_sample": targets["dt_sample"],
|
|
"tsPerSampling": targets["tsPerSampling"],
|
|
}
|
|
|
|
PORT_RE = re.compile(
|
|
r"PORT:\s*ro=\[(?P<ro>[^]]+)\],\s*r1=\[(?P<r1>[^]]+)\],\s*Z=(?P<Z>[+\-0-9.eE]+),\s*\|E\|=(?P<Emag>[+\-0-9.eE]+),\s*Flag=(?P<Flag>\d+),\s*Tdist=(?P<Tdist>\d+),\s*f=(?P<f>[+\-0-9.eE]+),\s*t0=(?P<t0>[+\-0-9.eE]+),\s*tau=(?P<tau>[+\-0-9.eE]+)"
|
|
)
|
|
|
|
def _parse_bracket_vec(s: str):
|
|
parts = [float(p) for p in s.split(",")]
|
|
if len(parts) != 3:
|
|
raise ValueError(f"Expected 3 components in vector, got: {s}")
|
|
return np.array(parts, dtype=float)
|
|
|
|
def parse_port_from_log(log_path: Path):
|
|
with log_path.open("r") as f:
|
|
for line in f:
|
|
if "PORT:" in line:
|
|
m = PORT_RE.search(line)
|
|
if not m:
|
|
continue
|
|
ro = _parse_bracket_vec(m.group("ro"))
|
|
r1 = _parse_bracket_vec(m.group("r1"))
|
|
Z = float(m.group("Z"))
|
|
Emag= float(m.group("Emag"))
|
|
Flag= int(m.group("Flag"))
|
|
Tdist=int(m.group("Tdist"))
|
|
fMHz = float(m.group("f")) # MHz
|
|
t0 = float(m.group("t0"))
|
|
tau = float(m.group("tau"))
|
|
|
|
khat = r1 - ro
|
|
nrm = np.linalg.norm(khat)
|
|
khat = khat / nrm if nrm > 0 else np.array([0.0, 0.0, 1.0], float)
|
|
|
|
return {
|
|
"ro": ro, "r1": r1, "Z": Z, "Emag": Emag,
|
|
"Flag": Flag, "Tdist": Tdist, "fMHz": fMHz,
|
|
"t0": t0, "tau": tau, "khat": khat
|
|
}
|
|
raise RuntimeError("No parsable PORT line found in log.")
|
|
|
|
# =========================
|
|
# Quadrature (6-pt / 9-pt) weights in same ordering as your C macros
|
|
# =========================
|
|
G6_W = np.array([0.109951743655322]*3 + [0.223381589678011]*3) # sum = 1/2 on ref triangle
|
|
G9_W = np.array([0.205950504760887]*3 + [0.063691414286223]*6) # sum = 1/2 on ref triangle
|
|
|
|
# =========================
|
|
# Geometry helpers
|
|
# =========================
|
|
def tri_area(v0, v1, v2):
|
|
return 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0))
|
|
|
|
def tri_normal(v0, v1, v2):
|
|
n = np.cross(v1 - v0, v2 - v0)
|
|
nn = np.linalg.norm(n)
|
|
if nn == 0:
|
|
raise ValueError("Degenerate triangle")
|
|
return n / nn
|
|
|
|
def build_port_quadrature_from_csv(df):
|
|
"""
|
|
df columns required:
|
|
exc_face_id, local_face_idx, quad_idx,
|
|
x1,y1,z1, x2,y2,z2, x3,y3,z3,
|
|
nx,ny,nz, # QUADRATURE-POINT coordinates on the face
|
|
Et_x,Et_y,Et_z, Ht_x,Ht_y,Ht_z
|
|
Returns:
|
|
rQ (Q,3) quad-point positions (nx,ny,nz)
|
|
EtQ (Q,3) modal tangential E at quad points
|
|
HtQ (Q,3) modal tangential H at quad points
|
|
w_phys (Q,) physical weights = (2*A_face) * W_ref[quad_idx]
|
|
n_face (Q,3) per-quad face unit normal (same per face),
|
|
flipped to point 'into device' if provided
|
|
"""
|
|
needed = ["exc_face_id","local_face_idx","quad_idx",
|
|
"x1","y1","z1","x2","y2","z2","x3","y3","z3",
|
|
"nx","ny","nz","Et_x","Et_y","Et_z","Ht_x","Ht_y","Ht_z"]
|
|
for col in needed:
|
|
if col not in df.columns:
|
|
raise ValueError(f"Missing column in CSV: {col}")
|
|
|
|
rQ = df[["nx","ny","nz"]].to_numpy(float)
|
|
EtQ = df[["Et_x","Et_y","Et_z"]].to_numpy(float)
|
|
HtQ = df[["Ht_x","Ht_y","Ht_z"]].to_numpy(float)
|
|
|
|
w_phys = np.zeros(len(df), float)
|
|
n_face = np.zeros((len(df), 3), float)
|
|
|
|
# normalize reference direction (into device) if provided
|
|
u_enf = np.array([0,0,1])
|
|
|
|
# group by face
|
|
groups = {}
|
|
for i, row in df.iterrows():
|
|
key = (int(row["exc_face_id"]), int(row["local_face_idx"]))
|
|
groups.setdefault(key, []).append(i)
|
|
|
|
for key, idxs in groups.items():
|
|
qidxs = df.loc[idxs, "quad_idx"].to_numpy(int)
|
|
Nq = int(qidxs.max() + 1)
|
|
if Nq == 6:
|
|
Wref = G6_W
|
|
elif Nq == 9:
|
|
Wref = G9_W
|
|
else:
|
|
raise ValueError(f"Face {key} has {Nq} quad points; expected 6 or 9.")
|
|
|
|
row0 = df.loc[idxs[0]]
|
|
v0 = np.array([row0["x1"], row0["y1"], row0["z1"]], float)
|
|
v1 = np.array([row0["x2"], row0["y2"], row0["z2"]], float)
|
|
v2 = np.array([row0["x3"], row0["y3"], row0["z3"]], float)
|
|
|
|
A = tri_area(v0, v1, v2)
|
|
n = tri_normal(v0, v1, v2)
|
|
if u_enf is not None and np.dot(n, u_enf) < 0:
|
|
n = -n
|
|
|
|
w_face = (2.0 * A) * Wref[qidxs] # physical quad weights
|
|
w_phys[idxs] = w_face
|
|
n_face[idxs, :] = n
|
|
|
|
# Optional quick check: per-face sum weights ≈ area
|
|
# import numpy as _np
|
|
# _np.testing.assert_allclose(w_face.sum(), A, rtol=1e-10, atol=1e-12)
|
|
|
|
return rQ, EtQ, HtQ, w_phys, n_face
|
|
|
|
|
|
|
|
|
|
Pi = np.pi
|
|
MEGA = 1e6
|
|
Vo = c0
|
|
|
|
def time_modulation_inc(flag, t, t0, khat, r, r0, freq_m, Emag, Hmag, tau):
|
|
omega = 2.0 * Pi * freq_m * MEGA
|
|
t = np.asarray(t, float)
|
|
|
|
if flag == 0:
|
|
phase = - omega * t[:, None]
|
|
IncE = Emag * np.sin(phase)
|
|
IncH = Hmag * np.sin(phase)
|
|
elif flag == 1:
|
|
Exponent = t[:, None] - t0
|
|
CosMod = np.cos(omega * (t[:, None] - t0))
|
|
env = np.exp(-(Exponent**2) / (tau*tau))
|
|
IncE = Emag * CosMod * env
|
|
IncH = Hmag * CosMod * env
|
|
elif flag == 2:
|
|
Exponent = t[:, None] - t0
|
|
Neuman = (2.0 * Exponent) / (tau*tau)
|
|
env = np.exp(-(Exponent**2) / (tau*tau))
|
|
IncE = Emag * Neuman * env
|
|
IncH = Hmag * Neuman * env
|
|
else:
|
|
raise ValueError("Unknown TimeDistributionFlag")
|
|
return IncE, IncH
|
|
|
|
|
|
|
|
# =========================
|
|
# Main
|
|
# =========================
|
|
|
|
# ---- parse log ----
|
|
sim = extract_selected_log_values(LOG_PATH)
|
|
port = parse_port_from_log(LOG_PATH)
|
|
|
|
dt_sample = sim["dt_sample"]
|
|
steps = sim["tsPerSampling"]
|
|
|
|
# Sampling frequency f_s = 1 / dt
|
|
f_s = 1.0 / dt_sample
|
|
# Nyquist frequency
|
|
f_nyq = f_s / 2.0
|
|
|
|
# Count .curJ files
|
|
nfiles = len(fnmatch.filter(os.listdir(PROBE_FOLDER), '*.csv'))
|
|
nfiles = int(nfiles)
|
|
print('Number of time domain files:', nfiles)
|
|
|
|
|
|
print("\n---- Parsed sim/log ----")
|
|
print(f"FinalTime = {sim['FinalTime']}")
|
|
print(f"dt_sample = {dt_sample}")
|
|
print(f"steps = {steps}")
|
|
print("\n---- Parsed PORT ----")
|
|
for k, v in port.items():
|
|
print(f"{k:>6} : {v}")
|
|
|
|
# Number of FFT samples
|
|
NFFT = nfiles * 5
|
|
|
|
# Time axis from sampling
|
|
t_idx = np.arange(nfiles, dtype=int) # 0..steps-1
|
|
t_sec = t_idx * dt_sample # seconds
|
|
|
|
|
|
# ---- load CSV ----
|
|
df = pd.read_csv(CSV_PATH)
|
|
|
|
# ---- build quad geometry/weights/normals ----
|
|
rQ, EtQ, HtQ, w_phys, n_face = build_port_quadrature_from_csv(df)
|
|
|
|
# ---- apply time modulation at quad points ----
|
|
Emag = port["Emag"]
|
|
# TEM-consistent option: Hmag = Emag / port["Z"]
|
|
Hmag = Emag
|
|
IncE, IncH = time_modulation_inc(
|
|
port["Tdist"], t_sec, port["t0"], port["khat"],
|
|
rQ, port["ro"], port["fMHz"], Emag, Hmag, port["tau"]
|
|
) # (T,Q)
|
|
|
|
# build vector fields (T,Q,3)
|
|
E_inc = IncE[:, :, None] * EtQ[None, :, :]
|
|
H_inc = IncH[:, :, None] * HtQ[None, :, :]
|
|
|
|
# ---- time-domain instantaneous & average power (surface Poynting) ----
|
|
S_t = np.cross(E_inc, H_inc) # (T,Q,3)
|
|
S_n_t = np.einsum('tqj,qj->tq', S_t, n_face) # project onto normal
|
|
P_t = np.sum(S_n_t * w_phys[None, :], axis=1) # (T,)
|
|
P_timeavg_W = float(np.mean(P_t))
|
|
print(f"\n[Port] Time-average incident power: {P_timeavg_W:.6e} W")
|
|
|
|
# ---- frequency-domain per-bin power (single-sided) ----
|
|
freqs = np.fft.rfftfreq(NFFT, d=dt_sample) # Hz
|
|
freqs_ghz = freqs * 1e-9
|
|
|
|
E_f = np.fft.rfft(E_inc, n=NFFT, axis=0) / NFFT # (F,Q,3)
|
|
H_f = np.fft.rfft(H_inc, n=NFFT, axis=0) / NFFT # (F,Q,3)
|
|
Cross_F = np.cross(E_f, np.conj(H_f)) # (F,Q,3) note H*
|
|
S_n_F = np.einsum('fqj,qj->fq', Cross_F, n_face) # (F,Q)
|
|
Sum_F = np.sum(S_n_F * w_phys[None, :], axis=1) # (F,) complex
|
|
|
|
scale = np.ones_like(Sum_F, float)
|
|
if scale.size > 2:
|
|
scale[1:-1] = 2.0 # single-sided doubling
|
|
P_per_freq_W = scale * np.real(Sum_F) # (F,)
|
|
P_total_bins_W = float(np.sum(P_per_freq_W))
|
|
|
|
print(f"[Port] Sum of per-frequency power: {P_total_bins_W:.6e} W")
|
|
if P_timeavg_W != 0:
|
|
rel = abs(P_total_bins_W - P_timeavg_W)/abs(P_timeavg_W)
|
|
print(f"[check] rel diff sum(P_k) vs <P(t)> = {rel:.3e}")
|
|
|
|
# ---- save outputs ----
|
|
# spectrum CSV
|
|
pd.DataFrame({
|
|
"f_Hz": freqs,
|
|
"f_GHz": freqs_ghz,
|
|
"P_W": P_per_freq_W
|
|
}).to_csv(f"{SAVE_PREFIX}_power_spectrum.csv", index=False)
|
|
|
|
# spectrum PNG
|
|
plt.figure(figsize=(9,5))
|
|
plt.plot(freqs_ghz, P_per_freq_W, lw=1.4)
|
|
plt.grid(True)
|
|
plt.xlabel("Frequency (GHz)")
|
|
plt.ylabel("Port power per bin (W)")
|
|
plt.title("Incident Port Power Spectrum (surface Poynting, single-sided)")
|
|
plt.tight_layout()
|
|
plt.savefig(f"{SAVE_PREFIX}_power_spectrum.png", dpi=180)
|
|
plt.close()
|
|
|
|
# time trace CSV
|
|
pd.DataFrame({"t_s": t_sec, "P_inst_W": P_t}).to_csv(f"{SAVE_PREFIX}_power_time_trace.csv", index=False)
|
|
|
|
print("\nSaved:")
|
|
print(f" - {SAVE_PREFIX}_power_spectrum.csv / .png")
|
|
print(f" - {SAVE_PREFIX}_power_time_trace.csv")
|
|
|
|
|
|
|
|
|
|
# ======= Area-weighted average port fields vs time (add after E_inc/H_inc) =======
|
|
A_total = np.sum(w_phys) # total port area
|
|
|
|
# Area-weighted averages of the *vector* fields at each time sample
|
|
# Shapes: (T,3)
|
|
E_avg_t = np.sum(E_inc * w_phys[None, :, None], axis=1) / A_total
|
|
H_avg_t = np.sum(H_inc * w_phys[None, :, None], axis=1) / A_total
|
|
|
|
# Magnitudes vs time
|
|
E_avg_mag = np.linalg.norm(E_avg_t, axis=1) # (T,)
|
|
H_avg_mag = np.linalg.norm(H_avg_t, axis=1) # (T,)
|
|
|
|
# Save to CSV
|
|
df_avg = pd.DataFrame({
|
|
"t_s": t_sec,
|
|
"Eavg_x": E_avg_t[:, 0], "Eavg_y": E_avg_t[:, 1], "Eavg_z": E_avg_t[:, 2],
|
|
"Eavg_mag": E_avg_mag,
|
|
"Havg_x": H_avg_t[:, 0], "Havg_y": H_avg_t[:, 1], "Havg_z": H_avg_t[:, 2],
|
|
"Havg_mag": H_avg_mag,
|
|
})
|
|
df_avg.to_csv(f"{SAVE_PREFIX}_avg_field_time.csv", index=False)
|
|
|
|
# Plot magnitudes vs time
|
|
# Plot E and H magnitudes vs time with separate axes
|
|
fig, ax1 = plt.subplots(figsize=(9,5))
|
|
|
|
color_e = "tab:blue"
|
|
ax1.set_xlabel("Time (s)")
|
|
ax1.set_ylabel("|E_avg(t)|", color=color_e)
|
|
ax1.plot(t_sec, E_avg_mag, color=color_e, lw=1.4)
|
|
ax1.tick_params(axis="y", labelcolor=color_e)
|
|
|
|
# Twin y-axis for H
|
|
ax2 = ax1.twinx()
|
|
color_h = "tab:red"
|
|
ax2.set_ylabel("|H_avg(t)|", color=color_h)
|
|
ax2.plot(t_sec, H_avg_mag, color=color_h, lw=1.4, linestyle="--")
|
|
ax2.tick_params(axis="y", labelcolor=color_h)
|
|
|
|
plt.title("Area-weighted average tangential fields vs time")
|
|
fig.tight_layout()
|
|
plt.savefig(f"{SAVE_PREFIX}_avg_field_time.png", dpi=180)
|
|
plt.close(fig)
|
|
|
|
|
|
print(f" - {SAVE_PREFIX}_avg_field_time.csv / .png")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================
|
|
# TOTAL FIELD processing @ port (power, averages, S11)
|
|
# =========================
|
|
|
|
import glob
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from tqdm import tqdm
|
|
|
|
# ---- Config for total-field files ----
|
|
FILE_BASENAME = "patch_model" # matches "patch_model.probe" and "Probes_patch_model_XXXX.csv"
|
|
SAVE_PREFIX_TOT = "total"
|
|
|
|
# ---- Helpers: probe IO & series reader (fixing names/case) ----
|
|
def read_probe_coords(path="patch_model.probe") -> pd.DataFrame:
|
|
dfp = pd.read_csv(path, sep=None, engine="python")
|
|
dfp.columns = [c.strip().upper() for c in dfp.columns]
|
|
assert set(["X","Y","Z"]).issubset(dfp.columns), "Probe file must have X,Y,Z columns"
|
|
dfp = dfp[["X","Y","Z"]].copy()
|
|
dfp.to_csv("probe_locations_clean.csv", index=False)
|
|
return dfp
|
|
|
|
_TF_COLS = ["Ex","Ey","Ez","Hx","Hy","Hz"]
|
|
|
|
def _extract_tidx(fname: str) -> int:
|
|
m = re.search(r"_([0-9]+)\.csv$", fname)
|
|
if not m:
|
|
raise ValueError(f"Cannot parse time index from filename: {fname}")
|
|
return int(m.group(1))
|
|
|
|
def _read_one_probe_csv(path: str) -> pd.DataFrame:
|
|
d = pd.read_csv(path, sep=None, engine="python", header=0)
|
|
d.columns = [c.strip() for c in d.columns]
|
|
keep = [c for c in _TF_COLS if c in d.columns]
|
|
if len(keep) < 6:
|
|
raise ValueError(f"Missing expected field columns in {path}")
|
|
d = d[keep].copy()
|
|
d["probe_idx"] = np.arange(len(d), dtype=int)
|
|
d["t_idx"] = _extract_tidx(Path(path).name)
|
|
return d
|
|
|
|
def read_total_field_series(probe_folder: Path, file_basename: str, workers: int = 12):
|
|
pat1 = str(probe_folder / f"Probes_{file_basename}_*.csv")
|
|
pat2 = str(probe_folder / f"Currents_{file_basename}_*.csv")
|
|
files = sorted(glob.glob(pat1)) or sorted(glob.glob(pat2))
|
|
if not files:
|
|
raise FileNotFoundError(f"No probe CSVs found in {probe_folder} matching {pat1} or {pat2}")
|
|
|
|
results = []
|
|
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
futures = {ex.submit(_read_one_probe_csv, f): f for f in files}
|
|
for fut in tqdm(as_completed(futures), total=len(futures),
|
|
desc="Reading probe CSVs", unit="file"):
|
|
results.append(fut.result())
|
|
|
|
total_df = pd.concat(results, ignore_index=True)
|
|
total_df.sort_values(["t_idx","probe_idx"], inplace=True, kind="mergesort")
|
|
total_df.reset_index(drop=True, inplace=True)
|
|
|
|
t_vals = np.sort(total_df["t_idx"].unique())
|
|
p_vals = np.sort(total_df["probe_idx"].unique())
|
|
T, P = len(t_vals), len(p_vals)
|
|
t_map = {t:i for i,t in enumerate(t_vals)}
|
|
|
|
E = np.empty((T, P, 3), dtype=float)
|
|
H = np.empty((T, P, 3), dtype=float)
|
|
|
|
# fast fill: rely on sort (t_idx first, then probe_idx)
|
|
arr = total_df[["t_idx","probe_idx"] + _TF_COLS].to_numpy()
|
|
# block-write is fastest, but just loop is fine:
|
|
for row in arr:
|
|
ti = t_map[int(row[0])]; pi = int(row[1])
|
|
E[ti, pi, :] = row[2:5]
|
|
H[ti, pi, :] = row[5:8]
|
|
|
|
return total_df, E, H
|
|
|
|
# ---- Load probe coordinates + total fields ----
|
|
probes_df = read_probe_coords(f"{FILE_BASENAME}.probe")
|
|
total_df, E_tot, H_tot = read_total_field_series(PROBE_FOLDER, FILE_BASENAME, workers=12)
|
|
|
|
print("total_df shape:", total_df.shape)
|
|
print("E_tot shape:", E_tot.shape, "H_tot shape:", H_tot.shape)
|
|
|
|
# ---- Align probes to quadrature points (so weights/normals match) ----
|
|
# rQ: (Q,3) from earlier; probes_df: (P,3). We expect Q == P for a face probe export.
|
|
probes_xyz = probes_df[["X","Y","Z"]].to_numpy(float)
|
|
Q = rQ.shape[0]
|
|
P = probes_xyz.shape[0]
|
|
|
|
if P != Q:
|
|
raise ValueError(f"Probe count ({P}) != quadrature count ({Q}). Ensure .probe uses port quad points.")
|
|
|
|
# map each quadrature point to its nearest probe index
|
|
dists = np.linalg.norm(rQ[:, None, :] - probes_xyz[None, :, :], axis=2) # (Q,P)
|
|
order_idx = np.argmin(dists, axis=1) # (Q,)
|
|
max_mm = 1e3 * float(np.max(dists[np.arange(Q), order_idx]))
|
|
if max_mm > 0.05:
|
|
print(f"[warn] Max rQ↔probe mismatch ≈ {max_mm:.3f} mm")
|
|
|
|
# reorder total fields along probe axis to match (rQ, w_phys, n_face) order
|
|
E_tot = E_tot[:, order_idx, :]
|
|
H_tot = H_tot[:, order_idx, :]
|
|
|
|
# ---- Time axis for total fields (use dt_sample from sim) ----
|
|
T_tot = E_tot.shape[0]
|
|
t_sec_tot = np.arange(T_tot, dtype=int) * dt_sample
|
|
|
|
# ---- Instantaneous NET power from total fields: P_net(t) = ∬ (E×H)·n dA ----
|
|
S_t_tot = np.cross(E_tot, H_tot) # (T,Q,3)
|
|
S_n_t_tot = np.einsum("tqj,qj->tq", S_t_tot, n_face) # (T,Q)
|
|
P_net_t = np.sum(S_n_t_tot * w_phys[None, :], axis=1) # (T,)
|
|
P_net_avg = float(np.mean(P_net_t))
|
|
print(f"[Port] Time-average NET power (total fields): {P_net_avg:.6e} W")
|
|
|
|
# ---- Frequency-domain NET power (single-sided) from total fields ----
|
|
E_f_tot = np.fft.rfft(E_tot, n=NFFT, axis=0) / NFFT # (F,Q,3) complex
|
|
H_f_tot = np.fft.rfft(H_tot, n=NFFT, axis=0) / NFFT # (F,Q,3) complex
|
|
Cross_F_tot = np.cross(E_f_tot, np.conj(H_f_tot)) # (F,Q,3)
|
|
S_n_F_tot = np.einsum("fqj,qj->fq", Cross_F_tot, n_face) # (F,Q)
|
|
Sum_F_tot = np.sum(S_n_F_tot * w_phys[None, :], axis=1) # (F,) complex
|
|
|
|
scale = np.ones_like(Sum_F_tot.real)
|
|
if scale.size > 2:
|
|
scale[1:-1] = 2.0
|
|
P_net_per_freq_W = scale * np.real(Sum_F_tot) # (F,)
|
|
|
|
# ---- Save total-field outputs ----
|
|
pd.DataFrame({"t_s": t_sec_tot, "P_net_W": P_net_t}).to_csv(f"{SAVE_PREFIX_TOT}_power_time_trace.csv", index=False)
|
|
|
|
plt.figure(figsize=(9,5))
|
|
plt.plot(freqs_ghz, P_net_per_freq_W, lw=1.4)
|
|
plt.grid(True)
|
|
plt.xlabel("Frequency (GHz)")
|
|
plt.ylabel("Net power per bin (W)")
|
|
plt.title("NET Port Power Spectrum from Total Fields (single-sided)")
|
|
plt.tight_layout()
|
|
plt.savefig(f"{SAVE_PREFIX_TOT}_power_spectrum.png", dpi=180)
|
|
plt.close()
|
|
|
|
pd.DataFrame({
|
|
"f_Hz": freqs,
|
|
"f_GHz": freqs_ghz,
|
|
"P_net_W": P_net_per_freq_W
|
|
}).to_csv(f"{SAVE_PREFIX_TOT}_power_spectrum.csv", index=False)
|
|
|
|
print("\nSaved:")
|
|
print(f" - {SAVE_PREFIX_TOT}_power_spectrum.csv / .png")
|
|
print(f" - {SAVE_PREFIX_TOT}_power_time_trace.csv")
|
|
|
|
# ---- Area-weighted average TOTAL fields vs time ----
|
|
A_total = np.sum(w_phys)
|
|
E_avg_tot_t = np.sum(E_tot * w_phys[None, :, None], axis=1) / A_total # (T,3)
|
|
H_avg_tot_t = np.sum(H_tot * w_phys[None, :, None], axis=1) / A_total # (T,3)
|
|
E_avg_tot_mag = np.linalg.norm(E_avg_tot_t, axis=1)
|
|
H_avg_tot_mag = np.linalg.norm(H_avg_tot_t, axis=1)
|
|
|
|
df_avg_tot = pd.DataFrame({
|
|
"t_s": t_sec_tot,
|
|
"Eavg_x": E_avg_tot_t[:,0], "Eavg_y": E_avg_tot_t[:,1], "Eavg_z": E_avg_tot_t[:,2],
|
|
"Eavg_mag": E_avg_tot_mag,
|
|
"Havg_x": H_avg_tot_t[:,0], "Havg_y": H_avg_tot_t[:,1], "Havg_z": H_avg_tot_t[:,2],
|
|
"Havg_mag": H_avg_tot_mag,
|
|
})
|
|
df_avg_tot.to_csv(f"{SAVE_PREFIX_TOT}_avg_field_time.csv", index=False)
|
|
|
|
fig, ax1 = plt.subplots(figsize=(9,5))
|
|
ax1.set_xlabel("Time (s)")
|
|
ax1.set_ylabel("|E_avg,tot(t)|")
|
|
ax1.plot(t_sec_tot, E_avg_tot_mag, lw=1.4)
|
|
ax2 = ax1.twinx()
|
|
ax2.set_ylabel("|H_avg,tot(t)|")
|
|
ax2.plot(t_sec_tot, H_avg_tot_mag, lw=1.4, linestyle="--")
|
|
plt.title("Area-weighted TOTAL tangential fields vs time")
|
|
fig.tight_layout()
|
|
plt.savefig(f"{SAVE_PREFIX_TOT}_avg_field_time.png", dpi=180)
|
|
plt.close(fig)
|
|
|
|
print(f" - {SAVE_PREFIX_TOT}_avg_field_time.csv / .png")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================
|
|
# Time-domain E-field plots: incident, total, and scattered
|
|
# =========================
|
|
|
|
# Align time lengths just in case
|
|
T_common = min(E_inc.shape[0], E_tot.shape[0])
|
|
t_common = t_sec[:T_common] # use incident's time axis (same dt)
|
|
|
|
E_inc_c = E_inc[:T_common] # (T,Q,3)
|
|
E_tot_c = E_tot[:T_common] # (T,Q,3)
|
|
E_scat_c = E_tot_c - E_inc_c # (T,Q,3)
|
|
|
|
# -------- Area-weighted |E|(t) magnitudes --------
|
|
A_total = float(np.sum(w_phys))
|
|
|
|
def area_weighted_vec(E_TQ3):
|
|
# (T,Q,3) -> (T,3) area-weighted average vector
|
|
return np.sum(E_TQ3 * w_phys[None, :, None], axis=1) / A_total
|
|
|
|
Eavg_inc_t = area_weighted_vec(E_inc_c) # (T,3)
|
|
Eavg_tot_t = area_weighted_vec(E_tot_c) # (T,3)
|
|
Eavg_scat_t = area_weighted_vec(E_scat_c) # (T,3)
|
|
|
|
Eavg_inc_mag = np.linalg.norm(Eavg_inc_t, axis=1)
|
|
Eavg_tot_mag = np.linalg.norm(Eavg_tot_t, axis=1)
|
|
Eavg_scat_mag = np.linalg.norm(Eavg_scat_t, axis=1)
|
|
|
|
# Save CSV of area-weighted magnitudes
|
|
pd.DataFrame({
|
|
"t_s": t_common,
|
|
"|E_inc_avg|(t)": Eavg_inc_mag,
|
|
"|E_tot_avg|(t)": Eavg_tot_mag,
|
|
"|E_scat_avg|(t)": Eavg_scat_mag,
|
|
"E_inc_avg_x": Eavg_inc_t[:,0], "E_inc_avg_y": Eavg_inc_t[:,1], "E_inc_avg_z": Eavg_inc_t[:,2],
|
|
"E_tot_avg_x": Eavg_tot_t[:,0], "E_tot_avg_y": Eavg_tot_t[:,1], "E_tot_avg_z": Eavg_tot_t[:,2],
|
|
"E_scat_avg_x": Eavg_scat_t[:,0], "E_scat_avg_y": Eavg_scat_t[:,1], "E_scat_avg_z": Eavg_scat_t[:,2],
|
|
}).to_csv("E_time_area_weighted.csv", index=False)
|
|
|
|
# Plot area-weighted magnitudes
|
|
plt.figure(figsize=(9,5))
|
|
plt.plot(t_common, Eavg_inc_mag, lw=1.6, label="|E_inc,avg|(t)")
|
|
plt.plot(t_common, Eavg_tot_mag, lw=1.6, label="|E_tot,avg|(t)")
|
|
plt.plot(t_common, Eavg_scat_mag, lw=1.6, label="|E_scat,avg|(t)")
|
|
plt.grid(True, alpha=0.4)
|
|
plt.xlabel("Time (s)")
|
|
plt.ylabel("Area-weighted |E|(t)")
|
|
plt.title("Area-weighted |E|(t): incident, total, scattered")
|
|
plt.legend(loc="best")
|
|
plt.tight_layout()
|
|
plt.savefig("E_time_area_weighted.png", dpi=180)
|
|
plt.close()
|
|
|
|
# -------- Sample quadrature point: components vs time --------
|
|
# choose the quadrature point closest to the face centroid for a representative sample
|
|
face_centroid = np.average(rQ, axis=0, weights=None)
|
|
q_sample = int(np.argmin(np.linalg.norm(rQ - face_centroid[None, :], axis=1)))
|
|
|
|
Einc_q = E_inc_c[:, q_sample, :] # (T,3)
|
|
Etot_q = E_tot_c[:, q_sample, :] # (T,3)
|
|
Escat_q = E_scat_c[:, q_sample, :] # (T,3)
|
|
|
|
# Save CSV for the sample point
|
|
pd.DataFrame({
|
|
"t_s": t_common,
|
|
"Einc_x": Einc_q[:,0], "Einc_y": Einc_q[:,1], "Einc_z": Einc_q[:,2],
|
|
"Etot_x": Etot_q[:,0], "Etot_y": Etot_q[:,1], "Etot_z": Etot_q[:,2],
|
|
"Escat_x": Escat_q[:,0], "Escat_y": Escat_q[:,1], "Escat_z": Escat_q[:,2],
|
|
}).to_csv("E_time_sample_point.csv", index=False)
|
|
|
|
# Plot components at the sample point (three stacked panels)
|
|
fig, axs = plt.subplots(3, 1, figsize=(10, 7), sharex=True)
|
|
labels = ["x", "y", "z"]
|
|
for i, ax in enumerate(axs):
|
|
ax.plot(t_common, Einc_q[:, i], lw=1.3, label=f"E_inc_{labels[i]}")
|
|
ax.plot(t_common, Etot_q[:, i], lw=1.3, label=f"E_tot_{labels[i]}")
|
|
ax.plot(t_common, Escat_q[:, i], lw=1.3, label=f"E_scat_{labels[i]}")
|
|
ax.grid(True, alpha=0.4)
|
|
ax.set_ylabel(f"{labels[i]}-comp")
|
|
ax.legend(loc="best", ncol=3, fontsize=9)
|
|
axs[-1].set_xlabel("Time (s)")
|
|
fig.suptitle(f"E(t) at sample quadrature point q={q_sample}")
|
|
plt.tight_layout()
|
|
plt.savefig("E_time_sample_components.png", dpi=180)
|
|
plt.close()
|
|
|
|
print("Saved: E_time_area_weighted.csv/.png, E_time_sample_point.csv, E_time_sample_components.png")
|
|
|
|
|
|
|
|
|
|
print("\n============= Calculate S Parameters (E-only inner product) ======================\n")
|
|
|
|
|
|
|
|
# One-sided frequency axis
|
|
dt = dt_sample
|
|
f_s = 1.0 / dt
|
|
f_nyq = f_s / 2.0
|
|
freqs = np.fft.rfftfreq(NFFT, d=dt) # Hz
|
|
freqs_ghz = freqs * 1e-9
|
|
|
|
# FFT of incident field (NO WINDOW)
|
|
# Shape: (F, Q, 3), complex
|
|
E_f = np.fft.rfft(E_inc, n=NFFT, axis=0) / NFFT
|
|
H_f = np.fft.rfft(H_inc, n=NFFT, axis=0) / NFFT
|
|
|
|
# Mean spectrum energy across probes & components
|
|
mean_spec_energy = np.mean(np.abs(E_f)**2, axis=(1, 2)) # (F,)
|
|
|
|
# Threshold at 10% of peak
|
|
peakE = float(mean_spec_energy.max()) if mean_spec_energy.size else 0.0
|
|
thresh = 0.10 * (peakE if peakE > 0 else 1.0)
|
|
|
|
mask = mean_spec_energy >= thresh
|
|
|
|
# Turn mask into contiguous frequency bands
|
|
def mask_to_bands(mask_bool, xvals):
|
|
idx = np.flatnonzero(mask_bool)
|
|
if idx.size == 0:
|
|
return []
|
|
bands = []
|
|
start = prev = idx[0]
|
|
for k in idx[1:]:
|
|
if k == prev + 1:
|
|
prev = k
|
|
else:
|
|
bands.append((start, prev))
|
|
start = prev = k
|
|
bands.append((start, prev))
|
|
# return with indices and frequency endpoints
|
|
return [(i0, i1, xvals[i0], xvals[i1]) for (i0, i1) in bands]
|
|
|
|
bands = mask_to_bands(mask, freqs)
|
|
|
|
print("=== Bands where mean |E_inc(f)|^2 ≥ 10% of peak ===")
|
|
if not bands:
|
|
print("None found. (Check dt/NFFT/time record.)")
|
|
else:
|
|
for (i0, i1, f0, f1) in bands:
|
|
print(f"Idx {i0}–{i1} | {f0*1e-9:.6f}–{f1*1e-9:.6f} GHz "
|
|
f"(points: {i1 - i0})")
|
|
|
|
# If you want a single sweep range (union):
|
|
if bands:
|
|
idx_min, fmin_hz = bands[0][0], bands[0][2]
|
|
idx_max, fmax_hz = bands[-1][1], bands[-1][3]
|
|
print(f"\nOverall sweep range: idx {idx_min}–{idx_max} | "
|
|
f"{fmin_hz*1e-9:.6f}–{fmax_hz*1e-9:.6f} GHz "
|
|
f"(points: {idx_max - idx_min})")
|
|
else:
|
|
idx_min = idx_max = 0
|
|
fmin_hz = fmax_hz = 0.0
|
|
|
|
# Plot normalized mean spectrum energy, highlight all ≥10% bands
|
|
norm_energy = mean_spec_energy / (peakE if peakE > 0 else 1.0)
|
|
|
|
plt.figure(figsize=(10, 5))
|
|
plt.plot(freqs_ghz, norm_energy, label="Mean |E_inc(f)|² (normalized)")
|
|
for (i0, i1, f0, f1) in bands:
|
|
plt.axvspan(f0*1e-9, f1*1e-9, alpha=0.25, label="≥10% band" if i0 == bands[0][0] else None)
|
|
plt.axhline(0.10, linestyle="--", linewidth=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================
|
|
# S11 from E-only inner product, restricted to detected bands
|
|
# =========================
|
|
print("\n============= S11(E-only) over detected bands ======================\n")
|
|
|
|
# Reflected phasors
|
|
E_f_ref = E_f_tot - E_f # (F, Q, 3), complex
|
|
|
|
def e_inner(A_fq3: np.ndarray, B_fq3: np.ndarray) -> np.ndarray:
|
|
"""
|
|
Weighted inner product: ∬ A·B* dA using quadrature weights.
|
|
A_fq3, B_fq3: (F, Q, 3) complex
|
|
returns (F,) complex
|
|
"""
|
|
prod_fq = np.sum(A_fq3 * np.conj(B_fq3), axis=2) # (F, Q)
|
|
return np.sum(prod_fq * w_phys[None, :], axis=1) # (F,)
|
|
|
|
# Full-spectrum S11(E-only) once
|
|
num_full = 4 * e_inner(E_f_ref, E_f) # (F,)
|
|
den_full = e_inner(E_f, E_f) # (F,)
|
|
S11_full = num_full / np.maximum(den_full, 1e-30) # (F,)
|
|
S11_mag_full = np.abs(S11_full)
|
|
S11_dB_full = 20.0 * np.log10(np.clip(S11_mag_full, 1e-12, None))
|
|
|
|
|
|
|
|
# =========================
|
|
# Save & plot inner-product numerator/denominator for S11 (E-only)
|
|
# =========================
|
|
num_ip = num_full.copy()
|
|
den_ip = den_full.copy()
|
|
|
|
ip_df = pd.DataFrame({
|
|
"f_Hz": freqs, "f_GHz": freqs_ghz,
|
|
"num_real": np.real(num_ip), "num_imag": np.imag(num_ip),
|
|
"num_abs": np.abs(num_ip), "num_phase_rad": np.angle(num_ip),
|
|
"den_real": np.real(den_ip), "den_imag": np.imag(den_ip),
|
|
"den_abs": np.abs(den_ip), "den_phase_rad": np.angle(den_ip),
|
|
})
|
|
ip_df.to_csv("S11_inner_products.csv", index=False)
|
|
|
|
plt.figure(figsize=(10,5))
|
|
plt.plot(freqs_ghz, np.abs(num_ip), lw=1.6, label="|⟨E_ref, E_inc⟩| (numerator)")
|
|
plt.plot(freqs_ghz, np.abs(den_ip), lw=1.6, linestyle="--", label="|⟨E_inc, E_inc⟩| (denominator)")
|
|
plt.xlabel("Frequency (GHz)"); plt.ylabel("Inner-product magnitude (SI units)")
|
|
plt.title("S11 (E-only) inner products — magnitudes")
|
|
plt.grid(True, alpha=0.4); plt.legend(); plt.tight_layout()
|
|
plt.savefig("S11_inner_products_mag.png", dpi=180); plt.close()
|
|
|
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10,7), sharex=True)
|
|
ax1.plot(freqs_ghz, np.real(num_ip), lw=1.4, label="Re{numerator}")
|
|
ax1.plot(freqs_ghz, np.real(den_ip), lw=1.4, linestyle="--", label="Re{denominator}")
|
|
ax1.set_ylabel("Real part"); ax1.grid(True, alpha=0.4); ax1.legend()
|
|
|
|
ax2.plot(freqs_ghz, np.imag(num_ip), lw=1.4, label="Im{numerator}")
|
|
ax2.plot(freqs_ghz, np.imag(den_ip), lw=1.4, linestyle="--", label="Im{denominator}")
|
|
ax2.set_xlabel("Frequency (GHz)"); ax2.set_ylabel("Imag part"); ax2.grid(True, alpha=0.4); ax2.legend()
|
|
|
|
fig.suptitle("S11 (E-only) inner products — real/imag parts")
|
|
plt.tight_layout(); plt.savefig("S11_inner_products_re_im.png", dpi=180); plt.close()
|
|
|
|
# Optional carrier print
|
|
f0_Hz = port["fMHz"] * 1e6
|
|
k0 = int(np.argmin(np.abs(freqs - f0_Hz)))
|
|
print(f"Carrier ~ {f0_Hz*1e-9:.6f} GHz → idx {k0}")
|
|
print(f" num = {num_ip[k0].real:+.4e} {num_ip[k0].imag:+.4e}j |num|={abs(num_ip[k0]):.4e}")
|
|
print(f" den = {den_ip[k0].real:+.4e} {den_ip[k0].imag:+.4e}j |den|={abs(den_ip[k0]):.4e}")
|
|
|
|
|
|
|
|
|
|
|
|
# =========================
|
|
# S11 from POWER spectra (single-sided bins)
|
|
# P_inc = P_per_freq_W
|
|
# P_net = P_net_per_freq_W
|
|
# P_ref = max(P_inc - P_net, 0)
|
|
# |S11| = sqrt(P_ref / P_inc)
|
|
# =========================
|
|
eps = 1e-30
|
|
P_inc = np.asarray(P_per_freq_W) # (F,)
|
|
P_net = np.asarray(P_net_per_freq_W) # (F,)
|
|
P_ref = np.maximum(abs(P_inc - P_net), 0.0) # (F,)
|
|
|
|
S11_pow_mag_full = np.sqrt(np.clip(P_ref / np.maximum(P_inc, eps), 0.0, 1.0))
|
|
S11_pow_dB_full = 20.0 * np.log10(np.clip(S11_pow_mag_full, 1e-12, None))
|
|
|
|
# Save a full-spectrum comparison CSV
|
|
pd.DataFrame({
|
|
"f_Hz": freqs,
|
|
"f_GHz": freqs_ghz,
|
|
"S11_Eonly_real": np.real(S11_full),
|
|
"S11_Eonly_imag": np.imag(S11_full),
|
|
"S11_Eonly_mag": S11_mag_full,
|
|
"S11_Eonly_dB": S11_dB_full,
|
|
"S11_power_mag": S11_pow_mag_full,
|
|
"S11_power_dB": S11_pow_dB_full,
|
|
"P_inc_W": P_inc,
|
|
"P_net_W": P_net,
|
|
"P_ref_W": P_ref,
|
|
}).to_csv("S11_compare_full.csv", index=False)
|
|
|
|
|
|
# =========================
|
|
# Plot/export by detected bands
|
|
# =========================
|
|
if not bands:
|
|
print("No bands found — exported full-spectrum S11 comparison (S11_compare_full.csv).")
|
|
|
|
# Full-spectrum overlay plot
|
|
plt.figure(figsize=(10,5))
|
|
plt.plot(freqs_ghz, S11_dB_full, lw=1.6, label="S11 (E-only)")
|
|
plt.plot(freqs_ghz, S11_pow_dB_full, lw=1.6, linestyle="--", label="S11 (power)")
|
|
plt.xlabel("Frequency (GHz)"); plt.ylabel("S11 (dB)")
|
|
plt.title("S11 comparison — full spectrum")
|
|
plt.grid(True, alpha=0.4); plt.legend(loc="best"); plt.tight_layout()
|
|
plt.savefig("S11_compare_full.png", dpi=180); plt.close()
|
|
else:
|
|
# Per-band CSVs and multi-panel plot (E-only vs power)
|
|
nB = len(bands)
|
|
fig, axs = plt.subplots(nB, 1, figsize=(10, max(4, 2.6*nB)), sharey=True)
|
|
if nB == 1:
|
|
axs = [axs]
|
|
|
|
concat_rows = []
|
|
for bi, (i0, i1, f0_hz, f1_hz) in enumerate(bands, start=1):
|
|
sl = slice(i0, i1+1)
|
|
|
|
f_hz = freqs[sl]; f_ghz = freqs_ghz[sl]
|
|
S11E = S11_dB_full[sl]
|
|
S11P = S11_pow_dB_full[sl]
|
|
|
|
# Save CSV per band (both methods)
|
|
df_band = pd.DataFrame({
|
|
"f_Hz": f_hz,
|
|
"f_GHz": f_ghz,
|
|
"S11_Eonly_mag": S11_mag_full[sl],
|
|
"S11_Eonly_dB": S11E,
|
|
"S11_power_mag": S11_pow_mag_full[sl],
|
|
"S11_power_dB": S11P,
|
|
"P_inc_W": P_inc[sl],
|
|
"P_net_W": P_net[sl],
|
|
"P_ref_W": P_ref[sl],
|
|
})
|
|
df_band.to_csv(f"S11_compare_band_{bi}.csv", index=False)
|
|
concat_rows.append(df_band)
|
|
|
|
# Plot this band
|
|
ax = axs[bi-1]
|
|
ax.plot(f_ghz, S11E, lw=1.6, label="E-only")
|
|
ax.plot(f_ghz, S11P, lw=1.6, linestyle="--", label="Power")
|
|
ax.set_xlim(f0_hz*1e-9, f1_hz*1e-9)
|
|
ax.set_xlabel("Frequency (GHz)")
|
|
ax.set_ylabel("S11 (dB)")
|
|
ax.grid(True, alpha=0.4)
|
|
ax.legend(loc="best")
|
|
ax.set_title(f"Band {bi}: {f0_hz*1e-9:.3f}–{f1_hz*1e-9:.3f} GHz")
|
|
|
|
fig.suptitle("S11 comparison per detected band (E-only vs Power)")
|
|
plt.tight_layout()
|
|
plt.savefig("S11_compare_bands.png", dpi=180)
|
|
plt.close(fig)
|
|
|
|
# Concatenate bands and save
|
|
df_concat = pd.concat(concat_rows, ignore_index=True)
|
|
df_concat.to_csv("S11_compare_bands_concat.csv", index=False)
|
|
|
|
# Union overlay across all bands
|
|
idx_min, fmin_hz = bands[0][0], bands[0][2]
|
|
idx_max, fmax_hz = bands[-1][1], bands[-1][3]
|
|
sl_union = slice(idx_min, idx_max+1)
|
|
|
|
plt.figure(figsize=(10,5))
|
|
plt.plot(freqs_ghz[sl_union], S11_dB_full[sl_union], lw=1.6, label="E-only")
|
|
plt.plot(freqs_ghz[sl_union], S11_pow_dB_full[sl_union], lw=1.6, linestyle="--", label="Power")
|
|
for (i0, i1, f0, f1) in bands:
|
|
plt.axvspan(f0*1e-9, f1*1e-9, alpha=0.12)
|
|
plt.xlabel("Frequency (GHz)")
|
|
plt.ylabel("S11 (dB)")
|
|
plt.title(f"S11 comparison (union): {fmin_hz*1e-9:.3f}–{fmax_hz*1e-9:.3f} GHz")
|
|
plt.grid(True, alpha=0.4); plt.legend(loc="best"); plt.tight_layout()
|
|
plt.savefig("S11_compare_union.png", dpi=180); plt.close()
|
|
|
|
# Optional: report at carrier if inside any band
|
|
f0_Hz = port["fMHz"] * 1e6
|
|
if any((f0_Hz >= f0) and (f0_Hz <= f1) for (_, _, f0, f1) in bands):
|
|
k0 = int(np.argmin(np.abs(freqs - f0_Hz)))
|
|
print(f"S11(E-only) @ f0={f0_Hz*1e-9:.3f} GHz: {S11_dB_full[k0]:.2f} dB")
|
|
print(f"S11(power) @ f0={f0_Hz*1e-9:.3f} GHz: {S11_pow_dB_full[k0]:.2f} dB")
|
|
print(f"P_inc={P_inc[k0]:.4e} W P_net={P_net[k0]:.4e} W P_ref={P_ref[k0]:.4e} W")
|
|
|
|
print("Saved: inc_energy_bands.png, S11_compare_full.csv, "
|
|
"S11_compare_bands.png (if bands), S11_compare_bands_concat.csv (if bands), "
|
|
"S11_compare_union.png (if bands), and per-band CSVs (S11_compare_band_*.csv).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================
|
|
# Animated 3D scatter GIFs of |E| and |H| over time (incident & total)
|
|
# =========================
|
|
|
|
# Field magnitudes at each quadrature point
|
|
E_inc_mag_q = np.linalg.norm(E_inc, axis=2) # (T,Q)
|
|
H_inc_mag_q = np.linalg.norm(H_inc, axis=2) # (T,Q)
|
|
E_tot_mag_q = np.linalg.norm(E_tot, axis=2) # (T,Q)
|
|
H_tot_mag_q = np.linalg.norm(H_tot, axis=2) # (T,Q)
|
|
|
|
# Consistent color limits across time
|
|
def finite_minmax(A):
|
|
a = np.asarray(A).ravel()
|
|
a = a[np.isfinite(a)]
|
|
if a.size == 0:
|
|
return 0.0, 1.0
|
|
return float(np.min(a)), float(np.max(a))
|
|
|
|
lims = {
|
|
"E_inc": finite_minmax(E_inc_mag_q),
|
|
"H_inc": finite_minmax(H_inc_mag_q),
|
|
"E_tot": finite_minmax(E_tot_mag_q),
|
|
"H_tot": finite_minmax(H_tot_mag_q),
|
|
}
|
|
|
|
# 3D scatter movie helper
|
|
def make_3d_scatter_gif(vals_TQ, fname, title, vmin, vmax, step=1,
|
|
elev=90, azim=0, duration=0.15):
|
|
|
|
print("Creating GIF for ", title)
|
|
frames = []
|
|
cmap = cm.get_cmap("viridis")
|
|
# axes limits
|
|
xmin,xmax = float(rQ[:,0].min()), float(rQ[:,0].max())
|
|
ymin,ymax = float(rQ[:,1].min()), float(rQ[:,1].max())
|
|
zmin,zmax = float(rQ[:,2].min()), float(rQ[:,2].max())
|
|
|
|
for t in range(0, vals_TQ.shape[0], step):
|
|
vals = vals_TQ[t]
|
|
fig = plt.figure(figsize=(6,5), dpi=120)
|
|
ax = fig.add_subplot(111, projection="3d")
|
|
sc = ax.scatter(rQ[:,0], rQ[:,1], rQ[:,2],
|
|
c=vals, s=14, cmap=cmap, vmin=vmin, vmax=vmax)
|
|
ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax); ax.set_zlim(zmin, zmax)
|
|
ax.set_xlabel("x (m)"); ax.set_ylabel("y (m)"); ax.set_zlabel("z (m)")
|
|
ax.set_title(f"{title}\n t = {t_sec[t]:.3e} s")
|
|
cb = plt.colorbar(sc, ax=ax, fraction=0.046, pad=0.04)
|
|
cb.set_label("|Field|")
|
|
# Top view
|
|
ax.view_init(elev=elev, azim=azim)
|
|
fig.canvas.draw()
|
|
w, h = fig.canvas.get_width_height()
|
|
img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8).reshape(h, w, 3)
|
|
frames.append(img)
|
|
plt.close(fig)
|
|
imageio.mimsave(fname, frames, duration=duration) # slower playback
|
|
print(f"Saved {fname} ({len(frames)} frames, duration={duration}s per frame)")
|
|
|
|
# Keep GIFs small: cap to ~500 frames
|
|
Tgif = min(len(t_sec), E_tot_mag_q.shape[0], H_tot_mag_q.shape[0])
|
|
stride = max(1, Tgif // 500)
|
|
|
|
make_3d_scatter_gif(E_inc_mag_q[:Tgif], "gif_E_inc_mag.gif",
|
|
"|E_inc|(r,t)", *lims["E_inc"], step=stride,
|
|
elev=90, azim=0, duration=0.5)
|
|
make_3d_scatter_gif(E_tot_mag_q[:Tgif], "gif_E_tot_mag.gif",
|
|
"|E_tot|(r,t)", *lims["E_tot"], step=stride,
|
|
elev=90, azim=0, duration=0.5)
|
|
make_3d_scatter_gif(H_inc_mag_q[:Tgif], "gif_H_inc_mag.gif",
|
|
"|H_inc|(r,t)", *lims["H_inc"], step=stride,
|
|
elev=90, azim=0, duration=0.5)
|
|
make_3d_scatter_gif(H_tot_mag_q[:Tgif], "gif_H_tot_mag.gif",
|
|
"|H_tot|(r,t)", *lims["H_tot"], step=stride,
|
|
elev=90, azim=0, duration=0.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|