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

Adding toroidal harmonic notebooks #3681

Open
wants to merge 2 commits into
base: feature/toroidal-harmonics
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza
# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh
# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short
#
# SPDX-License-Identifier: LGPL-2.1-or-later

"""
A collection of functions used to approximate toroidal harmonics.
"""

from math import factorial

Check warning on line 11 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L11

Added line #L11 was not covered by tests

import numpy as np
from scipy.special import gamma, poch

Check warning on line 14 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L13-L14

Added lines #L13 - L14 were not covered by tests


def f_hypergeometric(a, b, c, z, n_max):

Check warning on line 17 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L17

Added line #L17 was not covered by tests
"""Evaluates the hypergeometric power series up to n_max.

.. math::
F(a, b; c; z) = \\sum_0^{n_max} \\frac{(a)_{s} (b)_{s}}{Gamma(c + s) s!} z^{s}

See https://dlmf.nist.gov/15.2#E2 and https://dlmf.nist.gov/5.2#iii for more
information.
"""
F = 0
for s in range(n_max + 1):
F += (poch(a, s) * poch(b, s)) / (gamma(c + s) * factorial(s)) * z**s
return F

Check warning on line 29 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L26-L29

Added lines #L26 - L29 were not covered by tests


def my_legendre_p(lam, mu, x, n_max=20):

Check warning on line 32 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L32

Added line #L32 was not covered by tests
"""Evaluates the associated Legendre function of the first kind of degree lambda and order
minus mu as a function of x. See https://dlmf.nist.gov/14.3#E18 for more information.

TODO check domain of validity? Assumed validity is 1<x<inf

.. math::
P_{\\lambda}^{-\\mu} = 2^{-\\mu} x^{\\lambda - \\mu} (x^2 - 1)^{\\mu/2}
F(\\frac{1}{2}(\\mu - \\lambda), \\frac{1}{2}(\\mu - \\lambda + 1);
\\mu + 1; 1 - \\frac{1}{x^2})

where F is the hypergeometric function defined above as f_hypergeometric.

""" # noqa: W505, E501
a = 1 / 2 * (mu - lam)
b = 1 / 2 * (mu - lam + 1)
c = mu + 1
z = 1 - 1 / (x**2)
F_sum = f_hypergeometric(a=a, b=b, c=c, z=z, n_max=n_max) # noqa: N806
legP = 2 ** (-mu) * x ** (lam - mu) * (x**2 - 1) ** (mu / 2) * F_sum # noqa: N806
return legP # noqa: RET504

Check warning on line 52 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L46-L52

Added lines #L46 - L52 were not covered by tests


def my_legendre_q(lam, mu, x, n_max=20):

Check warning on line 55 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L55

Added line #L55 was not covered by tests
"""Evaluates Olver's associated Legendre function of the second kind of degree lambda
and order minus mu as a function of x. See https://dlmf.nist.gov/14, https://dlmf.nist.gov/14.3#E10,
and https://dlmf.nist.gov/14.3#E7 for more information.

TODO check domain of validity? Assumed validity is 1<x<inf

.. math::
Q_{\\lambda}^{-\\mu} = \\frac{\\pi^{\\frac{1}{2}} (x^2 - 1)^{\frac{\\mu}{2}}}
{2^{\\lambda + 1} x^{\\lambda + \\mu + 1}}
F(\\frac{1}{2}(\\lambda + \\mu)+1, \\frac{1}{2}(\\lambda
+ \\mu); \\lambda + \\frac{3}{2}; \\frac{1}{x^2})

where F is the hypergeometric function defined above as f_hypergeometric.

"""
a = 1 / 2 * (lam + mu) + 1
b = 1 / 2 * (lam + mu + 1)
c = lam + 3 / 2
z = 1 / (x**2)
F_sum = f_hypergeometric(a=a, b=b, c=c, z=z, n_max=n_max) # noqa: N806
legQ = ( # noqa: N806

Check warning on line 76 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L71-L76

Added lines #L71 - L76 were not covered by tests
(np.pi ** (1 / 2) * (x**2 - 1) ** (mu / 2))
/ (2 ** (lam + 1) * x ** (lam + mu + 1))
* F_sum
)
if isinstance(type(legQ), np.float64):
if x == 1:
legQ = np.inf # noqa: N806

Check warning on line 83 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L81-L83

Added lines #L81 - L83 were not covered by tests
else:
legQ[x == 1] = np.inf
return legQ

Check warning on line 86 in bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py

View check run for this annotation

Codecov / codecov/patch

bluemira/equilibria/optimisation/harmonics/toroidal_harmonics_approx_functions.py#L85-L86

Added lines #L85 - L86 were not covered by tests
164 changes: 164 additions & 0 deletions examples/equilibria/single_wire_field_toroidal_harmonics.ex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# ---
# jupyter:
# jupytext:
# cell_metadata_filter: tags,-all
# notebook_metadata_filter: -jupytext.text_representation.jupytext_version
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
# name: python3
# ---

# %% tags=["remove-cell"]
# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza
# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh
# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short
#
# SPDX-License-Identifier: LGPL-2.1-or-later

"""
Calculate solution due to a single wire as a sum of toroidal harmonics.
"""

# %% [markdown]
# # Example of calculating the flux solution due to a single wire as a sum of toroidal harmonics

# %% [markdown]
# ### Imports

# %%
from math import factorial

import matplotlib.pyplot as plt
import numpy as np

from bluemira.base.constants import MU_0
from bluemira.equilibria.optimisation.harmonics.toroidal_harmonics_approx_functions import ( # noqa: E501
my_legendre_p,
my_legendre_q,
)
from bluemira.equilibria.plotting import PLOT_DEFAULTS
from bluemira.utilities.tools import cylindrical_to_toroidal, toroidal_to_cylindrical

# %% [markdown]
# First we define the location in cylindrical coordinates of the focus $(R_0, z_0)$ and
# of the wire $(R_c, z_c)$, and the current in the wire, $I_c$.
#
# We then convert the location of the wire from cylindrical $(R_c, z_c)$ to toroidal
# coordinates $(\tau_c, \sigma_c)$ using the following relations:
# $$ \tau_c = \ln\frac{d_{1}}{d_{2}} $$
# $$ \sigma_c = {sign}(z - z_{0}) \arccos\frac{d_{1}^2 + d_{2}^2 - 4 R_{0}^2}{2 d_{1}
# d_{2}} $$
# where
# $$ d_{1}^2 = (R_{c} + R_{0})^2 + (z_c - z_{0})^2 $$
# $$ d_{2}^2 = (R_{c} - R_{0})^2 + (z_c - z_{0})^2 $$

# We need a range of $\tau$ in order to create the grid over which we want to solve. We
clmould marked this conversation as resolved.
Show resolved Hide resolved
# specify this using the value of $\tau$ at the wire, $\tau_c$ and the approximate
# minimum distance from the focus. This estimation is necessary as using coordinate
# transform functions would result in divide by zero errors.
#
# Once we have the grid in toroidal coordinates, we can convert this to cylindrical
# coordinates for use later using the following relations:
# $$ R = R_0 \frac{\sinh\tau}{\cosh \tau - \cos \sigma}$$
# $$ z - z_0 = R_0 \frac{\sin \sigma}{\cosh \tau - \cos \sigma}$$
# %%
# Wire location
R_c = 5.0
Z_c = 1.0
I_c = 5e6

# Focus of toroidal coordinates
R_0 = 3.0
Z_0 = 0.2

# Location of wire in toroidal coordinates
tau_c, sigma_c = cylindrical_to_toroidal(R_0=R_0, z_0=Z_0, R=R_c, Z=Z_c)

# Using approximate value for d2_min to avoid infinities
# Approximating tau at the focus instead of using coordinate transform functions
# (in order to avoid divide by 0 errors)
d2_min = 0.05
tau_max = np.log(2 * R_0 / d2_min)
n_tau = 200
tau = np.linspace(tau_c, tau_max, n_tau)
n_sigma = 150
sigma = np.linspace(-np.pi, np.pi, n_sigma)

# Create grid in toroidal coordinates
tau, sigma = np.meshgrid(tau, sigma)

# Convert to cylindrical coordinates
R, Z = toroidal_to_cylindrical(R_0=R_0, z_0=Z_0, tau=tau, sigma=sigma)


# %% [markdown]
# Now we want to calculate the following coefficients
# $$ A_m = \frac{\mu_0 I_c}{2^{\frac{5}{2}}} factorial\_term \frac{\sinh(\tau_c)}
# {\Delta_c^{\frac{1}{2}}} P_{m - \frac{1}{2}}^{-1}(\cosh(\tau_c)) $$
# $$ A_m^{sin} = A_m \sin(m \sigma_c) $$
# $$ A_m^{cos} = A_m \cos(m \sigma_c) $$
# where $$ \Delta_c = \cosh(\tau_c) - \cos(\sigma_c) $$
# and $$ factorial\_term = \prod_{0}^{m-1} \left( 1 + \frac{1}{2(m-i)}\right) $$
#

# %%
# Useful combinations
Delta = np.cosh(tau) - np.cos(sigma)
Deltac = np.cosh(tau_c) - np.cos(sigma_c)

# Calculate coefficients
m_max = 5
Am_cos = np.zeros(m_max + 1)
Am_sin = np.zeros(m_max + 1)

for m in range(m_max + 1):
factorial_term = 1 if m == 0 else np.prod(1 + 0.5 / np.arange(1, m + 1))
A_m = (
(MU_0 * I_c / 2 ** (5 / 2))
* factorial_term
* (np.sinh(tau_c) / np.sqrt(Deltac))
* my_legendre_p(m - 1 / 2, 1, np.cosh(tau_c))
)
Am_cos[m] = A_m * np.cos(m * sigma_c)
Am_sin[m] = A_m * np.sin(m * sigma_c)

# %% [markdown]
# Now we can use the following
Copy link
Contributor

Choose a reason for hiding this comment

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

You have written up the markdown equations in the order you use them, however, it might be a bit clearer if you introduce us to the TH equations at the start of the notebook so the reader understands what you are aiming for, before breaking it down into the calculation steps.

#
# $$ A(\tau, \sigma) = \sum_{m=0}^{\infty} A_m^{\cos} \epsilon_m m! \sqrt{\frac{2}{\pi}}
# \Delta^{\frac{1}{2}} Q_{m-\frac{1}{2}}^{1}(\cosh \tau) \cos(m \sigma) + A_m^{\sin}
# \epsilon_m m! \sqrt{\frac{2}{\pi}} \Delta^{\frac{1}{2}}
# Q_{m-\frac{1}{2}}^{1}(\cosh \tau) \sin(m \sigma) $$

# along with $$\psi = R A$$
# to calculate the solution and plot the psi graph. Here we have that
# $ \epsilon_0 = 1$ and $\epsilon_{m\ge 1} = 2$.

# %%
# TODO equation (19) from OB document
epsilon = 2 * np.ones(m_max + 1)
epsilon[0] = 1
A = np.zeros_like(R)

for m in range(m_max + 1):
A += Am_cos[m] * epsilon[m] * factorial(m) * np.sqrt(2 / np.pi) * np.sqrt(
Delta
) * my_legendre_q(m - 1 / 2, 1, np.cosh(tau)) * np.cos(m * sigma) + Am_sin[
m
] * epsilon[m] * factorial(m) * np.sqrt(2 / np.pi) * np.sqrt(Delta) * my_legendre_q(
m - 1 / 2, 1, np.cosh(tau)
) * np.sin(m * sigma)

psi = R * A
nlevels = PLOT_DEFAULTS["psi"]["nlevels"]
cmap = PLOT_DEFAULTS["psi"]["cmap"]
plt.contour(R, Z, psi, nlevels, cmap=cmap)
plt.xlabel("R")
plt.ylabel("Z")
plt.title("Psi")
# %%
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# ---
# jupyter:
# jupytext:
# cell_metadata_filter: tags,-all
# notebook_metadata_filter: -jupytext.text_representation.jupytext_version
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
# name: python3
# ---

# %% tags=["remove-cell"]
# SPDX-FileCopyrightText: 2021-present M. Coleman, J. Cook, F. Franza
# SPDX-FileCopyrightText: 2021-present I.A. Maione, S. McIntosh
# SPDX-FileCopyrightText: 2021-present J. Morris, D. Short
#
# SPDX-License-Identifier: LGPL-2.1-or-later

"""
An example of plotting the internal harmonics in toroidal coordinates about a focus
point.
"""

# %% [markdown]
# # Example of plotting the internal harmonics in toroidal coordinates

# This example shows how to plot the individual cos and sin toroidal harmonic
# contributions about a focus point.

# %% [markdown]
# ### Imports

# %%
import matplotlib.pyplot as plt
import numpy as np

from bluemira.equilibria.optimisation.harmonics.toroidal_harmonics_approx_functions import ( # noqa: E501
my_legendre_q,
)
from bluemira.equilibria.plotting import PLOT_DEFAULTS
from bluemira.utilities.tools import cylindrical_to_toroidal

# %% [markdown]
# First we need to set up the grid in cylindrical coordinates over which we will solve,
# and define the location of the focus point.
# We will convert this to toroidal coordinates.
#
#
# To convert from cylindrical coordinates $(R, z)$ to toroidal coordinates
# $(\tau, \sigma)$ about a focus point $(R_0, z_0)$ we have the following relations:
# $$ \tau = \ln\frac{d_{1}}{d_{2}} $$
# $$ \sigma = {sign}(z - z_{0}) \arccos\frac{d_{1}^2 + d_{2}^2 - 4 R_{0}^2}{2 d_{1}
# d_{2}} $$
# where
# $$ d_{1}^2 = (R + R_{0})^2 + (z - z_{0})^2 $$
# $$ d_{2}^2 = (R - R_{0})^2 + (z - z_{0})^2 $$
#
# The domains for the toroidal coordinates are $0 \le \tau < \infty$ and $-\pi < \sigma
# \le \pi$.

# %%
# Set up grid over which to solve
r = np.linspace(0, 6, 100)
z = np.linspace(-6, 6, 100)
R, Z = np.meshgrid(r, z)

# Define focus point
R_0 = 3.0
Z_0 = 0.0

# Convert to toroidal coordinates
tau, sigma = cylindrical_to_toroidal(R_0=R_0, z_0=Z_0, R=R, Z=Z)

# %% [markdown]
# Now we want to calculate and plot the internal harmonics in toroidal coordinates about
# the focus.
#
# We use the following equations
# $$ \psi_{sin} = R \sqrt{\cosh (\tau) - \cos (\sigma)} Q_{m - \frac{1}{2}}^1
# (\cosh \tau) \sin (m \sigma) $$
#
# $$ \psi_{cos} = R \sqrt{\cosh (\tau) - \cos (\sigma)} Q_{m - \frac{1}{2}}^1
# (\cosh \tau) \cos (m \sigma) $$
#
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you need to add a brief note hear about order and degree

# We evaluate these contributions to the harmonics about the focus and plot.
# %%
# Calculate and plot individual contributions from toroidal harmonics

nu = np.arange(0, 5)

# Setting up plots
fig_sin, axs_sin = plt.subplots(1, len(nu))
fig_sin.suptitle("sin")
fig_sin.supxlabel("R")
fig_sin.supylabel("Z")
fig_cos, axs_cos = plt.subplots(1, len(nu))
fig_cos.suptitle("cos")
fig_cos.supxlabel("R")
fig_cos.supylabel("Z")
nlevels = PLOT_DEFAULTS["psi"]["nlevels"]
cmap = PLOT_DEFAULTS["psi"]["cmap"]

for i_nu in range(len(nu)):
levels_m = np.linspace(-(i_nu + 1), i_nu + 1, 100 * i_nu)
foo = (
R
* np.sqrt(np.cosh(tau) - np.cos(sigma))
* my_legendre_q(nu[i_nu] - 1 / 2, 1, np.cosh(tau))
)
psi_sin = foo * np.sin(nu[i_nu] * sigma)
psi_cos = foo * np.cos(nu[i_nu] * sigma)
axs_sin[i_nu].contour(R, Z, psi_sin, levels=nlevels, cmap=cmap)
axs_sin[i_nu].title.set_text(f"m = {i_nu}")
axs_cos[i_nu].contour(R, Z, psi_cos, levels=nlevels, cmap=cmap)
axs_cos[i_nu].title.set_text(f"m = {i_nu}")

# %%
Loading