Skip to content

InnerLayer.SLAYER - FEATURE! - Add perpendicular Prandtl number options and an ion heat diffusivity profile - #501

Open
ebursch wants to merge 8 commits into
developfrom
feature/slayer-pperp-models
Open

ebursch wants to merge 8 commits into
developfrom
feature/slayer-pperp-models

Conversation

@ebursch

@ebursch ebursch commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: none at the default P_perp_model = "chi_perp_e" (harness @ d5937d0)
  • Migration: rename chi_perp to chi_perp_e in [SLAYER] of gpec.toml (and the chi_perp keyword of build_slayer_inputs / slayer_parameters).

SLAYER can now build the perpendicular Prandtl number P_⊥ in several ways, selected with the new [SLAYER] P_perp_model option. The kinetic HDF5 schema gains an optional chi_i (ion perpendicular heat diffusivity) profile, and the DIII-D-like example kinetic file now carries one.

P_perp_model P_⊥ Reference
chi_perp_e (default, unchanged behaviour) τ_R χ⊥,e / r_s² —
chi_perp_i τ_R χ⊥,i / r_s² —
P_phi P_⊥ = P_φ Fitzpatrick, Phys. Plasmas 30, 092512 (2023), Eq. 143
D_perp τ_R D⊥ / r_s², with D⊥ built from c_β, η, χ⊥,e, χ⊥,i and the n, T gradients Fitzpatrick, Phys. Plasmas 29, 032507 (2022), Eq. 16
c_beta P_⊥ = c_β² (the χ⊥ → 0 limit of D_perp; a floor on P_⊥) J.-K. Park, Phys. Plasmas 29, 072506 (2022), Eq. 10 with the thermal-conduction term K dropped
tau_E P_⊥ = P_φ = τ_R/τ_E (needs tau_E [s]) Fitzpatrick, Phys. Plasmas 30, 092512 (2023), Eq. 131

chi_perp_e still sets the critical-Δ dc_tmp in every mode.

This PR also corrects the Park citation in the SLAYER file headers (SLAYER.jl, Riccati.jl, LayerThickness.jl, LayerParameters.jl). They cited "Park et al., Phys. Plasmas 29, 122505 (2022)", which does not exist; the paper is J.-K. Park, Phys. Plasmas 29, 072506 (2022), doi:10.1063/5.0093079, as docs/src/citations.md already lists.

Regression report

regress --cases diiid_slayer_n1,diiid_n1 --refs 0e68a0553,d5937d04e --force on feynman (SLURM, develop vs head after the develop merge, both fresh, manifest pinned).

Regression Report: diiid_slayer_n1
=========================================================================
Ref 1: 0e68a0553  @ 0e68a0553 (2026-10-05)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 112 threads/8 BLAS
Ref 2: d5937d04e  @ d5937d04e (2026-10-05)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 112 threads/8 BLAS
-------------------------------------------------------------------------
Quantity                            0e68a0553  d5937d04e  Diff     Status
-------------------------------------------------------------------------
SLAYER surface indices              [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER poloidal m                   [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER toroidal n                   [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER minor radius rs              [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER r-based shear                [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER Lundquist S                  [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER D_norm                       [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER P_perp                       [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER tauk                         [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER iota_e                       [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER Q_e                          [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER Q_i                          [6 elem]   [6 elem]   0.0e+00  OK    
SLAYER Q_root [2/1,3/1,4/1]         [3 elem]   [3 elem]   5.2e-10  OK    
SLAYER ω_Hz [2/1,3/1,4/1]           [3 elem]   [3 elem]   2.7e-07  OK    
SLAYER γ_Hz [2/1,3/1,4/1]           [3 elem]   [3 elem]   1.3e-05  OK    
SLAYER no_root flags [2/1,3/1,4/1]  [3 elem]   [3 elem]   0.0e+00  OK    
SLAYER enabled flag                 1          1          0.0e+00  OK    
Runtime (s)                         268.8s     269.7s              --    
=========================================================================
Summary: 17 unchanged


================================================================
Case: diiid_n1 — DIII-D-like equilibrium, n=1, ideal + perturbed equilibrium
================================================================

Regression Report: diiid_n1
=================================================================================================
Ref 1: 0e68a0553  @ 0e68a0553 (2026-10-05)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 112 threads/8 BLAS
Ref 2: d5937d04e  @ d5937d04e (2026-10-05)
       env: julia 1.11.7, x86_64-linux-gnu, manifest f1e0eb38 (pinned), 112 threads/8 BLAS
-------------------------------------------------------------------------------------------------
Quantity                                      0e68a0553        d5937d04e        Diff       Status
-------------------------------------------------------------------------------------------------
total energy Re(et[1])                        8.012318e-01     8.012318e-01     0.0e+00    OK    
total energy Im(et[1])                        4.142530e-05     4.142530e-05     0.0e+00    OK    
plasma energy Re(ep[1])                       -1.348486e+00    -1.348486e+00    0.0e+00    OK    
vacuum energy Re(ev[1])                       2.149718e+00     2.149718e+00     0.0e+00    OK    
vacuum matrix min eigenvalue                  1.873976e-01     1.873976e-01     0.0e+00    OK    
plasma energy (all)                           [35 elem]        [35 elem]        0.0e+00    OK    
vacuum energy (all)                           [35 elem]        [35 elem]        0.0e+00    OK    
total energy (all)                            [35 elem]        [35 elem]        0.0e+00    OK    
ODE steps (saved)                             1429             1429             0.0e+00    OK    
ODE steps (total)                             1424             1424             0.0e+00    OK    
q0                                            1.204212e+00     1.204212e+00     0.0e+00    OK    
q95                                           4.781723e+00     4.781723e+00     0.0e+00    OK    
beta_t                                        1.327024e-02     1.327024e-02     0.0e+00    OK    
beta_n                                        1.372511e+00     1.372511e+00     0.0e+00    OK    
internal inductance li1                       8.842392e-01     8.842392e-01     0.0e+00    OK    
internal inductance li2                       7.080847e-01     7.080847e-01     0.0e+00    OK    
internal inductance li3                       7.304433e-01     7.304433e-01     0.0e+00    OK    
poloidal beta betap1                          6.680744e-01     6.680744e-01     0.0e+00    OK    
poloidal beta betap2                          5.349834e-01     5.349834e-01     0.0e+00    OK    
poloidal beta betap3                          5.518761e-01     5.518761e-01     0.0e+00    OK    
# singular surfaces                           5                5                0.0e+00    OK    
singular psi locations                        [5 elem]         [5 elem]         0.0e+00    OK    
singular q values                             [5 elem]         [5 elem]         0.0e+00    OK    
current beta betaj                            4.236478e-01     4.236478e-01     0.0e+00    OK    
plasma volume                                 1.829472e+01     1.829472e+01     0.0e+00    OK    
plasma current                                1.152130e+00     1.152130e+00     0.0e+00    OK    
mpert                                         35               35               0.0e+00    OK    
npert                                         1                1                0.0e+00    OK    
toroidal field bt0                            2.006573e+00     2.006573e+00     0.0e+00    OK    
wall field bwall                              3.880145e-01     3.880145e-01     0.0e+00    OK    
aspect ratio                                  2.845746e+00     2.845746e+00     0.0e+00    OK    
elongation kappa                              1.708350e+00     1.708350e+00     0.0e+00    OK    
q profile (checksum)                          0cd285cea88d...  0cd285cea88d...  identical  OK    
pressure profile (checksum)                   a1c48b266622...  a1c48b266622...  identical  OK    
Mercier D_I profile (checksum)                eeb06744e795...  eeb06744e795...  identical  OK    
resistive interchange D_R profile (checksum)  fa37296851f6...  fa37296851f6...  identical  OK    
ballooning Delta' profile (checksum)          6eb0ea075ccf...  6eb0ea075ccf...  identical  OK    
island half-widths                            [5 elem]         [5 elem]         0.0e+00    OK    
Chirikov parameter                            [5 elem]         [5 elem]         0.0e+00    OK    
||resonant area-weighted field||              5.189042e-04     5.189042e-04     0.0e+00    OK    
dominant-coupling singular values             [3 elem]         [3 elem]         0.0e+00    OK    
|forcing overlap with dominant mode|          1.415706e-04     1.415706e-04     0.0e+00    OK    
|delta_nominal| of coil set 1                 7.055344e-05     7.055344e-05     0.0e+00    OK    
||ddelta/d(shift)|| over coil sets            1.246238e-04     1.246238e-04     0.0e+00    OK    
||ddelta/d(tilt)|| over coil sets             4.180652e-07     4.180652e-07     0.0e+00    OK    
PE plasma energy                              3.422677e+00     3.422677e+00     0.0e+00    OK    
PE vacuum energy                              3.174510e+00     3.174510e+00     0.0e+00    OK    
PE surface energy                             5.826684e+00     5.826684e+00     0.0e+00    OK    
PE toroidal torque                            5.087465e-02     5.087465e-02     0.0e+00    OK    
NTV torque FGAR [N·m]                         5.296746e-01     5.296746e-01     0.0e+00    OK    
NTV kinetic energy dW FGAR [J]                7.924988e-02     7.924988e-02     0.0e+00    OK    
Runtime (s)                                   281.1s           285.0s                      --    
||forcing b~|| (root-area-weighted)           4.683328e-04     4.683328e-04     0.0e+00    OK    
resonant area-weighted field b^r              [5 elem]         [5 elem]         0.0e+00    OK    
=================================================================================================
Summary: 53 unchanged

Every quantity is unchanged in both cases: the default P_perp_model = "chi_perp_e" reproduces develop, and the new chi_i dataset is not read by the default model.

Notes for reviewers


🤖 Generated with Claude Code

ebursch and others added 5 commits October 5, 2026 10:28
… kinetic HDF5

Add an optional chi_i (ion perpendicular heat diffusivity) dataset to the
kinetic-profile reader and writer, alongside chi_e and chi_phi.

Ship chi_i in the DIII-D-like example kinetic file, built like its chi_e:
AOT CHI_I of DIII-D 147131 at 2300 ms, mapped rho_N -> psi_N with EFIT01
RHOVN, the spurious zero at psi_N = 1 dropped, Gaussian-smoothed with
sigma = 12 of 201 rho_N points (the width that best reproduces the
example chi_e from AOT CHI_E). Range 0.46-2.1 m^2/s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…i_perp to chi_perp_e

New [SLAYER] P_perp_model selects how the perpendicular Prandtl number is built:
- chi_perp_e (default, unchanged numbers): tau_R*chi_perp_e/r_s^2
- chi_perp_i: tau_R*chi_perp_i/r_s^2 (kinetic-file chi_i, scalar fallback)
- P_phi: P_perp = P_tor (Fitzpatrick, Phys. Plasmas 30, 092512 (2023), Eq. 143)
- D_perp: tau_perp = r_s^2/D_perp (Fitzpatrick, Phys. Plasmas 29, 032507 (2022))
- c_beta: C^2 = P_perp with C = c_beta (Park et al., Phys. Plasmas 29, 122505 (2022))

Breaking: the [SLAYER] chi_perp key is renamed chi_perp_e. chi_perp_e still sets
the critical-Delta offset in every mode. Regression harness diiid_slayer_n1
develop vs local: 17 unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…= tau_R/tau_E)

A single [SLAYER] tau_E [s] for the whole plasma sets P_perp = P_tor = tau_R/tau_E
at every surface (Fitzpatrick, Phys. Plasmas 30, 092512 (2023), Eq. 131).
validate() requires a positive tau_E when this model is selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…. 16

P_perp_model = D_perp now builds D_perp per surface from Eq. 16 of Fitzpatrick,
Phys. Plasmas 29, 032507 (2022), with eta_perp = eta_par:
  D_perp = c_beta^2 eta/mu0 + (2/3)(1 - c_beta^2)[eta_e tau_e/(1+eta_e) chi_perp_e
                                                 + eta_i tau_i/(1+eta_i) chi_perp_i]
using the n, T_e, T_i log-gradients at the surface (Eqs. 4-6, written flat-density
safe). The scalar D_perp input is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e DIII-D-like SLAYER example

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added changed-results Results move or an interface breaks - read before upgrading feature New capability labels Oct 5, 2026
@ebursch ebursch self-assigned this Oct 5, 2026
@ebursch

ebursch commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Effect of each P_perp_model on the DIII-D-like SLAYER example

Setup: examples/DIIID-like_SLAYER_example at 27d0744, run once per model with only P_perp_model changed. The run is uncoupled, uses AMR and is validity-gated. χ⊥,e, χ⊥,i and χ_φ come from the chi_e, chi_i and chi_phi profiles in TkMkr_D3Dlike_Hmode_kinetic.h5. For tau_E I used τ_E = 0.1 s, a typical DIII-D H-mode value, which is not derived from this equilibrium. All three surfaces found a root in every run.

P_perp_model P_⊥ (2/1, 3/1, 4/1) P_φ (2/1, 3/1, 4/1) γ [Hz] (2/1, 3/1, 4/1) Δγ vs default (2/1, 3/1, 4/1) ω [Hz] 2/1
chi_perp_e 50.3, 44, 35.7 34.7, 24.7, 19 192, -251, -1012 — -10527
chi_perp_i 29.9, 26.4, 12.5 34.7, 24.7, 19 213, -274, -1217 +11 %, -9 %, -20 % -10523
P_phi 34.7, 24.7, 19 34.7, 24.7, 19 206, -277, -1123 +8 %, -10 %, -11 % -10525
D_perp 16.4, 17.5, 10.9 34.7, 24.7, 19 242, -296, -1249 +26 %, -18 %, -23 % -10516
c_beta 0.0133, 0.0088, 0.00645 34.7, 24.7, 19 313, -588, -1669 +63 %, -134 %, -65 % -10133
tau_E 34.5, 30.8, 27.6 34.5, 30.8, 27.6 206, -264, -1034 +8 %, -5 %, -2 % -10525

The Δγ column is (γ − γ_default)/|γ_default|, so + means more unstable. For scale, the run-to-run AMR noise on γ is about 0.01 %.

Takeaways

  • Only c_beta changes the answer qualitatively. Its P_⊥ = c_β² ≈ 0.01, three to four orders of magnitude below every other model. The 2/1 drive rises about 60 %, the stable 3/1 and 4/1 roots damp 65–135 % harder, and ω on the 2/1 shifts about 4 %. Every other model shifts ω by less than 0.1 %.
  • The χ-based models move γ by roughly 10–25 %.
    • chi_perp_i uses the example's χ⊥,i, which is about 0.4–0.6 of χ⊥,e at these surfaces. That halves P_⊥ and moves γ by about 10 % at the 2/1 and 3/1 and 20 % at the 4/1.
    • D_perp gives the smallest P_⊥ of the transport-based models, so it moves γ the furthest, by 18–26 %.
    • Across these models, a lower P_⊥ makes the 2/1 more unstable and the 3/1 and 4/1 more stable.
  • P_phi and tau_E land close together. With τ_E = 0.1 s, τ_R/τ_E happens to be close to the χ_φ-based P_φ on the 2/1. tau_E also overrides P_φ, so its 3/1 and 4/1 sit nearer the default. γ scales with the chosen τ_E, so these numbers only illustrate the model.
  • chi_perp_e (the default) reproduces develop exactly; see the regression report in the PR description.

The unit tests (Pkg.test() at 27d0744 on feynman) pass.


🤖 Generated with Claude Code

@ebursch

ebursch commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

Example kinetic-file χ profiles vs the DIII-D 147131 AOT source

chi profiles: example .h5 vs AOT

Each panel compares TkMkr_D3Dlike_Hmode_kinetic.h5 (red dashed) with the AOT profile it was built from (blue): DIII-D 147131 at 2300 ms, mapped ρ_N → ψ_N with EFIT01 RHOVN. The grey lines mark the 2/1, 3/1 and 4/1 surfaces used in the SLAYER example. The bottom-right panel overlays the three .h5 profiles on one axis.

Dataset Status Source
chi_e (χ⊥,e) existing, unchanged AOT CHI_E, smoothed
chi_phi (χ_φ) existing, unchanged AOT ANG_MOM_DIFF, smoothed
chi_i (χ⊥,i) added in this PR AOT CHI_I; spurious zero at ψ_N = 1 dropped; Gaussian-smoothed with σ = 12 of 201 ρ_N points

At the SLAYER surfaces (new chi_i):

Surface ψ_N chi_i (.h5) AOT CHI_I .h5 vs AOT chi_i / chi_e
2/1 0.518 1.13 1.06 +7 % 0.59
3/1 0.770 1.80 2.10 −15 % 0.60
4/1 0.893 1.15 0.82 +39 % 0.35

Notes

  • Smoothing width: σ = 12 is the width that best rebuilds the existing chi_e from AOT CHI_E, about 10 % RMS in log over the profile.
  • Edge treatment differs: the existing chi_e and chi_phi also smooth over the pedestal well, the edge spike in CHI_E and the negative ANG_MOM_DIFF beyond ψ_N ≈ 0.94, so they stay positive and smooth to the edge. The file doesn't record how those edge treatments were done. The new chi_i uses only the Gaussian filter, so it follows the AOT well near ψ_N ≈ 0.93 and the rise to about 2 m²/s at the edge more closely.
  • 4/1 surface: the 4/1 sits on the inner flank of that well, where the smoothing lifts chi_i 39 % above AOT. A narrower σ would track AOT more closely there, at the cost of keeping more of the AOT structure elsewhere.

🤖 Generated with Claude Code

ebursch and others added 3 commits October 5, 2026 13:14
… and describe c_beta as the no-transport limit of D_perp

The SLAYER file headers and the c_beta P_perp_model cited "Park et al.,
Phys. Plasmas 29, 122505 (2022)", which does not exist. The paper is
J.-K. Park, Phys. Plasmas 29, 072506 (2022), doi:10.1063/5.0093079, as
docs/src/citations.md already lists. Its Eq. 10 defines
C² = c_β² + (1 − c_β²)K; C = c_β drops the thermal-conduction K, which
makes P_perp = c_β² the χ⊥ → 0 limit of the D_perp model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ebursch
ebursch marked this pull request as ready for review October 5, 2026 20:11
@ebursch
ebursch requested a review from d-burg October 5, 2026 20:11
@ebursch

ebursch commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

@logan-nc @d-burg This adds a variety of options for perpendicular diffusion inputs. It doesn't change the default yet, so selecting the chi_perp_e is the same as what currently exists. You can see in the comparison that the only really different one is the c_beta which is good news (and encouragement to have this increased fidelity vs the Fortran slayer implementation). I think that we should move to full D_perp as default since there is not a literature justification for chi_perp_e ~ D_perp and if people already need chi_phi and chi_perp_e inputs than its not too crazy to also have them input chi_perp_i. It's encouraging that the tau_E choice is right order of magnitude (using tau_E = chi_phi and P_perp = P_phi) so that is also a helpful, user friendly, and control friendly option. After this gets merged, then either @d-burg or I should make a PR changing the default to D_perp and removing the chi_perp_e and chi_perp_i options. Let me know if you have any questions on this. Thanks!

@logan-nc

logan-nc commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

At a high level, this looks good. I agree that if someone has chi_phi and chi_perp_e, they likely have chi_perp_i on hand. If they don't have things, or are lazy then letting them assert a "reasonable" tau_E for their device is a great way to get results quick.

@d-burg d-burg left a comment

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.

Thanks -- the switch is cleanly done, the default path is the same arithmetic as before, and the rename is complete. I checked the new models against the papers: D_perp matches Fitzpatrick (2022) Eqs. 4–6 and 16, c_beta matches Park (2022) and is the exact χ⊥ → 0 limit of the coded D_perp, and P_phi matches Fitzpatrick (2023) Eq. 143. tau_E does not yet match Eq. 131 (inline at line 400). I also compared the old and new example file: only chi_i and the provenance text were added, and every pre-existing dataset is byte-identical.

A mix of top level feedback from myself and claude:

Blocking

  1. Docs CI. Two docstring lines parse as Markdown links (suggestion inline at LayerParameters.jl:220).
  2. Rebase onto develop. This now conflicts in 7 files since #298 landed. Note for the binary file: #298 raised chi_e in TkMkr_D3Dlike_Hmode_kinetic.h5, and this branch's copy predates that, so chi_i needs to be added to develop's file rather than taking this branch's version, otherwise the chi_e change is undone. The chi_i/chi_e ratios in your table will change accordingly.

On making D_perp the default and removing chi_perp_e / chi_perp_i

I agree D_perp is the better-founded model, but I'd settle a few things before switching:

  • D_perp is Fitzpatrick's derived perpendicular particle diffusivity: c_β²η⊥/μ₀ plus (2/3)(1 − c_β²)(n T_e′χ⊥,e + n T_i′χ⊥,i)/p′, built from the electron and ion energy diffusivities. By comparison chi_perp_e is an electron-only proxy, so I'd describe it in the docstring and release note as the backward-compatible choice rather than "the" P⊥. One caveat for the default: where the temperature profiles are flat, Eq. 16 reduces to the c_beta floor, which your table shows moves γ by 60–130 %, so flat-temperature regions of a real profile would change answers a lot.
  • tau_E needs fixing before it is advertised as the quick option: Eq. 131's τ_E is the local r²/χ_E, not the whole-plasma confinement time, so as documented this mode is off by r̂_s²/κ_e (details inline).
  • χ⊥,e stays a required input in every mode because it sets the critical-Δ width, so it can't go away even if the model option does. This PR is already a breaking rename of a key every SLAYER deck sets; I'd rather agree the end state now so users migrate once.
  • Please add one harness case with a non-default P_perp_model (your table already has the reference numbers), so a later default change has a baseline.

Data question. The example file's provenance attribute now names DIII-D 147131 at 2300 ms as the source of chi_i; the previous text named no shot. Has releasing this AOT-derived profile in a public repository been cleared under the DIII-D data policy? Since the provenance is being rewritten anyway, it should also use the dataset names chi_e and chi_phi, not "chi_perp" and "chi_tor".

Small, outside the diff

  • docs/src/kinetic_forces.md documents the kinetic HDF5 schema without chi_i and calls chi_e "the" perpendicular heat diffusivity; the run_slayer docstring likewise mentions only chi_e/chi_phi.
  • An old deck with chi_perp = ... is rejected by the generic unknown-key error, which is the important part, but a one-line hint when chi_perp is among the unknown keys ("renamed to chi_perp_e") would save users a search through several dozen names.
  • Fitzpatrick (2022) and (2023) are cited for three of the six models but aren't in docs/resources; adding them would let later reviewers check the equations.

Comment on lines +220 to +223
- `chi_perp_i` -- ion perpendicular heat diffusivity [m²/s] (used by `P_perp_model=:chi_perp_i`)
- `dlnn`, `dlnTe`, `dlnTi` -- radial log-gradients of n, T_e, T_i in any common radial variable
(only their ratios enter; required by `P_perp_model=:D_perp`)
- `tau_E` -- whole-plasma energy confinement time [s] (required by `P_perp_model=:tau_E`)

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.

These two lines are what fails the Documentation job: [m²/s] (used by ...) and [s] (required by ...) parse as Markdown links (the parser skips the space between ] and (), and this docstring is spliced into inner_layer.md. A comma after the unit bracket, as the build_slayer_inputs docstring already does, removes both. The suggestion also adds :D_perp to the models that use chi_perp_i.

Suggested change
- `chi_perp_i` -- ion perpendicular heat diffusivity [m²/s] (used by `P_perp_model=:chi_perp_i`)
- `dlnn`, `dlnTe`, `dlnTi` -- radial log-gradients of n, T_e, T_i in any common radial variable
(only their ratios enter; required by `P_perp_model=:D_perp`)
- `tau_E` -- whole-plasma energy confinement time [s] (required by `P_perp_model=:tau_E`)
- `chi_perp_i` -- ion perpendicular heat diffusivity [m²/s], used by `P_perp_model=:chi_perp_i` and `:D_perp`
- `dlnn`, `dlnTe`, `dlnTi` -- radial log-gradients of n, T_e, T_i in any common radial variable
(only their ratios enter; required by `P_perp_model=:D_perp`)
- `tau_E` -- whole-plasma energy confinement time [s], required by `P_perp_model=:tau_E`

Comment on lines +410 to +416
# D_⊥ from Fitzpatrick, Phys. Plasmas 29, 032507 (2022), Eqs. 4-6 and 16 (η_⊥ = η_∥), with
# η_s/(1+η_s) = dlnT_s/(dlnn+dlnT_s) and τ = (T_e/T_i)(1+η_e)/(1+η_i) written flat-density safe.
frac_e = dlnTe / (dlnn + dlnTe)
frac_i = dlnTi / (dlnn + dlnTi)
tau_F = (t_e / t_i) * (dlnn + dlnTe) / (dlnn + dlnTi)
D_perp = c_beta^2 * eta / MU_0 +
(2 / 3) * (1 - c_beta^2) * (frac_e * tau_F / (1 + tau_F) * chi_perp_e + frac_i / (1 + tau_F) * chi_perp_i)

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 checked this against Fitzpatrick (2022) Eqs. 4–6 and 16: the expression and the docstring formula match the paper. Two small things.

The intermediate ratios have a singularity the formula itself does not: with a vanishing ion (or electron) pressure gradient, frac_i and tau_F both go infinite and the product gives Inf·0 = NaN, so this throws although Eq. 16 is finite there. Writing the weights over one common denominator is the same arithmetic (η_e τ_e/(1+η_e) = T_e·dlnTe/dp and likewise for ions), leaves only p′ = 0 singular, and makes the physical content visible: the bracket is (n T_e′ χ⊥,e + n T_i′ χ⊥,i)/p′.

Suggested change
# D_⊥ from Fitzpatrick, Phys. Plasmas 29, 032507 (2022), Eqs. 4-6 and 16 (η_⊥ = η_∥), with
# η_s/(1+η_s) = dlnT_s/(dlnn+dlnT_s) and τ = (T_e/T_i)(1+η_e)/(1+η_i) written flat-density safe.
frac_e = dlnTe / (dlnn + dlnTe)
frac_i = dlnTi / (dlnn + dlnTi)
tau_F = (t_e / t_i) * (dlnn + dlnTe) / (dlnn + dlnTi)
D_perp = c_beta^2 * eta / MU_0 +
(2 / 3) * (1 - c_beta^2) * (frac_e * tau_F / (1 + tau_F) * chi_perp_e + frac_i / (1 + tau_F) * chi_perp_i)
# D_⊥ from Fitzpatrick, Phys. Plasmas 29, 032507 (2022), Eqs. 4-6 and 16 (η_⊥ = η_∥); the weights η_s τ_s/(1+η_s) equal n T_s'/p'.
dp = t_e * (dlnn + dlnTe) + t_i * (dlnn + dlnTi)
D_perp = c_beta^2 * eta / MU_0 +
(2 / 3) * (1 - c_beta^2) * (t_e * dlnTe * chi_perp_e + t_i * dlnTi * chi_perp_i) / dp

It would also help to say in the docstring what Eq. 16 implies at the edges, since users may be surprised: where T_e and T_i are flat, the χ terms vanish and P⊥ falls to the c_beta value (≈ 0.01 here), and where the species pressure gradients have opposite signs (hollow density) the weights can exceed 1. A test at a flat-temperature point would pin the first.

chi_perp::Real, chi_tor::Real,
chi_perp_e::Real, chi_tor::Real,
m::Integer, n::Integer,
chi_perp_i::Real=1.0, tau_E::Real=NaN,

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.

tau_E and the log-gradients default to NaN so that a model which needs them fails loudly, but chi_perp_i defaults to 1.0 m²/s, so :chi_perp_i or :D_perp called without it returns a plausible-looking number. I'd default it to NaN here and in build_slayer_inputs, and move the isfinite(...) && ... > 0 check out of the :D_perp branch to just after the if block so it applies to P_perp for every model: today a zero, negative or NaN χ⊥,i (a partly-zero profile, or spline undershoot) gives P⊥ ≤ 0 in :chi_perp_i mode with no error.

+ `:D_perp` -- τ_R/τ_⊥ with τ_⊥ = r_s²/D_⊥ and D_⊥ from Eq. 16 of Fitzpatrick, Phys. Plasmas 29,
032507 (2022), with η_⊥ = η_∥:
D_⊥ = c_β² η/μ₀ + (2/3)(1 − c_β²)[η_e τ_e/(1+η_e) χ⊥,e + η_i τ_i/(1+η_i) χ⊥,i]
+ `:c_beta` -- P_perp = C² with C = c_β (J.-K. Park, Phys. Plasmas 29, 072506 (2022), Eq. 10, with the

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.

Small citation fix: in Park (2022) the C² term is in Eq. (9) and is defined in the text just below Eq. (10); Eq. (10) itself carries P, which is our P_tor. I'd cite "Eq. 9, with C² = c_β² + (1 − c_β²)K and K = 0" here and at line 423. On line 230, please also define τ_e = τ/(1+τ), τ_i = 1/(1+τ), η_s = dlnT_s/dlnn, and note that this τ is (T_e/T_i)(1+η_e)/(1+η_i), the opposite orientation to the tau = T_i/T_e field of SLAYERParameters.

chi_perp_e = loaded.chi_perp_e === nothing ? control.chi_perp_e : loaded.chi_perp_e
chi_tor = loaded.chi_tor === nothing ? control.chi_tor : loaded.chi_tor
(loaded.chi_perp === nothing || loaded.chi_tor === nothing) && @warn(
(loaded.chi_perp_e === nothing || loaded.chi_tor === nothing) && @warn(

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.

In tau_E mode P_tor is replaced by τ_R/τ_E, so the file's chi_phi and the scalar chi_tor are ignored; that is only stated in the slayer_parameters docstring. Please say it in the SLAYERControl docstring and the TOML annotation too (suggestion on the example deck), and log one @info when the mode is active. This warning should also depend on the model: as written it reports a chi_tor fallback in tau_E mode, and a chi_perp_e fallback in c_beta, P_phi and tau_E mode with dc_type = "none", where those values do not enter the result.

chi_perp_e = 1.0 # fallback only; the kinetic file supplies χ⊥,e(ψ)
chi_perp_i = 1.0 # fallback only; the kinetic file supplies χ⊥,i(ψ)
chi_tor = 1.0 # fallback only; the kinetic file supplies χ_φ(ψ)
P_perp_model = "chi_perp_e" # Perpendicular Prandtl number model: "chi_perp_e", "chi_perp_i", "P_phi", "D_perp", "c_beta", or "tau_E" (needs tau_E [s])

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.

Suggested change
P_perp_model = "chi_perp_e" # Perpendicular Prandtl number model: "chi_perp_e", "chi_perp_i", "P_phi", "D_perp", "c_beta", or "tau_E" (needs tau_E [s])
P_perp_model = "chi_perp_e" # Perpendicular Prandtl number model: "chi_perp_e", "chi_perp_i", "P_phi", "D_perp", "c_beta", or "tau_E" (needs tau_E [s]; also replaces the χ_φ-based P_tor)

# η_e = 2, η_i = 3, T_e = T_i ⇒ τ = 3/4, τ_e = 3/7, τ_i = 4/7
d1 = slayer_parameters(; base..., P_perp_model=:D_perp, grads..., chi_perp_i=2.0)
D_hand = d1.c_beta^2 * d1.eta / MU_0 + (2 / 3) * (1 - d1.c_beta^2) * ((2 / 3) * (3 / 7) * 1.0 + (3 / 4) * (4 / 7) * 2.0)
@test d1.P_perp ≈ d1.tau_r * D_hand / d1.rs^2 rtol = 1e-12

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.

With T_e = T_i this check passes whichever way round the temperature ratio is written, and the struct's own tau is T_i/T_e, so I'd add a T_e ≠ T_i point (hand numbers: τ = 3/2, τ_e = 3/5, τ_i = 2/5; not run). One build_slayer_inputs test with P_perp_model=:D_perp would also cover the gradient forwarding from the profile splines, which is otherwise untested. I haven't run this test myself so this is just a suggestion, you should confirm on your end whether it checks out.

Suggested change
@test d1.P_perp ≈ d1.tau_r * D_hand / d1.rs^2 rtol = 1e-12
@test d1.P_perp ≈ d1.tau_r * D_hand / d1.rs^2 rtol = 1e-12
# T_e = 2T_i ⇒ τ = 3/2, τ_e = 3/5, τ_i = 2/5: fixes the orientation of the temperature ratio
d2 = slayer_parameters(; base..., t_e=2000.0, P_perp_model=:D_perp, grads..., chi_perp_i=2.0)
D_hand2 = d2.c_beta^2 * d2.eta / MU_0 + (2 / 3) * (1 - d2.c_beta^2) * ((2 / 3) * (3 / 5) * 1.0 + (3 / 4) * (2 / 5) * 2.0)
@test d2.P_perp ≈ d2.tau_r * D_hand2 / d2.rs^2 rtol = 1e-12

ctrl.P_perp_model in _VALID_P_PERP_MODELS ||
throw(ArgumentError("SLAYERControl: P_perp_model=$(ctrl.P_perp_model) " *
"not in $(_VALID_P_PERP_MODELS)"))
ctrl.P_perp_model !== :tau_E || (ctrl.tau_E !== nothing && ctrl.tau_E > 0) ||

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.

Missing, non-positive and NaN tau_E are caught here, but inf gets through and only fails later inside slayer_parameters, after the equilibrium and stability stages. Adding isfinite makes the two checks agree.

Suggested change
ctrl.P_perp_model !== :tau_E || (ctrl.tau_E !== nothing && ctrl.tau_E > 0) ||
ctrl.P_perp_model !== :tau_E || (ctrl.tau_E !== nothing && isfinite(ctrl.tau_E) && ctrl.tau_E > 0) ||

isfinite(tau_E) && tau_E > 0 ||
throw(ArgumentError("slayer_parameters: P_perp_model=:tau_E needs a positive tau_E, got $tau_E"))
# P_⊥ = P_φ = τ_R/τ_E (Fitzpatrick, Phys. Plasmas 30, 092512 (2023), Eq. 131)
P_tor = tau_r / tau_E

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 checked this against Fitzpatrick (2023), Phys. Plasmas 30, 092512. Eq. 131 is P⊥ = P_φ = τ_R/τ_E, but the τ_E there is the local energy confinement time τ_E(r) = r²/χ_E with χ_E uniform (Eqs. 81–83), not the whole-plasma confinement time. Eqs. 140–142 make this explicit: P⊥(r̂) = (τ_R0/τ_E0)·ĵ_oh(r̂) with τ_E0 = a²/χ_E, so the r² cancels and P⊥ = μ₀χ_E/η(r_s). The whole-plasma time from W_e/P_e or a scaling law is τ̄_E = κ_e a²/χ_E (Eqs. 37–38), with κ_e = (3/2)∫₀¹ T̂_e r̂ dr̂ / [−dT̂_e(1)/dr̂].

With tau_E documented as the whole-plasma confinement time, this code's P⊥ differs from the paper's by r̂_s²/κ_e. For a T_e profile like (1 − r̂²), κ_e ≈ 0.19, so the 3/1 and 4/1 surfaces come out about 2–4× larger than Eq. 131 gives, and the error grows outward.

Two ways to make it match: take the whole-plasma τ̄_E and use P⊥ = τ_R(r_s)·κ_e a²/(r_s² τ̄_E), which needs κ_e from the T_e profile and the minor radius; or take a uniform χ_E (= κ_e a²/τ̄_E) as the input, which makes this mode the same as the χ-based P⊥ with one number for every surface. Either way, the docstring should say which confinement time is meant and cite Eqs. 81–83 alongside Eq. 131.

@logan-nc

logan-nc commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the thorough review @d-burg
I think @ebursch will tackle these changes post paternity leave.

I'd just like to reinforce that DIII-D data cannot be included in this open repo directly. The approach for the "DIIID-like" example should be to cite a paper if available and make a roughly matching functional form of the profile needed. Ideally, note the functional form somewhere in annotations / docs so it's clear its not a direct profile fit to data (eg "y = y0 (1 -x^2) where y0=3.0 roughly matches [Name Journal 2026]" or some such).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changed-results Results move or an interface breaks - read before upgrading feature New capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants