|
30 | 30 | # This example illustrates the inner workings of the bluemira toroidal harmonics |
31 | 31 | # approximation function (toroidal_harmonic_approximation) which can be used in |
32 | 32 | # coilset current and position optimisation for conventional aspect ratio tokamaks. |
33 | | -# For full details and all equations, look in the |
34 | | -# notebook toroidal_harmonics_component_function_walkthrough_and_verification.ex.py. |
| 33 | +# For an example of how toroidal_harmonic_approximation is used |
| 34 | +# to create toroidal harmonics constraints for use in a coilset optimisation problem, |
| 35 | +# please see |
| 36 | +# Toroidal_Harmonics_Optimisation_Set_Up.ex.py |
35 | 37 | # |
36 | 38 | # ## Premise: |
37 | 39 | # |
|
49 | 51 | # There are benefits to using TH as a minimal set of constraints: |
50 | 52 | # - We can choose not to re-solve for the plasma equilibrium at each step, since the |
51 | 53 | # coilset contribution to the core plasma (within the LCFS) is constrained. |
52 | | -# - We have a minimal set of constraints (a set of harmonic amplitudes) for the core |
53 | | -# plasma contribution, which can reduce the dimensionality of the problem we are |
| 54 | +# - We have a small, viable set of constraints (a set of harmonic amplitudes) for the |
| 55 | +# core plasma contribution, which can reduce the dimensionality of the problem we are |
54 | 56 | # considering. |
55 | 57 | # |
| 58 | +# %% [markdown] |
56 | 59 | # We get the TH amplitudes/coefficients, $A(\tau, \sigma)$, from the following equations: |
57 | 60 | # |
58 | 61 | # $$ A(\tau, \sigma) = \sum_{m=0}^{\infty} A_m^{\cos} \epsilon_m m! \sqrt{\frac{2}{\pi}} |
59 | | -# \Delta^{\frac{1}{2}} \textbf{Q}_{m-\frac{1}{2}}^{1}(\cosh \tau) \cos(m \sigma) + A_m^ |
60 | | -# {\sin} |
61 | | -# \epsilon_m m! \sqrt{\frac{2}{\pi}} \Delta^{\frac{1}{2}} |
| 62 | +# \Delta^{\frac{1}{2}} \textbf{Q}_{m-\frac{1}{2}}^{1}(\cosh \tau) \cos(m \sigma) |
| 63 | +# \\+ A_m^{\sin} \epsilon_m m! \sqrt{\frac{2}{\pi}} \Delta^{\frac{1}{2}} |
62 | 64 | # \textbf{Q}_{m-\frac{1}{2}}^{1}(\cosh \tau) \sin(m \sigma) $$ |
63 | 65 | # |
64 | 66 | # where |
65 | 67 | # |
66 | | -# $$ A_m^{\cos, \sin} = \frac{\mu_0 I_c}{2^{\frac{5}{2}}} factorial\_term \frac{\sinh( |
| 68 | +# $$ A_m^{\cos, \sin} = \frac{\mu_0 I_c}{2^{\frac{5}{2}}} \upsilon_{fact} \frac{\sinh( |
67 | 69 | # \tau_c)} |
68 | 70 | # {\Delta_c^{\frac{1}{2}}} P_{m - \frac{1}{2}}^{-1}(\cosh(\tau_c)) ^{\cos}_{\sin}(m |
69 | 71 | # \sigma_c) $$ |
|
82 | 84 | # - $\varepsilon_m = 1 $ for $m = 0$ and $\varepsilon_m = 2$ for $m \ge 1$ |
83 | 85 | # - $ \Delta = \cosh(\tau) - \cos(\sigma) $ |
84 | 86 | # - $ \Delta_c = \cosh(\tau_c) - \cos(\sigma_c) $ |
85 | | -# - $ factorial\_term = \prod_{i=0}^{m-1} \left( 1 + \frac{1}{2(m-i)}\right) $ |
| 87 | +# - The factorial term, $\upsilon_{fact}$ = 1 if $m = 0$, else $= \prod_{i=0}^{m-1} |
| 88 | +# \left( 1 + \frac{1}{2(m-i)}\right) $ |
86 | 89 | # |
87 | | -# %% [markdown] |
88 | | -# ## Imports |
| 90 | +# We can then obtain the flux, $\psi$ by using $\psi = R A$. |
89 | 91 |
|
90 | 92 | # %% |
91 | 93 | from copy import deepcopy |
92 | 94 | from pathlib import Path |
93 | 95 |
|
| 96 | +import matplotlib.patches as patch |
94 | 97 | import matplotlib.pyplot as plt |
95 | 98 | import numpy as np |
96 | 99 |
|
|
109 | 112 | from bluemira.geometry.coordinates import Coordinates |
110 | 113 |
|
111 | 114 | # %% [markdown] |
112 | | -# Get equilibrium from EQDSK file and plot |
| 115 | +# Get equilibrium from EQDSK file |
113 | 116 |
|
114 | 117 | # %% |
115 | 118 | # Get equilibrium |
116 | 119 | EQDATA = get_bluemira_path("equilibria/test_data", subfolder="tests") |
117 | 120 |
|
118 | | -# eq_name = "eqref_OOB.json" |
119 | | -# eq_name = Path(EQDATA, eq_name) |
120 | | -# eq = Equilibrium.from_eqdsk(eq_name, from_cocos=7) |
121 | | - |
122 | 121 | eq_name = "DN-DEMO_eqref.json" |
123 | 122 | eq_name = Path(EQDATA, eq_name) |
124 | 123 | eq = Equilibrium.from_eqdsk(eq_name, from_cocos=3, qpsi_positive=False) |
125 | 124 |
|
126 | | -# Plot the equilibrium |
127 | | -f, ax = plt.subplots() |
128 | | -eq.plot(ax=ax) |
129 | | -eq.coilset.plot(ax=ax) |
130 | | -plt.show() |
131 | 125 |
|
132 | 126 | # %% [markdown] |
133 | 127 | # Set the focus point |
134 | 128 | # |
135 | 129 | # Note: there is the possibility to experiment with the position of the focus, but |
136 | | -# the default is to use the plasma o point. |
| 130 | +# the default is to use the effective centre of the plasma. |
137 | 131 | # %% |
138 | 132 | # Set the focus point |
139 | 133 | R_0, Z_0 = eq.effective_centre() |
140 | 134 |
|
141 | 135 | # %% [markdown] |
142 | 136 | # Use the `toroidal_harmonic_grid_and_coil_setup` to obtain the necessary |
143 | 137 | # parameters required for the TH approximation, such as the R and Z coordinates of the |
144 | | -# TH grid. |
145 | | -# |
146 | | -# We then use the `toroidal_harmonic_approximate_psi` function to approximate the coilset |
147 | | -# contribution to the flux using toroidal harmonics. We need to provide this function |
148 | | -# with the equilibrium, eq, and the ToroidalHarmonicsParams dataclass, which contains |
149 | | -# necessary parameters for the TH approximation, such as the relevant coordinates |
150 | | -# and coil names for use in the approximation. The |
151 | | -# function returns the approx_coilset_psi array and the TH coefficient matrix A_m. |
152 | | -# The default focus point is the plasma o point. |
153 | | -# The white dot in the plot shows the focus point. |
| 138 | +# TH grid. We then plot the equilibrium and in orange we show the region over which |
| 139 | +# we will use our toroidal harmonic approximation. |
| 140 | + |
154 | 141 | # %% |
155 | 142 | # Approximate psi and plot |
156 | 143 | th_params = toroidal_harmonic_grid_and_coil_setup(eq=eq, R_0=R_0, Z_0=Z_0) |
157 | 144 |
|
158 | 145 | R_approx = th_params.R |
159 | 146 | Z_approx = th_params.Z |
160 | 147 |
|
| 148 | +# Plot the equilibrium and the region over which we approximate the flux using toroidal |
| 149 | +# harmonics. |
| 150 | +f, ax = plt.subplots() |
| 151 | +eq.plot(ax=ax) |
| 152 | +eq.coilset.plot(ax=ax) |
| 153 | +max_R = np.max(th_params.R) # noqa: N816 |
| 154 | +min_R = np.min(th_params.R) # noqa: N816 |
| 155 | +max_Z = np.max(th_params.Z) # noqa: N816 |
| 156 | +min_Z = np.min(th_params.Z) # noqa: N816 |
| 157 | +centre_R = (max_R - min_R) / 2 + min_R # noqa: N816 |
| 158 | +centre_Z = (max_Z - np.abs(min_Z)) / 2 # noqa: N816 |
| 159 | +radius = (max_R - min_R) / 2 |
| 160 | + |
| 161 | +ax.add_patch( |
| 162 | + patch.Circle((centre_R, centre_Z), radius, ec="orange", fill=True, fc="orange") |
| 163 | +) |
| 164 | +plt.show() |
| 165 | +# %% [markdown] |
| 166 | +# |
| 167 | +# We then use the `toroidal_harmonic_approximate_psi` function to approximate the coilset |
| 168 | +# contribution to the flux using toroidal harmonics. This makes use of the equation for |
| 169 | +# $A_m^{\cos, \sin}$ as displayed at the start of this notebook. |
| 170 | +# We need to provide this function |
| 171 | +# with the equilibrium, eq, and the ToroidalHarmonicsParams dataclass, which contains |
| 172 | +# necessary parameters for the TH approximation, such as the relevant coordinates |
| 173 | +# and coil names for use in the approximation. The |
| 174 | +# function returns the approx_coilset_psi array and the TH coefficient matrices Am_cos |
| 175 | +# and Am_sin. |
| 176 | + |
| 177 | +# %% |
161 | 178 | approx_coilset_psi, _, _ = toroidal_harmonic_approximate_psi( |
162 | 179 | eq=eq, th_params=th_params, max_degree=5 |
163 | 180 | ) |
164 | 181 |
|
165 | | -nlevels = PLOT_DEFAULTS["psi"]["nlevels"] |
166 | | -cmap = PLOT_DEFAULTS["psi"]["cmap"] |
167 | | -plt.contourf(R_approx, Z_approx, approx_coilset_psi, nlevels, cmap=cmap) |
168 | | -plt.xlabel("R") |
169 | | -plt.ylabel("Z") |
170 | | -plt.title("TH Approximation for Coilset Psi") |
171 | | -plt.show() |
172 | | - |
173 | 182 |
|
174 | 183 | # %% [markdown] |
175 | | -# Now we want to compare this approximation to the solution from bluemira. |
| 184 | +# Now we want to compare this approximation for the coilset psi to the solution from |
| 185 | +# bluemira. |
| 186 | +# Since we assume the flux in the plasma region is kept fixed, we can calculate |
| 187 | +# approximate total psi = bluemira plasma psi + toroidal harmonic coilset psi |
| 188 | +# approximation. |
| 189 | +# |
| 190 | +# We also create a difference plot to compare our TH coilset psi approximation to the |
| 191 | +# bluemira coilset psi. |
| 192 | +# We can see zero difference in the core region, which we expect as we are constraining |
| 193 | +# the flux in this region, and we see differences outside of the approximation |
| 194 | +# region, but this is expected as we are only requiring the core region to be kept |
| 195 | +# fixed. |
| 196 | +# Since the plasma psi is kept fixed, the total psi difference plot would look |
| 197 | +# the same as the coilset psi difference plot shown below. |
176 | 198 |
|
177 | 199 | # %% |
178 | | -# Plot total psi using approximate coilset psi from TH, and plasma psi from bluemira |
| 200 | +# Approximate total psi = approximate coilset psi from TH + plasma psi from bluemira |
179 | 201 |
|
180 | 202 | plasma_psi = eq.plasma.psi(R_approx, Z_approx) |
181 | 203 |
|
182 | 204 | total_psi = approx_coilset_psi + plasma_psi |
183 | 205 |
|
184 | | -# Find LCFS from TH approx |
| 206 | +# Find LCFS from TH approx to add to plot |
185 | 207 | approx_eq = deepcopy(eq) |
186 | 208 | o_points, x_points = approx_eq.get_OX_points(total_psi) |
187 | 209 |
|
|
197 | 219 | eq.get_LCFS() if np.isclose(psi_norm, 1.0) else eq.get_flux_surface(psi_norm) |
198 | 220 | ) |
199 | 221 |
|
| 222 | +# Difference in coilset psi between TH approximation and bluemira |
| 223 | +bluemira_coilset_psi = eq.coilset.psi(R_approx, Z_approx) |
| 224 | + |
| 225 | +coilset_psi_diff = np.abs(approx_coilset_psi - bluemira_coilset_psi) / np.max( |
| 226 | + np.abs(bluemira_coilset_psi) |
| 227 | +) |
| 228 | +coilset_psi_diff_plot = coilset_psi_diff |
| 229 | +f, axs = plt.subplots(1, 2) |
| 230 | + |
200 | 231 | # Plot |
201 | | -plt.contourf(R_approx, Z_approx, total_psi, nlevels, cmap=cmap) |
202 | | -plt.xlabel("R") |
203 | | -plt.ylabel("Z") |
204 | | -plt.plot( |
| 232 | +nlevels = PLOT_DEFAULTS["psi"]["nlevels"] |
| 233 | +cmap = PLOT_DEFAULTS["psi"]["cmap"] |
| 234 | +axs0_plot = axs[0].contourf(R_approx, Z_approx, total_psi, nlevels, cmap=cmap) |
| 235 | +axs[0].set_xlabel("R") |
| 236 | +axs[0].set_ylabel("Z") |
| 237 | +axs[0].plot( |
205 | 238 | approx_fs.x, |
206 | 239 | approx_fs.z, |
207 | 240 | color="red", |
208 | | - label=f"Closed Flux Surface (psi_n={psi_norm}) from TH approximation", |
| 241 | + label="Approx. FS from TH", |
209 | 242 | ) |
210 | | -plt.title("Total Psi using TH approximation for coilset psi") |
211 | | -plt.legend(loc="upper right") |
212 | | -plt.show() |
213 | | - |
214 | | -# %% |
215 | | -# Plot bluemira total psi |
216 | | -# Obtain psi from Bluemira coilset |
217 | | -bm_coil_psi = np.zeros(np.shape(eq.grid.x)) |
218 | | -for n in eq.coilset.name: |
219 | | - bm_coil_psi = np.sum([bm_coil_psi, eq.coilset[n].psi(eq.grid.x, eq.grid.z)], axis=0) |
220 | | - |
221 | | - |
222 | | -# Obtain total psi from Bluemira |
223 | | -bluemira_total_psi = eq.psi(R_approx, Z_approx) |
224 | | - |
225 | | -# Plotting |
226 | | -plt.contourf(R_approx, Z_approx, bluemira_total_psi, levels=nlevels, cmap=cmap) |
227 | | -plt.xlabel("R") |
228 | | -plt.ylabel("Z") |
229 | | -plt.title("Total Bluemira Psi") |
230 | | -plt.show() |
231 | | -# %% |
232 | | -# Plot bluemira coilset psi |
233 | | -coilset_psi = eq.coilset.psi(R_approx, Z_approx) |
234 | | - |
235 | | -# Plotting |
236 | | -plt.contourf(R_approx, Z_approx, coilset_psi, nlevels, cmap=cmap) |
237 | | -plt.xlabel("R") |
238 | | -plt.ylabel("Z") |
239 | | -plt.title("Bluemira Coilset Psi") |
240 | | -plt.show() |
241 | | -# %% |
242 | | -# Difference plot to compare TH approximation to Bluemira coilset psi |
243 | | -# We see zero difference in the core region, which we expect as we are constraining |
244 | | -# the flux in this region, and we see larger differences outside of the approximation |
245 | | -# region. |
246 | | -coilset_psi_diff = np.abs(approx_coilset_psi - coilset_psi) / np.max(np.abs(coilset_psi)) |
247 | | -coilset_psi_diff_plot = coilset_psi_diff |
248 | | -f, ax = plt.subplots() |
249 | | -ax.plot(approx_fs.x, approx_fs.z, color="red", label="Approximate LCFS from TH") |
250 | | -ax.plot(original_fs.x, original_fs.z, color="blue", label="LCFS from Bluemira") |
251 | | -im = ax.contourf(R_approx, Z_approx, coilset_psi_diff_plot, levels=nlevels, cmap=cmap) |
252 | | -f.colorbar(mappable=im) |
253 | | -ax.set_title("Absolute relative difference between coilset psi and TH approximation psi") |
254 | | -ax.legend(loc="upper right", bbox_to_anchor=(1.1, 1.0)) |
255 | | -eq.coilset.plot(ax=ax) |
256 | | -plt.show() |
257 | | - |
258 | | - |
259 | | -# %% |
260 | | -# Difference plot to compare TH approximation to Bluemira total psi |
261 | | -total_psi_diff = np.abs(total_psi - bluemira_total_psi) / np.max( |
262 | | - np.abs(bluemira_total_psi) |
| 243 | +axs[0].set_title("Total Psi using TH approximation for coilset psi") |
| 244 | +axs[0].legend(loc="upper right") |
| 245 | + |
| 246 | +axs[1].plot(approx_fs.x, approx_fs.z, color="red", label="Approx. FS from TH") |
| 247 | +axs[1].plot( |
| 248 | + original_fs.x, |
| 249 | + original_fs.z, |
| 250 | + color="c", |
| 251 | + linestyle="dashed", |
| 252 | + label="LCFS from Bluemira", |
263 | 253 | ) |
264 | | -total_psi_diff_plot = total_psi_diff |
265 | | -f, ax = plt.subplots() |
266 | | -ax.plot(approx_fs.x, approx_fs.z, color="red", label="Approx FS from TH") |
267 | | -ax.plot(original_fs.x, original_fs.z, color="blue", label="FS from Bluemira") |
268 | | -im = ax.contourf(R_approx, Z_approx, total_psi_diff_plot, levels=nlevels, cmap=cmap) |
269 | | -f.colorbar(mappable=im) |
270 | | -ax.set_title("Absolute relative difference between total psi and TH approximation psi") |
271 | | -ax.legend(loc="upper right") |
272 | | -eq.coilset.plot(ax=ax) |
| 254 | +axs1_plot = axs[1].contourf( |
| 255 | + R_approx, Z_approx, coilset_psi_diff_plot, levels=nlevels, cmap=cmap |
| 256 | +) |
| 257 | +f.colorbar(axs1_plot, ax=axs[1], fraction=0.05) |
| 258 | +axs[1].set_title( |
| 259 | + "Absolute relative difference between coilset psi \nand TH approximation psi" |
| 260 | +) |
| 261 | +axs[1].legend(loc="upper right", bbox_to_anchor=(1.1, 1.0)) |
| 262 | +eq.coilset.plot(ax=axs[1]) |
| 263 | +axs[0].set_aspect("equal") |
| 264 | +axs[1].set_aspect("equal") |
273 | 265 | plt.show() |
274 | 266 |
|
| 267 | + |
275 | 268 | # %% [markdown] |
276 | 269 | # We use a fit metric to evaluate the TH approximation. |
277 | | - |
| 270 | +# This involves |
| 271 | +# finding a flux surface (here corresponding to a psi_norm=0.95) for our |
| 272 | +# approximate total psi, and comparing to the equivalent flux surface from |
| 273 | +# the bluemira total psi. The fit metric we use for this flux surface |
| 274 | +# comparison is as follows: fit metric value = total area within one but not |
| 275 | +# both flux surfaces / (bluemira flux surface area + approximation flux surface area). |
| 276 | +# A fit metric of 0 would correspond to a perfect match between our approximation |
| 277 | +# and bluemira, and a fit metric of 1 would correspond to no overlap between the |
| 278 | +# approximation and bluemira flux surfaces. |
278 | 279 | # %% |
279 | 280 | # Fit metric to evaluate TH approximation |
280 | 281 | fit_metric_value = fs_fit_metric(original_fs, approx_fs) |
|
287 | 288 | # number of degrees to use for the approximation. |
288 | 289 | # |
289 | 290 | # Here is an example of using the function, setting plot to True outputs a graph of the |
290 | | -# difference in total psi between the TH approximation and bluemira. |
| 291 | +# difference in total psi between the TH approximation and bluemira. This graph |
| 292 | +# is the same as the one we produced in this notebook above. |
291 | 293 | # %% |
292 | 294 | ( |
293 | 295 | toroidal_harmonics_params, |
|
0 commit comments