Skip to content

More robust algorithm for finding and extending past walls #197

@johnomotani

Description

@johnomotani

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.

def temporaryExtend(
self, *, psi, extend_lower=0, extend_upper=0, ds_lower=None, ds_upper=None
):
)

Rather than just extrapolating from the existing grid point positions, 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 $\nabla \psi$, that is the vector [eq.f_Z, -eq.f_R], where eq is an Equilibrium object.

@Equilibrium.handleMultiLocationArray
def f_R(self, R, Z):
"""returns the R component of the vector Grad(psi)/|Grad(psi)|**2."""
R = numpy.clip(R, Rmin, Rmax)
Z = numpy.clip(Z, Zmin, Zmax)
dpsidR = self.psi_func(R, Z, dx=1, grid=False)
dpsidZ = self.psi_func(R, Z, dy=1, grid=False)
return dpsidR / (dpsidR**2 + dpsidZ**2)
self.f_R = f_R.__get__(self)
@Equilibrium.handleMultiLocationArray
def f_Z(self, R, Z):
"""returns the Z component of the vector Grad(psi)/|Grad(psi)|**2."""
R = numpy.clip(R, Rmin, Rmax)
Z = numpy.clip(Z, Zmin, Zmax)
dpsidR = self.psi_func(R, Z, dx=1, grid=False)
dpsidZ = self.psi_func(R, Z, dy=1, grid=False)
return dpsidZ / (dpsidR**2 + dpsidZ**2)

This would be similar to

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]

but where that follows perpendicular to flux surfaces, this would follow along them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions