quadcoil 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quadcoil/__init__.py +13 -0
- quadcoil/io/__init__.py +7 -0
- quadcoil/io/coil_cutting.py +257 -0
- quadcoil/io/desc.py +195 -0
- quadcoil/io/file.py +0 -0
- quadcoil/io/focus.py +65 -0
- quadcoil/io/jax.py +163 -0
- quadcoil/io/simsopt.py +18 -0
- quadcoil/math_utils.py +196 -0
- quadcoil/plotting.py +253 -0
- quadcoil/quadcoil.py +1129 -0
- quadcoil/quadcoil_params.py +506 -0
- quadcoil/quantity/__init__.py +8 -0
- quadcoil/quantity/current.py +162 -0
- quadcoil/quantity/curvature.py +77 -0
- quadcoil/quantity/dipole.py +114 -0
- quadcoil/quantity/force.py +547 -0
- quadcoil/quantity/helpers.py +1 -0
- quadcoil/quantity/magnetic_field.py +116 -0
- quadcoil/quantity/quantity.py +631 -0
- quadcoil/quantity/regularization.py +21 -0
- quadcoil/solver.py +640 -0
- quadcoil/surfacerzfourier_jax.py +536 -0
- quadcoil/winding_surface.py +457 -0
- quadcoil/wrapper.py +383 -0
- quadcoil-0.1.0.dist-info/METADATA +58 -0
- quadcoil-0.1.0.dist-info/RECORD +30 -0
- quadcoil-0.1.0.dist-info/WHEEL +5 -0
- quadcoil-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- quadcoil-0.1.0.dist-info/top_level.txt +1 -0
quadcoil/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .math_utils import *
|
|
2
|
+
from .quadcoil_params import *
|
|
3
|
+
from .surfacerzfourier_jax import *
|
|
4
|
+
from .winding_surface import *
|
|
5
|
+
from .wrapper import *
|
|
6
|
+
from .solver import *
|
|
7
|
+
from .quadcoil import *
|
|
8
|
+
from . import io
|
|
9
|
+
from . import quantity
|
|
10
|
+
# from .conf import *
|
|
11
|
+
# All submodules uses quadcoil. So, we will
|
|
12
|
+
# not import them here to avoid circular
|
|
13
|
+
# imports.
|
quadcoil/io/__init__.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
|
|
2
|
+
# Import packages.
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from scipy.fft import rfft
|
|
6
|
+
# Importing simsopt
|
|
7
|
+
from quadcoil import QuadcoilParams
|
|
8
|
+
from quadcoil.quantity import Phi_with_net_current
|
|
9
|
+
|
|
10
|
+
def coil_zeta_theta_from_qp(
|
|
11
|
+
qp:QuadcoilParams,
|
|
12
|
+
dofs,
|
|
13
|
+
coils_per_half_period=5,
|
|
14
|
+
theta_shift=0):
|
|
15
|
+
stellsym = qp.stellsym
|
|
16
|
+
nzeta_coil = len(qp.quadpoints_phi)
|
|
17
|
+
nfp = qp.nfp
|
|
18
|
+
theta = qp.quadpoints_theta * 2 * np.pi
|
|
19
|
+
zeta = qp.quadpoints_phi * 2 * np.pi
|
|
20
|
+
net_poloidal_current_amperes = qp.net_poloidal_current_amperes
|
|
21
|
+
|
|
22
|
+
# ------------------------
|
|
23
|
+
# Load current potential
|
|
24
|
+
# ------------------------
|
|
25
|
+
current_potential = Phi_with_net_current(qp, dofs)
|
|
26
|
+
|
|
27
|
+
# Now just generate a new monotonic array with the correct first value:
|
|
28
|
+
theta = theta[0] + np.linspace(0,2*np.pi,len(theta),endpoint=False)
|
|
29
|
+
|
|
30
|
+
d = 2*np.pi/nfp
|
|
31
|
+
# We add a field period to the "left" and "right" of the
|
|
32
|
+
# present field period so that no contours are cutoff.
|
|
33
|
+
zeta_3 = np.concatenate((zeta-d, zeta, zeta+d))
|
|
34
|
+
# If the net current is non-neglegible, use the net current to calculate spacing.
|
|
35
|
+
# Else, use the maximum phi to calculate spacing.
|
|
36
|
+
# we normalize the current potential correspondingly into "data",
|
|
37
|
+
# and also pad it to contain 3 field periods.
|
|
38
|
+
if abs(net_poloidal_current_amperes) > np.finfo(float).eps:
|
|
39
|
+
data = current_potential / net_poloidal_current_amperes * nfp
|
|
40
|
+
data_3 = np.concatenate((data-1,data,data+1))
|
|
41
|
+
else:
|
|
42
|
+
data = current_potential / np.max(current_potential)
|
|
43
|
+
data_3 = np.concatenate((data,data,data))
|
|
44
|
+
|
|
45
|
+
# We only generate contours based on the "middle"
|
|
46
|
+
# field period.
|
|
47
|
+
d = 0.5/coils_per_half_period
|
|
48
|
+
level_shift = theta_shift % 1 * d
|
|
49
|
+
if stellsym:
|
|
50
|
+
if theta_shift != 0:
|
|
51
|
+
print('Warning: non-zero theta-shift is not permitted when stell-sym is true.')
|
|
52
|
+
levels = np.linspace(0,0.5,coils_per_half_period,endpoint=False)
|
|
53
|
+
else:
|
|
54
|
+
levels = np.linspace(0,1,coils_per_half_period*2,endpoint=False) + level_shift
|
|
55
|
+
# The contours are symmetric w.r.t. 2pi/nfp/2, i.e. stellarator
|
|
56
|
+
# symmetric, by default.
|
|
57
|
+
levels = levels + d/2
|
|
58
|
+
fig = plt.figure()
|
|
59
|
+
cdata = plt.contour(zeta_3, theta, np.transpose(data_3), levels, colors='k')
|
|
60
|
+
plt.close(fig)
|
|
61
|
+
num_coils_found = len(cdata.levels)
|
|
62
|
+
if num_coils_found != 2*coils_per_half_period:
|
|
63
|
+
print("Warning: The expected number of levels != 2 * coils_per_half_period.")
|
|
64
|
+
|
|
65
|
+
contour_zeta=[]
|
|
66
|
+
contour_theta=[]
|
|
67
|
+
num_coils = 0
|
|
68
|
+
for j in range(num_coils_found):
|
|
69
|
+
paths_in_level = cdata.allsegs[j]
|
|
70
|
+
# Each level can contain many paths.
|
|
71
|
+
for p in paths_in_level:
|
|
72
|
+
plt.plot(p[:, 0], p[:, 1])
|
|
73
|
+
# These are the zeta (toroidal angles)
|
|
74
|
+
# and theta of the paths.
|
|
75
|
+
contour_zeta.append(p[:, 0])
|
|
76
|
+
contour_theta.append(p[:, 1])
|
|
77
|
+
num_coils += 1
|
|
78
|
+
|
|
79
|
+
print('Cutting complete. Number of coils/half field period:', num_coils)
|
|
80
|
+
return(contour_zeta, contour_theta)
|
|
81
|
+
|
|
82
|
+
# IFFT a array in real space to a sin/cos series used by sinsopt.geo.curve
|
|
83
|
+
def ifft_simsopt(x, order):
|
|
84
|
+
assert len(x) >= 2*order # the order of the fft is limited by the number of samples
|
|
85
|
+
xf = rfft(x) / len(x)
|
|
86
|
+
|
|
87
|
+
fft_0 = [xf[0].real] # find the 0 order coefficient
|
|
88
|
+
fft_cos = 2 * xf[1:order + 1].real # find the cosine coefficients
|
|
89
|
+
fft_sin = (-2 * xf[:order + 1].imag)[1:] # find the sine coefficients
|
|
90
|
+
dof = np.zeros(order*2+1)
|
|
91
|
+
dof[0] = fft_0[0]
|
|
92
|
+
dof[1::2] = fft_sin
|
|
93
|
+
dof[2::2] = fft_cos
|
|
94
|
+
|
|
95
|
+
return dof
|
|
96
|
+
|
|
97
|
+
# This script assumes the contours do not zig-zag back and forth across the theta=0 line,
|
|
98
|
+
# after shifting the current potential by theta_shift grid points.
|
|
99
|
+
# The nescin file is used to provide the coil winding surface, so make sure this is consistent with the regcoil run.
|
|
100
|
+
# ilambda is the index in the lambda scan which you want to select.
|
|
101
|
+
# def cut_coil(qp, qpst):
|
|
102
|
+
# filename = 'regcoil_out.li383.nc' # sys.argv[1]
|
|
103
|
+
# TODO: these tow have a lot of duplicate code. Clean up when finalizing how QUADCOIL is integrated.
|
|
104
|
+
def coil_xyz_from_qp(
|
|
105
|
+
qp:QuadcoilParams,
|
|
106
|
+
dofs,
|
|
107
|
+
coils_per_half_period=1,
|
|
108
|
+
theta_shift=0,
|
|
109
|
+
save=False, save_name='placeholder'):
|
|
110
|
+
contour_zeta, contour_theta = coil_zeta_theta_from_qp(
|
|
111
|
+
qp=qp,
|
|
112
|
+
dofs=dofs,
|
|
113
|
+
coils_per_half_period=coils_per_half_period,
|
|
114
|
+
theta_shift=theta_shift
|
|
115
|
+
)
|
|
116
|
+
num_coils = len(contour_zeta)
|
|
117
|
+
nfp = qp.nfp
|
|
118
|
+
net_poloidal_current_amperes = qp.net_poloidal_current_amperes
|
|
119
|
+
|
|
120
|
+
# ------------------------
|
|
121
|
+
# Load surface shape
|
|
122
|
+
# ------------------------
|
|
123
|
+
contour_R = []
|
|
124
|
+
contour_Z = []
|
|
125
|
+
for j in range(num_coils):
|
|
126
|
+
contour_R.append(np.zeros_like(contour_zeta[j]))
|
|
127
|
+
contour_Z.append(np.zeros_like(contour_zeta[j]))
|
|
128
|
+
|
|
129
|
+
surf = qp.winding_surface.to_simsopt()
|
|
130
|
+
for m in range(surf.mpol+1): # 0 to mpol
|
|
131
|
+
for i in range(2*surf.ntor+1):
|
|
132
|
+
n = i-surf.ntor
|
|
133
|
+
crc = surf.rc[m, i]
|
|
134
|
+
czs = surf.zs[m, i]
|
|
135
|
+
if surf.stellsym:
|
|
136
|
+
crs = 0
|
|
137
|
+
czc = 0
|
|
138
|
+
else: # Returns ValueError for stellsym cases
|
|
139
|
+
crs = surf.get_rs(m, n)
|
|
140
|
+
czc = surf.get_zc(m, n)
|
|
141
|
+
for j in range(num_coils):
|
|
142
|
+
angle = m*contour_theta[j] - n*contour_zeta[j]*surf.nfp
|
|
143
|
+
# Was filled with zeroes.
|
|
144
|
+
# Are lists because contou lengths are not uniform.
|
|
145
|
+
contour_R[j] = contour_R[j] + crc*np.cos(angle) + crs*np.sin(angle)
|
|
146
|
+
contour_Z[j] = contour_Z[j] + czs*np.sin(angle) + czc*np.cos(angle)
|
|
147
|
+
|
|
148
|
+
contour_X = []
|
|
149
|
+
contour_Y = []
|
|
150
|
+
for j in range(num_coils):
|
|
151
|
+
contour_X.append(contour_R[j]*np.cos(contour_zeta[j]))
|
|
152
|
+
contour_Y.append(contour_R[j]*np.sin(contour_zeta[j]))
|
|
153
|
+
coil_currents = net_poloidal_current_amperes / num_coils / qp.nfp
|
|
154
|
+
if qp.stellsym:
|
|
155
|
+
coil_currents = coil_currents/2
|
|
156
|
+
if save:
|
|
157
|
+
coilsFilename='coils.'+save_name
|
|
158
|
+
print("coilsFilename:",coilsFilename)
|
|
159
|
+
# Write coils file
|
|
160
|
+
f = open(coilsFilename,'w')
|
|
161
|
+
f.write('periods '+str(nfp)+'\n')
|
|
162
|
+
f.write('begin filament\n')
|
|
163
|
+
f.write('mirror NIL\n')
|
|
164
|
+
|
|
165
|
+
for j in range(num_coils):
|
|
166
|
+
N = len(contour_X[j])
|
|
167
|
+
for k in range(N):
|
|
168
|
+
f.write('{:14.22e} {:14.22e} {:14.22e} {:14.22e}\n'.format(contour_X[j][k],contour_Y[j][k],contour_Z[j][k],coil_currents))
|
|
169
|
+
# Close the loop
|
|
170
|
+
k=0
|
|
171
|
+
f.write('{:14.22e} {:14.22e} {:14.22e} {:14.22e} 1 Modular\n'.format(contour_X[j][k],contour_Y[j][k],contour_Z[j][k],0))
|
|
172
|
+
|
|
173
|
+
f.write('end\n')
|
|
174
|
+
f.close()
|
|
175
|
+
return (
|
|
176
|
+
contour_X,
|
|
177
|
+
contour_Y,
|
|
178
|
+
contour_Z,
|
|
179
|
+
coil_currents,
|
|
180
|
+
# np.sqrt(minSeparation2)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Load curves from lists of arrays containing x, y, and z.
|
|
184
|
+
def simsopt_curves_from_xyz(
|
|
185
|
+
contour_X,
|
|
186
|
+
contour_Y,
|
|
187
|
+
contour_Z,
|
|
188
|
+
order=None, ppp=20):
|
|
189
|
+
num_coils = len(contour_X)
|
|
190
|
+
try:
|
|
191
|
+
from simsopt.geo import CurveXYZFourier
|
|
192
|
+
except:
|
|
193
|
+
raise ImportError('Simsopt is required to use the coil-cutting features.')
|
|
194
|
+
# Calculating order
|
|
195
|
+
if not order:
|
|
196
|
+
order=float('inf')
|
|
197
|
+
for i in range(num_coils):
|
|
198
|
+
xArr = contour_X[i]
|
|
199
|
+
yArr = contour_Y[i]
|
|
200
|
+
zArr = contour_Z[i]
|
|
201
|
+
for x in [xArr, yArr, zArr]:
|
|
202
|
+
if len(x)//2<order:
|
|
203
|
+
order = len(x)//2
|
|
204
|
+
|
|
205
|
+
coils = [CurveXYZFourier(order*ppp, order) for i in range(num_coils)]
|
|
206
|
+
# Compute the Fourier coefficients for each coil
|
|
207
|
+
for ic in range(num_coils):
|
|
208
|
+
xArr = contour_X[ic]
|
|
209
|
+
yArr = contour_Y[ic]
|
|
210
|
+
zArr = contour_Z[ic]
|
|
211
|
+
|
|
212
|
+
# Compute the Fourier coefficients
|
|
213
|
+
dofs=[]
|
|
214
|
+
for x in [xArr, yArr, zArr]:
|
|
215
|
+
dof_i = ifft_simsopt(x, order)
|
|
216
|
+
dofs.append(dof_i)
|
|
217
|
+
|
|
218
|
+
coils[ic].local_x = np.concatenate(dofs)
|
|
219
|
+
return coils
|
|
220
|
+
|
|
221
|
+
def simsopt_coil_from_qp(
|
|
222
|
+
qp, dofs, coils_per_half_period, theta_shift=0,
|
|
223
|
+
method=coil_xyz_from_qp,
|
|
224
|
+
base_mode=False,
|
|
225
|
+
order=10, ppp=40):
|
|
226
|
+
try:
|
|
227
|
+
from simsopt.field import Current # , Coil
|
|
228
|
+
from simsopt.field.coil import coils_via_symmetries
|
|
229
|
+
except:
|
|
230
|
+
raise ImportError('Simsopt is required to use the coil-cutting features.')
|
|
231
|
+
(
|
|
232
|
+
contour_X,
|
|
233
|
+
contour_Y,
|
|
234
|
+
contour_Z,
|
|
235
|
+
coil_currents,
|
|
236
|
+
# min_separation
|
|
237
|
+
) = method(
|
|
238
|
+
qp=qp,
|
|
239
|
+
dofs=dofs,
|
|
240
|
+
coils_per_half_period=coils_per_half_period,
|
|
241
|
+
theta_shift=theta_shift,
|
|
242
|
+
save=False
|
|
243
|
+
)
|
|
244
|
+
curves = simsopt_curves_from_xyz(
|
|
245
|
+
contour_X,
|
|
246
|
+
contour_Y,
|
|
247
|
+
contour_Z,
|
|
248
|
+
order=order,
|
|
249
|
+
ppp=ppp)
|
|
250
|
+
currents=[]
|
|
251
|
+
for i in range(len(curves)):
|
|
252
|
+
currents.append(Current(coil_currents))
|
|
253
|
+
if base_mode:
|
|
254
|
+
return curves, currents
|
|
255
|
+
coils = coils_via_symmetries(curves, currents, qp.nfp, qp.stellsym)
|
|
256
|
+
return coils
|
|
257
|
+
|
quadcoil/io/desc.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from quadcoil import quadcoil, SurfaceRZFourierJAX, get_quantity
|
|
2
|
+
import jax.numpy as jnp
|
|
3
|
+
from scipy.constants import mu_0
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_desc_scaling(objective_name, constraint_name, scales):
|
|
7
|
+
'''
|
|
8
|
+
A helper method for calculating the units of each objectives and constraints
|
|
9
|
+
using quantities scales = desc.objectives.normalization.compute_scaling_factors(eq).
|
|
10
|
+
The formula must be first implemented as quadcoil.objective.<quantity_name>_desc_unit.
|
|
11
|
+
'''
|
|
12
|
+
if isinstance(objective_name, str):
|
|
13
|
+
objective_unit = get_quantity(objective_name).desc_unit(scales)
|
|
14
|
+
else:
|
|
15
|
+
objective_unit_new = []
|
|
16
|
+
for obj_name in objective_name:
|
|
17
|
+
objective_unit_new.append(get_quantity(obj_name).desc_unit(scales))
|
|
18
|
+
objective_unit = tuple(objective_unit_new)
|
|
19
|
+
|
|
20
|
+
constraint_unit_new = []
|
|
21
|
+
for cons_name in constraint_name:
|
|
22
|
+
constraint_unit_new.append(get_quantity(cons_name).desc_unit(scales))
|
|
23
|
+
constraint_unit = tuple(constraint_unit_new)
|
|
24
|
+
return(objective_unit, constraint_unit)
|
|
25
|
+
|
|
26
|
+
# ----- An earlier, simpler wrapper. May be discontinued later. -----
|
|
27
|
+
def quadcoil_desc(
|
|
28
|
+
desc_eq,
|
|
29
|
+
vacuum:bool,
|
|
30
|
+
plasma_M_theta:int,
|
|
31
|
+
plasma_N_phi:int,
|
|
32
|
+
desc_scaling:bool=True,
|
|
33
|
+
**kwargs
|
|
34
|
+
):
|
|
35
|
+
'''
|
|
36
|
+
A simple DESC interface for ``quadcoil.quadcoil``, that replaces the following
|
|
37
|
+
parameters using information extracted from DESC:
|
|
38
|
+
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
nfp
|
|
42
|
+
stellsym
|
|
43
|
+
plasma_mpol
|
|
44
|
+
plasma_ntor
|
|
45
|
+
plasma_quadpoints_phi
|
|
46
|
+
plasma_quadpoints_theta
|
|
47
|
+
plasma_dofs
|
|
48
|
+
net_poloidal_current_amperes
|
|
49
|
+
Bnormal_plasma
|
|
50
|
+
winding_dofs
|
|
51
|
+
|
|
52
|
+
'''
|
|
53
|
+
try:
|
|
54
|
+
from desc.integrals import compute_B_plasma
|
|
55
|
+
from desc.grid import LinearGrid
|
|
56
|
+
from desc.objectives.normalization import compute_scaling_factors
|
|
57
|
+
except:
|
|
58
|
+
raise ModuleNotFoundError('DESC must be installed for the... DESC interface to run.')
|
|
59
|
+
if 'plasma_coil_distance' in kwargs:
|
|
60
|
+
# DESC surfaces have different handed-ness
|
|
61
|
+
# compared to simsopt surfaces. Because QUADCOIL
|
|
62
|
+
# uses simsopt conventions, we flip the sign
|
|
63
|
+
# of offset direction here.
|
|
64
|
+
kwargs['plasma_coil_distance'] = -kwargs['plasma_coil_distance']
|
|
65
|
+
# We want to make sure DESC and QUADCOIL
|
|
66
|
+
# uses the same grid on the plasma surface.
|
|
67
|
+
# In case we want to use Bnormal_plasma
|
|
68
|
+
# from DESC.
|
|
69
|
+
# To do this, we create a grid inside DESC.
|
|
70
|
+
surface_grid = LinearGrid(
|
|
71
|
+
NFP=desc_eq.NFP,
|
|
72
|
+
# If we set this to sym it'll only evaluate
|
|
73
|
+
# theta from 0 to pi.
|
|
74
|
+
sym=False,
|
|
75
|
+
M=plasma_M_theta,#Poloidal grid resolution.
|
|
76
|
+
N=plasma_N_phi,
|
|
77
|
+
rho=1.0
|
|
78
|
+
)
|
|
79
|
+
# Recovering quadpoints from the LinearGrid
|
|
80
|
+
# for use in QUADCOIL and simsopt
|
|
81
|
+
desc_plasma_quadpoints_theta = surface_grid.nodes[surface_grid.unique_theta_idx, 1]/jnp.pi/2
|
|
82
|
+
desc_plasma_quadpoints_phi = surface_grid.nodes[surface_grid.unique_zeta_idx, 2]/jnp.pi/2
|
|
83
|
+
desc_plasma_surf = SurfaceRZFourierJAX.from_desc(
|
|
84
|
+
desc_eq.surface,
|
|
85
|
+
desc_plasma_quadpoints_phi,
|
|
86
|
+
desc_plasma_quadpoints_theta
|
|
87
|
+
)
|
|
88
|
+
desc_net_poloidal_current_amperes = (
|
|
89
|
+
-desc_eq.compute("G", grid=LinearGrid(rho=jnp.array(1.0)))["G"][0]
|
|
90
|
+
/ mu_0
|
|
91
|
+
* 2
|
|
92
|
+
* jnp.pi
|
|
93
|
+
)
|
|
94
|
+
# Calculating B_plasma if not vacuum
|
|
95
|
+
if not vacuum:
|
|
96
|
+
desc_Bnormal_plasma = compute_B_plasma(
|
|
97
|
+
desc_eq,
|
|
98
|
+
eval_grid=surface_grid,
|
|
99
|
+
normal_only=True
|
|
100
|
+
# chunk_size=B_plasma_chunk_size
|
|
101
|
+
)
|
|
102
|
+
# Reshape the output so that axis=0 is zeta (the toroidal coordinate)
|
|
103
|
+
desc_Bnormal_plasma = surface_grid.meshgrid_reshape(desc_Bnormal_plasma, order='rzt')[0]
|
|
104
|
+
else:
|
|
105
|
+
desc_Bnormal_plasma = jnp.zeros((
|
|
106
|
+
len(desc_plasma_quadpoints_phi),
|
|
107
|
+
len(desc_plasma_quadpoints_theta)
|
|
108
|
+
))
|
|
109
|
+
print('Finished reading DESC equilibrium. G =', desc_net_poloidal_current_amperes)
|
|
110
|
+
print('Finished reading DESC equilibrium. G =', type(desc_net_poloidal_current_amperes))
|
|
111
|
+
# Detect if the user has provided any arguments
|
|
112
|
+
# that will also-be auto-calculated using DESC.
|
|
113
|
+
# if any, these objectives will be discarded.
|
|
114
|
+
redundant_arg_names = set(_DESC_DERIVED_ARGNAMES) & kwargs.keys()
|
|
115
|
+
if redundant_arg_names:
|
|
116
|
+
warnings.warn(
|
|
117
|
+
'Redundant arguments detected: '
|
|
118
|
+
+ str(redundant_arg_names)
|
|
119
|
+
+ '. These arguments must be calculated from '\
|
|
120
|
+
'the equilibrium. The provided values will be discarded.'
|
|
121
|
+
)
|
|
122
|
+
# Add all the equilibrium-dependent, non-differentiable args to quadcopil_args
|
|
123
|
+
# Non-differentiable
|
|
124
|
+
# (This distinction is only for internal reference.
|
|
125
|
+
# the desc _Objective wrapper of QUADCOIL uses a custom_jvp rule in
|
|
126
|
+
# quadcoil.io.jax, and that function makes the distinction between
|
|
127
|
+
# differentiable and non-differentiable args.)
|
|
128
|
+
kwargs['nfp'] = desc_eq.NFP
|
|
129
|
+
kwargs['stellsym'] = desc_eq.sym
|
|
130
|
+
kwargs['plasma_mpol'] = desc_plasma_surf.mpol
|
|
131
|
+
kwargs['plasma_ntor'] = desc_plasma_surf.ntor
|
|
132
|
+
kwargs['plasma_quadpoints_phi'] = desc_plasma_quadpoints_phi
|
|
133
|
+
kwargs['plasma_quadpoints_theta'] = desc_plasma_quadpoints_theta
|
|
134
|
+
# Differentiable
|
|
135
|
+
kwargs['plasma_dofs'] = desc_plasma_surf.dofs
|
|
136
|
+
kwargs['net_poloidal_current_amperes'] = desc_net_poloidal_current_amperes
|
|
137
|
+
kwargs['Bnormal_plasma'] = desc_Bnormal_plasma
|
|
138
|
+
# For unconstrained cases, kwargs will not contain
|
|
139
|
+
# 'constraint_name'. But we still want to use the scaling helper function.
|
|
140
|
+
# So we manually add a blank entry.
|
|
141
|
+
if 'constraint_name' not in kwargs:
|
|
142
|
+
kwargs['constraint_name'] = ()
|
|
143
|
+
if desc_scaling:
|
|
144
|
+
print('Overriding provided/default scaling with DESC scaling.')
|
|
145
|
+
obj_unit_new, cons_unit_new = generate_desc_scaling(
|
|
146
|
+
kwargs['objective_name'],
|
|
147
|
+
kwargs['constraint_name'],
|
|
148
|
+
compute_scaling_factors(desc_eq)
|
|
149
|
+
)
|
|
150
|
+
kwargs['objective_unit'] = obj_unit_new
|
|
151
|
+
kwargs['constraint_unit'] = cons_unit_new
|
|
152
|
+
return(
|
|
153
|
+
quadcoil(**kwargs)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def load_FourierCurrentPotentialField(eq, scf, winding_quadpoints_phi=None, winding_quadpoints_theta=None, source_grid=None):
|
|
157
|
+
try:
|
|
158
|
+
from desc.vmec_utils import ptolemy_identity_rev
|
|
159
|
+
except:
|
|
160
|
+
raise ModuleNotFoundError(
|
|
161
|
+
'DESC must be installed to load '
|
|
162
|
+
'a FourierCurrentPotentialField.'
|
|
163
|
+
)
|
|
164
|
+
mm, nn, phis_raw, phic_raw = ptolemy_identity_rev(scf.Phi_basis.modes[:,1], scf.Phi_basis.modes[:,2], scf.Phi_mn)
|
|
165
|
+
mpol = scf.M
|
|
166
|
+
ntor = scf.N
|
|
167
|
+
stellsym = scf.sym
|
|
168
|
+
nfp = scf.NFP
|
|
169
|
+
phic = phic_raw.flatten()
|
|
170
|
+
phis = phis_raw.flatten()[1:]
|
|
171
|
+
# When stellsym is enabled, Phi is a sin fourier series.
|
|
172
|
+
# Otherwise, it's a sin-cos series.
|
|
173
|
+
if stellsym:
|
|
174
|
+
phi = jnp.concatenate([phis])
|
|
175
|
+
else:
|
|
176
|
+
phi = jnp.concatenate([phis, phic])
|
|
177
|
+
if quadpoints_phi and quadpoints_theta:
|
|
178
|
+
if source_grid:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
'The user should provide either '
|
|
181
|
+
'(quadpoints_phi, quadpoints_theta) or source_grid, '
|
|
182
|
+
'not both.'
|
|
183
|
+
)
|
|
184
|
+
winding_quadpoints_phi_temp = winding_quadpoints_phi
|
|
185
|
+
winding_quadpoints_theta_temp = winding_quadpoints_theta
|
|
186
|
+
else:
|
|
187
|
+
winding_quadpoints_phi_temp = jnp.linspace(0, 1, (source_grid.M*2+1) * nfp, endpoint=False)
|
|
188
|
+
winding_quadpoints_theta_temp = jnp.linspace(0, 1, source_grid.N*2+1, endpoint=False)
|
|
189
|
+
|
|
190
|
+
winding_surface = SurfaceRZFourierJAX.from_desc(
|
|
191
|
+
scf_nescoil,
|
|
192
|
+
quadpoints_phi = winding_quadpoints_phi_temp,
|
|
193
|
+
quadpoints_theta = winding_quadpoints_theta_temp,
|
|
194
|
+
)
|
|
195
|
+
return (qp, {'phi': phi})
|
quadcoil/io/file.py
ADDED
|
File without changes
|
quadcoil/io/focus.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from quadcoil import SurfaceRZFourierJAX
|
|
3
|
+
def save_focus(surface, filename):
|
|
4
|
+
'''
|
|
5
|
+
Saves a Simsopt `SurfaceRZFourier`, a DESC `FourierRZToroidalSurface`,
|
|
6
|
+
or a QUADCOIL `SurfaceRZFourierJAX`
|
|
7
|
+
to a `.plasma` file used for FOCUS or FAMUS under
|
|
8
|
+
`CASE_SURFACE=0`.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
surface
|
|
13
|
+
The surface tp save.
|
|
14
|
+
filename
|
|
15
|
+
The filename to save.
|
|
16
|
+
'''
|
|
17
|
+
try:
|
|
18
|
+
from simsopt.geo import SurfaceRZFourier
|
|
19
|
+
except ModuleNotFoundError:
|
|
20
|
+
raise ModuleNotFoundError('Simsopt must be installed to output as FOCUS.')
|
|
21
|
+
if isinstance(surface, SurfaceRZFourierJAX):
|
|
22
|
+
surface = surface.to_simsopt()
|
|
23
|
+
elif isinstance(surface, SurfaceRZFourier):
|
|
24
|
+
pass
|
|
25
|
+
else:
|
|
26
|
+
# This order of the type circumvents the DESC import unless
|
|
27
|
+
# necessary.
|
|
28
|
+
from desc.geometry.surface import FourierRZToroidalSurface
|
|
29
|
+
if not isinstance(surface, FourierRZToroidalSurface):
|
|
30
|
+
raise TypeError(
|
|
31
|
+
'surface must be a Simsopt SurfaceRZFourier, '
|
|
32
|
+
'a DESC FourierRZToroidalSurface, '
|
|
33
|
+
'or a QUADCOIL `SurfaceRZFourierJAX`'
|
|
34
|
+
)
|
|
35
|
+
surface = SurfaceRZFourierJAX.from_desc(
|
|
36
|
+
surface,
|
|
37
|
+
quadpoints_phi=np.linspace(0, 1, 32, endpoint=False),
|
|
38
|
+
quadpoints_theta=np.linspace(0, 1, 32, endpoint=False)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
replace_err = lambda x: 0 if isinstance(x, ValueError) or not np.isfinite(x) else x
|
|
42
|
+
lines_to_write = [
|
|
43
|
+
'# N_modes N_fp N_bn\n',
|
|
44
|
+
str(len(surface.get_dofs())) + ' ' + str(surface.nfp) + ' ' + '0\n', # No Bnorm modes
|
|
45
|
+
'#------- plasma boundary------\n',
|
|
46
|
+
'# n m Rbc Rbs Zbc Zbs\n',
|
|
47
|
+
]
|
|
48
|
+
for m in np.arange(0, surface.mpol+1):
|
|
49
|
+
for n in np.arange(-surface.ntor, surface.ntor+1):
|
|
50
|
+
Rbc = surface.get_rc(m, n)
|
|
51
|
+
Rbs = surface.get_rs(m, n)
|
|
52
|
+
Zbc = surface.get_zc(m, n)
|
|
53
|
+
Zbs = surface.get_zs(m, n)
|
|
54
|
+
lines_to_write+=[
|
|
55
|
+
str(n) + ' '
|
|
56
|
+
+ str(m) + ' '
|
|
57
|
+
+ str(replace_err(Rbc)) + ' '
|
|
58
|
+
+ str(replace_err(Rbs)) + ' '
|
|
59
|
+
+ str(replace_err(Zbc)) + ' '
|
|
60
|
+
+ str(replace_err(Zbs)) + '\n'
|
|
61
|
+
]
|
|
62
|
+
if not filename.endswith(".plasma"):
|
|
63
|
+
filename += ".plasma"
|
|
64
|
+
with open(filename, "w") as f:
|
|
65
|
+
f.writelines(lines_to_write)
|