import numpy as np
from scipy.integrate import solve_bvp
import matplotlib.pyplot as plt
Force free magnetic fields are defined as magnetic fields without Lorentz force
\begin{align*} \mathbf{J} \times \mathbf{B} & = \mathbf{0} \\ \nabla \cdot \mathbf{B} & = 0 \end{align*}
In magnetohydrodynamics (MHD), the current density \mathbf{J} is given by (in SI units)
\mathbf{J} = \frac{1}{\mu_0} \nabla \times \mathbf{B}
Therefore, force free magnetic fields are determined by the following partial differential equations (PDEs)
\begin{align*} \mathbf{(\nabla \times \mathbf{B})} \times \mathbf{B} & = \mathbf{0} \\ \nabla \cdot \mathbf{B} & = 0 \end{align*}
The analytical solution of these PDEs in a general case is unknown. But Low and Lou (1990) shows that we can calculate “axisymmetric” force free fields. If we rotate the plane perpendicular to the axis of symmetry, we can generate a quite general force free fields. In this post, I try to calculate this Low and Lou fields, referencing this code.
Low-Lou ODE
To calculate Low and Lou fields, we have to solve the following ordinary differential equation (ODE).
\begin{cases} \displaystyle (1-\mu^2)\frac{d^2 P}{d\mu^2} + n(n+1)P + a^2 \frac{1+n}{n}P^{1 + \frac{2}{n}} = 0 \\ \mu = \cos\theta \in [-1, 1] \\ P(-1) = 0 \\ P(1) = 0 \\ \\ P'(-1) = 10 \text{ for numerical normalization} \end{cases}
For fixed n, a serves as a eignvalue for this homogenous BVP.
For n=1, we can list the positive eigenvalues in ascending order with m=0, 1, 2, ....
We denote a eigenvalue as a^2 _{n, m} and the corresponding eigenfunction as P_{n, m}.
For n=1, m=0, 1, 2,
\begin{align} a^2 _{1, 0} &= 0 \\ a^2 _{1, 1} &= 0.425 \\ a^2 _{1, 2} &= 2.55 \\ \end{align}
If you carefully see the Figure 1 by Low and Lou (1990), we can notice that
P_{1,0} (\mu) \sim \cos\left(\displaystyle\frac{\pi}{2} \mu\right)
P_{1,1} (\mu) \sim -\sin\left(\displaystyle\pi \mu\right)
P_{1,2} (\mu) \sim -\cos\left(\displaystyle\frac{3\pi}{2} \mu\right)
Then we can generally say that
For m=0, 2 (even m)
P_{1,m} (\mu) \sim \cos\left(\displaystyle\frac{(m + 1)\pi}{2} \mu\right)
For m=1 (odd m) P_{1,m} (\mu) \sim \sin\left(\displaystyle\frac{(m + 1)\pi}{2} \mu\right)
This is the initial guess of the solution.
Rewrite Low-Lou ODE using \mathbf{S}(\mu)
(1-\mu^2)P'' + n(n+1)P + a^2 \frac{1+n}{n}P^{1 + \frac{2}{n}} = 0
\rightarrow P'' = \frac{1}{1-\mu^2}\left[- n(n+1)P - a^2 \frac{1+n}{n}P^{1 + \frac{2}{n}}\right]
\rightarrow P'' = \frac{-1}{1-\mu^2 + \epsilon}\left[n(n+1)P + a^2 \frac{1+n}{n}P^{1 + \frac{2}{n}}\right]
where \epsilon = 10^{-6} for numerical stability.
Target y
\mathbf{S}(\mu) = \begin{bmatrix}
P(\mu) \\
P'(\mu)
\end{bmatrix}
= \begin{bmatrix}
y[0] \\
y[1]
\end{bmatrix}
ODE system F(x, y)
\frac{d\mathbf{S}}{d\mu} = \mathbf{F}(\mu, \mathbf{S}(\mu))
= \begin{bmatrix}
P'(\mu) \\
P''(\mu)
\end{bmatrix}
= \begin{bmatrix}
y[1] \\
\displaystyle \frac{-1}{1-\mu^2 + \epsilon}\left[n(n+1)P + a^2 \frac{1+n}{n}P^{1 + \frac{2}{n}}\right]
\end{bmatrix}
\begin{cases} \mu = \cos\theta \in [-1, 1] \\ P(-1) = 0 \\ P(1) = 0 \\ \end{cases}
P'(-1) = 10
Domain
= [-1, 1]
mu_span = 100 # number of points
N = np.linspace(mu_span[0], mu_span[1], N) mu
Boundary condition function bc
defined from ya
, yb
\text{ya} = \mathbf{S}(-1) = \begin{bmatrix} P(-1) \\ P'(-1) \end{bmatrix} = \begin{bmatrix} 0 \\ 10 \end{bmatrix} = \begin{bmatrix} \text{ya}[0] \\ \text{ya}[1] \end{bmatrix}
\text{yb} = \mathbf{S}(1) = \begin{bmatrix} P(1) \\ P'(1) \end{bmatrix} = \begin{bmatrix} 0 \\ ? \end{bmatrix} = \begin{bmatrix} \text{yb}[0] \\ \text{yb}[1] \end{bmatrix}
\text{bc} = \begin{bmatrix} \text{ya} - \mathbf{S}(-1) \\ \text{yb} - \mathbf{S}(1) \end{bmatrix} = \begin{bmatrix} \text{ya}[0] - 0 \\ \text{ya}[1] - 10 \\ \text{yb}[0] - 0 \end{bmatrix} = \begin{bmatrix} \text{ya}[0] \\ \text{ya}[1] - 10\\ \text{yb}[0] \end{bmatrix}
Initial guess y0
For the given spacing h
,
\begin{align} \text{y0} & = [\mathbf{S}(-1), \mathbf{S}(-1 + h), \mathbf{S}(-1 + 2h), \cdots, \mathbf{S}(1)] \\ & = \begin{bmatrix} P(\mu=-1) & P(\mu=-1+h) & P(\mu=-1+2h) & \cdots & P(\mu=1) \\ P'(\mu=-1) & P'(\mu=-1+h) & P'(\mu=-1+2h) & \cdots & P'(\mu=1) \\ \end{bmatrix} \end{align}
For m=0, 2 (even m)
P_{1,m} (\mu) \sim \cos\left(\displaystyle\frac{(m + 1)\pi}{2} \mu\right)
For m=1 (odd m) P_{1,m} (\mu) \sim \sin\left(\displaystyle\frac{(m + 1)\pi}{2} \mu\right)
if m % 2 == 0:
= np.cos(mu * (m + 1) * np.pi / 2)
P_init else:
= np.sin(mu * (m + 1) * np.pi / 2) P_init
Since P'(-1) = 10,
= 10*np.ones_like(mu) dP_init
Then, together,
= np.vstack([P_init, dP_init]) S_init
def find_P_and_a2(n, m):
# ODE system
# Define BVP (Low and Lou 1990)
# a2 -> eigenvalue
# S = [P, dP/dmu]
# F = dSdmu
#
# dP/dmu = 10 at mu = -1
def F(x, y, p):
= x
mu = y[0]
P = y[1]
dP = p[0]
a2
= (-1)*(n*(n+1)*P + a2*((1+n)/n)*P**(1+2/n)) / (1-mu**2 + 1e-6)
ddP
return [dP, ddP]
# Boundary Condition
def bc(ya, yb, p):
return [ya[0], ya[1]-10, yb[0]]
# Domain
= [-1, 1]
mu_span = 100
N = np.linspace(mu_span[0], mu_span[1], N)
mu
# Initial guess
# For given m, use different initial guess
if m % 2 == 0:
= np.cos(mu * (m + 1) * np.pi / 2)
P_guess else:
= np.sin(mu * (m + 1) * np.pi / 2)
P_guess
# For initial guess of dP/dmu, just use BC value
= 10*np.ones_like(mu)
dP_guess
= np.vstack([P_guess, dP_guess])
y_guess
# For each initial eigenvalue, solve the problem.
# If it is successful, return that otherwise do not return.
# np.vectorize -> for loop & return type : array
@np.vectorize
def solve_eigenvalue_problem(a2_0):
= solve_bvp(F, bc, mu, y_guess, p=[a2_0], tol=1e-6)
sol if sol.success == True:
return sol
else:
return None
= np.linspace(0.0, 10.0, 100)
a2_0_list
= solve_eigenvalue_problem(a2_0_list)
results = np.array([sol.p for sol in results if sol is not None])
eigenvalues
# round & unique value & sorting
= np.sort(np.unique(np.round(eigenvalues, 4)))
eigenvalues
# The smallest value for given m is desired eigenvalue
= eigenvalues[0]
eigenvalue # If this eigenvalue is zero for nonzero m, choose the next big eigenvalue
if m > 0:
if not (eigenvalue > 0):
= eigenvalues[1]
eigenvalue
# Solve again with that eigenvalue
= solve_eigenvalue_problem([eigenvalue])[0]
sol
return sol.sol, sol.p[0]
= plt.subplots(figsize=(8, 6))
fig, ax True)
ax.grid(0, color='k', lw=2)
ax.axhline(0, color='k', lw=2)
ax.axvline(r'$\mu$')
ax.set_xlabel(r'P($\mu$)')
ax.set_ylabel(
= np.linspace(-1, 1, 1000)
mu_plot
= 1
n for m in [0, 1, 2]:
= find_P_and_a2(n, m)
S, a2 = S(mu_plot)[0]
P_plot
if a2 < 1e-3:
= 'P' r'$_{' f'{n}, {m}' r'}(\mu)$ with $a^2' r'_{' f'{n}, {m}' r'}$ = 0'
P_label else:
= 'P' r'$_{' f'{n}, {m}' r'}(\mu)$ with $a^2' r'_{' f'{n}, {m}' r'}$ = ' f'{a2:.3g}'
P_label =P_label)
ax.plot(mu_plot, P_plot, label
fig.legend()
We successfuly solve Low-Lou ODE for n=1, m=0, 1, 2. (see Figure 1 by Low and Lou (1990))
Parameters
bounds
bounds=[x_min, x_max, y_min, y_max, z_min, z_max]
resolutions
resolutions=[Nx, Ny, Nz]
where Nx
, Ny
, Nz
respectively mean that the number of points in x-, y-, z-axis.
n & m
P_{n,m}(\mu) & a^2 _{n,m} are eigenfunction and eigenvalues for Low-Lou ODE with fixed n. And m is just used for denoting differenct eigenfunction and eigenvalues.
\displaystyle (1-\mu^2)\frac{d^2 P_{n,m}}{d\mu^2} + n(n+1)P_{n,m} + a^2 _{n,m} \frac{1+n}{n}P_{n,m}^{1 + \frac{2}{n}} = 0
l & \Phi
See Figure 2 by Low and Lou (1990)
Algorithm
For each physical coordinate (x, y, z),
Calculate corresponding
- local Cartesian coordinate (X, Y, Z)
- local spherical coordinate (r, \theta, \phi)
- \mu = \cos\theta
- P'_{n,m}(\mu) and P_{n,m}(\mu) by solving Low-Lou ODE
- (B_r, B_\theta, B_\phi)
- (B_X, B_Y, B_Z)
- (B_x, B_y, B_z)
Then, we get a vector (B_x, B_y, B_z) at the point (x, y, z)
The local spherical coordinates are
r = \sqrt{X^2 + Y^2 + Z^2}
\theta= \arccos\frac{Z}{r}
\phi = \arctan2\frac{Y}{X}
S
refers to \mathbf{S}_{n,m}(\mu) and a2
refers to a^2 _{n,m}
\mathbf{S}_{n,m}(\mu) = \begin{bmatrix} P_{n,m}(\mu) \\ P'_{n,m}(\mu) \end{bmatrix}
The magnetic field is
\mathbf{B}(r, \theta, \phi) = B_r \hat{r} + B_\theta \hat{\theta} + B_\phi \hat{\phi}
where
B_r = \frac{1}{r^2 \sin\theta} \frac{\partial A}{\partial \theta}
B_\theta = -\frac{1}{r \sin\theta} \frac{\partial A}{\partial r}
B_\phi = \frac{Q}{r \sin\theta}
Here A is
A = (r^{-n})P_{n,m}(\mu)
\frac{\partial A}{\partial \theta} = (- r^{-n}\sin\theta) P'_{n,m}(\mu)
\frac{\partial A}{\partial r} = (-n r^{-n-1}) P_{n,m}(\mu)
For n=1,
Q(A) = \sqrt{a^2 _{n,m}} A^{1 + \frac{1}{n}}
\alpha = \sqrt{a^2 _{n,m}} \left(1 + \frac{1}{n} \right) A^{\frac{1}{n}}
import pyvista as pv
'static')
pv.set_jupyter_backend(= True
pv.global_theme.notebook pv.start_xvfb()
class LowLouMag:
"A Low and Lou (1990) NLFFF"
def __init__(self,
=[-1,1,-1,1,0,2],
bounds=[64,64,64],
resolutions=1, m=1,
n=0.3, Phi=np.pi/2):
lself.bounds = bounds
self.resolutions = resolutions
self.n = n
self.m = m
self.l = l
self.Phi = Phi
def __str__(self):
return (
"### Low and Lou (1990) NLFFF\n"
f"bounds = {self.bounds}<br>\n"
f"resolutions = {self.resolutions}<br>\n"
f"n = {self.n}<br>\n"
f"m = {self.m}<br>\n"
f"l = {self.l}<br>\n"
f"Phi = {self.Phi/np.pi}π<br>\n"
)= __str__
_repr_markdown_
def create_physical_coordinates(self):
= np.linspace(self.bounds[0], self.bounds[1], self.resolutions[0])
x_1D = np.linspace(self.bounds[2], self.bounds[3], self.resolutions[1])
y_1D = np.linspace(self.bounds[4], self.bounds[5], self.resolutions[2])
z_1D = np.diff(x_1D)[0]
x_spacing = np.diff(y_1D)[0]
y_spacing = np.diff(z_1D)[0]
z_spacing = (x_spacing, y_spacing, z_spacing)
spacing = (x_1D[0], y_1D[0], z_1D[0]) # The bottom left corner of the data set
origin self.grid = pv.ImageData(dimensions=self.resolutions, spacing=spacing, origin=origin)
self.x_1D = x_1D
self.y_1D = y_1D
self.z_1D = z_1D
return self.grid
def calculate_local_Cartesian_coordinates(self):
# information of point source & Z-axis
= self.l
l = self.Phi
Phi
# physical coordinates (x, y, z)
= self.grid.x
x = self.grid.y
y = self.grid.z
z
# local Cartesian coordinates (X, Y, Z)
= x*np.cos(Phi) - (z+l)*np.sin(Phi)
X = y
Y = x*np.sin(Phi) + (z+l)*np.cos(Phi)
Z
self.X, self.Y, self.Z = X, Y, Z
def calculate_local_spherical_coordinates(self):
# local Cartesian coordinates (X, Y, Z)
= self.X
X = self.Y
Y = self.Z
Z
# local spherical coordinates (r, theta, phi)
= np.sqrt(X**2 + Y**2 + Z**2)
r = np.arccos(Z/r)
theta = np.arctan2(Y, X)
phi
self.r, self.theta, self.phi = r, theta, phi
def calculate_eigenfunctions(self):
# calculate mu=cos(theta)
= np.cos(self.theta)
mu
# eigenfunction parameter n, m
= self.n
n = self.m
m
# calculate eigenfunction & its derivates and eigenvalues
# S = [P, dP]
= find_P_and_a2(n, m)
S, a2 = S(mu)
P, dP
self.P, self.dP, self.a2 = P, dP, a2
def calculate_local_spherical_magnetic_fields(self):
# eigenfunctions and eigenvalue
= self.n, self.m
n, m = self.P, self.dP, self.a2
P, dP, a2
# r, theta info
= self.r, self.theta
r, theta
= (r**(-n)) * P
A = -(r**(-n)) * np.sin(theta) * dP
dA_dtheta = -(n*(r**(-n-1))) * P
dA_dr = np.sqrt(a2) * A * np.abs(A)**(1/n)
Q
= np.sqrt(a2) * (1 + 1/n) * A**(1/n)
alpha
= (r**2 * np.sin(theta))**(-1) * dA_dtheta
Br = -1 * (r*np.sin(theta))**(-1) * dA_dr
Btheta = (r*np.sin(theta))**(-1) * Q
Bphi
self.Br, self.Btheta, self.Bphi, self.alpha = Br, Btheta, Bphi, alpha
def calculate_local_Cartesian_magnetic_fields(self):
= self.Br, self.Btheta, self.Bphi
Br, Btheta, Bphi = self.r, self.theta, self.phi
r, theta, phi
= Br * np.sin(theta) * np.cos(phi) + Btheta * np.cos(theta) * np.cos(phi) - Bphi * np.sin(phi)
BX = Br * np.sin(theta) * np.sin(phi) + Btheta * np.cos(theta) * np.sin(phi) + Bphi * np.cos(phi)
BY = Br * np.cos(theta) - Bphi * np.sin(theta)
BZ
self.BX, self.BY, self.BZ = BX, BY, BZ
def calculate_physical_magnetic_fields(self):
= self.BX, self.BY, self.BZ
BX, BY, BZ = self.Phi
Phi
= BX * np.cos(Phi) + BZ * np.sin(Phi)
Bx = BY
By = - BX * np.sin(Phi) + BZ * np.cos(Phi)
Bz
self.Bx, self.By, self.Bz = Bx, By, Bz
def calculate_final_magnetic_fields(self):
= self.Bx.reshape(self.resolutions).transpose(2, 1, 0)
bx = self.By.reshape(self.resolutions).transpose(2, 1, 0)
by = self.Bz.reshape(self.resolutions).transpose(2, 1, 0)
bz return bx, by, bz
##------ All in one ------##
def calculate(self):
self.create_physical_coordinates()
self.calculate_local_Cartesian_coordinates()
self.calculate_local_spherical_coordinates()
self.calculate_eigenfunctions()
self.calculate_local_spherical_magnetic_fields()
self.calculate_local_Cartesian_magnetic_fields()
self.calculate_physical_magnetic_fields()
= self.calculate_final_magnetic_fields()
bx, by, bz return bx, by, bz
= LowLouMag()
b b
Low and Lou (1990) NLFFF
bounds = [-1, 1, -1, 1, 0, 2]
resolutions = [64, 64, 64]
n = 1
m = 1
l = 0.3
Phi = 0.5π
b.create_physical_coordinates()
ImageData | Information |
N Cells | 250047 |
N Points | 262144 |
X Bounds | -1.000e+00, 1.000e+00 |
Y Bounds | -1.000e+00, 1.000e+00 |
Z Bounds | 0.000e+00, 2.000e+00 |
Dimensions | 64, 64, 64 |
Spacing | 3.175e-02, 3.175e-02, 3.175e-02 |
N Arrays | 0 |
b.calculate_local_Cartesian_coordinates()
0], b.grid.y[0], b.grid.z[0] b.grid.x[
(-1.0, -1.0, 0.0)
0], b.Y[0], b.Z[0] b.X[
(-0.30000000000000004, -1.0, -1.0)
b.calculate_local_spherical_coordinates()
0], b.theta[0], b.phi[0] b.r[
(1.445683229480096, 2.3346567297775978, -1.8622531212727638)
b.calculate_eigenfunctions()
0], b.dP[0], b.a2 b.P[
(2.3671821639686197, 4.145376627987454, 0.4274037235234833)
b.calculate_local_spherical_magnetic_fields()
0], b.Btheta[0], b.Bphi[0], b.alpha[0] b.Br[
(-1.3719698429432043,
1.0848561237109233,
1.6788928338597737,
2.140955710310132)
b.calculate_local_Cartesian_magnetic_fields()
0], b.BY[0], b.BZ[0] b.BX[
(2.108420021547058, 1.185348144787035, -0.26343650351723624)
b.calculate_physical_magnetic_fields()
0], b.By[0], b.Bz[0] b.Bx[
(-0.26343650351723613, 1.185348144787035, -2.108420021547058)
= b.calculate_final_magnetic_fields() bx, by, bz
!pip install -q git+https://github.com/mgjeon/magnetic_field_line.git
from magplot.base import create_mesh, mag_plotter
= create_mesh(bx, by, bz) mesh
= mag_plotter(mesh)
b_plot = b_plot.create_mesh(i_siz=32, j_siz=32,
b_tube, b_bottom, b_dargs =8, j_resolution=8,
i_resolution=-100, vmax=100,
vmin=10000) max_time
= pv.Plotter()
p
p.add_mesh(b_plot.grid.outline())='gray', **b_dargs)
p.add_mesh(b_bottom, cmap=False, color='blue')
p.add_mesh(b_tube, lighting= 'xy'
p.camera_position
p.show_bounds() p.show()
= pv.Plotter(off_screen=False)
p
p.add_mesh(b_plot.grid.outline())=(mesh.center[0], mesh.center[1], -1), direction=(0, 0, 1), i_size=64, j_size=64), color='gray')
p.add_mesh(pv.Plane(center=b_bottom['vector'][:, 2]), cmap='bwr', **b_dargs)
p.add_mesh(b_bottom.contour(scalars=False, color='black')
p.add_mesh(b_tube, lighting= 'xy'
p.camera_position
p.show_bounds() p.show()
It seems to correspond to Figure 8, not Figure 3. I don’t know why.
= create_mesh(*LowLouMag(Phi=np.pi/4).calculate()).reflect((0, 1, 0))
mesh_new
= mag_plotter(mesh_new)
b_plot_new = b_plot_new.create_mesh(i_siz=32, j_siz=32,
b_tube_new, b_bottom_new, b_dargs_new =8, j_resolution=8,
i_resolution=-100, vmax=100,
vmin=10000)
max_time
= pv.Plotter(off_screen=False)
p
p.add_mesh(b_plot_new.grid.outline())=(mesh_new.center[0], mesh_new.center[1], -1), direction=(0, 0, 1), i_size=64, j_size=64), color='gray')
p.add_mesh(pv.Plane(center=b_bottom_new['vector'][:, 2]), cmap='bwr', **b_dargs_new)
p.add_mesh(b_bottom_new.contour(scalars=False, color='black')
p.add_mesh(b_tube_new, lighting= 'xy'
p.camera_position
p.show_bounds() p.show()
= create_mesh(*LowLouMag(Phi=0.47).calculate()).reflect((0, 1, 0))
mesh_new
= mag_plotter(mesh_new)
b_plot_new = b_plot_new.create_mesh(i_siz=40, j_siz=40,
b_tube_new, b_bottom_new, b_dargs_new =8, j_resolution=8,
i_resolution=-100, vmax=100,
vmin=10000)
max_time
= pv.Plotter(off_screen=False)
p
p.add_mesh(b_plot_new.grid.outline())=(mesh_new.center[0], mesh_new.center[1], -1), direction=(0, 0, 1), i_size=64, j_size=64), color='gray')
p.add_mesh(pv.Plane(center=b_bottom_new['vector'][:, 2]), cmap='bwr', **b_dargs_new)
p.add_mesh(b_bottom_new.contour(scalars=False, color='black')
p.add_mesh(b_tube_new, lighting= 'xy'
p.camera_position
p.show_bounds() p.show()
= create_mesh(*LowLouMag(Phi=0.27).calculate()).reflect((0, 1, 0))
mesh_new
= mag_plotter(mesh_new)
b_plot_new = b_plot_new.create_mesh(i_siz=40, j_siz=40,
b_tube_new, b_bottom_new, b_dargs_new =8, j_resolution=8,
i_resolution=-100, vmax=100,
vmin=10000)
max_time
= pv.Plotter(off_screen=False)
p
p.add_mesh(b_plot_new.grid.outline())=(mesh_new.center[0], mesh_new.center[1], -1), direction=(0, 0, 1), i_size=64, j_size=64), color='gray')
p.add_mesh(pv.Plane(center=b_bottom_new['vector'][:, 2]), cmap='bwr', **b_dargs_new)
p.add_mesh(b_bottom_new.contour(scalars=False, color='black')
p.add_mesh(b_tube_new, lighting= 'xy'
p.camera_position
p.show_bounds() p.show()
With reflect((0, 1, 0))
, these seems to correspond to Figure 4, 5, and 6, respectively. However, I don’t know why I need to use reflect((0, 1, 0))
.