|
| 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) |
0 commit comments