Skip to content

Commit 023f7e5

Browse files
authored
Merge pull request #296 from hiddenSymmetries/permanent_magnets
Permanent magnet branch merge
2 parents 6bd0f62 + a5edd73 commit 023f7e5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+341763
-55
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pybind11_add_module(${PROJECT_NAME}
118118
src/simsoptpp/regular_grid_interpolant_3d_py.cpp
119119
src/simsoptpp/curve.cpp src/simsoptpp/curverzfourier.cpp src/simsoptpp/curvexyzfourier.cpp
120120
src/simsoptpp/surface.cpp src/simsoptpp/surfacerzfourier.cpp src/simsoptpp/surfacexyzfourier.cpp
121+
src/simsoptpp/dipole_field.cpp src/simsoptpp/permanent_magnet_optimization.cpp
121122
src/simsoptpp/dommaschk.cpp src/simsoptpp/reiman.cpp src/simsoptpp/tracing.cpp
122123
src/simsoptpp/magneticfield_biotsavart.cpp src/simsoptpp/python_boozermagneticfield.cpp
123124
src/simsoptpp/boozerradialinterpolant.cpp
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
#!/usr/bin/env python
2+
r"""
3+
This example script uses the GPMO
4+
greedy algorithm for solving permanent
5+
magnet optimization on the MUSE grid. This
6+
algorithm is described in the following paper:
7+
A. A. Kaptanoglu, R. Conlin, and M. Landreman,
8+
Greedy permanent magnet optimization,
9+
Nuclear Fusion 63, 036016 (2023)
10+
11+
The script should be run as:
12+
mpirun -n 1 python permanent_magnet_MUSE.py
13+
on a cluster machine but
14+
python permanent_magnet_MUSE.py
15+
is sufficient on other machines. Note that the code is
16+
parallelized via OpenMP and XSIMD, so will run substantially
17+
faster on multi-core machines (make sure that all the cores
18+
are available to OpenMP, e.g. through setting OMP_NUM_THREADS).
19+
20+
For high-resolution and more realistic designs, please see the script files at
21+
https://github.com/akaptano/simsopt_permanent_magnet_advanced_scripts.git
22+
"""
23+
24+
import os
25+
import pickle
26+
import time
27+
from pathlib import Path
28+
29+
import numpy as np
30+
from matplotlib import pyplot as plt
31+
32+
from simsopt.field import BiotSavart, DipoleField
33+
from simsopt.geo import PermanentMagnetGrid, SurfaceRZFourier
34+
from simsopt.objectives import SquaredFlux
35+
from simsopt.solve import GPMO
36+
from simsopt.util import FocusData, discretize_polarizations, polarization_axes
37+
from simsopt.util.permanent_magnet_helper_functions import *
38+
39+
t_start = time.time()
40+
41+
# Set some parameters -- if doing CI, lower the resolution
42+
ci = "CI" in os.environ and os.environ['CI'].lower() in ['1', 'true']
43+
if ci:
44+
nphi = 2
45+
nIter_max = 100
46+
nBacktracking = 50
47+
max_nMagnets = 20
48+
downsample = 100 # downsample the FAMUS grid of magnets by this factor
49+
else:
50+
nphi = 16 # >= 64 for high-resolution runs
51+
nIter_max = 10000
52+
nBacktracking = 200
53+
max_nMagnets = 1000
54+
downsample = 1
55+
56+
ntheta = nphi # same as above
57+
dr = 0.01 # Radial extent in meters of the cylindrical permanent magnet bricks
58+
input_name = 'input.muse'
59+
60+
# Read in the plasma equilibrium file
61+
TEST_DIR = (Path(__file__).parent / ".." / ".." / "tests" / "test_files").resolve()
62+
famus_filename = TEST_DIR / 'zot80.focus'
63+
surface_filename = TEST_DIR / input_name
64+
s = SurfaceRZFourier.from_focus(surface_filename, range="half period", nphi=nphi, ntheta=ntheta)
65+
s_inner = SurfaceRZFourier.from_focus(surface_filename, range="half period", nphi=nphi, ntheta=ntheta)
66+
s_outer = SurfaceRZFourier.from_focus(surface_filename, range="half period", nphi=nphi, ntheta=ntheta)
67+
68+
# Make the output directory -- warning, saved data can get big!
69+
# On NERSC, recommended to change this directory to point to SCRATCH!
70+
out_dir = Path("output_permanent_magnet_GPMO_MUSE")
71+
out_dir.mkdir(parents=True, exist_ok=True)
72+
73+
# initialize the coils
74+
base_curves, curves, coils = initialize_coils('muse_famus', TEST_DIR, s, out_dir)
75+
76+
# Set up BiotSavart fields
77+
bs = BiotSavart(coils)
78+
79+
# Calculate average, approximate on-axis B field strength
80+
calculate_on_axis_B(bs, s)
81+
82+
# Make higher resolution surface for plotting Bnormal
83+
qphi = 2 * nphi
84+
quadpoints_phi = np.linspace(0, 1, qphi, endpoint=True)
85+
quadpoints_theta = np.linspace(0, 1, ntheta, endpoint=True)
86+
s_plot = SurfaceRZFourier.from_focus(
87+
surface_filename,
88+
quadpoints_phi=quadpoints_phi,
89+
quadpoints_theta=quadpoints_theta
90+
)
91+
92+
# Plot initial Bnormal on plasma surface from un-optimized BiotSavart coils
93+
make_Bnormal_plots(bs, s_plot, out_dir, "biot_savart_initial")
94+
95+
# Set up correct Bnormal from TF coils
96+
bs.set_points(s.gamma().reshape((-1, 3)))
97+
Bnormal = np.sum(bs.B().reshape((nphi, ntheta, 3)) * s.unitnormal(), axis=2)
98+
99+
# Load a downsampled version of the magnet grid from a FAMUS file
100+
mag_data = FocusData(famus_filename, downsample=downsample)
101+
102+
# Set the allowable orientations of the magnets to be face-aligned
103+
# i.e. the local x, y, or z directions
104+
pol_axes = np.zeros((0, 3))
105+
pol_type = np.zeros(0, dtype=int)
106+
pol_axes_f, pol_type_f = polarization_axes(['face'])
107+
ntype_f = int(len(pol_type_f)/2)
108+
pol_axes_f = pol_axes_f[:ntype_f, :]
109+
pol_type_f = pol_type_f[:ntype_f]
110+
pol_axes = np.concatenate((pol_axes, pol_axes_f), axis=0)
111+
pol_type = np.concatenate((pol_type, pol_type_f))
112+
113+
# Optionally add additional types of allowed orientations
114+
PM4Stell_orientations = False
115+
if PM4Stell_orientations:
116+
pol_axes_fe_ftri, pol_type_fe_ftri = polarization_axes(['fe_ftri'])
117+
ntype_fe_ftri = int(len(pol_type_fe_ftri)/2)
118+
pol_axes_fe_ftri = pol_axes_fe_ftri[:ntype_fe_ftri, :]
119+
pol_type_fe_ftri = pol_type_fe_ftri[:ntype_fe_ftri] + 1
120+
pol_axes = np.concatenate((pol_axes, pol_axes_fe_ftri), axis=0)
121+
pol_type = np.concatenate((pol_type, pol_type_fe_ftri))
122+
pol_axes_fc_ftri, pol_type_fc_ftri = polarization_axes(['fc_ftri'])
123+
ntype_fc_ftri = int(len(pol_type_fc_ftri)/2)
124+
pol_axes_fc_ftri = pol_axes_fc_ftri[:ntype_fc_ftri, :]
125+
pol_type_fc_ftri = pol_type_fc_ftri[:ntype_fc_ftri] + 2
126+
pol_axes = np.concatenate((pol_axes, pol_axes_fc_ftri), axis=0)
127+
pol_type = np.concatenate((pol_type, pol_type_fc_ftri))
128+
129+
ophi = np.arctan2(mag_data.oy, mag_data.ox)
130+
discretize_polarizations(mag_data, ophi, pol_axes, pol_type)
131+
pol_vectors = np.zeros((mag_data.nMagnets, len(pol_type), 3))
132+
pol_vectors[:, :, 0] = mag_data.pol_x
133+
pol_vectors[:, :, 1] = mag_data.pol_y
134+
pol_vectors[:, :, 2] = mag_data.pol_z
135+
print('pol_vectors_shape = ', pol_vectors.shape)
136+
137+
# pol_vectors is only used for the greedy algorithms with cartesian coordinate_flag
138+
# which is the default, so no need to specify it here.
139+
kwargs = {"pol_vectors": pol_vectors, "downsample": downsample, "dr": dr}
140+
141+
# Finally, initialize the permanent magnet class
142+
pm_opt = PermanentMagnetGrid.geo_setup_from_famus(s, Bnormal, famus_filename, **kwargs)
143+
144+
print('Number of available dipoles = ', pm_opt.ndipoles)
145+
146+
# Set some hyperparameters for the optimization
147+
algorithm = 'ArbVec_backtracking' # Algorithm to use
148+
nAdjacent = 1 # How many magnets to consider "adjacent" to one another
149+
nHistory = 20 # How often to save the algorithm progress
150+
thresh_angle = np.pi # The angle between two "adjacent" dipoles such that they should be removed
151+
kwargs = initialize_default_kwargs('GPMO')
152+
kwargs['K'] = nIter_max # Maximum number of GPMO iterations to run
153+
kwargs['nhistory'] = nHistory
154+
if algorithm == 'backtracking' or algorithm == 'ArbVec_backtracking':
155+
kwargs['backtracking'] = nBacktracking # How often to perform the backtrackinig
156+
kwargs['Nadjacent'] = nAdjacent
157+
kwargs['dipole_grid_xyz'] = np.ascontiguousarray(pm_opt.dipole_grid_xyz)
158+
kwargs['max_nMagnets'] = max_nMagnets
159+
if algorithm == 'ArbVec_backtracking':
160+
kwargs['thresh_angle'] = thresh_angle
161+
162+
# Optimize the permanent magnets greedily
163+
t1 = time.time()
164+
R2_history, Bn_history, m_history = GPMO(pm_opt, algorithm, **kwargs)
165+
t2 = time.time()
166+
print('GPMO took t = ', t2 - t1, ' s')
167+
168+
# plot the MSE history
169+
iterations = np.linspace(0, kwargs['max_nMagnets'], len(R2_history), endpoint=False)
170+
plt.figure()
171+
plt.semilogy(iterations, R2_history, label=r'$f_B$')
172+
plt.semilogy(iterations, Bn_history, label=r'$<|Bn|>$')
173+
plt.grid(True)
174+
plt.xlabel('K')
175+
plt.ylabel('Metric values')
176+
plt.legend()
177+
plt.savefig(out_dir / 'GPMO_MSE_history.png')
178+
179+
# Set final m to the minimum achieved during the optimization
180+
min_ind = np.argmin(R2_history)
181+
pm_opt.m = np.ravel(m_history[:, :, min_ind])
182+
183+
# Print effective permanent magnet volume
184+
B_max = 1.465
185+
mu0 = 4 * np.pi * 1e-7
186+
M_max = B_max / mu0
187+
dipoles = pm_opt.m.reshape(pm_opt.ndipoles, 3)
188+
print('Volume of permanent magnets is = ', np.sum(np.sqrt(np.sum(dipoles ** 2, axis=-1))) / M_max)
189+
print('sum(|m_i|)', np.sum(np.sqrt(np.sum(dipoles ** 2, axis=-1))))
190+
191+
save_plots = False
192+
if save_plots:
193+
# Save the MSE history and history of the m vectors
194+
np.savetxt(
195+
out_dir / f"mhistory_K{kwargs['K']}_nphi{nphi}_ntheta{ntheta}.txt",
196+
m_history.reshape(pm_opt.ndipoles * 3, kwargs['nhistory'] + 1)
197+
)
198+
np.savetxt(
199+
out_dir / f"R2history_K{kwargs['K']}_nphi{nphi}_ntheta{ntheta}.txt",
200+
R2_history
201+
)
202+
# Plot the SIMSOPT GPMO solution
203+
bs.set_points(s_plot.gamma().reshape((-1, 3)))
204+
Bnormal = np.sum(bs.B().reshape((qphi, ntheta, 3)) * s_plot.unitnormal(), axis=2)
205+
make_Bnormal_plots(bs, s_plot, out_dir, "biot_savart_optimized")
206+
207+
# Look through the solutions as function of K and make plots
208+
for k in range(0, kwargs["nhistory"] + 1, 50):
209+
mk = m_history[:, :, k].reshape(pm_opt.ndipoles * 3)
210+
b_dipole = DipoleField(
211+
pm_opt.dipole_grid_xyz,
212+
mk,
213+
nfp=s.nfp,
214+
coordinate_flag=pm_opt.coordinate_flag,
215+
m_maxima=pm_opt.m_maxima,
216+
)
217+
b_dipole.set_points(s_plot.gamma().reshape((-1, 3)))
218+
K_save = int(kwargs['K'] / kwargs['nhistory'] * k)
219+
b_dipole._toVTK(out_dir / f"Dipole_Fields_K{K_save}_nphi{nphi}_ntheta{ntheta}")
220+
print("Total fB = ", 0.5 * np.sum((pm_opt.A_obj @ mk - pm_opt.b_obj) ** 2))
221+
Bnormal_dipoles = np.sum(b_dipole.B().reshape((qphi, ntheta, 3)) * s_plot.unitnormal(), axis=-1)
222+
Bnormal_total = Bnormal + Bnormal_dipoles
223+
224+
# For plotting Bn on the full torus surface at the end with just the dipole fields
225+
make_Bnormal_plots(b_dipole, s_plot, out_dir, "only_m_optimized_K{K_save}_nphi{nphi}_ntheta{ntheta}")
226+
pointData = {"B_N": Bnormal_total[:, :, None]}
227+
s_plot.to_vtk(out_dir / "m_optimized_K{K_save}_nphi{nphi}_ntheta{ntheta}", extra_data=pointData)
228+
229+
# write solution to FAMUS-type file
230+
pm_opt.write_to_famus(out_dir)
231+
232+
# Compute metrics with permanent magnet results
233+
dipoles_m = pm_opt.m.reshape(pm_opt.ndipoles, 3)
234+
num_nonzero = np.count_nonzero(np.sum(dipoles_m ** 2, axis=-1)) / pm_opt.ndipoles * 100
235+
print("Number of possible dipoles = ", pm_opt.ndipoles)
236+
print("% of dipoles that are nonzero = ", num_nonzero)
237+
238+
# Print optimized f_B and other metrics
239+
### Note this will only agree with the optimization in the high-resolution
240+
### limit where nphi ~ ntheta >= 64!
241+
b_dipole = DipoleField(
242+
pm_opt.dipole_grid_xyz,
243+
pm_opt.m,
244+
nfp=s.nfp,
245+
coordinate_flag=pm_opt.coordinate_flag,
246+
m_maxima=pm_opt.m_maxima,
247+
)
248+
b_dipole.set_points(s_plot.gamma().reshape((-1, 3)))
249+
bs.set_points(s_plot.gamma().reshape((-1, 3)))
250+
Bnormal = np.sum(bs.B().reshape((qphi, ntheta, 3)) * s_plot.unitnormal(), axis=2)
251+
f_B_sf = SquaredFlux(s_plot, b_dipole, -Bnormal).J()
252+
print('f_B = ', f_B_sf)
253+
total_volume = np.sum(np.sqrt(np.sum(pm_opt.m.reshape(pm_opt.ndipoles, 3) ** 2, axis=-1))) * s.nfp * 2 * mu0 / B_max
254+
print('Total volume = ', total_volume)
255+
256+
# Optionally make a QFM and pass it to VMEC
257+
# This is worthless unless plasma
258+
# surface is at least 64 x 64 resolution.
259+
vmec_flag = False
260+
if vmec_flag:
261+
from mpi4py import MPI
262+
from simsopt.mhd.vmec import Vmec
263+
from simsopt.util.mpi import MpiPartition
264+
mpi = MpiPartition(ngroups=1)
265+
266+
# Make the QFM surfaces
267+
t1 = time.time()
268+
Bfield = bs + b_dipole
269+
Bfield.set_points(s_plot.gamma().reshape((-1, 3)))
270+
qfm_surf = make_qfm(s_plot, Bfield)
271+
qfm_surf = qfm_surf.surface
272+
t2 = time.time()
273+
print("Making the QFM surface took ", t2 - t1, " s")
274+
275+
# Run VMEC with new QFM surface
276+
t1 = time.time()
277+
278+
### Always use the QA VMEC file and just change the boundary
279+
vmec_input = "../../tests/test_files/input.LandremanPaul2021_QA"
280+
equil = Vmec(vmec_input, mpi)
281+
equil.boundary = qfm_surf
282+
equil.run()
283+
284+
t_end = time.time()
285+
print('Total time = ', t_end - t_start)
286+
# plt.show()

0 commit comments

Comments
 (0)