Bound states in a continuum and Q-factors

In this example we apply legume to calculate bound states in a continuum in a PhC slab. This example is related to Sec. 4.1 of the CPC paper.

[1]:
import legume
print(f"Version of the imported legume : {legume.__version__}")

import numpy as np
import matplotlib.pyplot as plt
import copy
Version of the imported legume : 1.0.0

Define the PhC

The PhC slab consists of a square lattice of period \(a=400\) nm, hole radius \(r=100\) nm, etched in a suspended slab of thickness \(d=80\) nm with refractive index \(n=3.45\). It is the same parameters of Fig. 8 of the CPC paper, but we use dimensionless units here.

[2]:
D = 0.2 # slab thickness in units of a
r = 0.25  # hole radius in units of a
eps_c = 1          # permittivity of circular holes
eps_b = 3.45**2    # background permittivity of slab
eps_lower, eps_upper = 1, 1  # permittivities of lower and upper claddings

lattice = legume.Lattice('square')
phc = legume.PhotCryst(lattice, eps_l=eps_lower, eps_u=eps_upper)
phc.add_layer(d=D, eps_b=eps_b)
phc.layers[-1].add_shape(legume.Circle(eps=eps_c, r=r, x_cent=0., y_cent=0))

gme = legume.GuidedModeExp(phc, gmax=4.5, truncate_g='abs')
npw = np.shape(gme.gvec)[1]  # number of plane waves in the expansion
print('Number of reciprocal lattice vectors in the expansion: npw = ', npw)
Number of reciprocal lattice vectors in the expansion: npw =  69
[3]:
# Run the guided-mode expansion
numeig, verbose = 20, True
nk = 20
path = lattice.bz_path(['X', 'G', 'M'], [nk])

gme.run(kpoints=path['kpoints'], gmode_inds=[0, 1, 2, 3], numeig=numeig, verbose=True)
freqs = gme.freqs

nkappa, nfreq = freqs.shape[0], freqs.shape[1]
print(f'Number of wavevectros = {nkappa}, number of frequencies = {nfreq}')
Running gme k-points: │██████████████████████████████│ 41 of 41
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Steps in GuidedModeExp: 69 plane waves and 4 guided modes  Time (s)                  % vs total T ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Guided modes computation with gmode_compute='exact'        1.641     │████████████--------│   63% │
│ Inverse matrix of Fourier-space permittivity               0.002     │--------------------│    0% │
│ Matrix diagionalization using the 'eigh' solver            0.181     │█-------------------│    7% │
│ Creating GME matrix                                        0.756     │█████---------------│   29% │
├───────────────────────────────────────────────────────────┼──────────┼──────────────────────────────┤
│ Total time for real part of frequencies for 41 k-points    2.601     │████████████████████│  100% │
└───────────────────────────────────────────────────────────┴──────────┴──────────────────────────────┘
Running GME losses k-point: │██████████████████████████████│ 41 of 41
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Steps in GuidedModeExp: 69 plane waves and 4 guided modes        Time (s) ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total time for imaginary part of frequencies for 820 eigenmodes  3.580    │
└─────────────────────────────────────────────────────────────────┴──────────┘
Number of wavevectros = 41, number of frequencies = 20

Plot the photonic bands and Q-factors

In the legume 1.0.0 version, viz.bands plots the frequencies as a function of wavevector along the path in the Brillouin zone by means of the kw argument k_units=True, which requires setting the xticks with path['k_indexes'].

[4]:
def plot_bands(gme):
    fig, ax = plt.subplots(1, figsize = (6, 6))
    legume.viz.bands(gme, Q=True, ax=ax, cone=True, k_units=True, conecolor='lightgrey',
                     markersize=5, markeredgecolor='black', markeredgewidth=0.5)
    ax.set_xticks(path['k_indexes'])
    ax.set_xticklabels(path['labels'])
    ax.xaxis.grid('True')
    ax.set_ylim([0., 0.7])

plot_bands(gme)
../_images/examples_13_BIC_and_Q_factor_8_0.png

Study of a symmetry-proteceted BIC

We see that there are several states at \({\bf k}=0\) with \(Q\rightarrow\infty\), which are symmetry-protected BICs. In the square lattice, all nondegenerate states at the \(\Gamma\) point are symmetry-protected BICs, and vice-versa (while the hexagonal lattice supports twofold degenerate BICs: see example 08).

Here we focus on the lowest BIC with a dimensionless frequency around 0.45, which is called BIC1 in Fig. 8(a) of the CPC paper. Let’s plot the Q-factor close to \({\bf k}=0\): for this, however, we need a denser set of \({\bf k}\)-points and we focus on a region closer to the \(\Gamma\) point. We define the points \(X_1=[0.1\pi,0]\) and \(M_1=[0.1\pi,0.1\pi]\) and we calculate for wavevectors in the path \(X_1\to\Gamma\to M_1\).

[5]:
# Run the guided-mode expansion
numeig, verbose = 20, True
nk = 50

X1 = [0.1*np.pi, 0]
M1 = [0.1*np.pi, 0.1*np.pi]
path = lattice.bz_path([X1, 'G', M1], [nk])

gme = legume.GuidedModeExp(phc, gmax=4.5, truncate_g='abs')
gme.run(kpoints=path['kpoints'], gmode_inds=[0, 1, 2, 3], numeig=numeig, verbose=True)
freqs = gme.freqs
freqs_im = gme.freqs_im
nkappa, nfreq = freqs.shape[0], freqs.shape[1]
print(f'Number of wavevectors = {nkappa}, number of frequencies = {nfreq}')
Running gme k-points: │██████████████████████████████│ 101 of 101
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Steps in GuidedModeExp: 69 plane waves and 4 guided modes  Time (s)                  % vs total T ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Guided modes computation with gmode_compute='exact'        4.670     │█████████████-------│   66% │
│ Inverse matrix of Fourier-space permittivity               0.001     │--------------------│    0% │
│ Matrix diagionalization using the 'eigh' solver            0.442     │█-------------------│    6% │
│ Creating GME matrix                                        1.925     │█████---------------│   27% │
├───────────────────────────────────────────────────────────┼──────────┼──────────────────────────────┤
│ Total time for real part of frequencies for 101 k-points   7.085     │████████████████████│  100% │
└───────────────────────────────────────────────────────────┴──────────┴──────────────────────────────┘
Running GME losses k-point: │██████████████████████████████│ 101 of 101
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Steps in GuidedModeExp: 69 plane waves and 4 guided modes         Time (s) ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Total time for imaginary part of frequencies for 2020 eigenmodes  8.673    │
└──────────────────────────────────────────────────────────────────┴──────────┘
Number of wavevectors = 101, number of frequencies = 20

We now want to plot the Q-factor versus the wavevector for the chosen mode, which has index jmode=2.

But there is an annoying issue we should fix: unfortunately the photonic bands are discontinuous at \({\bf k}=0\), as the code calculates one less frequency there.

[6]:
jmode = 2
plt.plot(range(nkappa), freqs[:,jmode])
[6]:
[<matplotlib.lines.Line2D at 0x76dde978d090>]
../_images/examples_13_BIC_and_Q_factor_13_1.png

Thus, we first make the frequencies and imaginary parts to be continuous, then we calculate and plot the Q-factor of the chosen mode. Compare with Fig. 9 of the CPC paper.

[7]:
# shift the frequencies at k=0 to make them continuous
freqs_cured    = copy.deepcopy(freqs)
freqs_im_cured = copy.deepcopy(freqs_im)
freqs_cured[nk, 0] = 0
for j in range(1, nfreq-1):
  freqs_cured   [nk,j] = freqs   [nk, j-1]
  freqs_im_cured[nk,j] = freqs_im[nk, j-1]

# then we calculate and plot the Q-factor of that mode
jmode=2
qfactor = freqs_cured[:,jmode]/2/freqs_im_cured[:,jmode]
kappa = range(nkappa)

xticks, xticklabels = path['indexes'], [r'X$\leftarrow$', '$\Gamma$', r'$\rightarrow$ M']
markersize = 3

fig, ax = plt.subplots(nrows=1, ncols=2, constrained_layout=True, figsize=(5, 4))
for a in ax:
  a.set_xlim([0, gme.freqs.shape[0]-1])
  a.set_xlabel("Wavevector")
  a.set_xticks(path['indexes'])
  a.set_xticklabels(xticklabels)

ax[0].plot(kappa, freqs_cured[:, jmode])
ax[0].set_ylabel("$\omega a/2\pi c$")
ax[0].xaxis.grid('True')

ax[1].plot(kappa, qfactor)
ax[1].set_yscale('log')
ax[1].set_ylim([10**2, 10**8])
ax[1].set_ylabel("Q factor")
[7]:
Text(0, 0.5, 'Q factor')
../_images/examples_13_BIC_and_Q_factor_15_1.png

The Q-factor actually diverges as \(Q\propto k^{-2}\) for \({\bf k}\rightarrow0\). This can be seen by using a double-log scale (see CPC paper, Fig. 9(b), inset) or by plotting the imaginary part of the frequency as a function of \(k\). Compare also with Fig. S3 of Zagaglia et el, Opt. Lett. 48, 5017 (2023).

[8]:
markersize = 1

fig, ax = plt.subplots(1, constrained_layout=True, figsize=(5, 4))
ax.plot(kappa, freqs_im_cured[:,2], markersize=markersize)
ax.set_xticks(path['indexes'])
ax.set_xticklabels(xticklabels)
ax.xaxis.grid('True')

#ax.set_yscale('log')
#ax.set_ylim([10**2, 10**8])
ax.set_xlim([0, gme.freqs.shape[0]-1])
ax.set_xlabel("Wavevector")
ax.set_ylabel("$\mathcal{I}(\omega a/2\pi c)$")
[8]:
Text(0, 0.5, '$\\mathcal{I}(\\omega a/2\\pi c)$')
../_images/examples_13_BIC_and_Q_factor_17_1.png

We see that there are many BICs with infinite Q-factor at \(k=0\), which are BICs. (For symmetry reasons, in the square lattice with circular holes, all nondegenerate states are symmetry-protected BICs, and vice-versa).

Here we focus on the lowest BIC with dimensionless frequency around 0.45. Let’s visualize the field components on the top of the slab (z=D). [The lowest cladding in legume extends from \(z=-\infty\) to \(z=0\).]

[9]:
# to avoid the issue of the band discontinuity, for a given k we find the index j that correspond to the target mode
jk = nk
result = [(j, freqs[jk,j]) for j in range(nfreq) if ((freqs[jk,j] > 0.44) and (freqs[jk,j] < 0.46)) ]
(jmode, frequency) = result[0]
plt.rcParams['figure.figsize'] = [10,4] # We change the defualt figure size to better show field profiles
print(f"Target mode at band index {jmode} has frequency = {frequency:.6f}")

ax=legume.viz.field(gme, 'E', nk, jmode, z=D, component='xyz', val='re', N1=200, N2=200)
plt.figure().set_figwidth(32)
# be careful about real and imaginary parts... usually both of them can be nonzero
# in this particular case, the electric field is real, while the magnetic field is imaginary
Target mode at band index 1 has frequency = 0.451974
../_images/examples_13_BIC_and_Q_factor_19_1.png
<Figure size 3200x400 with 0 Axes>
[10]:
plt.figure(figsize=(15,3))
ax2=legume.viz.field(gme, 'H', nk, jmode, z=D, component='xyz', val='im', N1=200, N2=200)
<Figure size 1500x300 with 0 Axes>
../_images/examples_13_BIC_and_Q_factor_20_1.png

See Appendix B of the CPC paper for a symmetry analysis of the BIC field profiles. This figure more or less corresponds to Fig. B.15 (which has a more sophisticated graphics).