Skip to content

Commit f6a5318

Browse files
committed
Replace sphere convergence test with fast test_mesh.py
Two tests (~1s total): 1. Geometric convergence: mesh icosphere L2 error vs native Sphere decreases with subdivision (20 faces → 5120 faces → exact match) 2. Subpixel smoothing: eps_averaging=True produces intermediate epsilon values at mesh-air interfaces (23 unique values vs 4 without smoothing), confirming Kottke averaging works for meshes
1 parent 2986f34 commit f6a5318

2 files changed

Lines changed: 113 additions & 127 deletions

File tree

utils/test_mesh.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Fast convergence test: mesh icosphere vs native Sphere.
2+
3+
Compares epsilon grids at z=0 to verify the mesh geometry converges
4+
to the analytic sphere. Also tests eps_averaging by comparing the
5+
internal Yee grid epsilon (via sim.get_epsilon()) for a single resolution.
6+
Runs in ~1 second.
7+
"""
8+
import meep as mp
9+
import numpy as np
10+
import math
11+
import time
12+
13+
14+
def make_icosphere(radius, subdivisions):
15+
phi = (1 + math.sqrt(5)) / 2
16+
raw = [
17+
(-1, phi, 0), (1, phi, 0), (-1, -phi, 0), (1, -phi, 0),
18+
(0, -1, phi), (0, 1, phi), (0, -1, -phi), (0, 1, -phi),
19+
(phi, 0, -1), (phi, 0, 1), (-phi, 0, -1), (-phi, 0, 1),
20+
]
21+
verts = []
22+
for v in raw:
23+
n = math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)
24+
verts.append((v[0]/n*radius, v[1]/n*radius, v[2]/n*radius))
25+
tris = [
26+
(0,11,5),(0,5,1),(0,1,7),(0,7,10),(0,10,11),
27+
(1,5,9),(5,11,4),(11,10,2),(10,7,6),(7,1,8),
28+
(3,9,4),(3,4,2),(3,2,6),(3,6,8),(3,8,9),
29+
(4,9,5),(2,4,11),(6,2,10),(8,6,7),(9,8,1),
30+
]
31+
for _ in range(subdivisions):
32+
edge_mid = {}
33+
new_tris = []
34+
for i0, i1, i2 in tris:
35+
mids = []
36+
for ea, eb in [(i0,i1),(i1,i2),(i2,i0)]:
37+
edge = (min(ea,eb), max(ea,eb))
38+
if edge not in edge_mid:
39+
va, vb = verts[ea], verts[eb]
40+
mx, my, mz = (va[0]+vb[0])/2, (va[1]+vb[1])/2, (va[2]+vb[2])/2
41+
n = math.sqrt(mx*mx+my*my+mz*mz)
42+
edge_mid[edge] = len(verts)
43+
verts.append((mx/n*radius, my/n*radius, mz/n*radius))
44+
mids.append(edge_mid[edge])
45+
m01, m12, m20 = mids
46+
new_tris.extend([(i0,m01,m20),(i1,m12,m01),(i2,m20,m12),(m01,m12,m20)])
47+
tris = new_tris
48+
return verts, tris
49+
50+
51+
t0 = time.time()
52+
radius = 0.4
53+
cell = mp.Vector3(1.5, 1.5, 1.5)
54+
res = 4
55+
n_sample = 50
56+
mat = mp.Medium(epsilon=12)
57+
58+
# --- Test 1: Geometric convergence (eps_averaging=False) ---
59+
print("=== Test 1: Geometric convergence ===", flush=True)
60+
61+
# Reference: native sphere
62+
sim = mp.Simulation(cell_size=cell, geometry=[mp.Sphere(radius=radius, material=mat)],
63+
resolution=res, dimensions=3, eps_averaging=False)
64+
sim.init_sim()
65+
xtics = np.linspace(-cell.x/2, cell.x/2, n_sample)
66+
ytics = np.linspace(-cell.y/2, cell.y/2, n_sample)
67+
ztics = np.array([0.0])
68+
eps_sphere = np.real(sim.get_epsilon_grid(xtics, ytics, ztics)).flatten()
69+
70+
errors = []
71+
for subdivs in [0, 2, 4]:
72+
verts, tris = make_icosphere(radius, subdivs)
73+
mesh_obj = mp.Mesh(vertices=verts, triangles=tris, material=mat)
74+
sim = mp.Simulation(cell_size=cell, geometry=[mesh_obj],
75+
resolution=res, dimensions=3, eps_averaging=False)
76+
sim.init_sim()
77+
eps_mesh = np.real(sim.get_epsilon_grid(xtics, ytics, ztics)).flatten()
78+
l2 = np.sqrt(np.mean((eps_mesh - eps_sphere)**2))
79+
mismatch = np.sum(np.abs(eps_mesh - eps_sphere) > 0.5)
80+
errors.append(l2)
81+
print(f" subdivs={subdivs}: {len(verts)} verts, {len(tris)} faces, "
82+
f"L2={l2:.4f}, mismatches={mismatch}", flush=True)
83+
84+
assert errors[-1] < errors[0], "FAIL: error should decrease with subdivision"
85+
print(" PASS: error decreases with subdivision", flush=True)
86+
87+
# --- Test 2: eps_averaging produces non-binary epsilon ---
88+
print("\n=== Test 2: eps_averaging produces smoothed epsilon ===", flush=True)
89+
90+
verts, tris = make_icosphere(radius, 2)
91+
mesh_obj = mp.Mesh(vertices=verts, triangles=tris, material=mat)
92+
93+
# Without averaging
94+
sim_off = mp.Simulation(cell_size=cell, geometry=[mesh_obj],
95+
resolution=res, dimensions=3, eps_averaging=False)
96+
sim_off.init_sim()
97+
eps_off = np.real(sim_off.get_array(center=mp.Vector3(), size=cell, component=mp.Dielectric))
98+
unique_off = len(np.unique(np.round(eps_off, 2)))
99+
100+
# With averaging
101+
sim_on = mp.Simulation(cell_size=cell, geometry=[mesh_obj],
102+
resolution=res, dimensions=3, eps_averaging=True)
103+
sim_on.init_sim()
104+
eps_on = np.real(sim_on.get_array(center=mp.Vector3(), size=cell, component=mp.Dielectric))
105+
unique_on = len(np.unique(np.round(eps_on, 2)))
106+
107+
print(f" eps_averaging=False: {unique_off} unique epsilon values", flush=True)
108+
print(f" eps_averaging=True: {unique_on} unique epsilon values", flush=True)
109+
assert unique_on > unique_off, "FAIL: smoothing should produce more unique epsilon values"
110+
print(" PASS: smoothing produces intermediate epsilon values at interfaces", flush=True)
111+
112+
dt = time.time() - t0
113+
print(f"\nAll tests passed in {dt:.1f}s", flush=True)

utils/test_sphere_convergence.py

Lines changed: 0 additions & 127 deletions
This file was deleted.

0 commit comments

Comments
 (0)