Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
69fbb2b
Add gk_distf command for building real distribution function from Jf …
Maxwell-Rosen Mar 13, 2026
11acdc3
Add support for mc2nu. This has been something I've wanted for a long…
Maxwell-Rosen Mar 13, 2026
d61df54
Add a source boolean to plot sources
Maxwell-Rosen Mar 13, 2026
3a2caeb
Update tool to have a version which is callable from python. Here is …
Maxwell-Rosen Mar 14, 2026
c35a7e2
Add grid type detection to GData context based on grid shape. This wa…
Maxwell-Rosen Mar 14, 2026
74b1570
Enhance gk_distf to support frame ranges and improve frame resolution…
Maxwell-Rosen Mar 14, 2026
1bf42c6
rename build_gk_distf to load_gk_distf, which is more in line with ho…
Maxwell-Rosen Mar 14, 2026
3144a14
Add multiblock implementation
Maxwell-Rosen Mar 14, 2026
81610a7
Update kwargs for some shortcuts
Maxwell-Rosen Mar 14, 2026
5c6e110
Refactor the file for organization. Match a few more conventions of t…
Maxwell-Rosen Mar 15, 2026
0311199
Enhance documentation and variable naming in gk_distf.py for clarity …
Maxwell-Rosen Mar 15, 2026
b52f285
Refactor file handling in gk_distf.py: streamline file resolution and…
Maxwell-Rosen Mar 15, 2026
e1d1ef3
Refactor gk_distf.py: remove redundant file parsing functions, stream…
Maxwell-Rosen Mar 15, 2026
6f825af
Refactor gk_distf.py: improve documentation by clarifying grid deform…
Maxwell-Rosen Mar 15, 2026
24200f5
Refactor gk_distf.py: remove unnecessary CLI command comment separato…
Maxwell-Rosen Mar 15, 2026
ecb53b0
Fix load_gk_distf function: reorder mc2nu_file assignment for consist…
Maxwell-Rosen Mar 15, 2026
d1520e6
Simplify non-uniform map
Maxwell-Rosen Mar 15, 2026
4a54724
Simplify the non-uniform mapping part of this workflow
Maxwell-Rosen Mar 16, 2026
42c0eb8
re-order the subtraction
Maxwell-Rosen Mar 16, 2026
e9d9630
Improve clarity in the cell centered to nodal operation. Remove tag i…
Maxwell-Rosen Mar 16, 2026
0763f71
Fix typo in comment for index initialization in _extract_values_along…
Maxwell-Rosen Mar 16, 2026
eda9e40
Add clarification comment for GData object in load_gk_distf function
Maxwell-Rosen Mar 16, 2026
efc49bc
Add two clarifying comments
Maxwell-Rosen Mar 16, 2026
96b97a8
Set a default for the non-uniform map. I didn't realize that this did…
Maxwell-Rosen Mar 16, 2026
ca5bc18
Add a comma separated list of frames to load
Maxwell-Rosen Mar 16, 2026
dbf77d8
Fix typo
Maxwell-Rosen Mar 16, 2026
7e950c3
Add comments to describe the LLM involvement in this PR
Maxwell-Rosen Mar 16, 2026
329a75d
Remove changes to gdata. Add a generalized suffix to the species so y…
Maxwell-Rosen Mar 26, 2026
5725479
Enhance load_gk_distf function with optional filename overrides and u…
Maxwell-Rosen Mar 26, 2026
e29c3f3
Add support for optional mapc2p file in load_gk_distf function
Maxwell-Rosen Mar 26, 2026
951ee6c
Remove unused import and clean up load_gk_distf function parameters
Maxwell-Rosen Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/postgkyl/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from postgkyl.commands.mhd import mhd
from postgkyl.commands.parrotate import parrotate
from postgkyl.commands.gk_energy_balance import gk_energy_balance
from postgkyl.commands.gk_distf import gk_distf
from postgkyl.commands.gk_particle_balance import gk_particle_balance
from postgkyl.commands.perprotate import perprotate
from postgkyl.commands.plot import plot
Expand Down
223 changes: 223 additions & 0 deletions src/postgkyl/commands/gk_distf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import os

import click
import numpy as np

from postgkyl.data import GData
from postgkyl.data import GInterpModal
from postgkyl.utils import verb_print


def _resolve_path(path: str, file_name: str) -> str:
if os.path.isabs(file_name):
return file_name
return os.path.join(path, file_name)


def _resolve_files(name: str, species: str, frame: int, path: str, source: bool = False):
if source:
frame_file = _resolve_path(path, f"{name}-{species}_source_{frame}.gkyl")
else:
frame_file = _resolve_path(path, f"{name}-{species}_{frame}.gkyl")
mapc2p_vel_file = _resolve_path(path, f"{name}-{species}_mapc2p_vel.gkyl")
mc2nu_file = _resolve_path(path, f"{name}-mc2nu_pos_deflated.gkyl")
jacobvel_file = _resolve_path(path, f"{name}-{species}_jacobvel.gkyl")
jacobtot_inv_file = _resolve_path(path, f"{name}-jacobtot_inv.gkyl")
return frame_file, mapc2p_vel_file, mc2nu_file, jacobvel_file, jacobtot_inv_file


def _resolve_mapping_kwargs(use_c2p_vel: bool, mapc2p_vel_file: str) -> dict:
if use_c2p_vel:
return {"mapc2p_vel_name": mapc2p_vel_file}
return {"mapc2p_vel_name": ""}


def _cell_centers_to_nodes(cell_centers: np.ndarray) -> np.ndarray:
nodes = np.zeros(cell_centers.size + 1, dtype=cell_centers.dtype)
nodes[1:-1] = 0.5 * (cell_centers[:-1] + cell_centers[1:])
nodes[0] = cell_centers[0] - (nodes[1] - cell_centers[0])
nodes[-1] = cell_centers[-1] + (cell_centers[-1] - nodes[-2])
return nodes


def _get_dim_split(out_grid: list, mc2nu_data: GData) -> tuple[int, int]:
total_dims = len(out_grid)
cdim = mc2nu_data.get_num_dims()
if cdim < 1 or cdim > total_dims:
raise ValueError(f"Invalid cdim={cdim} for total_dims={total_dims}")
vdim = total_dims - cdim
return cdim, vdim


def _extract_mapped_axis(mapped_values: np.ndarray, axis: int, cdim: int) -> np.ndarray:
if cdim == 1:
return np.asarray(mapped_values[..., axis]).reshape(-1)

# Extract one mapped coordinate direction at a reference point in the
# remaining configuration directions to construct a 1D axis grid.
idx = [0] * (cdim + 1)
idx[axis] = slice(None)
idx[-1] = axis
return np.asarray(mapped_values[tuple(idx)]).reshape(-1)


def _apply_mc2nu_grid(out_grid: list, mc2nu_file: str, debug: bool) -> tuple[list, int, int]:
mc2nu_data = GData(mc2nu_file)
cdim, vdim = _get_dim_split(out_grid, mc2nu_data)

mc2nu_interp = GInterpModal(mc2nu_data, 1, "ms")
_, mc2nu_values = mc2nu_interp.interpolate(tuple(range(cdim)))
mapped_values = np.asarray(mc2nu_values)

deformed_grid = list(out_grid)
for d in range(cdim):
mapped_cfg = _extract_mapped_axis(mapped_values, d, cdim)

old_cfg = np.asarray(out_grid[d])
if mapped_cfg.size == old_cfg.size:
new_cfg = mapped_cfg
elif mapped_cfg.size + 1 == old_cfg.size:
new_cfg = _cell_centers_to_nodes(mapped_cfg)
else:
raise ValueError(
"mc2nu mapping size is incompatible with configuration grid on axis "
f"{d}: {mapped_cfg.size} vs {old_cfg.size}"
)

deformed_grid[d] = new_cfg

if debug:
click.echo(f"gk_distf: cdim={cdim}, vdim={vdim}")
click.echo(f"gk_distf: mc2nu mapped {cdim} configuration axis/axes")

return deformed_grid, cdim, vdim


def _assert_files_exist(files: dict):
for kind, file_name in files.items():
if not os.path.exists(file_name):
raise FileNotFoundError(f"Missing {kind} file: {file_name}")


def _compute_fjx(jf_values: np.ndarray, jac_values: np.ndarray) -> np.ndarray:
if jf_values.shape == jac_values.shape:
return jf_values / jac_values

if jf_values.shape[:-1] != jac_values.shape[:-1]:
raise ValueError(
"Jf and jacobvel spatial shapes do not match: "
f"{jf_values.shape} vs {jac_values.shape}"
)

if jac_values.shape[-1] != 1:
raise ValueError(
"jacobvel component axis must be 1 or match Jf: "
f"{jf_values.shape} vs {jac_values.shape}"
)

return jf_values / jac_values


def _broadcast_multiply(distf: np.ndarray, jacobtot_inv: np.ndarray) -> np.ndarray:
if jacobtot_inv.ndim > distf.ndim:
raise ValueError(
"jacobtot_inv has more dimensions than distribution function: "
f"{jacobtot_inv.ndim} > {distf.ndim}"
)

expected_prefix = distf.shape[:jacobtot_inv.ndim]
if expected_prefix != jacobtot_inv.shape:
raise ValueError(
"Cannot broadcast jacobtot_inv over distribution function. "
f"Expected prefix {expected_prefix}, got {jacobtot_inv.shape}"
)

jacob_shape = jacobtot_inv.shape + (1,) * (distf.ndim - jacobtot_inv.ndim)
return distf * jacobtot_inv.reshape(jacob_shape)


def _interpolate_fjx_and_jacob(fjx_data: GData, jacobtot_inv_data: GData):
fjx_interp = GInterpModal(fjx_data, 1, "gkhyb")
jacob_interp = GInterpModal(jacobtot_inv_data, 1, "ms")

grid, fjx_values = fjx_interp.interpolate()
_, jacob_values = jacob_interp.interpolate()

fjx_values = np.squeeze(fjx_values)
jacob_values = np.squeeze(jacob_values)
return grid, fjx_values, jacob_values


@click.command()
@click.option("--name", "-n", required=True, type=click.STRING,
help="Simulation name prefix (e.g. gk_lorentzian_mirror).")
@click.option("--species", "-s", required=True, type=click.STRING,
help="Species name (e.g. ion or elc).")
@click.option("--frame", "-f", required=True, type=click.INT,
help="Frame number.")
@click.option("--source", is_flag=True,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't like this. It adds an argument for a very specific file from one of many other modules in Gkeyll. What about a Maxwellian from a BGK collision op? what about the heating term from our heating module? etc

In general this command should work with any phase-space quantity, with option to divide by J_x, J_v and/or B, or not.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could specify something like --suffix source instead of --source, and that would append the suffix of the file that is loaded. We can do this if it were more general.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No extra flag is needed. I think it should be possible to do
pgkyl gk-distf <sim_name>-<species>_<frame>.gkyl
or
pgkyl gk-distf <sim_name>-<species>_source_<frame>.gkyl
without passing the optional arguments -n or -s. Likewise, --c2p, --c2p-vel, --mc2nu (and possibly --jacobx, --jacobv and --jacobgyro (the latter for B)) should be optional and possibly take the file name corresponding to these quantities. If the file is given, it uses that. If the flag is given without a file, it uses the hardcoded names. If the flag is not given, it doesn't do that transformation/scaling.

help="Use <name>-<species>_source_<frame>.gkyl as the input distribution.")
@click.option("--path", "-p", default="./", type=click.STRING,
help="Path to simulation data.")
@click.option("--tag", "-t", default="df", type=click.STRING,
help="Tag for output dataset.")
@click.option("--c2p-vel/--no-c2p-vel", default=False,
help="Use <name>-<species>_mapc2p_vel.gkyl when loading Jf.")
@click.option("--mc2nu", is_flag=True,
help="Use <name>-mc2nu_pos_deflated.gkyl to deform configuration-space grid.")
@click.option("--debug", is_flag=True,
help="Print resolved file names and shape diagnostics.")
@click.pass_context
def gk_distf(ctx, **kwargs):
"""Gyrokinetics: build real distribution function from saved Jf data."""
verb_print(ctx, "Starting gk_distf")
data = ctx.obj["data"]

files = {}
jf_file, mapc2p_vel_file, mc2nu_file, jacobvel_file, jacobtot_inv_file = _resolve_files(
kwargs["name"], kwargs["species"], kwargs["frame"], kwargs["path"], kwargs["source"])
files["Jf"] = jf_file
files["jacobvel"] = jacobvel_file
files["jacobtot_inv"] = jacobtot_inv_file

if kwargs["c2p_vel"]:
files["mapc2p_vel"] = mapc2p_vel_file
if kwargs["mc2nu"]:
files["mc2nu"] = mc2nu_file

_assert_files_exist(files)

map_kwargs = _resolve_mapping_kwargs(kwargs["c2p_vel"], mapc2p_vel_file)

if kwargs["debug"]:
click.echo(f"gk_distf: Jf={jf_file}")
click.echo(f"gk_distf: jacobvel={jacobvel_file}")
click.echo(f"gk_distf: jacobtot_inv={jacobtot_inv_file}")
if kwargs["c2p_vel"]:
click.echo(f"gk_distf: mapc2p_vel={mapc2p_vel_file}")
if kwargs["mc2nu"]:
click.echo(f"gk_distf: mc2nu={mc2nu_file}")

jf_data = GData(jf_file, mapc2p_vel_name=map_kwargs["mapc2p_vel_name"])
jacobvel_data = GData(jacobvel_file)
jacobtot_inv_data = GData(jacobtot_inv_file)

fjx_data = GData(tag=kwargs["tag"], ctx=jf_data.ctx)
fjx_values = _compute_fjx(jf_data.get_values(), jacobvel_data.get_values())
fjx_data.push(jf_data.get_grid(), fjx_values)

out_grid, fjx_interp, jacob_interp = _interpolate_fjx_and_jacob(fjx_data, jacobtot_inv_data)
distf_values = _broadcast_multiply(fjx_interp, jacob_interp)

if kwargs["mc2nu"]:
out_grid, cdim, vdim = _apply_mc2nu_grid(out_grid, mc2nu_file, kwargs["debug"])
jf_data.ctx["grid_type"] = "mc2nu"

if kwargs["debug"]:
click.echo(f"gk_distf: output shape={distf_values.shape}")

out = GData(tag=kwargs["tag"], ctx=jf_data.ctx)
out.push(out_grid, np.asarray(distf_values)[..., np.newaxis])
data.add(out)

verb_print(ctx, "Finishing gk_distf")
1 change: 1 addition & 0 deletions src/postgkyl/pgkyl.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def cli(ctx, **kwargs):
cli.add_command(cmd.extractinput)
cli.add_command(cmd.fft)
cli.add_command(cmd.gk_nodes)
cli.add_command(cmd.gk_distf)
cli.add_command(cmd.grid)
cli.add_command(cmd.growth)
cli.add_command(cmd.info)
Expand Down
Loading