Skip to content

PyroScan: how fields, eigenfunctions and time are reduced when loading a scan - #601

Closed
FelixWattsYork wants to merge 32 commits into
feature/claude/pyroscan-ragged-gridsfrom
feature/claude/pyroscan-field-dims
Closed

FelixWattsYork wants to merge 32 commits into
feature/claude/pyroscan-ragged-gridsfrom
feature/claude/pyroscan-field-dims

Conversation

@FelixWattsYork

@FelixWattsYork FelixWattsYork commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

PyroScan.load_gk_output reduced its outputs in ways the user could not change:

  • phi, apar and bpar were cut to ky[0] and the smallest |kx|. For a nonlinear run that is the ky=0, kx=0 mode.
  • Nonlinear fields were averaged over time as complex values. The phase of each Fourier coefficient keeps moving, so that mean cancels.
  • Linear growth_rate and mode_frequency always took the last time point.
  • Time handling was set per regime (linear vs nonlinear) inside the function.

Change

One time setting, for linear and nonlinear runs alike. The output already says which regime it is, so the user isn't asked to state it again.

time_mode applies to every output:

  • "average" (default): the mean over [tolerance_time_range * t_max, t_max], with tolerance_time_range defaulting to 0.8.
  • "last": the final time point.
  • "trace": the full time series.

Fields and eigenfunctions under "average". A time mean of a complex field is meaningless: linear fields are normalised to their final amplitude, and nonlinear Fourier coefficients keep changing phase. So for fields and eigenfunctions that have a time axis, "average" averages |field|**2 instead. It is taken before any kx/ky sum, so a summed result is a sum of squared amplitudes. The result is stored as phi_squared, apar_squared, bpar_squared and eigenfunctions_squared, with squared units, never under the field names. Outputs without a time axis, such as TGLF fields, keep their values and names.

kx and ky. Fields and eigenfunctions go through one shared code path (select_kx_ky_time) and keep kx and ky unless sum_kx / sum_ky are set. Both default to False, and sum_ky also applies to fluxes.

SaturationRules uses eigenfunctions_squared when present, and otherwise |eigenfunctions|**2 at the final time. It selects the smallest |kx| itself.

This combines the designs of fix/pyroscan-fields-sum-ky-kx and feature/pyroscan-linear-time-average-clean, and includes the author's follow-up changes.

Behaviour changes for existing users

  • Linear growth_rate, mode_frequency and fluxes are averaged over the window rather than taken at the last time.
  • By default, fields and eigenfunctions become *_squared, averaged in time. Use time_mode="trace" or "last" to get the complex fields.
  • Fields and eigenfunctions keep kx. Fields, eigenfunctions and fluxes keep ky by default.

Independence from #600

This PR is separate from #600 (stacking runs whose grids differ) and is based on main. One limitation without #600: time_mode="trace" on a scan whose runs have different time grids, e.g. the CGYRO_linear_scan fixture, cannot be stacked. The default "average" removes the time axis, so it is unaffected.

Tests

In tests/test_pyroscan.py:

  • apar/bpar are tested on the STELLA and GX linear outputs (GX has more than one ky), with sum_ky/sum_kx and eigenfunctions checked against the fields.
  • test_pyroscan_nonlinear_field_spectrum is ported from fix/pyroscan-fields-sum-ky-kx.
  • time_mode is tested with each value, on linear and nonlinear runs, including the *_squared names and units.

The scan tests pass locally on main alone (27).

Supersedes

  • fix/pyroscan-fields-sum-ky-kx
  • feature/pyroscan-linear-time-average-clean

🤖 Generated with Claude Code

https://claude.ai/code/session_015uAPcpjjmFiayk58HuLCXu

FelixWattsYork and others added 29 commits July 13, 2026 14:44
TGLF keys are stored lowercase by parse_tglf and uppercased on write, so
add_flags({"NBASIS_MAX": 6}) added a second key and the written file
contained NBASIS_MAX twice. Match flags to existing keys regardless of
case and store new keys in lowercase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
Move the case-insensitive key matching into GKInput._add_flags_case_insensitive
and use it for TGLF, CGYRO and NEO. New keys take the code's stored case:
lowercase for TGLF, uppercase for CGYRO and NEO. Add NEO input tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
Use GKInput._add_flags_case_insensitive as TGLF does, so e.g.
add_flags({"NBASIS_MAX": 6}) overwrites the parsed nbasis_max instead
of writing NBASIS_MAX twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
TGLF keys are stored lowercase by parse_tglf and uppercased on write, so
add_flags({"NBASIS_MAX": 6}) added a second key and the written file
contained NBASIS_MAX twice. Match flags to existing keys regardless of
case and store new keys in lowercase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
Move the case-insensitive key matching into GKInput._add_flags_case_insensitive
and use it for TGLF, CGYRO and NEO. New keys take the code's stored case:
lowercase for TGLF, uppercase for CGYRO and NEO. Add NEO input tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
add_flags({"NBASIS_MAX": 6}) overwrites the parsed nbasis_max instead of
adding a second key that is written as a duplicate NBASIS_MAX line. New
keys are stored in lowercase, as parse_gftm does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
Replace the per-code add_flags overrides and the
_add_flags_case_insensitive helper with one GKInput.add_flags. Top-level
keys match existing keys case-insensitively; new keys are converted with
the class attribute flag_key_case (lowercase for TGLF, uppercase for
CGYRO and NEO). dict values are merged into namelist groups as before.
The GS2, GX, GKW, STELLA and GENE overrides only called super() and are
removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
Flag behaviour depends on the input format, not the code. Replace the
single add_flags that dispatched on isinstance(value, dict) with one
implementation per format:

- GKInput.add_flags: grouped inputs (Fortran namelists, TOML tables).
  Raises TypeError if a value is not a dict of parameters.
- GKInputFlat.add_flags: flat KEY = value inputs (TGLF, CGYRO, NEO).
  Matches keys case-insensitively, converts new keys with flag_key_case,
  and raises TypeError if a value is a dict.

Both validate all flags before changing self.data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwWRH3BAPwMdmNx4D3gEbV
@FelixWattsYork
FelixWattsYork changed the base branch from claude/default to feature/claude/pyroscan-ragged-grids September 28, 2026 13:48
MantasA411 and others added 3 commits September 28, 2026 15:32
GFTM Implementation in Pyrokinetics
…pitilsation

Case-insensitive add_flags, with one implementation per input format
…g a scan

load_gk_output reduced phi, apar and bpar to ky[0] and the smallest |kx|,
averaged nonlinear fields over time as complex values (a mean that cancels),
always took linear growth rates at the last time, and set its time handling
per regime.

- One time_mode for linear and nonlinear runs alike: "average" (default, over
  [tolerance_time_range * t_max, t_max], tolerance_time_range 0.8), "last" or
  "trace". Adds VALID_TIME_MODES and the "trace" mode of reduce_time.
- A time mean of a complex field is meaningless, so with "average", fields and
  eigenfunctions with a time axis are reduced to |field|**2 averaged in time,
  taken before any kx/ky sum, and stored as phi_squared, apar_squared,
  bpar_squared and eigenfunctions_squared. Outputs without a time axis keep
  their values and names.
- Fields and eigenfunctions go through the same select_kx_ky_time call and
  keep kx and ky unless summed with sum_kx / sum_ky (both default False;
  sum_ky also applies to fluxes).
- SaturationRules uses eigenfunctions_squared when present, otherwise
  |eigenfunctions|**2 at the final time, and selects the smallest |kx| itself.

Combines fix/pyroscan-fields-sum-ky-kx and
feature/pyroscan-linear-time-average-clean, with eigenfunctions made
consistent with the fields. Tests use the STELLA and GX linear outputs, which
carry apar and bpar, and port test_pyroscan_nonlinear_field_spectrum.

Rebuilt from main without the ragged-grid stacking of #600, which is a
separate change.

Co-Authored-By: FelixWattsYork <hmq514@york.ac.uk>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uAPcpjjmFiayk58HuLCXu
@FelixWattsYork

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #599, which is open from the same branch against main.

feature/claude/pyroscan-field-dims has been rebuilt from main without the ragged-grid stacking of #600, so the two changes are now separate. Against this PR's base (feature/claude/pyroscan-ragged-grids), the diff would wrongly show that code being removed. The description has moved to #599.


Generated by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants