Source code for torx.equilibrium.io.kisslinger_io_m

"""
Read and write flux-surface files in the Kisslinger format.

Turns a PoincarePolygonSet (the ordered flux-surface polygons plus its
mean-area and rho labels) into the ``{prefix}_isurf_NNN.txt`` surface
files and the ``{prefix}_flux_labels.txt`` label file. This is the
SOL-capable output of the field-line-traced flux surfing workflow: it
uses the per-surface area label already stored on the polygon set (see
PoincarePolygonSet.mean_area) rather than interpolating rho from the
raw Poincare data. The companion ``read_kisslinger_flux_surfaces``
parses those files back into coordinate arrays and labels.
"""
from pathlib import Path
import numpy as np
from torx.geometry import PoincarePolygonSet
from torx.decorators import autodoc_function
from torx.units import Quantity

[docs] @autodoc_function def write_kisslinger_flux_surfaces(out_directory: str, prefix: str, polygon_set: PoincarePolygonSet, r0_cm: float | Quantity, z0_cm: float | Quantity=0.0, mtor: int=1, skip_first: bool=True) -> list[str]: """ Write Kisslinger-format flux-surface files from a PoincarePolygonSet. For each exported surface a ``{prefix}_isurf_NNN.txt`` file is written with the poloidal polygon at every toroidal plane, and a single ``{prefix}_flux_labels.txt`` file lists the unnormalized mean area and the normalized rho label of each surface. Polygon coordinates are assumed normalized to the axis radius and are scaled by ``r0_cm`` (with ``z0_cm`` added to Z) to obtain centimeters. Parameters ---------- out_directory : str Directory the files are written to. Created if it does not exist. prefix : str Output file-name prefix. polygon_set : PoincarePolygonSet The processed flux-surface polygons. Its ``mean_area`` and ``rho_data`` labels are written directly to the file. r0_cm : float or Quantity Axis major radius used to scale the normalized polygons. A bare float is assumed to already be in cm; a Quantity is converted to cm. z0_cm : float or Quantity, optional Vertical offset added to every Z coordinate. A bare float is assumed to already be in cm; a Quantity is converted to cm. mtor : int, optional Number of toroidal field periods, written into each surface header. skip_first : bool, optional Skip the first surface (the magnetic axis, a degenerate point), as the reference tool does. Returns ------- list of str The paths of the surface files written, in order. """ if isinstance(r0_cm, Quantity): r0_cm = r0_cm.to("cm").magnitude if isinstance(z0_cm, Quantity): z0_cm = z0_cm.to("cm").magnitude out_path = Path(out_directory) out_path.mkdir(parents=True, exist_ok=True) mean_area = np.asarray(polygon_set.mean_area.values, float) rho = np.asarray(polygon_set.rho_data.values, float) phi_deg = np.degrees(np.asarray(polygon_set.phi_array.values, float)) n_surfaces = polygon_set.n_surfaces n_planes = polygon_set.n_planes first = 1 if skip_first else 0 written = [] for s in range(first, n_surfaces): fpath = out_path / f"{prefix}_isurf_{s:03d}.txt" _write_surface_file(fpath, polygon_set, s, n_planes, phi_deg, mean_area[s], r0_cm, z0_cm, mtor) written.append(str(fpath)) label_path = out_path / f"{prefix}_flux_labels.txt" with label_path.open("w") as label_file: label_file.write(f"{n_surfaces - first}\n") for s in range(first, n_surfaces): label_file.write( f"{r0_cm**2 * mean_area[s]:.5e} {rho[s]:.5e}\n") return written
[docs] @autodoc_function def read_kisslinger_flux_surfaces(folder: str, prefix: str="") -> dict: """ Read Kisslinger-format flux-surface files back into arrays. Parses every ``{prefix}*isurf_*.txt`` file in ``folder`` (and the companion ``{prefix}_flux_labels.txt`` label file if present) into coordinate arrays and per-surface labels. Coordinates are returned in the units they were written in (centimeters). Parameters ---------- folder : str Directory containing the isurf text files. prefix : str, optional Only read files whose name starts with this prefix. Returns ------- dict Keys: ``paths`` (list of str, in order); ``phi_deg`` (n_planes array of toroidal angles in degrees); ``mtor`` (int, field periods); ``R`` and ``Z`` (each a list of (n_planes, n_points) arrays, one per surface, in cm); and ``mean_area`` / ``rho`` (n_surfaces arrays from the label file, or None when it is absent). Raises ------ FileNotFoundError If ``folder`` does not exist, or no matching surface files are found within it. """ folder_path = Path(folder) if not folder_path.is_dir(): raise FileNotFoundError(f"No such folder: {folder}") files = sorted(folder_path.glob(f"{prefix}*isurf_*.txt")) if not files: raise FileNotFoundError( f"No files matching {prefix}*isurf_*.txt in {folder}") phi_deg = None mtor = None r_surfaces = [] z_surfaces = [] for path in files: phi_deg, r, z, mtor = _read_surface_file(path) r_surfaces.append(r) z_surfaces.append(z) result = { "paths": [str(p) for p in files], "phi_deg": phi_deg, "mtor": mtor, "R": r_surfaces, "Z": z_surfaces, "mean_area": None, "rho": None, } label_path = folder_path / f"{prefix}_flux_labels.txt" if label_path.exists(): lines = label_path.read_text().splitlines() n_surfaces = int(lines[0]) labels = np.array([[float(v) for v in line.split()] for line in lines[1:1 + n_surfaces]]) result["mean_area"] = labels[:, 0] result["rho"] = labels[:, 1] return result
def _read_surface_file(path: Path) -> tuple: """Read one Kisslinger surface file into phi_deg, R, Z and mtor.""" lines = path.read_text().splitlines() header = lines[1].split() n_planes, n_points, mtor = int(header[0]), int(header[1]), int(header[2]) phi_deg = np.empty(n_planes) r = np.empty((n_planes, n_points)) z = np.empty((n_planes, n_points)) idx = 2 for ip in range(n_planes): phi_deg[ip] = float(lines[idx]) idx += 1 coords = np.array( [line.split() for line in lines[idx:idx + n_points]], float) r[ip] = coords[:, 0] z[ip] = coords[:, 1] idx += n_points return phi_deg, r, z, mtor def _write_surface_file(fpath: Path, polygon_set: PoincarePolygonSet, s: int, n_planes: int, phi_deg: np.ndarray, mean_area_s: float, r0_cm: float, z0_cm: float, mtor: int) -> None: """Write one surface's polygons across all planes to fpath.""" first_polygon = polygon_set.polygon_data.isel( dim_surf=s, dim_plane=0).item() n_points = len(first_polygon.x_points) descr = (f"Flux surface is={s:03d}, mean_flux_area ={mean_area_s:.5e}") with fpath.open("w") as surface_file: surface_file.write(descr + "\n") surface_file.write(f"{n_planes} {n_points} {mtor} " f"{0.0:.5e} {0.0:.5e}\n") for ip in range(n_planes): surface_file.write(f"{phi_deg[ip]:.5e} \n") polygon = polygon_set.polygon_data.isel( dim_surf=s, dim_plane=ip).item() r = r0_cm * np.asarray(polygon.x_points, float) z = r0_cm * np.asarray(polygon.y_points, float) + z0_cm for k in range(n_points): surface_file.write(f"{r[k]:.5e} {z[k]:.5e} \n")