Skip to content

Commit 4a67e73

Browse files
authored
Merge pull request #93 from FusionComputingLab/fix-normal-mode-greens
Correct normal-mode Green transformation
2 parents 482dde0 + d8922ce commit 4a67e73

4 files changed

Lines changed: 107 additions & 19 deletions

File tree

examples/example10 - growth_rates.ipynb

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,17 @@
337337
"cell_type": "markdown",
338338
"metadata": {},
339339
"source": [
340-
"We can visualise the poloidal flux produced by the unstable mode using the following code. We can also do this for other (stable) modes if required (just change the index `i` in the cell below)."
340+
"We can visualise the poloidal flux produced by the unstable mode using the following code. We can also do this for other (stable) modes if required (just change the index `i` in the cell below).\n",
341+
"\n",
342+
"The physical metal currents $\\vec{I}_m$ and normal-mode currents $\\vec{I}_d$ are related by\n",
343+
"\n",
344+
"$$\\vec{I}_m = P\\vec{I}_d.$$\n",
345+
"\n",
346+
"Writing the physical-metal Green maps as $G_m$, the flux is therefore\n",
347+
"\n",
348+
"$$\\psi = \\vec{I}_m^T G_m = \\vec{I}_d^T P^T G_m.$$\n",
349+
"\n",
350+
"Thus the Green maps per unit normal-mode current are $G_d=P^T G_m$. Notice that this is a covector transformation: $P^{-1}$ is used to recover modal currents from physical currents, but it is not the transformation for the Green maps."
341351
]
342352
},
343353
{
@@ -348,16 +358,16 @@
348358
"source": [
349359
"# mode number (choose which one you want to visualise)\n",
350360
"i = idx # default is unstable mode\n",
351-
"mode_currents = np.real(modes[:,i])\n",
361+
"mode_currents = np.real(modes[:-1, i])\n",
352362
"\n",
353363
"# the associated instability timescale and growth rate\n",
354364
"print(f\"Mode {i} ---> {'stable' if np.real(timescales[i]) < 0 else 'unstable'}\")\n",
355365
"print(f\"Growth rate = {np.real(growth_rates[i]):.2e} [1/s]\")\n",
356366
"print(f\"Timescale = {np.real(timescales[i]):.2e} [s]\")\n",
357367
"\n",
358-
"# multiply each metal current (from the eigenvector) with its corresponding Greens matrix and sum\n",
359-
"# (don't forget to omit the plasma current mode, i.e. the final element)\n",
360-
"flux = np.sum(mode_currents[0:-1, np.newaxis, np.newaxis] * nonlinear_solver.vessel_modes_greens[i], axis=0)\n",
368+
"# Contract the modal currents with the modal Green maps G_d = P.T @ G_m.\n",
369+
"# The final eigenvector element, representing plasma current, was omitted above.\n",
370+
"flux = np.einsum(\"i,ijk->jk\", mode_currents, nonlinear_solver.vessel_modes_greens)\n",
361371
"\n",
362372
"# plot\n",
363373
"fig, ax = plt.subplots(1, 1, figsize=(5, 8), dpi=60)\n",
@@ -743,7 +753,7 @@
743753
"cell_type": "markdown",
744754
"metadata": {},
745755
"source": [
746-
"Again, we can plot how these look using similar code as before, except we need to remember that now we've done a mode decomposition, the eigenmode currents $\\vec{I}_d$ need to be transformed back into the original metal currents $\\vec{I}_m$. This is done in the plotting cell below."
756+
"Again, we can plot how these look using similar code as before. Here we explicitly transform the retained normal-mode currents back to physical metal currents using $\\vec{I}_m=P\\vec{I}_d$, then contract those currents with the physical-metal Green maps $G_m$. This is equivalent to contracting $\\vec{I}_d$ with the corresponding retained rows of $G_d=P^T G_m$."
747757
]
748758
},
749759
{
@@ -784,15 +794,17 @@
784794
"\n",
785795
"# this function transforms the (decomposed) mode currents back to regular metal currents\n",
786796
"# (don't forget to omit the plasma current mode, i.e. the final element)\n",
787-
"mode_currents = nonlinear_solver_option_1.evol_metal_curr.IdtoIvessel(np.real(modes[0:-1,i]))\n",
797+
"mode_currents = nonlinear_solver_option_1.evol_metal_curr.IdtoIvessel(\n",
798+
" np.real(modes[:-1, i])\n",
799+
")\n",
788800
"\n",
789801
"# the associated instability timescale and growth rate\n",
790802
"print(f\"Mode {i} ---> {'stable' if np.real(timescales[i]) < 0 else 'unstable'}\")\n",
791803
"print(f\"Growth rate = {np.real(growth_rates[i]):.2e} [1/s]\")\n",
792804
"print(f\"Timescale = {np.real(timescales[i]):.2e} [s]\")\n",
793805
"\n",
794-
"# multiply each metal current (from the eigenvector) with its corresponding Greens matrix and sum\n",
795-
"flux = np.sum(mode_currents[:, np.newaxis, np.newaxis] * nonlinear_solver_option_1.vessel_modes_greens[i], axis=0)\n",
806+
"# Contract reconstructed physical currents with the physical-metal Green maps.\n",
807+
"flux = np.einsum(\"i,ijk->jk\", mode_currents, eq._vgreen)\n",
796808
"\n",
797809
"# plot\n",
798810
"fig, ax = plt.subplots(1, 1, figsize=(5, 8), dpi=60)\n",

freegsnke/normal_modes.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,21 @@ def __init__(self, coil_resist, coil_self_ind, n_coils, n_active_coils):
114114

115115
def normal_modes_greens(self, eq_vgreen):
116116
"""
117-
Calculates the green functions of the vessel normal modes,
118-
i.e. the psi flux per unit current for each mode.
117+
Calculate the Green functions of the vessel normal modes.
118+
119+
If ``I_m`` contains the physical metal currents and ``I_d`` contains
120+
the normal-mode currents, the current transformation is
121+
122+
``I_m = Pmatrix @ I_d``.
123+
124+
Since the physical-coil Green functions satisfy
125+
``psi = I_m.T @ G_m``, substitution gives
126+
127+
``psi = I_d.T @ Pmatrix.T @ G_m``.
128+
129+
The modal Green functions are therefore ``Pmatrix.T @ G_m``. The
130+
inverse transformation is instead used to recover modal currents from
131+
physical currents and must not be applied to the Green functions.
119132
120133
Parameters
121134
----------
@@ -124,10 +137,6 @@ def normal_modes_greens(self, eq_vgreen):
124137
Can be found at eq._vgreen. np.shape(eq_vgreen)=(n_coils, nx, ny)
125138
"""
126139

127-
dgreen = np.sum(
128-
eq_vgreen[np.newaxis, :, :, :]
129-
* self.Pmatrix_inverse[:, :, np.newaxis, np.newaxis],
130-
axis=1,
131-
)
132-
133-
return dgreen
140+
grid_shape = eq_vgreen.shape[1:]
141+
physical_greens = eq_vgreen.reshape(self.n_coils, -1)
142+
return (self.Pmatrix.T @ physical_greens).reshape(self.n_coils, *grid_shape)

freegsnke/tests/test_dynamics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def create_machine():
9696

9797
def test_linearised_growth_rate(create_machine):
9898
tokamak, eq, profiles, stepping = create_machine
99-
true_GR = 0.0586
99+
true_GR = 0.05867
100100
# check that
101101
assert (
102102
abs((stepping.linearised_sol.instability_timescale[0] - true_GR) / true_GR)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Tests for passive-structure normal-mode transformations."""
2+
3+
import numpy as np
4+
5+
from freegsnke.normal_modes import mode_decomposition
6+
7+
8+
def _mode_decomposition():
9+
"""Return a small decomposition whose passive modes are not orthogonal."""
10+
11+
resistances = np.array([0.8, 1.1, 1.6, 2.0])
12+
inductances = np.array(
13+
[
14+
[4.0, 0.2, 0.1, 0.0],
15+
[0.2, 3.2, 0.5, 0.1],
16+
[0.1, 0.5, 2.8, 0.4],
17+
[0.0, 0.1, 0.4, 2.5],
18+
]
19+
)
20+
return mode_decomposition(
21+
coil_resist=resistances,
22+
coil_self_ind=inductances,
23+
n_coils=4,
24+
n_active_coils=1,
25+
)
26+
27+
28+
def test_current_transform_uses_inverse_not_transpose():
29+
"""The non-orthogonal mode basis requires its inverse for currents."""
30+
31+
modes = _mode_decomposition()
32+
modal_currents = np.array([1.0, -0.4, 0.7, 0.2])
33+
physical_currents = modes.Pmatrix @ modal_currents
34+
35+
assert not np.allclose(modes.Pmatrix.T, modes.Pmatrix_inverse)
36+
np.testing.assert_allclose(
37+
modes.Pmatrix_inverse @ physical_currents,
38+
modal_currents,
39+
rtol=1e-13,
40+
atol=1e-13,
41+
)
42+
43+
44+
def test_mode_greens_reproduce_physical_current_flux():
45+
"""Modal and reconstructed physical currents must produce the same flux."""
46+
47+
modes = _mode_decomposition()
48+
rng = np.random.default_rng(42)
49+
physical_greens = rng.normal(size=(modes.n_coils, 5, 6))
50+
modal_currents = np.array([1.0, -0.4, 0.7, 0.2])
51+
52+
physical_currents = modes.Pmatrix @ modal_currents
53+
flux_from_physical_currents = np.einsum(
54+
"i,ijk->jk", physical_currents, physical_greens
55+
)
56+
flux_from_modal_currents = np.einsum(
57+
"i,ijk->jk",
58+
modal_currents,
59+
modes.normal_modes_greens(physical_greens),
60+
)
61+
62+
np.testing.assert_allclose(
63+
flux_from_modal_currents,
64+
flux_from_physical_currents,
65+
rtol=1e-13,
66+
atol=1e-13,
67+
)

0 commit comments

Comments
 (0)