gray/scripts/gray_visual.py

566 lines
20 KiB
Python
Raw Normal View History

'''
Script to quickly visualise both the inputs and output files of GRAY
Usage: python -m scripts.gray_visual -h
'''
from pathlib import Path
from matplotlib.collections import LineCollection
from matplotlib.contour import ContourSet
import argparse
import matplotlib.pyplot as plt
import numpy as np
import scripts.gray as gray
def cli_args() -> argparse.Namespace:
'''
Command line arguments
'''
parser = argparse.ArgumentParser(
prog='gray_visual',
description='visualises the results of a GRAY simulation')
parser.add_argument('inputs', type=Path,
help='filepath of the inputs directory '
'(containing gray.ini)')
parser.add_argument('outputs', type=Path,
help='filepath of the outputs directory')
parser.add_argument('--kind', '-k', default=['raytracing'], nargs='+',
choices=['raytracing', 'beam', 'profiles', 'inputs'],
help='which kind of information to visualise')
parser.add_argument('--outfile', '-o',
metavar='FILE', type=Path, default=None,
help='save the plot figure to FILE')
parser.add_argument('--interactive', '-i', action='store_true',
help='show the plot in an interactive window')
parser.add_argument('--legend',
action=argparse.BooleanOptionalAction, default=True,
help='whether to show plot legend')
return parser.parse_args()
def align_yaxis(*axes: [plt.Axes]):
'''
Aligns the origins of two axes
'''
extrema = np.array([ax.get_ylim() for ax in axes])
tops = extrema[:, 1] / (extrema[:, 1] - extrema[:, 0])
# Ensure that plots (intervals) are ordered bottom to top:
if tops[0] > tops[1]:
axes, extrema, tops = [a[::-1] for a in (axes, extrema, tops)]
# How much would the plot overflow if we kept current zoom levels?
tot_span = tops[1] + 1 - tops[0]
extrema[0, 1] = extrema[0, 0] + tot_span * (extrema[0, 1] - extrema[0, 0])
extrema[1, 0] = extrema[1, 1] + tot_span * (extrema[1, 0] - extrema[1, 1])
[axes[i].set_ylim(*extrema[i]) for i in range(2)]
def add_alt_axis(ax: plt.Axes, x1: np.array, x2: np.array,
label: str) -> plt.Axes:
'''
Adds an alternative x axis to the top of a plot.
x1: existing variable
x2: new variable
'''
from matplotlib.ticker import AutoMinorLocator
from scipy.interpolate import interp1d
ax.xaxis.set_minor_locator(AutoMinorLocator())
ax.grid()
f = interp1d(x1, x2, fill_value='extrapolate')
g = interp1d(x2, x1, fill_value='extrapolate')
alt = ax.secondary_xaxis('top', functions=(f, g))
alt.set_xlabel(label, labelpad=8)
alt.xaxis.set_minor_locator(AutoMinorLocator())
return alt
def plot_circle(ax: plt.Axes, radius: float, **args) -> plt.Circle:
'''
Plots a circle at the origin
'''
c = plt.Circle((0, 0), radius, linewidth=1.5,
fill=False, **args)
ax.add_patch(c)
ax.autoscale_view()
return c
def plot_line_color(ax: plt.Axes, x: np.array, y: np.array,
c: np.array, **args) -> LineCollection:
'''
Plots a line with variable color
'''
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lines = LineCollection(segments, **args)
lines.set_array(c)
ax.add_collection(lines)
ax.autoscale_view()
return lines
def plot_poloidal(inputs: Path, outputs: Path, ax: plt.Axes):
'''
Plots raytracing projected in the poloidal plane
'''
conf = gray.read_conf(inputs / 'gray.ini')
central_ray = gray.read_table(outputs / 'central-ray.4.txt')
ax.set_title('poloidal view', loc='right')
ax.set_xlabel('$R$ / m')
ax.set_ylabel('$z$ / m')
# load flux surfaces
surfaces = gray.read_table(outputs / 'flux-surfaces.71.txt')
surfaces = surfaces.reshape(-1, int(surfaces['i'].max()))
# plot plasma boundary
bound = np.argmin(1 - surfaces['ψ_n'][:, 0])
ax.plot(surfaces[bound]['R'], surfaces[bound]['z'],
c='xkcd:slate grey', label='plasma boundary')
# manual contourplot of ψ(R,z)
for i, surf in enumerate(surfaces[:bound]):
ax.plot(surf['R'], surf['z'], c='xkcd:grey',
label='flux surfaces' if i == 0 else '')
# label the rational surfaces
labels = ['q=2', 'q=3/2', 'q=1']
for label, surf in zip(labels, surfaces[1+bound:][::-1]):
line = np.vstack([surf['R'], surf['z']]).T
cont = ContourSet(ax, [1], [[line]], filled=False,
colors='xkcd:ocean blue',
linestyles='--')
ax.clabel(cont, [1], inline=True,
fontsize=10, fmt={1: label})
ax.plot(np.nan, np.nan, '--', color='xkcd:ocean blue',
label='rational surfaces')
# load limiter
limiter = gray.get_limiter(conf, inputs)
ax.plot(*limiter, c='xkcd:black', label='limiter')
# plot cold resonance curve
try:
resonance = gray.read_table(outputs / 'ec-resonance.70.txt')
for n in np.unique(resonance['n']):
harm = resonance['n'] == n
ax.plot(resonance['R'][harm], resonance['z'][harm],
c='xkcd:orange', alpha=n/resonance['n'].max(),
label=f"resonance $n={int(n)}$")
except FileNotFoundError:
pass
# central ray
plot_line_color(ax, central_ray['R'], central_ray['z'],
central_ray['α'] * np.exp(-central_ray['τ']),
cmap='plasma', label='rays')
# plot outer rays
try:
outer_rays = gray.read_table(outputs / 'outer-rays.33.txt')
for k in np.unique(outer_rays['k']):
ray = outer_rays[outer_rays['k'] == k]
plot_line_color(ax, ray['R'], ray['z'], ray['α']*np.exp(-ray['τ']),
cmap='plasma', alpha=0.3)
except FileNotFoundError:
pass
def plot_toroidal(inputs: Path, outputs: Path, ax: plt.Axes):
'''
Plots raytracing projected in the poloidal plane
'''
conf = gray.read_conf(inputs / 'gray.ini')
central_ray = gray.read_table(outputs / 'central-ray.4.txt')
ax.set_title('toroidal view', loc='right')
ax.set_xlabel('$x$ / m')
ax.set_ylabel('$y$ / m')
limiter = gray.get_limiter(conf, inputs)
# plot plasma boundary
surfaces = gray.read_table(outputs / 'flux-surfaces.71.txt')
boundary = surfaces[np.isclose(surfaces['ψ_n'], 1, 1e-3)]
# plot plasma boundary
plot_circle(ax, radius=boundary['R'].min(), color='xkcd:slate gray')
plot_circle(ax, radius=boundary['R'].max(), color='xkcd:slate gray')
# plot limiter
if limiter[0].size > 0:
Rmax = limiter[0].max() * 1.1
plot_circle(ax, radius=limiter[0].min(),
color='xkcd:black')
plot_circle(ax, radius=limiter[0].max(),
color='xkcd:black')
# set bounds
ax.set_xlim(-Rmax, Rmax)
ax.set_ylim(-Rmax, Rmax)
# plot cold resonance curve
try:
resonance = gray.read_table(outputs / 'ec-resonance.70.txt')
for n in np.unique(resonance['n']):
harm = resonance['n'] == n
plot_circle(ax, radius=resonance['R'][harm].max(),
color='xkcd:orange', alpha=n/resonance['n'].max())
except FileNotFoundError:
pass
# central ray
for rt in np.unique(central_ray['index_rt']):
ray = central_ray[central_ray['index_rt'] == rt]
x = ray['R'] * np.cos(np.radians(ray['φ']))
y = ray['R'] * np.sin(np.radians(ray['φ']))
dPds = ray['α'] * np.exp(-ray['τ'])
plot_line_color(ax, x, y, dPds, cmap='plasma')
# outer rays
try:
outer_rays = gray.read_table(outputs / 'outer-rays.33.txt')
for k in np.unique(outer_rays['k']):
for rt in np.unique(outer_rays['index_rt']):
ray = outer_rays[ (outer_rays['k'] == k)
& (outer_rays['index_rt'] == rt)]
dPds = ray['α'] * np.exp(-ray['τ'])
plot_line_color(ax, ray['x'], ray['y'], dPds,
cmap='plasma', alpha=0.3)
except FileNotFoundError:
pass
def plot_beam(outputs: Path, axes: [plt.Axes]):
'''
Plots the outermost rays in the local beam frame
'''
rays = gray.read_table(outputs / 'beam-shape.8.txt')
nrays = int(max(rays['k']))
cm = 0.01 # 1cm in m
cmap = plt.cm.viridis
# side view of rays, y(z)
axes['A'].set_title('rays, side view', loc='right')
axes['A'].set_ylabel('$y$ / cm')
# top view of rays, x(z)
axes['B'].set_title('rays, top view', loc='right')
axes['B'].set_ylabel('$x$ / cm')
axes['B'].set_xlabel('$z$ / m')
# 3D view
axes['C'].set_title('beam surface', loc='right')
axes['C'].set_xlabel('$z$ / m', labelpad=11)
axes['C'].set_ylabel('$x$ / cm', labelpad=-2)
axes['C'].set_zlabel('$y$ / cm', labelpad=-2)
axes['C'].set_box_aspect(aspect=(2.5, 1, 1), zoom=1.2)
axes['C'].view_init(azim=-44)
# plot the beam envelope
try:
size = gray.read_table(outputs / 'beam-size.12.txt')
for i in ['A', 'B']:
style = dict(c='xkcd:grey', ls=':', lw=1)
axes[i].plot(size['s']*cm, +size['r_max'], **style)
axes[i].plot(size['s']*cm, -size['r_max'], **style)
axes[i].plot(size['s']*cm, +size['r_min'], **style)
axes[i].plot(size['s']*cm, -size['r_min'], **style)
except FileNotFoundError:
pass
for k in np.arange(1, nrays+1):
ray = rays[rays['k'] == k]
color = cmap(k / max(rays['k']))
z = (ray['s'] + ray['z']) * cm
# top projections
axes['B'].plot(z, ray['x'], alpha=0.6, c=color, lw=1)
axes['C'].plot(z, ray['x'], zs=rays['y'].min(), zdir='z',
alpha=0.6, c=color, lw=1)
# side projections
axes['A'].plot(z, ray['y'], alpha=0.8, c=color, lw=1)
axes['C'].plot(z, ray['y'], zs=rays['x'].max(), zdir='y',
alpha=0.6, c=color, lw=1)
# adjust ticks spacing
plt.setp(axes['C'].get_yticklabels(), rotation=-25, ha='left')
axes['C'].tick_params(axis='y', pad=-5)
axes['C'].tick_params(axis='z', pad=0)
# wrap arrays into 2D meshes for plot_surface
k = rays['k'].reshape(-1, nrays)
x = rays['x'].reshape(-1, nrays)
y = rays['y'].reshape(-1, nrays)
z = (rays['s'] + rays['z']).reshape(-1, nrays) * cm
# make the xy slices closed
x = np.column_stack([x, x[:, 0]])
y = np.column_stack([y, y[:, 0]])
z = np.column_stack([z, z[:, 0]])
# plot beam surface, colored by ray indices
axes['C'].plot_surface(z, x, y, facecolors=cmap(k/nrays), alpha=0.6)
def plot_profiles(outputs: Path, axes: [plt.Axes]):
'''
Plots the power and current profiles
'''
profiles = gray.read_table(outputs / 'ec-profiles.48.txt')
first = profiles['index_rt'] == profiles['index_rt'].min()
ρ_p, ρ_t = profiles[first]['ρ_p'], profiles[first]['ρ_t']
for _, ax in axes.items():
# set ρ_t to the x axis
ax.set_xlabel('$ρ_t$')
ax.set_xlim(-0.05, 1.05)
ax.ticklabel_format(useMathText=True)
# add secondary x axis for ρ_p
add_alt_axis(ax, ρ_t, ρ_p, label='$ρ_p$')
cur_dens = axes['A']
cur_dens.set_title('current density', loc='right')
cur_dens.set_ylabel(r'$J_\text{CD} \;/\;$ kA/m³')
pow_dens = axes['B']
pow_dens.set_title('power density', loc='right')
pow_dens.set_ylabel(r'$dP/dV \;/\;$ MW/m³')
cur_ins = axes['C']
cur_ins.set_title('current within $ρ$', loc='right')
cur_ins.set_ylabel(r'$I_\text{CD,inside} \;/\;$ kA')
pow_ins = axes['D']
pow_ins.set_title('power within $ρ$', loc='right')
pow_ins.set_ylabel(r'$P_\text{inside} \;/\;$ MW')
styles = [':', '-', '--', '-.']
colors = dict(O='xkcd:orange', X='xkcd:ocean blue')
for i in np.unique(profiles['index_rt']):
mode, passes = gray.decode_index_rt(int(i))
style = dict(c=colors[mode], ls=styles[passes % 4])
slice = profiles[ (profiles['index_rt'] == i)
& (profiles['dPdV'] > 1e-15)]
cur_dens.plot(slice['ρ_p'], slice['J_cdb'], **style)
pow_dens.plot(slice['ρ_p'], slice['dPdV'], **style)
slice = profiles[ (profiles['index_rt'] == i)
& (profiles['P_inside'] > 1e-15)]
cur_ins.plot(slice['ρ_p'], slice['I_cd_inside'], **style)
pow_ins.plot(slice['ρ_p'], slice['P_inside'], **style)
# legend
plt.plot(np.nan, np.nan, label='O mode', c='xkcd:orange')
plt.plot(np.nan, np.nan, label='X mode', c='xkcd:ocean blue')
plt.plot(np.nan, np.nan, label='1° pass', c='xkcd:black', ls=styles[1])
plt.plot(np.nan, np.nan, label='2° pass', c='xkcd:black', ls=styles[2])
plt.plot(np.nan, np.nan, label='3° pass', c='xkcd:black', ls=styles[3])
plt.plot(np.nan, np.nan, label='4° pass', c='xkcd:black', ls=styles[0])
def plot_inputs(inputs: Path, outputs: Path, axes: [plt.Axes]):
'''
Plot the input plasma profiles and MHD equilibrium
'''
from matplotlib.colors import TwoSlopeNorm
maps = gray.read_table(outputs / 'inputs-maps.72.txt')
# filter valid points
maps = maps[maps['ψ_n'] > 0]
flux = maps['R'], maps['z'], maps['ψ_n']
# contour levels
inner_levels = np.linspace(0, 0.99, 8)
outer_levels = np.linspace(0.9991, maps['ψ_n'].max()*0.99, 6)
all_levels = np.concatenate([inner_levels, outer_levels])
# contour style
norm = TwoSlopeNorm(vmin=0, vcenter=1, vmax=maps['ψ_n'].max())
cmap = plt.cm.RdYlGn_r
borders = dict(colors='xkcd:grey', linestyles='-', linewidths=0.8)
# interpolated equilibrium
interp = axes['B']
interp.set_title('poloidal flux', loc='right')
interp.set_xlabel('$R$ / m')
interp.set_ylabel('$z$ / m')
interp.tricontourf(*flux, levels=all_levels, norm=norm, cmap=cmap)
interp.tricontour(*flux, levels=all_levels, **borders)
interp.tricontour(*flux, levels=[1], colors='xkcd:slate gray')
interp.plot(np.nan, np.nan, c='xkcd:slate gray', label='plasma boundary')
# add limiter
conf = gray.read_conf(inputs / 'gray.ini')
limiter = gray.get_limiter(conf, inputs)
interp.plot(*limiter, c='xkcd:black', label='limiter')
# original MHD equilibrium
orig = axes['A']
if conf['equilibrium'].get('iequil') != 'EQ_ANALYTICAL':
file = conf['equilibrium']['filenm'].strip('"')
eqdsk = gray.read_eqdsk(inputs / file)
orig.set_title('input G-EQDSK flux', loc='right')
orig.set_xlabel('$R$ / m')
orig.set_ylabel('$z$ / m')
orig.contourf(*eqdsk.flux, levels=inner_levels, norm=norm, cmap=cmap)
orig.contour(*eqdsk.flux, levels=inner_levels, **borders)
orig.plot(*eqdsk.limiter, c='xkcd:black')
orig.plot(*eqdsk.boundary, c='xkcd:slate gray')
else:
orig.axis('off')
# colorbar
bar = plt.colorbar(plt.cm.ScalarMappable(norm, cmap), cax=axes['C'])
bar.set_label(label='normalised poloidal flux', labelpad=10)
# Plasma radial profiles
profiles = gray.read_table(outputs / 'kinetic-profiles.55.txt')
axes['D'].set_title('plasma profiles', loc='right')
add_alt_axis(axes['D'], profiles['ρ_t'], profiles['ψ_n']**0.5,
label='$ρ_p$')
# density
dens = axes['D']
dens.set_xlabel('$ρ_t$')
dens.set_ylabel('$n_e$ / 10²⁰m⁻³', color='xkcd:ocean blue')
dens.plot(profiles['ρ_t'], profiles['n_e'], c='xkcd:ocean blue')
# temperature
temp = axes['D'].twinx()
temp.set_ylabel('$T_e$ / keV', color='xkcd:orange')
temp.plot(profiles['ρ_t'], profiles['T_e'], c='xkcd:orange')
# safety factor
inside = profiles['ρ_t'] < 1
saft = axes['D'].twinx()
saft.set_ylabel('$q$', color='xkcd:moss')
saft.plot(profiles['ρ_t'][inside], profiles['q'][inside], c='xkcd:moss')
saft.spines["right"].set_position(("axes", 1.11))
align_yaxis(dens, temp)
align_yaxis(temp, saft)
# original data points
if conf['profiles']['iprof'] == 'PROF_NUMERIC':
# load input profiles
orig = np.loadtxt(inputs / conf['profiles']['filenm'].strip('"'),
skiprows=1)
# covert 1st column to ρ_t
var = conf['profiles']['irho']
if var == 'RHO_TOR':
prof_ρ = orig[:, 0]
elif var == 'RHO_POL':
prof_ρ = np.interp(orig[:, 0], profiles['ψ_n']**0.5,
profiles['ρ_t'])
elif var == 'RHO_PSI':
prof_ρ = np.interp(orig[:, 0], profiles['ψ_n'], profiles['ρ_t'])
saft_ρ = np.interp(eqdsk.q[0], profiles['ψ_n']**0.5, profiles['ρ_t'])
# add to existing plot
style = dict(marker='o', s=2)
temp.scatter(prof_ρ, orig[:, 1], c='xkcd:orange', **style)
dens.scatter(prof_ρ, orig[:, 2], c='xkcd:ocean blue', **style)
saft.scatter(saft_ρ, eqdsk.q[1], c='xkcd:moss', **style)
# legend
axes['D'].plot(np.nan, np.nan, label='spline', c='k')
axes['D'].scatter(np.nan, np.nan, label='data', c='k', **style)
axes['D'].legend(loc='center left')
def main(kind: str, args: argparse.Namespace) -> (plt.Figure, [float]):
'''
Draws a single figure of a given kind
'''
cm = 0.3937 # 1cm in inches
rect = [0, 0, 1, 1] # default layout rect
if kind == 'raytracing':
fig, axes = plt.subplots(
1, 2, figsize=(19.8*cm, 11*cm),
tight_layout=True,
subplot_kw=dict(aspect='equal'))
plot_poloidal(args.inputs, args.outputs, axes[0])
plot_toroidal(args.inputs, args.outputs, axes[1])
if args.legend:
rect = [0.2, 0, 1, 1]
fig.tight_layout(rect=rect)
fig.legend(loc='upper left')
elif kind == 'beam':
fig, axes = plt.subplot_mosaic(
'ACD;BCD', figsize=(24*cm, 10*cm),
per_subplot_kw={'C': dict(projection='3d')},
width_ratios=[1, 1, 0.21])
# manually adjust spacing (tight_layout is failing)
axes['D'].axis('off')
plt.subplots_adjust(hspace=0.5)
plot_beam(args.outputs, axes)
elif kind == 'profiles':
fig, axes = plt.subplot_mosaic(
'AB;CD', figsize=(23.11*cm, 13*cm),
tight_layout=True)
plot_profiles(args.outputs, axes)
if args.legend:
rect = [0.12, 0, 1, 1]
fig.tight_layout(rect=rect)
fig.legend(loc='upper left')
elif kind == 'inputs':
fig, axes = plt.subplot_mosaic(
'ABC;DDD', figsize=(19.8*cm, 20*cm),
tight_layout=True,
height_ratios=[1.5, 1],
width_ratios=[1, 1, 0.05],
per_subplot_kw={'AB': dict(aspect='equal')})
plot_inputs(args.inputs, args.outputs, axes)
return fig, rect
if __name__ == '__main__':
args = cli_args()
def on_resize(event):
'''
Update layout on window resizes
'''
fig.tight_layout(rect=rect)
fig.canvas.draw()
if args.interactive:
plt.ion()
for kind in args.kind:
fig, rect = main(kind, args)
if args.interactive:
fig.canvas.mpl_connect('resize_event', on_resize)
if args.outfile is not None:
if len(args.kind) > 1:
# append plot kind to the output name
fname = args.outfile.with_stem(args.outfile.stem + '-' + kind)
else:
fname = args.outfile
fig.savefig(fname, bbox_inches='tight')
if args.interactive:
input('press enter to quit')