quadcoil 0.1.0__tar.gz

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.
Files changed (43) hide show
  1. quadcoil-0.1.0/LICENSE.md +21 -0
  2. quadcoil-0.1.0/PKG-INFO +58 -0
  3. quadcoil-0.1.0/README.md +37 -0
  4. quadcoil-0.1.0/pyproject.toml +23 -0
  5. quadcoil-0.1.0/setup.cfg +4 -0
  6. quadcoil-0.1.0/src/quadcoil/__init__.py +13 -0
  7. quadcoil-0.1.0/src/quadcoil/io/__init__.py +7 -0
  8. quadcoil-0.1.0/src/quadcoil/io/coil_cutting.py +257 -0
  9. quadcoil-0.1.0/src/quadcoil/io/desc.py +195 -0
  10. quadcoil-0.1.0/src/quadcoil/io/file.py +0 -0
  11. quadcoil-0.1.0/src/quadcoil/io/focus.py +65 -0
  12. quadcoil-0.1.0/src/quadcoil/io/jax.py +163 -0
  13. quadcoil-0.1.0/src/quadcoil/io/simsopt.py +18 -0
  14. quadcoil-0.1.0/src/quadcoil/math_utils.py +196 -0
  15. quadcoil-0.1.0/src/quadcoil/plotting.py +253 -0
  16. quadcoil-0.1.0/src/quadcoil/quadcoil.py +1129 -0
  17. quadcoil-0.1.0/src/quadcoil/quadcoil_params.py +506 -0
  18. quadcoil-0.1.0/src/quadcoil/quantity/__init__.py +8 -0
  19. quadcoil-0.1.0/src/quadcoil/quantity/current.py +162 -0
  20. quadcoil-0.1.0/src/quadcoil/quantity/curvature.py +77 -0
  21. quadcoil-0.1.0/src/quadcoil/quantity/dipole.py +114 -0
  22. quadcoil-0.1.0/src/quadcoil/quantity/force.py +547 -0
  23. quadcoil-0.1.0/src/quadcoil/quantity/helpers.py +1 -0
  24. quadcoil-0.1.0/src/quadcoil/quantity/magnetic_field.py +116 -0
  25. quadcoil-0.1.0/src/quadcoil/quantity/quantity.py +631 -0
  26. quadcoil-0.1.0/src/quadcoil/quantity/regularization.py +21 -0
  27. quadcoil-0.1.0/src/quadcoil/solver.py +640 -0
  28. quadcoil-0.1.0/src/quadcoil/surfacerzfourier_jax.py +536 -0
  29. quadcoil-0.1.0/src/quadcoil/winding_surface.py +457 -0
  30. quadcoil-0.1.0/src/quadcoil/wrapper.py +383 -0
  31. quadcoil-0.1.0/src/quadcoil.egg-info/PKG-INFO +58 -0
  32. quadcoil-0.1.0/src/quadcoil.egg-info/SOURCES.txt +41 -0
  33. quadcoil-0.1.0/src/quadcoil.egg-info/dependency_links.txt +1 -0
  34. quadcoil-0.1.0/src/quadcoil.egg-info/requires.txt +6 -0
  35. quadcoil-0.1.0/src/quadcoil.egg-info/top_level.txt +1 -0
  36. quadcoil-0.1.0/tests/test_B.py +53 -0
  37. quadcoil-0.1.0/tests/test_K.py +27 -0
  38. quadcoil-0.1.0/tests/test_KK.py +96 -0
  39. quadcoil-0.1.0/tests/test_Phi.py +24 -0
  40. quadcoil-0.1.0/tests/test_desc.py +112 -0
  41. quadcoil-0.1.0/tests/test_jvp.py +78 -0
  42. quadcoil-0.1.0/tests/test_regcoil.py +201 -0
  43. quadcoil-0.1.0/tests/test_wrapper.py +189 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Simsopt contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: quadcoil
3
+ Version: 0.1.0
4
+ Summary: QUADCOIL, a global stellarator coil optimizer and coil complexity proxy
5
+ Author-email: lanke_fu <ffu@pppl.gov>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lankef/quadcoil
8
+ Project-URL: Issues, https://github.com/lankef/quadcoil/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.md
14
+ Requires-Dist: numpy
15
+ Requires-Dist: jax>=0.5.0
16
+ Requires-Dist: optax>=0.2.5
17
+ Requires-Dist: lineax
18
+ Requires-Dist: scipy
19
+ Requires-Dist: matplotlib
20
+ Dynamic: license-file
21
+
22
+ # QUADCOIL
23
+
24
+ Welcome to the github page of QUADCOIL, the stellarator coil complexity proxy/global coil optimizer!
25
+
26
+ Developer contact: Lanke Fu, LF2869@nyu.edu
27
+
28
+ QUADCOIL is a global coil optimization code that approximates coils with a smooth sheet current.
29
+ In other words, it's a "winding surface" code. However, unlike other winding surface codes, QUADCOIL:
30
+
31
+ - Supports constrained optimization.
32
+ - Supports non-convex quadratic penalties/constraints, such as curvature
33
+ $\mathbf{K} \cdot \nabla \mathbf{K}$.
34
+ - Includes robust winding surface generators that do not produce self-intersections.
35
+ - Calculates derivatives with respect to plasma shape, winding surface shape, objective weights, and constraint thresholds.
36
+
37
+ Read QUADCOIL documentations [here](https://quadcoil.readthedocs.io/en/latest/index.html).
38
+
39
+ ## Release / PyPI
40
+
41
+ Before publishing, update `version` in `pyproject.toml` and record the release hash with:
42
+
43
+ ```bash
44
+ git rev-parse main
45
+ ```
46
+
47
+ Build and validate:
48
+
49
+ ```bash
50
+ python -m build
51
+ twine check dist/*
52
+ ```
53
+
54
+ Upload (recommend TestPyPI first, then PyPI):
55
+
56
+ ```bash
57
+ twine upload dist/*
58
+ ```
@@ -0,0 +1,37 @@
1
+ # QUADCOIL
2
+
3
+ Welcome to the github page of QUADCOIL, the stellarator coil complexity proxy/global coil optimizer!
4
+
5
+ Developer contact: Lanke Fu, LF2869@nyu.edu
6
+
7
+ QUADCOIL is a global coil optimization code that approximates coils with a smooth sheet current.
8
+ In other words, it's a "winding surface" code. However, unlike other winding surface codes, QUADCOIL:
9
+
10
+ - Supports constrained optimization.
11
+ - Supports non-convex quadratic penalties/constraints, such as curvature
12
+ $\mathbf{K} \cdot \nabla \mathbf{K}$.
13
+ - Includes robust winding surface generators that do not produce self-intersections.
14
+ - Calculates derivatives with respect to plasma shape, winding surface shape, objective weights, and constraint thresholds.
15
+
16
+ Read QUADCOIL documentations [here](https://quadcoil.readthedocs.io/en/latest/index.html).
17
+
18
+ ## Release / PyPI
19
+
20
+ Before publishing, update `version` in `pyproject.toml` and record the release hash with:
21
+
22
+ ```bash
23
+ git rev-parse main
24
+ ```
25
+
26
+ Build and validate:
27
+
28
+ ```bash
29
+ python -m build
30
+ twine check dist/*
31
+ ```
32
+
33
+ Upload (recommend TestPyPI first, then PyPI):
34
+
35
+ ```bash
36
+ twine upload dist/*
37
+ ```
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quadcoil"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="lanke_fu", email="ffu@pppl.gov" },
10
+ ]
11
+ description = "QUADCOIL, a global stellarator coil optimizer and coil complexity proxy"
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ dependencies = ["numpy", "jax >= 0.5.0", "optax >= 0.2.5", "lineax", "scipy", "matplotlib"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ license = "MIT"
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/lankef/quadcoil"
23
+ Issues = "https://github.com/lankef/quadcoil/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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.
@@ -0,0 +1,7 @@
1
+ from .jax import *
2
+ from .file import *
3
+ from .desc import *
4
+ from .simsopt import *
5
+ from .coil_cutting import *
6
+ from .focus import *
7
+
@@ -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
+
@@ -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})
File without changes
@@ -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)