Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).

### Added

- **FP library: `fp_get_state` returns seven global scalars** after the
profile arrays: `TOTAL_IP` [MA], `STORED_ENERGY` [MJ], `COLLISION_POWER`,
`ABSORPTION_POWER`, `ABSORPTION_WR`, `ABSORPTION_WM` [MW] (species sums at
the latest `NTG1` sample, what FP otherwise only writes to unit 6) and
`PLASMA_VOLUME` [m^3]. They are what shows an EQ to FP handoff: the six
profiles are local quantities and stay bit-identical when only the
equilibrium changes (without radial transport, waves or current drive).
`fp_state_t` grows by 56 bytes at its end (layout version 2, #246), so
`libfpapi.so` and its callers are rebuilt together: `fplib` refuses a
library built before this change with `FplibAbiError` instead of reading
the scalars as zeros. `FpState.scalars`, `to_dict()["scalars"]`
and the `scalars` object of fp_mcp's state carry them, and an FP step of
the `totlib` pipeline keeps them beside `timefp` and its counts. They are
zero until the first run, and again after any successful parameter set
(`fp_set_param`, `fp_set_param_str`, or tot's `fp:` parameters, which the
registry counts since #253) until the next run that is made.
- **EQ library: `eq_save`, `Eq.save(path)` and the `save` tool of `eq_mcp`**
write the equilibrium of the last `eq_run` to a TASK/EQ binary file through
`EQSAVE`, the file that `MODELG=3` with `KNAMEQ` loads. A save whose file
Expand Down
26 changes: 18 additions & 8 deletions docs/fp-library/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,17 @@ run is made without that species' collisions with its own kind).
`nthmax`, `ntg2`), the simulation time `timefp` (double), and six 2-D
profile arrays `RNT`, `RWT`, `RTT`, `RJT`, `RPCT`, `RPWT` of shape
`[FP_MAX_NSAMAX][FP_MAX_NRMAX]` = `[8][100]`. The runtime copy uses
only the `[0:nsamax][0:nrmax]` slice.
only the `[0:nsamax][0:nrmax]` slice. After the arrays come seven
doubles: six volume-integrated globals summed over the species at the
latest `NTG1` sample, `TOTAL_IP` [MA], `STORED_ENERGY` [MJ],
`COLLISION_POWER`, `ABSORPTION_POWER`, `ABSORPTION_WR`, `ABSORPTION_WM`
[MW], and `PLASMA_VOLUME` [m^3] (`TVOLR`). They are zero until the first
run, and again after any successful parameter set (`fp_set_param`,
`fp_set_param_str`, or tot's `fp:` parameters: the registry counts every
set, #253) until the next run that is made,
while the profiles above are still those of the last run. The
struct grows at its end only, and `fp/fp_state.f90`, `fp/fp_api.h` and
`fplib._ffi.FpStateC` change together with a rebuilt `libfpapi.so`.

### Fortran backend

Expand Down Expand Up @@ -173,7 +183,7 @@ only the `[0:nsamax][0:nrmax]` slice.
to the bracket syntax.
- `fplib.FpState` is a `@dataclass` snapshot; `to_dict()` is
JSON-serialisable and uses a
`{"NRMAX": ..., "NSAMAX": ..., "profile": [{"NSA": i, "RNT": ...}, ...]}`
`{"NRMAX": ..., "NSAMAX": ..., "scalars": {"TOTAL_IP": ..., ...}, "profile": [{"NSA": i, "RNT": ...}, ...]}`
layout so downstream tools can diff wrapper output against `fp`
CLI runs.

Expand Down Expand Up @@ -214,10 +224,9 @@ Mirrors the tr spec's §12.2 for the FP module:
lifecycle. Repeated cycles (as in the 3×3 sweep) work in practice
because `fp_init` resets the necessary SAVE state, but long-running
processes can leak FPCOMM allocations.
- **String parameters not wired.** `KNAMFP` and similar namelist
keys cannot flow through the float-only C ABI; they stay at their
defaults. A follow-up PR with a `fp_set_param_str` entry point is
tracked separately (mirrors the trlib L-L approach).
- **One string parameter.** `fp_set_param_str` (`Fplib.set_param_str`)
sets `KNAMEQ`, the equilibrium file of `MODELG=3`; `KNAMFP` and the
other string keys of the namelist stay at their defaults.
- **Compile-time mesh caps.** `FP_MAX_NRMAX=100` and
`FP_MAX_NSAMAX=8` are baked into the exported `fp_state_t`; runs
needing larger meshes require bumping the constants in
Expand All @@ -231,8 +240,9 @@ Mirrors the tr spec's §12.2 for the FP module:
one fixed layout, filled by `fp_get_state`. Changing it:

1. append the field at the end of both (never reorder, never remove);
2. bump `FP_STATE_ABI_VERSION` **in both files** (the `#define` and the
`PARAMETER`; currently `1`);
2. bump `FP_STATE_ABI_VERSION` **in all three places** (the `#define`, the
`PARAMETER` and the constant in `python/fplib/_ffi.py`, without which
`load_library` refuses the rebuilt library; currently `2`);
3. mirror the field in `python/fplib/_ffi.py` (`FpStateC._fields_`) and
`state.py`;
4. rebuild `libfpapi.so` in the same commit.
Expand Down
33 changes: 27 additions & 6 deletions docs/sphinx/modules/fp/en/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,36 @@ the distribution function $f(r, p, \theta)$ itself — it exposes only

## Scalar quantities

`state.scalars` for `fp` contains **only `timefp`**. Unlike `tr`/`eq`, it
does not carry the 13/12 plasma-performance scalars — by design, `fp`
focuses on distribution-function moments and leaves derived quantities
(`BETA`, `TAUE`, etc.) to user-side post-processing.
The simulation time is a top-level field, and `state.scalars` is a dict
of **seven global quantities**: six volume-integrated diagnostics, each
summed over the active species at the latest global-diagnostic sample
(`NTG1`), and the plasma volume (`TVOLR`, the sum of the radial cell
volumes):

```python
state.timefp # simulation time [s]
state.timefp # simulation time [s]
state.scalars["TOTAL_IP"] # total plasma current [MA]
```

| Key of `state.scalars` | Quantity | Unit |
|---|---|---|
| `TOTAL_IP` | Total plasma current | MA |
| `STORED_ENERGY` | Stored energy | MJ |
| `COLLISION_POWER` | Total collisional power | MW |
| `ABSORPTION_POWER` | Total absorbed wave power | MW |
| `ABSORPTION_WR` | Ray-tracing (WR) part of the absorbed power | MW |
| `ABSORPTION_WM` | Full-wave (WM) part of the absorbed power | MW |
| `PLASMA_VOLUME` | Plasma volume | m³ |

All seven are zero until the first `run()`, and again after a
`set_param` or `set_param_str` until the next `run()` that is made, while
the profiles are
still those of the last run. Unlike the per-unit-volume profiles
below, they depend on the equilibrium through the volume element. `fp`
still does not carry the plasma-performance scalars of `tr`/`eq`:
derived quantities (`BETA`, `TAUE`, etc.) are left to user-side
post-processing.

## Profile quantities (radial × species moment arrays)

The main `fp` outputs. Each is a **2D array of shape `[nsamax][nrmax]`**
Expand Down Expand Up @@ -71,7 +92,7 @@ For the full attribute list, see the `FpState` autodoc in
|---|---|---|---|
| **Species axis** | NSMAX | active NSA_MAX | active NSAMAX |
| **Phase space** | 1D radial | 1D radial | **5D** (r, p, θ, species, t) |
| **Number of scalars** | 13 | 2 + 2int | **1** (timefp) |
| **Number of scalars** | 13 | 2 + 2int | **7** global (+ `timefp`) |
| **Main outputs** | RN, RT, AJ, QP | RNA, RTA, RUA, RBP, ZEFF, BETA | RNT, RWT, RTT, RJT, RPCT, RPWT |
| **Meaning** | Fluid moments | Integrated-transport moments | Distribution-function moments |
| **Memory cost** | Low | Medium | **High** (5D) |
29 changes: 23 additions & 6 deletions docs/sphinx/modules/fp/ja/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,32 @@

## スカラー量

`fp` の `state.scalars` は **`timefp` 1 個のみ** です. tr/eq のような
13/12 個のプラズマ性能指標は持ちません — これは fp の出力が分布関数
モーメントが中心で, 派生量 (`BETA`, `TAUE` など) はユーザ側で計算する
設計だからです.
シミュレーション時刻はトップレベルのフィールドで, `state.scalars` は
**7 個の大域量** の dict です. うち 6 個は体積積分された診断量で, active
粒子種について和をとった最新の大域診断サンプル (`NTG1`) の値, 残る 1 個は
プラズマ体積 (`TVOLR`, 半径方向セル体積の和) です.

```python
state.timefp # シミュレーション時刻 [s]
state.timefp # シミュレーション時刻 [s]
state.scalars["TOTAL_IP"] # 全プラズマ電流 [MA]
```

| `state.scalars` のキー | 物理量 | 単位 |
|---|---|---|
| `TOTAL_IP` | 全プラズマ電流 | MA |
| `STORED_ENERGY` | 蓄積エネルギー | MJ |
| `COLLISION_POWER` | 衝突パワーの合計 | MW |
| `ABSORPTION_POWER` | 波動吸収パワーの合計 | MW |
| `ABSORPTION_WR` | そのうちレイトレーシング (WR) 分 | MW |
| `ABSORPTION_WM` | そのうちフルウェーブ (WM) 分 | MW |
| `PLASMA_VOLUME` | プラズマ体積 | m³ |

7 個とも最初の `run()` までは 0 で, `set_param` または `set_param_str` の後は次に `run()` が
実行されるまで再び 0 になります (その間もプロファイルは直前の run のままです).
下の単位体積あたりのプロファイルと違い, 体積要素を通じて
平衡に依存します. tr/eq のようなプラズマ性能指標は引き続き持ちません —
派生量 (`BETA`, `TAUE` など) はユーザ側で計算する設計です.

## プロファイル量 (radial × species のモーメント配列)

fp の主要出力. **`[nsamax][nrmax]` の 2 次元配列** で, active 粒子種ごとに
Expand Down Expand Up @@ -67,7 +84,7 @@ state.to_dict() # JSON-ready dict
|---|---|---|---|
| **種別軸** | NSMAX | active NSA_MAX | active NSAMAX |
| **位相空間** | 1D 半径 | 1D 半径 | **5D** (r, p, θ, species, t) |
| **スカラー数** | 13 | 2 + 2int | **1** (timefp) |
| **スカラー数** | 13 | 2 + 2int | **7** 大域量 (+ `timefp`) |
| **主出力** | RN, RT, AJ, QP | RNA, RTA, RUA, RBP, ZEFF, BETA | RNT, RWT, RTT, RJT, RPCT, RPWT |
| **意味** | 流体モーメント | 統合輸送モーメント | 分布関数モーメント |
| **メモリ消費** | 低 | 中 | **高** (5D) |
2 changes: 1 addition & 1 deletion docs/sphinx/modules/wr/en/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ For the complete attribute list see the `WrState` autodoc in
|---|---|---|---|
| **Time** | yes (`nt`) | yes (`timefp`) | **no** (spatial integration) |
| **Dimensions** | radius × species | 5D phase space (moments) | **rays × steps** + radial profiles |
| **Scalars** | 13 (T, BETAN, ...) | 1 (timefp) | **4** (peak position + value) |
| **Scalars** | 13 (T, BETAN, ...) | 7 global (+ `timefp`) | **4** (peak position + value) |
| **Main output** | profiles | moments | **ray trajectories + absorption profile** |
| **Physical units** | fluid quantities | distribution-function moments | RF power deposition |
2 changes: 1 addition & 1 deletion docs/sphinx/modules/wr/ja/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ state.to_dict() # JSON-ready dict
|---|---|---|---|
| **時間** | あり (`nt`) | あり (`timefp`) | **なし** (空間積分) |
| **次元** | 半径 × 種 | 5D 位相空間 (モーメント化) | **レイ × ステップ** + 半径プロファイル |
| **スカラー** | 13 (T, BETAN, ...) | 1 (timefp) | **4** (ピーク位置 + 値) |
| **スカラー** | 13 (T, BETAN, ...) | 7 大域量 (+ `timefp`) | **4** (ピーク位置 + 値) |
| **主要出力** | プロファイル | モーメント | **レイ軌跡 + 吸収プロファイル** |
| **物理単位** | 流体量 | 分布関数モーメント | RF パワー堆積 |
54 changes: 50 additions & 4 deletions fp/fp_api.f90
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
! fp_get_state -> populates the fp_state_c struct from the same set of
! FPCOMM scalars/arrays that fpregress.f90 dumps:
! NRMAX, NSAMAX, NPMAX, NTHMAX, NTG2, TIMEFP, and
! profile arrays RNT/RWT/RTT/RJT/RPCT/RPWT(NR,NSA).
! profile arrays RNT/RWT/RTT/RJT/RPCT/RPWT(NR,NSA),
! plus the volume-integrated global scalars that
! FPWRTGLB otherwise only WRITEs to unit 6.
! fp_finalize -> fp_deallocate (+ ntg1/ntg2) and clears g_* flags.
!
! The Fortran-side names are fp_api_* to avoid colliding with the existing
Expand All @@ -34,8 +36,9 @@ MODULE fp_api
USE fp_state, ONLY: fp_state_c, FP_MAX_NRMAX, FP_MAX_NSAMAX
USE fpcomm, ONLY: rkind, &
NRMAX, NSAMAX, NPMAX, NTHMAX, NTG2, TIMEFP, &
NTMAX, &
RNT, RWT, RTT, RJT, RPCT, RPWT
NTMAX, NTG1, TVOLR, &
RNT, RWT, RTT, RJT, RPCT, RPWT, &
PIT, PWT, PPCT, PPWT, PWRT, PWMT
USE fp_param_registry, ONLY: fp_param_set, fp_param_set_str, fp_param_generation
USE plinit, ONLY: pl_init
USE equnit, ONLY: eq_init
Expand Down Expand Up @@ -262,11 +265,18 @@ END FUNCTION fp_api_run
! in Fortran column-major order, which matches the C-side declaration
! double RNT[FP_MAX_NSAMAX][FP_MAX_NRMAX] (row-major) byte-for-byte.
! Only state%RNT(1:NRMAX, 1:NSAMAX) carries valid runtime data.
!
! The 7 appended global scalars are the species sums FPWRTGLB builds
! before printing them (fp/fpsave.f90:260-311). They are the only part
! of the state that sees the volume element VOLR / TVOLR: the six
! profile arrays are per-unit-volume moments and stay bit-identical
! when only the equilibrium changes (MODELD=0, no wave, no CD), so
! without these an EQ->FP handoff is invisible to the caller.
!-------------------------------------------------------------------
FUNCTION fp_api_get_state(state) RESULT(ierr) BIND(C, NAME="fp_get_state")
TYPE(fp_state_c), INTENT(OUT) :: state
INTEGER(C_INT) :: ierr
INTEGER :: nr, nsa, ntg_last, nr_alloc, nsa_alloc
INTEGER :: nr, nsa, ntg_last, ntg1_last, nr_alloc, nsa_alloc

! Always zero the struct so callers never see uninitialized memory.
state%nrmax = 0
Expand All @@ -281,6 +291,13 @@ FUNCTION fp_api_get_state(state) RESULT(ierr) BIND(C, NAME="fp_get_state")
state%RJT = 0.0_C_DOUBLE
state%RPCT = 0.0_C_DOUBLE
state%RPWT = 0.0_C_DOUBLE
state%TOTAL_IP = 0.0_C_DOUBLE
state%STORED_ENERGY = 0.0_C_DOUBLE
state%COLLISION_POWER = 0.0_C_DOUBLE
state%ABSORPTION_POWER = 0.0_C_DOUBLE
state%ABSORPTION_WR = 0.0_C_DOUBLE
state%ABSORPTION_WM = 0.0_C_DOUBLE
state%PLASMA_VOLUME = 0.0_C_DOUBLE

IF (.NOT. g_initialized) THEN
ierr = FP_ERR_NOT_INIT
Expand Down Expand Up @@ -340,6 +357,35 @@ FUNCTION fp_api_get_state(state) RESULT(ierr) BIND(C, NAME="fp_get_state")
END DO
END IF

! Global scalars. TVOLR and the P*T(NSA,NTG1) accumulators only
! exist once fp_prep has run (TVOLR: fp/fpprep.f90:231-234; the
! arrays: fpcomm::fp_allocate_ntg1) and only carry a sample once
! FPSGLB has bumped NTG1 (fp/fploop.f90:200). Before that they stay
! at the zeros set above rather than exposing uninitialised memory.
! The same holds after a successful set, from fp_set_param,
! fp_set_param_str or tot's "fp:" dispatcher (the registry's count
! then differs from the one of the last fp_prep, #253): the profiles
! above are then still the last run's (within the allocated extents,
! #245) while these read zero until the next fp_prep.
IF (g_prepared .AND. g_prep_generation == fp_param_generation()) THEN
state%PLASMA_VOLUME = TVOLR
IF (NTG1 >= 1 .AND. ALLOCATED(PIT) .AND. ALLOCATED(PWT) .AND. &
ALLOCATED(PPCT) .AND. ALLOCATED(PPWT) .AND. &
ALLOCATED(PWRT) .AND. ALLOCATED(PWMT)) THEN
ntg1_last = MIN(NTG1, SIZE(PIT, 2))
DO nsa = 1, MIN(NSAMAX, SIZE(PIT, 1))
! Species sums, mirroring FPWRTGLB's rtotal* accumulators
! (fp/fpsave.f90:277-286).
state%TOTAL_IP = state%TOTAL_IP + PIT (nsa, ntg1_last)
state%STORED_ENERGY = state%STORED_ENERGY + PWT (nsa, ntg1_last)
state%COLLISION_POWER = state%COLLISION_POWER + PPCT(nsa, ntg1_last)
state%ABSORPTION_POWER = state%ABSORPTION_POWER + PPWT(nsa, ntg1_last)
state%ABSORPTION_WR = state%ABSORPTION_WR + PWRT(nsa, ntg1_last)
state%ABSORPTION_WM = state%ABSORPTION_WM + PWMT(nsa, ntg1_last)
END DO
END IF
END IF

ierr = FP_OK
END FUNCTION fp_api_get_state

Expand Down
26 changes: 24 additions & 2 deletions fp/fp_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ extern "C" {
* fp_run -> fp_prep on first call + fp_loop with NTMAX set to
* the requested step count.
* fp_get_state -> populates scalars + profile arrays (RNT/RWT/RTT/
* RJT/RPCT/RPWT) from FPCOMM.
* RJT/RPCT/RPWT) + the volume-integrated global
* scalars (TOTAL_IP ... PLASMA_VOLUME) from FPCOMM.
* fp_finalize -> clears lifecycle flags. Note: FPCOMM arrays are
* NOT deallocated (fp_allocate / fp_deallocate
* asymmetry) so a single fp_init/fp_run/fp_finalize
Expand All @@ -34,6 +35,11 @@ extern "C" {
* matching declaration is RNT(NRMAX, NSAMAX) (column-major). Layouts agree
* byte-for-byte, but only RNT[0..nsamax-1][0..nrmax-1] are valid runtime
* values (the remainder is padding up to FP_MAX_*).
*
* ABI note: fp_state_t only ever grows at the END of the struct, so the
* offsets of pre-existing members are stable. Every change must be
* mirrored in fp/fp_state.f90 and python/fplib/_ffi.py::FpStateC, and
* libfpapi.so rebuilt (`make -C fp libfpapi.so`).
*/

#define FP_MAX_NRMAX 100
Expand All @@ -50,8 +56,10 @@ extern "C" {
*
* History:
* 1 -> initial layout
* 2 -> appended the seven global scalars TOTAL_IP .. PLASMA_VOLUME
* (56 bytes); existing field offsets preserved.
*/
#define FP_STATE_ABI_VERSION 1
#define FP_STATE_ABI_VERSION 2

/* Error codes returned by every fp_* entry point. */
enum fp_error {
Expand All @@ -75,6 +83,20 @@ typedef struct {
double RJT [FP_MAX_NSAMAX][FP_MAX_NRMAX];
double RPCT[FP_MAX_NSAMAX][FP_MAX_NRMAX];
double RPWT[FP_MAX_NSAMAX][FP_MAX_NRMAX];
/* Global (volume-integrated) plasma scalars, species-summed at the
* latest NTG1 sample. These are the quantities FP otherwise only
* WRITEs to unit 6: the rtotal* line block (fp/fpsave.f90:305-311),
* the W column of the per-species line (fp/fpsave.f90:252) and the
* TVOLR banner (fp/fpprep.f90:237). Zero until the first run, and
* again after any successful set (fp_set_param, fp_set_param_str, or
* tot's fp: parameters; #253) until the next run that is made. */
double TOTAL_IP; /* total plasma current [MA] */
double STORED_ENERGY; /* stored energy [MJ] */
double COLLISION_POWER; /* total collision power [MW] */
double ABSORPTION_POWER; /* total absorbed power [MW] */
double ABSORPTION_WR; /* WR (ray-tracing) part [MW] */
double ABSORPTION_WM; /* WM (full-wave) part [MW] */
double PLASMA_VOLUME; /* TVOLR [m^3] */
} fp_state_t;

int fp_init(void);
Expand Down
Loading
Loading