Imported from CaMaLabs/M3DC1 (
unstructured/idl/python/AGENTS.md). Install upstream withnpx skills add CaMaLabs/M3DC1 --skill python. Copyright stays with the author.
AGENTS.md
Project Scope
- This repository ports IDL
.proroutines to Python under them3dc1module. - Keep Python file/function structure close to IDL call structure (1-to-1 where practical).
User Preferences (Persistent)
- Use Homebrew Python 3.14 for commands:
/opt/homebrew/bin/python3.14
- Implement module-style code only:
- No
mainfunction / no script-only entry requirement. - Must support
import m3dc1.<module>usage.
- No
- When IDL calls a subroutine/function, create a separate Python file for it and call it from other modules.
- Reuse existing Python implementations if already present (do not reimplement duplicates), e.g.
get_normalizations,convert_units. - For missing field names in HDF5 reads:
read_field.pyshould raiseKeyError.plot_field.pyshould raiseKeyErrorwhen underlying field is missing.
Plot/Return Behavior
plot_mesh.pyshould return(figure, axis).plot_field.pyshould return(figure, axis).plot_flux_average.pyshould return(figure, axis).- Support
isooption in plotting functions viaax.set_aspect('equal'). - Default
contourflevels should be100. - In
contour_and_legend.py, use:plasmacolormap if data is all positive or all negative.turbocolormap if data crosses zero.
String/Label Conventions
- Remove IDL text formatting escapes like
!X,!6in Python label strings. - Convert IDL-style math labels to LaTeX-like strings directly in-place (no extra wrapper function), using
$...$where needed. - Preserve intended subscript semantics (e.g.
I_{tot}), not flattened alternatives. - Apply similar label conversion for
read_scalar.py,read_field.py, and unit parsing labels.
Global Migration Rules
- Preserve IDL-style logging/print behavior across all migrated files.
- This rule applies to all future IDL-to-Python migration work in this repository unless explicitly overridden by the user.
read_field / eval_field Notes
eval_field.pyis currently Numba-based (njit+prange) for element-loop acceleration.- Worker control is environment-driven:
M3DC1_EVAL_WORKERScontrols requested threads.- Clamp to valid Numba thread range internally.
Performance Testing Conventions
- Prefer direct benchmark comparisons with fixed
points, reporting:- runtime for each worker setting
- speedup ratio
- max absolute difference between outputs
Geometry Mapping Notes (Persistent)
read_field.pynow implementsigeometry=1mapping behavior:- For primitive reads, when
igeometry > 0andlogical=False, it readsrst/zst, builds map indices withcreate_map, and remaps data withmap_field. logical=Truemust bypass geometry remapping.
- For primitive reads, when
- IDL subroutine split rule is required here:
- Keep mapping logic in separate modules:
create_map.pyandmap_field.py, called fromread_field.py.
- Keep mapping logic in separate modules:
create_map.pyimplementation requirement:- Keep the Numba-decorated implementation.
- It must still run when Numba is unavailable by using a no-op
njitfallback (same function body executes in Python mode).
map_field.pyimplementation requirement:- Use
scipy.interpolate.RegularGridInterpolator(not custom_interp2). - Use vectorized interpolation calls (e.g., interpolate all
(mx,my)points at once), not per-point Python-loop interpolation calls.
- Use
Session Decisions (Persistent)
eval_field.py:- Keep Numba thread chunksize fixed at
16viaset_parallel_chunksize(16). - Keep
debug_spinsupport active ineval_fieldand kernel path. - In the
debug_spinloop, call_eval_poly_numba(...)(current debug workload behavior).
- Keep Numba thread chunksize fixed at
read_field.py/plot_field.pyinterface updates:- Use
timeslicesargument name (notslices/time). read_field.pyno longer usesx,y,tpositional arguments.plot_field.pyno longer usesx,yarguments and usestimeslices.
- Use
- Multi-timeslice behavior:
read_field.pysupports multipletimeslicesincludingdiffbehavior.plot_field.pyshould pass multi-timeslicesthrough toread_field.py.
- Numba runtime/testing conventions used in this repo:
- For Numba tests in this environment, use:
source /etc/profilemodule load py-numba
- Common benchmark settings used in this session include larger
points(e.g.,1000/10000) and worker comparisons (workers=1vsworkers=4).
- For Numba tests in this environment, use:
- Plot mesh remap details:
- In
plot_mesh.py, when handlingrst_meta/zst_meta, fill masked points using 8-neighbor averaging. - Fill target condition is
mask[i, j] == 1(useout_maskfrom meta). - Neighbor inclusion condition is
mask[ii, jj] != 1(with finite-value check).
- In
plot_mesh.pyinterface update:- Add
phiargument and pass it toread_field("rst", ...)/read_field("zst", ...).
- Add
plot_field.pyinterface update:- Add explicit
logicalargument inplot_field(...). - Pass
logicalthrough toread_field(...). - Pass
logicalthrough to all internalplot_mesh(...)calls.
- Add explicit
- Magnetic probe plotting naming:
- Python entry is
plot_mag_probes(...)(plural), corresponding to IDLplot_mag_probes.proin the upper-level IDL directory. - It returns
(tdata, data)and wrapsplot_signals("mag_probes", ...).
- Python entry is
- Plot printing helpers:
plot_scalar.pysupportsprint=Trueto print the plotted 1D y-data.plot_flux_average.pysupportsprint=Trueto print the plotted 1D y-data.- Printed numeric output should use fixed-width aligned columns with 8 values per row and general-format precision equivalent to
"{value:12.6g}".
- Plot auto-ylim conventions:
plot_scalar.py,plot_flux_average.py,plot_signals.py, andplot_field_spectrum.pyuse 10% padding on both sides of the plotted y-range when auto-settingylim.- Add a horizontal line at
y=0when the padded y-limits cross zero.
plot_scalar.pyaxis conventions:- Public arguments use
xrangeandyrange. - If
xrangeis not set andxlogis false, the x-axis should start at0. - If
xrangeis set andyrangeis not, derive y-limits from the visible x-window only. - Use a tolerance of
1e-9when deciding whether data is effectively all positive or all negative.
- Public arguments use
flux_average.py/plot_flux_average.pyinterface updates:- Public argument name is
timeslices. - Accept
psi_norm,phi_norm, andrhoto select the x-axis coordinate. - In
plot_flux_average.py, force the x-axis to[0, 1]on linear scale. - In
plot_flux_average.py, use a tolerance of0.001for sign/near-zero decisions. plot_flux_average('q', timeslices)should build flux coordinates using the requestedtimeslicesvalue, not silently fall back to slice0.- For
eqsubtract=1files, flux-coordinate construction forplot_flux_average('q', timeslices)should use total fields at the requested slice, which meansread_fieldcombines the perturbation attimesliceswith equilibrium slice-1. flux_average.pyshould preloadpsifor flux-coordinate construction withequilibrium=Falseand passslice=int(timeslices or 0)intoflux_coordinates(...).
- Public argument name is
flux_average_field.pycorrectness fix:- Do not transpose the
field_at_point(...)output before flux-surface averaging.
- Do not transpose the
field_spectrum.py/read_field_spectrum.py/plot_field_spectrum.pyconventions:- Default to reading fields with
complex=Truefor spectrum calculations, with fallback to real data if the complex companion field is missing. read_field_spectrum.pydefaults topest=Trueif none ofpest,boozer,hamada, orfastis selected.field_spectrum.pyandread_field_spectrum.pysupportm_val;read_field_spectrum.pyalso acceptsm_valsas an alias.field_spectrum.pysupports x-axis selection viapsi_norm,phi_norm, andrho.plot_field_spectrum.pyshould use Matplotlib defaultaxes.prop_cyclecolors.- If
m_valis not provided toplot_field_spectrum.py, choose the 5mvalues with the largest maximum absolute amplitudes and plot those. - When auto-selecting
mvalues inplot_field_spectrum.py, ignoreNaNamplitudes and rank using finite amplitudes across both positive and negativem. - For each
q_targetinplot_field_spectrum.py, draw vertical resonance lines for all profile crossings, not just one interpolated crossing.
- Default to reading fields with
read_scalar.py/plot_scalar.pyinterface update:- Use public argument name
growthinstead ofgrowth_rate. - Keep
growth_rateonly as a backward-compatible alias when needed.
- Use public argument name
read_hmn.py/plot_hmn.pyconventions:- Keep harmonics reading and plotting split across two files:
read_hmn.pyfor data loading / metadata assemblyplot_hmn.pyfor plotting only
read_hmn.pyshould return raw array data or metadata viareturn_meta=True.plot_hmn.pyshould callread_hmn(...)rather than duplicating harmonics read logic.- Respect
cgsandmksin both functions, and pass them through toread_scalar("time", ...). plot_hmn.pyshould use Matplotlib defaultaxes.prop_cyclecolors.plot_hmn.pyx-axis should start at0by default and accept explicitxrange.- When
me=True, autoylimshould be computed from all components exceptn=0. - Auto
yliminplot_hmn.pyshould extend the upper bound by 10%. - When
growth=True, add a horizontal line aty=0. plot_hmn.pysupportsprint=<int>to print the selected harmonic componentnvalues with aligned formatting, 8 values per row.
- Keep harmonics reading and plotting split across two files:
contour_and_legend.pylevel handling:- In
contour_and_legend_single(...), if explicit contour level values are provided, expand that existing level span by 1% and regenerate the same number of levels over the expanded span. - If contour levels are not provided, or only a level count is provided, build levels from the panel min/max and expand that span by 1%.
contour_and_legend.pysupportsfill=True/False:fill=Truekeeps the filled contour behavior.fill=Falseshould draw contour lines only and skip the colorbar path.
- In
plot_field.pyinterface update:- Add
colorbarargument; whencolorbar=False, do not create a colorbar for the contour plot.
- Add
- LCFS helper behavior:
get_lcfs.pymust filter kwargs before forwarding toread_field(...)andread_lcfs(...), so plotting-only kwargs likelinesdo not break LCFS reads.
flux_average.py/flux_average_field.pyreturn-meta behavior:return_meta=Trueshould return structured Python objects, not bare tuples.flux_average.pyreturns an object withdata,title,symbol,units, andfc.flux_average_field.pyreturns an object withdata,flux,nflux,area,volume, andr0.
flux_coordinates.pytoroidal-flux normalization:- When building
phi_norm/rho, use the last finite toroidal-flux value instead of blindly usingphi[-1]. - Build
rhofrom a clipped nonnegative normalized toroidal flux to avoid all-NaNoutput when the last toroidal-flux point is invalid. - When
slice >= 0,flux_coordinates.pyshould readpsi,psi_r,psi_z, andIwithequilibrium=Falseso total-field reconstruction works correctly foreqsubtract=1. - When
slice < 0,flux_coordinates.pyshould still useequilibrium=Truefor equilibrium-only reads.
- When building
read_field.pyequilibrium handling:- For
equilibrium=Truewitheqsubtract=1, force reads to slice-1to avoid recursive total-field reconstruction loops. flux_coordinates.pyshould pass itssliceargument through tolcfs(...)so LCFS quantities are read from the matching time slice.
- For
read_field.pyequilibrium handling:- For
equilibrium=Truewitheqsubtract=1, force reads to slice-1to avoid recursive total-field reconstruction loops.
- For
lcfs.py/read_lcfs.pyslice behavior:lcfs.pyshould accept an explicitsliceargument and pass it directly toread_lcfs(...).read_lcfs.pyshould respect negative slices in Python-style form, e.g.slice=-1means the last available slice.read_lcfs.pyshould reproduce the IDL print behavior:slice time = ...time step time: ...time slice: ...
- Legend helper behavior:
plot_legend.pyshould not hardcode legend font size; it should follow the active Matplotlib rc settings.plot_legend.pycurrently usesframeon=False.
a2cc.f90migration notes:- Python port lives in
m3dc1/a2cc.pyand related EQDSK-A parsing logic lives in a separate module. - Preserve Fortran-style comments and split helper modules when the original Fortran calls another file.
- Map Fortran
write(*,...)to Pythonprint(...)on stdout. - Map Fortran
write(0,...)to Pythonprint(..., file=sys.stderr).
- Python port lives in