-
Andreas Klöckner authoredAndreas Klöckner authored
analytic_solutions.py 12.50 KiB
# grudge - the Hybrid'n'Easy DG Environment
# Copyright (C) 2007 Andreas Kloeckner
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from grudge.tools import \
cyl_bessel_j, \
cyl_bessel_j_prime
from math import sqrt, pi, sin, cos, atan2, acos
import numpy
import numpy.linalg as la
import cmath
from six.moves import range
from six.moves import zip
# solution adapters -----------------------------------------------------------
class RealPartAdapter:
def __init__(self, adaptee):
self.adaptee = adaptee
@property
def shape(self):
return self.adaptee.shape
def __call__(self, x, el):
return [xi.real for xi in self.adaptee(x, el)]
class SplitComplexAdapter:
def __init__(self, adaptee):
self.adaptee = adaptee
@property
def shape(self):
(n,) = self.adaptee.shape
return (n*2,)
def __call__(self, x, el):
ad_x = self.adaptee(x, el)
return [xi.real for xi in ad_x] + [xi.imag for xi in ad_x]
class CylindricalFieldAdapter:
"""Turns a field returned as (r, phi, z) components into
(x,y,z) components.
"""
def __init__(self, adaptee):
self.adaptee = adaptee
@property
def shape(self):
return self.adaptee.shape
def __call__(self, x, el):
xy = x[:2]
r = la.norm(xy)
phi = atan2(x[1], x[0])
prev_result = self.adaptee(x, el)
result = []
i = 0
while i < len(prev_result):
fr, fphi, fz = prev_result[i:i+3]
result.extend([
cos(phi)*fr - sin(phi)*fphi, # ex
sin(phi)*fr + cos(phi)*fphi, # ey
fz,
])
i += 3
return result
class SphericalFieldAdapter:
"""Turns the adaptee's field C{(Er, Etheta, Ephi)} components into
C{(Ex,Ey,Ez)} components.
Conventions are those of
http://mathworld.wolfram.com/SphericalCoordinates.html
"""
def __init__(self, adaptee):
self.adaptee = adaptee
@property
def shape(self):
return self.adaptee.shape
@staticmethod
def r_theta_phi_from_xyz(x):
r = la.norm(x)
return r, atan2(x[1], x[0]), acos(x[2]/r)
def __call__(self, x, el):
r, theta, phi = self.r_theta_phi_from_xyz(x)
er = numpy.array([
cos(theta)*sin(phi),
sin(theta)*sin(phi),
cos(phi)])
etheta = numpy.array([
-sin(theta),
cos(theta),
0])
ephi = numpy.array([
cos(theta)*cos(phi),
sin(theta)*cos(phi),
-sin(phi)])
adaptee_result = self.adaptee(x, el)
result = numpy.empty_like(adaptee_result)
i = 0
while i < len(adaptee_result):
fr, ftheta, fphi = adaptee_result[i:i+3]
result[i:i+3] = fr*er + ftheta*etheta + fphi*ephi
i += 3
return result
# actual solutions ------------------------------------------------------------
class CylindricalCavityMode:
"""A cylindrical TM cavity mode.
Taken from:
J.D. Jackson, Classical Electrodynamics, Wiley.
3rd edition, 2001.
ch 8.7, p. 368f.
"""
def __init__(self, m, n, p, radius, height, epsilon, mu):
try:
from bessel_zeros import bessel_zeros
except ImportError:
print("*** You need to generate the bessel root data file.")
print("*** Execute generate-bessel-zeros.py at the command line.")
raise
assert m >= 0 and m == int(m)
assert n >= 1 and n == int(n)
assert p >= 0 and p == int(p)
self.m = m
self.n = n
self.p = p
self.phi_sign = 1
R = self.radius = radius
d = self.height = height
self.epsilon = epsilon
self.mu = mu
self.t = 0
x_mn = bessel_zeros[m][n-1]
self.omega = 1 / sqrt(mu*epsilon) * sqrt(
x_mn**2 / R**2
+ p**2 * pi**2 / d**2)
self.gamma_mn = x_mn/R
def set_time(self, t):
self.t = t
shape = (6,)
def __call__(self, x, el):
# coordinates -----------------------------------------------------
xy = x[:2]
r = sqrt(xy*xy)
phi = atan2(x[1], x[0])
z = x[2]
# copy instance variables for easier access -----------------------
m = self.m
p = self.p
gamma = self.gamma_mn
phi_sign = self.phi_sign
omega = self.omega
d = self.height
epsilon = self.epsilon
# common subexpressions -------------------------------------------
tdep = cmath.exp(-1j * omega * self.t)
phi_factor = cmath.exp(phi_sign * 1j * m * phi)
# psi and derivatives ---------------------------------------------
psi = cyl_bessel_j(m, gamma * r) * phi_factor
psi_dr = gamma*cyl_bessel_j_prime(m, gamma*r) * phi_factor
psi_dphi = (cyl_bessel_j(m, gamma * r)
* 1/r * phi_sign*1j*m * phi_factor)
# field components in polar coordinates ---------------------------
ez = tdep * cos(p * pi * z / d) * psi
e_transverse_factor = (tdep
* (-p*pi/(d*gamma**2))
* sin(p * pi * z / d))
er = e_transverse_factor * psi_dr
ephi = e_transverse_factor * psi_dphi
hz = 0j
# z x grad psi = z x (psi_x, psi_y) = (-psi_y, psi_x)
# z x grad psi = z x (psi_r, psi_phi) = (-psi_phi, psi_r)
h_transverse_factor = (tdep
* 1j*epsilon*omega/gamma**2
* cos(p * pi * z / d))
hr = h_transverse_factor * (-psi_dphi)
hphi = h_transverse_factor * psi_dr
return [er, ephi, ez, hr, hphi, hz]
class RectangularWaveguideMode:
"""A rectangular TM cavity mode."""
def __init__(self, epsilon, mu, mode_indices,
dimensions=(1,1,1), coefficients=(1,0,0),
forward_coeff=1, backward_coeff=0):
for n in mode_indices:
assert n >= 0 and n == int(n)
self.mode_indices = mode_indices
self.dimensions = dimensions
self.coefficients = coefficients
self.forward_coeff = forward_coeff
self.backward_coeff = backward_coeff
self.epsilon = epsilon
self.mu = mu
self.t = 0
self.factors = [n*pi/a for n, a in zip(self.mode_indices, self.dimensions)]
c = 1/sqrt(mu*epsilon)
self.k = sqrt(sum(f**2 for f in self.factors))
self.omega = self.k*c
def set_time(self, t):
self.t = t
def __call__(self, discr):
f,g,h = self.factors
omega = self.omega
k = self.k
x = discr.nodes[:,0]
y = discr.nodes[:,1]
if discr.dimensions > 2:
z = discr.nodes[:,2]
else:
z = discr.volume_zeros()
sx = numpy.sin(f*x)
sy = numpy.sin(g*y)
cx = numpy.cos(f*x)
cy = numpy.cos(g*y)
zdep_add = numpy.exp(1j*h*z)+numpy.exp(-1j*h*z)
zdep_sub = numpy.exp(1j*h*z)-numpy.exp(-1j*h*z)
tdep = numpy.exp(-1j * omega * self.t)
C = 1j/(f**2+g**2)
result = numpy.zeros((6,len(x)), dtype=zdep_add.dtype)
result[0] = C*f*h*cx*sy*zdep_sub*tdep # ex
result[1] = C*g*h*sx*cy*zdep_sub*tdep # ey
result[2] = sx*sy*zdep_add*tdep # ez
result[3] = -C*g*self.epsilon*omega*sx*cy*zdep_add*tdep # hx
result[4] = C*f*self.epsilon*omega*cx*sy*zdep_add*tdep
return result
class RectangularCavityMode(RectangularWaveguideMode):
"""A rectangular TM cavity mode."""
def __init__(self, *args, **kwargs):
if "scale" in kwargs:
kwargs["forward_coeff"] = scale
kwargs["backward_coeff"] = scale
else:
kwargs["forward_coeff"] = 1
kwargs["backward_coeff"] = 1
RectangularWaveguideMode.__init__(self, *args, **kwargs)
# dipole solution -------------------------------------------------------------
class DipoleFarField:
"""Dipole EM field.
See U{http://piki.tiker.net/wiki/Dipole}.
"""
def __init__(self, q, d, omega, epsilon, mu):
self.q = q
self.d = d
self.epsilon = epsilon
self.mu = mu
self.c = c = 1/sqrt(mu*epsilon)
self.omega = omega
freq = omega/(2*pi)
self.wavelength = c/freq
def far_field_mask(self, x, el):
if la.norm(x) > 0.5*self.wavelength:
return 1
else:
return 0
def set_time(self, t):
self.t = t
shape = (6,)
def __call__(self, x, el):
# coordinates ---------------------------------------------------------
r, theta, phi = SphericalFieldAdapter.r_theta_phi_from_xyz(x)
# copy instance variables for easier access ---------------------------
q = self.q
d = self.d
epsilon = self.epsilon
mu = self.mu
c = self.c
omega = self.omega
t = self.t
# field components ----------------------------------------------------
tdep = omega*t-omega*r/c
e_r = (2*cos(phi)*q*d) / (4*pi*epsilon) * (
1/r**3 * sin(tdep)
+omega/(c*r**2) * cos(tdep))
e_phi = (sin(phi)*q*d) / (4*pi*epsilon) * (
(1/r**3 - omega**2/(c**2*r)*sin(tdep))
+ omega/(c*r**2)*cos(tdep))
h_theta = (omega*sin(phi)*q*d)/(4*pi) * (
- omega/(c*r)*sin(tdep)
+ 1/r**2*cos(tdep))
return [e_r, 0, e_phi, 0, h_theta, 0]
def source_modulation(self, t):
j0 = self.q*self.d*self.omega/self.epsilon
return j0*cos(self.omega*t)
# analytic solution tools -----------------------------------------------------
def check_time_harmonic_solution(discr, mode, c_sol):
from grudge.discretization import bind_nabla, bind_mass_matrix
from grudge.visualization import SiloVisualizer
from grudge.silo import SiloFile
from grudge.tools import dot, cross
from grudge.silo import DB_VARTYPE_VECTOR
def curl(field):
return cross(nabla, field)
vis = SiloVisualizer(discr)
nabla = bind_nabla(discr)
mass = bind_mass_matrix(discr)
def rel_l2_error(err, base):
def l2_norm(field):
return sqrt(dot(field, mass*field))
base_l2 = l2_norm(base)
err_l2 = l2_norm(err)
if base_l2 == 0:
if err_l2 == 0:
return 0.
else:
return float("inf")
else:
return err_l2/base_l2
dt = 0.1
for step in range(10):
t = step*dt
mode.set_time(t)
fields = discr.interpolate_volume_function(c_sol)
er = fields[0:3]
hr = fields[3:6]
ei = fields[6:9]
hi = fields[9:12]
silo = SiloFile("em-complex-%04d.silo" % step)
vis.add_to_silo(silo,
vectors=[
("curl_er", curl(er)),
("om_hi", -mode.mu*mode.omega*hi),
("curl_hr", curl(hr)),
("om_ei", mode.epsilon*mode.omega*hi),
],
expressions=[
("diff_er", "curl_er-om_hi", DB_VARTYPE_VECTOR),
("diff_hr", "curl_hr-om_ei", DB_VARTYPE_VECTOR),
],
write_coarse_mesh=True,
time=t, step=step
)
er_res = curl(er) + mode.mu *mode.omega*hi
ei_res = curl(ei) - mode.mu *mode.omega*hr
hr_res = curl(hr) - mode.epsilon*mode.omega*ei
hi_res = curl(hi) + mode.epsilon*mode.omega*er
print("time=%f, rel l2 residual in Re[E]=%g\tIm[E]=%g\tRe[H]=%g\tIm[H]=%g" % (
t,
rel_l2_error(er_res, er),
rel_l2_error(ei_res, ei),
rel_l2_error(hr_res, hr),
rel_l2_error(hi_res, hi),
))