Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the Poynting vector calculation #115

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/photonics/examples/effective_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@
neff_dict = dict()
aeff_dict = dict()
tm_dict = dict()
p_dict = dict()
for h in h_list:
neff_list = []
aeff_list = []
tm_list = []
p_list = []
for width in w_list:
width = width * 1e-3
nc = shapely.geometry.box(
Expand Down Expand Up @@ -95,12 +97,14 @@
neff_list.append(np.real(mode.n_eff))
aeff_list.append(mode.calculate_effective_area())
tm_list.append(mode.transversality)
p_list.append(mode.Pz)
break
else:
print(f"no TM mode found for {width}")
neff_dict[str(h)] = neff_list
aeff_dict[str(h)] = aeff_list
tm_dict[str(h)] = tm_list
p_dict[str(h)] = p_list
# %% [markdown]
# Plot the result

Expand Down Expand Up @@ -184,3 +188,52 @@
ax4.legend()

plt.show()

# %% [markdown]
# We can also plot the modes to compare them with the reference article

# %%
# Data figure h
w_fig_h = 500
idx_fig_h = min(range(len(w_list)), key=lambda x: abs(w_list[x] - w_fig_h))
h_fig_h = 0.7
basis_fig_h, Pz_fig_h = p_dict[str(h_fig_h)][idx_fig_h]

# Data figure f
w_fig_f = 600
idx_fig_f = min(range(len(w_list)), key=lambda x: abs(w_list[x] - w_fig_f))
h_fig_f = 0.5
basis_fig_f, Pz_fig_f = p_dict[str(h_fig_f)][idx_fig_f]

fig, ax = plt.subplots(1, 2)

basis_fig_h.plot(np.abs(Pz_fig_h), ax=ax[0], aspect="equal")
ax[0].set_title(
f"Poynting vector $S_z$\nfor h = {h_fig_h}μm & w = {w_fig_h}nm\n(Reproduction of Fig.1.h)"
)
ax[0].set_xlim(-w_fig_h * 1e-3 / 2 - 0.1, w_fig_h * 1e-3 / 2 + 0.1)
ax[0].set_ylim(capital_h - 0.1, capital_h + h_fig_h + 0.1)
# Turn off the axis wrt to the article figure
ax[0].axis("off")
# Add the contour
for subdomain in basis_fig_h.mesh.subdomains.keys() - {"gmsh:bounding_entities"}:
basis_fig_h.mesh.restrict(subdomain).draw(
ax=ax[0], boundaries_only=True, color="k", linewidth=1.0
)

basis_fig_f.plot(np.abs(Pz_fig_f), ax=ax[1], aspect="equal")
ax[1].set_title(
f"Poynting vector $S_z$\nfor h = {h_fig_f}μm & w = {w_fig_f}nm\n(Reproduction of Fig.1.f)"
)
ax[1].set_xlim(-w_list[idx_fig_f] * 1e-3 / 2 - 0.1, w_list[idx_fig_f] * 1e-3 / 2 + 0.1)
ax[1].set_ylim(capital_h - 0.1, capital_h + h_fig_f + 0.1)
# Turn off the axis wrt to the article figure
ax[1].axis("off")
# Add the contour
for subdomain in basis_fig_f.mesh.subdomains.keys() - {"gmsh:bounding_entities"}:
basis_fig_f.mesh.restrict(subdomain).draw(
ax=ax[1], boundaries_only=True, color="k", linewidth=1.0
)

fig.tight_layout()
plt.show()
44 changes: 43 additions & 1 deletion femwell/maxwell/waveguide.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,45 @@ def n_eff(self):
"""Effective refractive index of the mode"""
return self.k / self.k0

@cached_property
def poynting(self):
"""Poynting vector of the mode"""

# Extraction of the fields
(Ex, Ey), Ez = self.basis.interpolate(self.E)
(Hx, Hy), Hz = self.basis.interpolate(self.H)

# New basis will be the discontinuous variant used by the solved Ez-field
(Et, Et_basis), (En, En_basis) = self.basis.split(self.E)
poynting_basis = self.basis.with_element(ElementDG(ElementTriP1()))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant as this line we should reuse the element for Ez of the basis, i.e.

Suggested change
poynting_basis = self.basis.with_element(ElementDG(ElementTriP1()))
poynting_basis = self.basis.with_element(ElementDG(self.basis.elems[1]))

would brobably do it.

Even better would be to have here a

Suggested change
poynting_basis = self.basis.with_element(ElementDG(ElementTriP1()))
poynting_basis = self.basis.with_element(ElementDG(ElementVector(self.basis.elems[1],3)))

as this way we could directly project using just one basis. This way we'd just have one field to return.

Some background:
ElementDG converts Elements in their discontinous counterpart. For example, ElementTriP1 has its degrees of freedom (DOFs) on the nodes around (Tri->Triangles->3 nodes/dofs).
As those nodes are shared with the neighboring triangles, DOFs are shared with the neighbors (just imagine the triangles's node-heights being the values and the triangles the surfaces in-between the nodes. A continuous element would lead to a continuous surface as the values at the nodes are reused.)

Here, we don't expect it to be continuous as epsilon makes discrete jumps as the boundaries, thus we have to consider that in how we choose the element.
For maxwell's equations we know that the tangential component of the field is continuous while the normal component is not -> best choice for Ex/Ey is a H(curl)-conforming element like the Nedelec-element. For Ez we know it's continuous so we're best of with a Lagrange element.

For the pointing vector, i'd guess it's save to use a vector of Lagrangian elements. As we see in the plots that it's discontinuous at boundaries, we need the discontinuous version of it.
Here, the question would be if there's any continuity across boundaries of the poynting vector which we could have in our choice of element as that would reduce the needed number of DOFs and would probably also be a bit more precise. (@DorisReiter do you know if there's any component of the pointing vector continuous across boundaries?)

For now I think we can just get it in with the Vector of Lagrangian elements, as we don't do a solve using those elements.

Copy link
Contributor Author

@lucasgrjn lucasgrjn Dec 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see for the use of basis.elem.elems.
(In the beginning I wanted/was thinking to use split() and get the elements from En_basis, but this way is much cleaner!)
(Corrected on 98cb853)

Copy link
Contributor Author

@lucasgrjn lucasgrjn Dec 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation! :)

I don't get the second part of your message to use a function like self.basis.with_element(ElementDG(ElementVector(self.basis.elems[1],3))). Why will we be allowed to project using one basis? Don't we already make that with the poynting_basis.project() ?

For the Poynting vector, from what I remember, you have the Poynting theorem which is an expression for the conservation of energy:
$$-\frac{\partial u}{\partial t} = \nabla\cdot\mathbf{S} + \mathbf{J}\cdot\mathbf{E}$$
with $u$ the energy density, $S$ the Poynting vector, $J$ the current density and $E$ the electric field. If we consider a mode (i.e. no source), we should obtain $\nabla\cdot\mathbf{S}=0$ but I think @DorisReiter will have better insights!

Copy link
Owner

@HelgeGehring HelgeGehring Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the moment, we have three sets of DOFs as the element is a scalar element.
if we use a vector-element we can have all three components in one set of DOFs (which we can split if we need)

something like

poynting_basis = self.basis.with_element(ElementDG(ElementVector(self.basis.elems[1],3)))
... = poynting_basis.project(np.stack([Px,Py,Pz]))

should do it (I always forget if stack/hstack/vstack, it should be stacked as a new last axis)

alternatively, there should also be a way to combine the DOFs into one set and use it with the vector basis, but I think it's better readable if we treat it directly as a vector.

About the continuity: that equation doesn't tell us about the interface-conditions of a single component (as a discrete change in one component can be compensated in another one)
So I guess it's better to assume it's discontinuous (the pictures you posted/in the paper also show that it's not continuous across interfaces)

Copy link
Owner

@HelgeGehring HelgeGehring Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also wondering if treating it as a vector makes sense at all 🤔
we're calculating eigenmodes, which should mean that the mode propagates only in z direction...
That lead to $S_x$=$S_y$=0?
Or is it only the integral over the whole domain which needs to be zero for those two components?

Edit: seems like it can be non-zero https://doi.org/10.1364/OE.472148

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also wondering if treating it as a vector makes sense at all 🤔 we're calculating eigenmodes, which should mean that the mode propagates only in z direction... That lead to Sx=$S_y$=0? Or is it only the integral over the whole domain which needs to be zero for those two components?

Honestly that's a good question... I also found an article which states about that. So I think it is safer to keep the 3 components :)


# Calculation of the Poynting vector
Px = Ey * Hz - Ez * Hy
Py = Ez * Hx - Ex * Hz
Pz = Ex * Hy - Ey * Hx

# Projection of the Poynting vector on the new basis
Px_proj = poynting_basis.project(Px, dtype=np.complex64)
Py_proj = poynting_basis.project(Py, dtype=np.complex64)
Pz_proj = poynting_basis.project(Pz, dtype=np.complex64)
HelgeGehring marked this conversation as resolved.
Show resolved Hide resolved

return poynting_basis, np.array([Px_proj, Py_proj, Pz_proj])

@cached_property
def Px(self):
basis, _P = self.poynting
return basis, _P[0]

@cached_property
def Py(self):
basis, _P = self.poynting
return basis, _P[1]

@cached_property
def Pz(self):
basis, _P = self.poynting
return basis, _P[2]
HelgeGehring marked this conversation as resolved.
Show resolved Hide resolved

@cached_property
def te_fraction(self):
"""TE-fraction of the mode"""
Expand Down Expand Up @@ -330,7 +369,10 @@ def compute_modes(
if order == 1:
element = ElementTriN1() * ElementTriP1()
elif order == 2:
element = ElementTriN2() * ElementTriP2()
# element = ElementTriN2() * ElementTriP2()
from skfem.element import ElementQuad2

element = ElementTriN2() * ElementQuad2 # ElementTriP2()
HelgeGehring marked this conversation as resolved.
Show resolved Hide resolved
else:
raise AssertionError("Only order 1 and 2 implemented by now.")

Expand Down