|
def followPerpendicular( |
|
i, |
|
p0, |
|
psi0, |
|
*, |
|
f_R, |
|
f_Z, |
|
psivals, |
|
rtol=2.0e-8, |
|
atol=1.0e-8, |
|
maxits: int = 1000, |
|
recover: bool = False, |
|
**kwargs, |
|
): |
|
"""Follow a line perpendicular to Bp from point p0 until psi_target is reached. |
|
|
|
# Arguments |
|
|
|
maxits : int |
|
Maximum number of iterations to be taken. If exceeded then the range |
|
of psi will be truncated. |
|
|
|
recover : bool |
|
Recover from failures by changing the psivals of |
|
the grid points. This will result in an incorrect grid, but |
|
is useful when adjusting settings. |
|
|
|
""" |
|
if i is not None: |
|
print(f"Following perpendicular: {i + 1}", end="\r", flush=True) |
|
|
|
# psi0 might be in somewhere in the range of psivals, rather than at one end |
|
if min(psivals) < psi0 < max(psivals): |
|
# Integrate in each direction, then put together |
|
# Partition into left and right halves |
|
if psivals[0] < psi0: |
|
left = [psi for psi in psivals if psi < psi0] |
|
right = [psi for psi in psivals if psi >= psi0] |
|
else: |
|
left = [psi for psi in psivals if psi >= psi0] |
|
right = [psi for psi in psivals if psi < psi0] |
|
|
|
return followPerpendicular( |
|
None, |
|
p0, |
|
psi0, |
|
f_R=f_R, |
|
f_Z=f_Z, |
|
psivals=left[::-1], |
|
rtol=rtol, |
|
atol=atol, |
|
maxits=maxits, |
|
recover=recover, |
|
)[::-1] + followPerpendicular( |
|
None, |
|
p0, |
|
psi0, |
|
f_R=f_R, |
|
f_Z=f_Z, |
|
psivals=right, |
|
rtol=rtol, |
|
atol=atol, |
|
maxits=maxits, |
|
recover=recover, |
|
) |
|
|
|
if abs(psivals[-1] - psi0) < abs(psivals[0] - psi0): |
|
# Closer at the end than the start -> Reverse |
|
return followPerpendicular( |
|
None, |
|
p0, |
|
psi0, |
|
f_R=f_R, |
|
f_Z=f_Z, |
|
psivals=psivals[::-1], |
|
rtol=rtol, |
|
atol=atol, |
|
maxits=maxits, |
|
recover=recover, |
|
)[::-1] |
|
psivals = psivals.copy() |
|
|
|
class MaxIterException(Exception): |
|
def __init__(self, maxits, psi): |
|
super().__init__( |
|
f"Max iterations {maxits} reached at psi = {psi}\n" |
|
" To recover from this and generate a grid anyway, " |
|
"set follow_perpendicular_recover to True.\n" |
|
" The resulting grid will not align to flux surfaces." |
|
) |
|
self.psi = psi |
|
|
|
call_counter = 0 |
|
|
|
def f(psi, x): |
|
# Implement a maximum number of iterations because this is not provided by |
|
# the solve_ivp API. |
|
nonlocal call_counter |
|
call_counter += 1 |
|
if call_counter >= maxits: |
|
raise MaxIterException(maxits, psi) |
|
return (f_R(x[0], x[1]), f_Z(x[0], x[1])) |
|
|
|
psirange = (psi0, psivals[-1]) |
|
# make sure rounding errors do not cause exception: |
|
if psirange[1] - psirange[0] > 0: |
|
# psi increasing in this interval |
|
if psivals[0] < psirange[0] and psirange[0] - psivals[0] < 1.0e-15 * numpy.abs( |
|
psirange[0] |
|
): |
|
# rounding error present, reset psivals[0] |
|
psivals[0] = psirange[0] |
|
else: |
|
# psi decreasing in this interval |
|
if psivals[0] > psirange[0] and psivals[0] - psirange[0] < 1.0e-15 * numpy.abs( |
|
psirange[0] |
|
): |
|
# rounding error present, reset psivals[0] |
|
psivals[0] = psirange[0] |
|
try: |
|
solution = solve_ivp( |
|
f, |
|
psirange, |
|
tuple(p0), |
|
t_eval=psivals, |
|
rtol=rtol, |
|
atol=atol, |
|
vectorized=True, |
|
) |
|
except MaxIterException as e: |
|
print(f"followPerpendicular failed at psi = {e.psi}") |
|
if recover: |
|
print( |
|
" WARNING: Recovering by changing psi values.\n" |
|
" The resulting grid will not align to flux surfaces." |
|
) |
|
new_psivals = numpy.linspace(psi0, e.psi, len(psivals) + 1, endpoint=False)[ |
|
1: |
|
] |
|
return followPerpendicular( |
|
None, |
|
p0, |
|
psi0, |
|
f_R=f_R, |
|
f_Z=f_Z, |
|
psivals=new_psivals, |
|
rtol=rtol, |
|
atol=atol, |
|
maxits=maxits, |
|
recover=recover, |
|
) |
|
else: |
|
raise e |
|
|
|
except ValueError: |
|
print(psirange, psivals) |
|
raise |
|
|
|
return [Point2D(*p) for p in solution.y.T] |
I don't think this step is the major limitation to robustness, but the function that extends contours to or past walls (
PsiContour.temporaryExtend) could be improved.hypnotoad/hypnotoad/core/equilibrium.py
Lines 1910 to 1912 in 0365640
Rather than just extrapolating from the existing grid point positions,$\nabla \psi$ , that is the vector
scipy.solve_ivp()could be used to follow the direction of the flux surface to place the new points, by following a vector in the direction orthogonal to[eq.f_Z, -eq.f_R], whereeqis anEquilibriumobject.hypnotoad/hypnotoad/core/equilibrium.py
Lines 3975 to 3993 in 0365640
This would be similar to
hypnotoad/hypnotoad/core/mesh.py
Lines 3291 to 3449 in 0365640
but where that follows perpendicular to flux surfaces, this would follow along them.