Skip to content

feat(eqdsk): add R,Z to flux coordinate interpolation and Sphinx docs#254

Merged
krystophny merged 2 commits intomainfrom
feature/eqdsk-flux-interpolation
Jan 15, 2026
Merged

feat(eqdsk): add R,Z to flux coordinate interpolation and Sphinx docs#254
krystophny merged 2 commits intomainfrom
feature/eqdsk-flux-interpolation

Conversation

@krystophny
Copy link
Copy Markdown
Member

@krystophny krystophny commented Jan 15, 2026

User description

Summary

  • Add R,Z to flux coordinate conversion methods to eqdsk_file class in Python library
  • Set up Sphinx documentation infrastructure
  • Configure GitHub Pages deployment for documentation

New Features

Flux Coordinate Interpolation

The eqdsk_file class now supports converting arbitrary (R, Z) cylindrical coordinates to flux coordinates:

from libneo import eqdsk_file

eq = eqdsk_file('equilibrium.geqdsk')

# Convert measurement points to flux coordinates
R_data = np.array([1.7, 1.8, 1.9])
Z_data = np.array([0.0, 0.1, -0.1])
s_pol, theta = eq.rz_to_flux_coords(R_data, Z_data)

# s_pol is normalized poloidal flux (0 at axis, 1 at LCFS)
# theta is geometric poloidal angle

Individual methods are also available:

  • psi_at_rz(R, Z) - raw poloidal flux interpolation
  • spol_at_rz(R, Z) - normalized flux coordinate
  • theta_geometric_at_rz(R, Z) - geometric angle

Sphinx Documentation

  • New docs/ directory with Sphinx configuration
  • Documentation includes usage examples for plotting scalar data vs flux coordinates
  • API reference generated from docstrings

GitHub Pages

  • Docs deployed to root of GitHub Pages site
  • Test dashboard moved to /test/ subdirectory
  • Landing page links to both docs and test dashboard

Test plan

  • Unit tests for all new interpolation methods (14 tests)
  • All Python tests pass (52 passed, 9 skipped)
  • Sphinx documentation builds successfully
  • GitHub Actions workflow runs successfully after merge

PR Type

Enhancement, Documentation, Tests


Description

  • Add flux coordinate interpolation methods to eqdsk_file class

    • psi_at_rz(): interpolate poloidal flux at arbitrary (R, Z) points
    • spol_at_rz(): compute normalized poloidal flux (0 at axis, 1 at LCFS)
    • theta_geometric_at_rz(): compute geometric poloidal angle
    • rz_to_flux_coords(): combined conversion to (s_pol, theta) coordinates
  • Set up Sphinx documentation infrastructure with usage examples

    • Installation guide, EQDSK coordinate conversion tutorial, API reference
    • Automatic COCOS sign convention handling for different file sources
  • Add comprehensive test suite with 14 unit tests for interpolation methods

  • Configure GitHub Actions workflow to build and deploy docs to GitHub Pages

    • Docs deployed at root, test dashboard at /test/ subdirectory

Diagram Walkthrough

flowchart LR
  A["eqdsk_file class"] -->|"adds methods"| B["Flux coordinate<br/>interpolation"]
  B -->|"psi_at_rz"| C["Poloidal flux"]
  B -->|"spol_at_rz"| D["Normalized flux"]
  B -->|"theta_geometric_at_rz"| E["Poloidal angle"]
  B -->|"rz_to_flux_coords"| F["Combined conversion"]
  G["Sphinx docs"] -->|"includes"| H["Installation guide"]
  G -->|"includes"| I["EQDSK tutorial"]
  G -->|"includes"| J["API reference"]
  K["GitHub Actions"] -->|"builds"| G
  K -->|"deploys to"| L["GitHub Pages"]
  M["Test suite"] -->|"validates"| B
Loading

File Walkthrough

Relevant files
Enhancement
1 files
eqdsk.py
Add flux coordinate interpolation methods                               
+125/-1 
Tests
1 files
test_eqdsk_interpolation.py
Add comprehensive interpolation method tests                         
+137/-0 
Documentation
6 files
conf.py
Sphinx configuration for documentation                                     
+38/-0   
index.rst
Documentation landing page and navigation                               
+27/-0   
installation.rst
Installation instructions and requirements                             
+24/-0   
eqdsk.rst
EQDSK usage guide with coordinate conversion examples       
+143/-0 
api.rst
Auto-generated API reference documentation                             
+26/-0   
Makefile
Sphinx build automation makefile                                                 
+14/-0   
Configuration changes
1 files
main.yml
Add docs build and GitHub Pages deployment workflow           
+50/-3   
Dependencies
1 files
pyproject.toml
Add docs dependencies group                                                           
+5/-0     

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review bot commented Jan 15, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Supply chain risk

Description: The workflow installs documentation build dependencies directly from PyPI without pinning
exact versions (e.g., python -m pip install sphinx sphinx-rtd-theme scipy numpy), which
creates a realistic supply-chain risk where a compromised or malicious upstream release
could be pulled during CI runs.
main.yml [130-137]

Referred Code
- name: Install docs dependencies
  run: |
    python -m pip install --upgrade pip
    python -m pip install sphinx sphinx-rtd-theme scipy numpy

- name: Build docs
  run: |
    cd docs && sphinx-build -b html . _build/html
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing edge checks: The new interpolation/coordinate conversion methods do not validate mismatched R/Z shapes
(non-grid mode) or guard against a zero/near-zero denominator in spol_at_rz, so failures
may surface as opaque NumPy/SciPy exceptions or invalid values.

Referred Code
def psi_at_rz(self, R, Z, grid=False):
  """
  Interpolate poloidal flux psi at arbitrary (R, Z) coordinates.

  Parameters
  ----------
  R : float or array_like
      Major radius coordinate(s) in meters.
  Z : float or array_like
      Vertical coordinate(s) in meters.
  grid : bool, optional
      If True, evaluate on the grid formed by R and Z arrays.
      If False (default), evaluate at corresponding pairs (R[i], Z[i]).

  Returns
  -------
  psi : float or ndarray
      Poloidal flux value(s) at the given coordinates.
  """
  import numpy as np
  if self._psi_interpolator is None:


 ... (clipped 89 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
No input validation: The new public methods (psi_at_rz, spol_at_rz, theta_geometric_at_rz, rz_to_flux_coords)
accept arbitrary R/Z inputs without explicit validation (type/finite checks, shape
compatibility, bounds expectations), relying on downstream NumPy/SciPy behavior and
potentially producing nan/unexpected results for malformed or non-finite inputs.

Referred Code
def psi_at_rz(self, R, Z, grid=False):
  """
  Interpolate poloidal flux psi at arbitrary (R, Z) coordinates.

  Parameters
  ----------
  R : float or array_like
      Major radius coordinate(s) in meters.
  Z : float or array_like
      Vertical coordinate(s) in meters.
  grid : bool, optional
      If True, evaluate on the grid formed by R and Z arrays.
      If False (default), evaluate at corresponding pairs (R[i], Z[i]).

  Returns
  -------
  psi : float or ndarray
      Poloidal flux value(s) at the given coordinates.
  """
  import numpy as np
  if self._psi_interpolator is None:


 ... (clipped 89 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review bot commented Jan 15, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent division by zero in normalization

Add a check in spol_at_rz to prevent division by zero by ensuring _psi_at_edge
and _psi_at_axis are not equal before performing the normalization.

python/libneo/eqdsk.py [80]

-return (psi - self._psi_at_axis) / (self._psi_at_edge - self._psi_at_axis)
+denom = self._psi_at_edge - self._psi_at_axis
+if np.isclose(denom, 0):
+    raise ValueError("psi_edge and psi_axis are equal; cannot normalize flux.")
+return (psi - self._psi_at_axis) / denom
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential division-by-zero error and proposes adding a check to prevent it, which significantly improves the robustness of the code.

Medium
Ensure correct return type for scalars

To ensure the return type matches the input type, check if the original R and Z
inputs were scalars before converting a single-element array result back to a
scalar.

python/libneo/eqdsk.py [54-55]

-if result.size == 1:
+if np.isscalar(R) and np.isscalar(Z):
   result = result.item()
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion improves the robustness of the return type handling by explicitly checking if the inputs were scalars, making the API's behavior more predictable for different input types.

Low
General
Improve accuracy by using interpolation

Improve the accuracy of _psi_at_axis by using the _psi_interpolator at the exact
magnetic axis coordinates instead of using the value from the nearest grid
point.

python/libneo/eqdsk.py [14-16]

-i_r_axis = np.argmin(np.abs(self.R - self.Rpsi0))
-i_z_axis = np.argmin(np.abs(self.Z - self.Zpsi0))
-self._psi_at_axis = self.PsiVs[i_z_axis, i_r_axis]
+self._psi_at_axis = self._psi_interpolator((self.Zpsi0, self.Rpsi0)).item()
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that using the interpolator provides a more accurate value for psi at the magnetic axis, improving the precision of subsequent flux normalization calculations.

Medium
Improve test by checking against header

Modify test_psi_at_magnetic_axis_equals_psi_axis to assert that the interpolated
psi at the magnetic axis is close to eq.PsiaxisVs from the file header, not the
nearest grid point value.

test/python/test_eqdsk_interpolation.py [20-27]

 def test_psi_at_magnetic_axis_equals_psi_axis(self, eq):
     R_axis = eq.Rpsi0
     Z_axis = eq.Zpsi0
     psi = eq.psi_at_rz(R_axis, Z_axis)
-    i_r = np.argmin(np.abs(eq.R - R_axis))
-    i_z = np.argmin(np.abs(eq.Z - Z_axis))
-    psi_grid_at_axis = eq.PsiVs[i_z, i_r]
-    assert np.isclose(psi, psi_grid_at_axis, rtol=1e-2)
+    assert np.isclose(psi, eq.PsiaxisVs, rtol=1e-2)
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This is an excellent suggestion that improves the test's correctness by comparing the interpolated result against the ground truth value from the file header (PsiaxisVs) rather than an implementation detail.

Medium
Enable extrapolation for psi interpolation

Enable extrapolation in the RegularGridInterpolator by setting fill_value=None,
allowing it to estimate values outside the defined grid.

python/libneo/eqdsk.py [11-13]

 self._psi_interpolator = RegularGridInterpolator(
-    (self.Z, self.R), self.PsiVs, method='cubic', bounds_error=False
+    (self.Z, self.R),
+    self.PsiVs,
+    method='cubic',
+    bounds_error=False,
+    fill_value=None
 )
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies how to enable extrapolation, which can be a useful feature, though the default behavior of returning NaN for out-of-bounds points is also a safe and valid choice.

Low
  • Update

Add methods to eqdsk_file class for converting cylindrical (R, Z)
coordinates to flux coordinates (s_pol, theta):

- psi_at_rz: interpolate poloidal flux at arbitrary (R, Z) points
- spol_at_rz: compute normalized poloidal flux (0 at axis, 1 at LCFS)
- theta_geometric_at_rz: compute geometric poloidal angle
- rz_to_flux_coords: combined conversion to (s_pol, theta)

Uses scipy RegularGridInterpolator with cubic interpolation.
Automatically handles COCOS sign convention mismatches.

Also adds:
- Sphinx documentation with usage examples for plotting scalar data
  against flux coordinates
- GitHub Actions workflow to build and deploy docs to GitHub Pages
- Docs landing page at root, test dashboard at /test/
@krystophny krystophny force-pushed the feature/eqdsk-flux-interpolation branch from cb7d1ba to dd486d3 Compare January 15, 2026 10:05
@krystophny krystophny requested review from phizenz and troiav January 15, 2026 10:06
@krystophny
Copy link
Copy Markdown
Member Author

krystophny commented Jan 15, 2026

Visual Evidence: AUG 30835 @ 3200ms

Tested the flux coordinate interpolation with AUG shot 30835 at 3200ms.

AUG 30835 Flux Interpolation

Left: Flux surface contours (s_pol = 0 to 1) via spol_at_rz()

Middle: Same surfaces in rho_pol = sqrt(s_pol) - the commonly used radial coordinate

Right: Radial profiles along midplane (R = 1.0 to 2.6 m) comparing s_pol (blue) and rho_pol (green dashed)

Results:

s_pol at axis:   -0.0004 (expected: 0)
s_pol at LCFS:    1.0002 (expected: 1)
rho_pol at LCFS:  1.0001 (expected: 1)
Monotonicity:     True

@krystophny krystophny merged commit 882aeb6 into main Jan 15, 2026
4 checks passed
@krystophny krystophny deleted the feature/eqdsk-flux-interpolation branch January 15, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant