import numpy as np
from matplotlib.tri import LinearTriInterpolator
import matplotlib.colors as mcolors # << NEW import
from xgc_analysis.plane import Plane
[docs]
class PlaneData:
"""
Stores field data for a single plane.
For a scalar field, data is a 1D NumPy array of shape (n,).
For a vector field, data is a 2D NumPy array of shape (n, n_components), where for each vertex
the vector components are stored contiguously in memory.
Attributes:
- n: number of vertices in this plane.
- field_type: 'scalar' or 'vector'.
- n_components: number of components (1 for scalar, 2 or 3 for vector fields).
- dtype: data type for the array.
- data: the NumPy array storing the field data.
"""
def __init__(self, plane, data_array=None, n_components=1, dtype=np.float64):
self.plane = plane # geometry REFERENCE (!)
self.n = plane.n_n
self.n_components = n_components
self.dtype = dtype
if (n_components > 1):
self.field_type = 'vector'
else:
self.field_type = 'scalar'
# Pre‑allocate placeholders for flux‑surface operations so they always exist.
# Scalar → 1‑D arrays, vector → 2‑D with n_components columns.
nsurf = plane.nsurf
if n_components == 1:
self.flux_avg_psi = np.empty(nsurf, dtype=dtype)
self.flux_avg_plane = np.empty(self.n, dtype=dtype)
# `flux_avg_norm` only meaningful for vector data; skip.
else:
self.flux_avg_psi = np.empty((nsurf, n_components), dtype=dtype)
self.flux_avg_plane = np.empty((self.n, n_components), dtype=dtype)
self.flux_avg_norm_psi = np.empty(nsurf, dtype=dtype)
if (data_array is not None):
if (self.field_type == 'vector'):
if ((self.n != data_array.shape[0]) or (self.n_components != data_array.shape[1])):
raise ValueError("Field type is set to vector, but the dimensions of the input data are inconsistent")
elif (self.field_type == 'scalar'):
if (self.n != data_array.shape[0]):
raise ValueError("Field type is set to scalar, but the dimentions of the input data are inconsistent")
self.data = np.zeros((self.n,self.n_components),dtype=self.dtype)
if (data_array is not None):
self.data = data_array
[docs]
def get_data(self):
"""Returns the underlying NumPy array."""
return self.data
# --- Flux-surface averaging ---
[docs]
def flux_avg_to_surf(self):
"""Compute flux-surface-average and store it in `self.flux_avg_psi`."""
self.flux_avg_psi[...] = self.plane.flux_avg_to_surf(self.data)
[docs]
def flux_avg_from_surf(self):
"""Back‑project `flux_avg_psi` and store result in `self.flux_avg_plane`."""
self.flux_avg_plane[...] = self.plane.flux_avg_from_surf(self.flux_avg_psi)
[docs]
def flux_avg_to_from_surf(self):
"""Compute the flux-surface average of `self.data` and project back to the plane in `self.flux_avg_plane`."""
self.flux_avg_plane[...] = self.plane.flux_avg_to_from_surf(self.data)
[docs]
def flux_avg_to_surf_norm(self):
"""Compute flux-surface-average of the norm of a vector field and store it in `self.flux_avg_norm_psi`."""
if self.field_type != 'vector':
raise ValueError("flux_avg_to_surf_norm is only applicable for vector fields.")
self.flux_avg_norm_psi[...] = self.plane.flux_avg_to_surf_norm(self.data)
# --- Gradients ---
[docs]
def evaluate_scalar_gradients(self) -> "PlaneData":
"""
Compute ∇_{R,Z}f and return the result as a new PlaneData instance
(two-component vector on the same plane).
Returns
-------
PlaneData # field_type = 'vector'
"""
if getattr(self, "field_type", "scalar") != "scalar":
raise TypeError("Gradient evaluation is only defined for scalar PlaneData.")
grad_arr = self.plane.evaluate_scalar_gradients(self.data)
# New PlaneData on this plane, now holding a 2-component vector field
return PlaneData(
plane=self.plane,
data=grad_arr,
field_type="vector" # ← use whatever keyword your class expects
)
# --- Plotting ---
[docs]
def plot_contour(
self,
plane: Plane,
**kwargs,
):
"""
Convenience wrapper that forwards to Plane.plot_contour using this
instance’s data.
Any keyword arguments accepted by Plane.plot_contour can be supplied.
"""
plane.plot_contour(self.data, **kwargs)