Skip to content

SeaState: On-demand block partitioning of wave kinematics to reduce memory (WvKinBlockMod)#3388

Open
mayankchetan wants to merge 17 commits into
OpenFAST:devfrom
mayankchetan:f/seastate-block-partition
Open

SeaState: On-demand block partitioning of wave kinematics to reduce memory (WvKinBlockMod)#3388
mayankchetan wants to merge 17 commits into
OpenFAST:devfrom
mayankchetan:f/seastate-block-partition

Conversation

@mayankchetan

Copy link
Copy Markdown
Contributor

SeaState: on-demand wave-kinematics block partitioning (WvKinBlockMod)

This PR is ready to be merged.

Feature or improvement description

Adds an optional on-demand block partitioning mode for the SeaState
wave-kinematics volume arrays (velocity, acceleration, dynamic pressure, and —
when enabled — the MCF acceleration). Today SeaState precomputes these arrays
over the entire wave grid at initialization and holds them resident for the
whole simulation. For grids that are large relative to the region a structure
actually visits, most of that memory is never touched.

With WvKinBlockMod = 1, the grid is divided into cube blocks partitioned in
X, Y, and Z. Each block is computed the first time wave kinematics are
requested at a point inside it, and (optionally) freed after a configurable idle time.
Evicted blocks are transparently and deterministically recomputed on next
access.

Physical behavior is unchanged by default: WvKinBlockMod = 0 (full-domain
precompute) is byte-identical to the previous code. Note that the three new
input lines are required additions to the SeaState input file
(WvKinBlockMod has no parser default; the other two accept DEFAULT) — the
companion r-test submodule bump adds them to every SeaState deck.

seastate_blocks_2x2.mp4

New inputs (SeaState SPATIAL DISCRETIZATION section)

Input Type Default Meaning
WvKinBlockMod int — (required) 0 = precompute full domain (current behavior); 1 = on-demand blocks
WvKinBlockSize real (m) 100 Target edge length of a cube block, applied to all three of X, Y, and Z (snapped to whole cells, min 8 cells/side; the non-uniform cosine-distributed Z grid snaps via the mean Z spacing, so coarse-Z grids may remain a single Z block); used only when WvKinBlockMod = 1
WvKinBlockFreeT real (s) 600 Free a block after this much sim time without an access; ≤ 0 disables freeing; used only when WvKinBlockMod = 1

Guards: WaveMod = 0 and WrWvKinMod = 2 force mode 0; WaveMod = 6
(externally-supplied full kinematics) with mode 1 is a fatal input error.

Block VTK visualization

Adds an optional <root>.SeaSt.WaveBlocks.<frame>.vtk output series so the
on-demand block partition can be watched evolve during a run:

  • Triggers, no new glue inputs. In the glue code it rides the existing
    WrVTK = 2 animation output — no new .fst/SeaState input is added. The
    standalone SeaState driver gets one new optional trailing input,
    WrBlockVTK (write interval in steps; 0/absent = off, preserving
    compatibility with every existing driver input file).
  • Encoding. One VTK cell per block; the BlockLife cell scalar is -1
    for a block never populated, 0 for an evicted block, and (0, 1] for a
    resident block (normalized time remaining before eviction, pinned at 1
    when WvKinBlockFreeT <= 0).
  • Restart-safe. The per-block EverPopulated flag is packed/unpacked via
    the registry, and the non-owning BlockStore alias on the WaveField is
    re-established on restart, so a restarted run resumes with the correct
    never/evicted/active block states.

Related issue, if one exists

None.

Impacted areas of the software

  • SeaState (modules/seastate/src/): SeaSt_WaveField.f90/.txt,
    SeaState.f90/.txt, SeaState_Input.f90, SeaState_DriverCode.f90,
    Waves.f90, Waves2.f90/.txt, UserWaves.f90, registry-generated
    *_Types.f90.
  • OpenFAST glue (modules/openfast-library/src/FAST_Subs.f90): the guarded
    block-VTK call in WrVTK_Surfaces.
  • openfast_io (openfast_io/openfast_io/FAST_reader.py, FAST_writer.py):
    read/write the three new inputs.
  • Documentation: docs/source/user/seastate/input_files.rst, appendix.rst.
  • r-test: submodule bump adding the three inputs to all SeaState decks
    (branch f/seastate-block-partition on the r-test fork).

Additional supporting information

Design

  • Shared seed-driven kernels. The first- and second-order wave-kinematics
    column computations were extracted into WaveKinKernel_ComputeColumns
    (Waves.f90) and WaveKinKernel_AddSecondOrderColumns (Waves2.f90). Mode 0
    (full-grid loop) and mode 1 (per-block fill) call the same kernels, so the
    two modes agree to solver precision and evicted blocks regenerate
    bit-identically. Kernels depend only on captured seeds and immutable grid
    data — no run-time mutable state, no date/random. The kernels take
    iPtZ0/nPtZ Z sub-range arguments; mode 0 always passes the full Z
    range, so its loop structure (and output) is unchanged by the Z
    partitioning.
  • Block store. Owned by the SeaState MiscVar (m%WaveBlockStore); the
    WaveField carries a non-owning pointer. Blocks partition grid cells in
    all three axes and store a halo of three extra grid planes per axis (one
    below, two above the block's cells) to satisfy the 4-point cubic
    interpolation stencil.
  • Eviction. WaveField_SweepBlocks frees idle blocks under an
    !$OMP CRITICAL(SeaSt_BlockPop) guard; the just-accessed block is timestamped
    before any sweep so it can never evict itself.
  • Init memory report. SeaState prints a one-time report of full-domain vs
    block resident memory at the end of initialization.

Measured guidance (added to the user docs)

Block-size × free-time sweeps on 5MW OC4-Semi and MHK_RM1_Floating:

  • On-demand partitioning helps most when the grid is much larger than the
    queried footprint. For station-keeping platforms (static footprint), savings
    come from block size, not eviction.
  • The halo makes small blocks counterproductive: WvKinBlockSize
    = 50 m split the MHK footprint across 3 blocks and used ~17% more peak
    memory than the full-domain default. 100–200 m used a single block at
    parity-to-modest savings (OC4-Semi: −5% at the 100 m default).
  • WvKinBlockFreeT has no effect for station-keeping platforms (footprint
    never idles); it matters only for a query that migrates across the grid.

Generative AI usage

Substantial portions of this feature were developed with Anthropic Claude
(Claude Code); all design decisions were made and approved by the human
author. A multi-agent workflow was used: Claude Fable 5 orchestrated (design
interview, implementation planning, adjudication of review findings), with
implementation and per-task independent code reviews delegated to smaller
Claude models, and two independent whole-branch reviews performed by Claude
Opus. Every task was gated by an independent review pass (spec compliance +
code quality) before proceeding, and all changes are backed by the test
evidence below. Development relied heavily on the OpenFAST Agent Kit (OAK) —
OpenFAST-specific sub-agents and MCP tooling for reading/editing/running
cases, searching the docs/code corpus, and navigating the Fortran source.

Co-authored-by: Anthropic Claude claude@anthropic.com

Test results, if applicable

Mode-0 byte-identity (the primary gate). All 11 SeaState module regression
cases produce byte-identical .SeaSt.out data vs the post-kernel-refactor
baseline. This gate was re-run and re-passed 11/11 after each later structural
change (the Z/cube-block partitioning and the block VTK export). The one-time
kernel extraction introduces a floating-point reassociation drift of ≤ 1 ULP of
the printed output precision (max relative ≈ 8×10⁻⁷, well under the r-test
tolerance of ~2×10⁻⁵) vs the pre-refactor snapshot — documented for
transparency.

Mode 0 ≡ Mode 1. Re-verified on the final code (after the Z/cube-block
change): nine twin pairs across five cases — seastate_1 (3×3×2 forced cube
layout), seastate_wavemod5 (3×3×2), seastate_WaveMod7_WaveStMod2,
hd_MCF_WaveStMod2 (1×1×4 pure-Z stack, MacCamy-Fuchs array per block), and
5MW_OC3Mnpl_DLL_WTurb_WavesIrr (second-order QTFs filled over true Z
sub-ranges, 1×1×2) plus 5MW_OC4Semi_WSt_WavesWN under the full glue stack —
all bit-identical to the mode-0 twin (binary channel data max abs diff
0.0; text outputs differ only in run-timestamp header lines). Wave kinematics
were also compared directly: a 9-probe twin (NWaveKin = 9, probes placed
to sample every XY block column and both Z layers of a 3×3×2 partition) shows
all 63 kinematics output channels (FVelJ*, FAccJ*, FDynPJ) exactly equal
between the two modes at every output step. Deterministic
regeneration after eviction: a 1000 s drifting-platform run with 166
evictions/repopulations is payload-byte-identical to its eviction-disabled
twin. Earlier smoke coverage (py_seastate_1 binding, 5MW_OC3Mnpl restart,
eviction-on/off) also passed byte-identical.

Block VTK verified end-to-end. A drifting-platform run on 5MW OC4-Semi
(CompMooring = 0, HydroDyn WaveDisp = 1 so the kinematics footprint follows
the drift, TMax = 300, WvKinBlockFreeT = 30) produced a 301-frame series
showing all three states (never / evicted / active); a pinned run
(WvKinBlockFreeT = -1) showed only never/active with every resident block
exactly at 1.0; the standalone-driver trigger was exercised on
seastate_wr_kin1 (needs NWaveKin > 0) with well-formed frames every 5
steps. Three additional 1000 s OC4-Semi drifting-platform visualization runs
(mode 0 / eviction-off / WvKinBlockFreeT = 60) confirmed the full block life
cycle in the exported series: block counts growing with the drift, the eviction
countdown, and 15 blocks evicted by the end of the eviction-enabled run.

Full regression suite. ctest -j 8 (196 tests). 7 failures, all triaged as
unrelated to this feature:

Test Cause
MHK_RM1_Floating_MR_Linear pre-existing known failure (linearization)
Damped_Beam_Rotated, 5MW_Land_BD_Linear, IEA22MW_ModalDamping BeamDyn/linearization — this diff touches no BeamDyn code
openfast_io_library environment: pytest not installed
py_wavetank_test1 environment: nptdms not installed
5MW_OC4Jckt_DLL_WTurb_WavesIrr_MGrowth HD load channels differ ~0.6% rel; zero wave-channel diff; mode-0 SeaState output byte-identical — attributable to inherited upstream HydroDyn (NLFKHst) changes / turbulent-DLL chaotic divergence vs the gold, not to this feature

Independent whole-branch code review (Claude Opus): no Critical or Important
findings. All six binding invariants (mode-0 byte-identity, mode 0 ≡ mode 1,
deterministic regeneration, halo correctness, guards, eviction/ownership
safety) verified by source trace. Three Minor, non-blocking observations
recorded.

  • r-test branch merging required
    (f/seastate-block-partition on the r-test fork — adds the three new input
    lines to all SeaState decks)

  • please squash commits before merge

mayankchetan and others added 17 commits April 1, 2025 09:39
…ts (parsing only, behavior unchanged)

New required lines in the SPATIAL DISCRETIZATION section of the SeaState
primary input file, after NZ:
  WvKinBlockMod   (switch) 0: precompute full domain (current behavior),
                           1: on-demand block partitioning
  WvKinBlockSize  (m)      target XY block edge length, DEFAULT=100
  WvKinBlockFreeT (s)      idle time before a block is freed, <=0 never,
                           DEFAULT=600

Parsed, validated (mode in {0,1}; size>0; fatal with WaveMod=6), and
mirrored into SeaSt_WaveFieldType as immutable config. No behavior
change: mode 1 does nothing yet. openfast_io reader/writer and user
docs updated; r-test submodule bumped to the companion branch with all
SeaState decks carrying the new lines.

Gate: seastate/hydrodyn/glue regression subset matches the clean-binary
baseline inventory; all SeaState module channel outputs byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGPxtJHRToYcregeghmrvn
…ematics block store (no behavior change)

Adds SeaSt_WaveBlockType / SeaSt_WaveBlockStoreType to the SeaSt_WaveField
registry, an owning &WaveBlockStore in the SeaState MiscVarType, and a
non-owning *BlockStore pointer in SeaSt_WaveFieldType (associated only when
WvKinBlockMod=1, wired before Waves_Init).

Generation seeds for later on-demand block population:
- Per-frequency seeds (WaveNmbrArr, OmegaIArr, MCFCArr) are captured inside
  VariousWaves_Init's frequency loop from the exact values it computes,
  guarded by ASSOCIATED(BlockStore) so WvKinBlockMod=0 executes nothing new.
  Recomputing them in another compilation unit is NOT bit-identical: moving
  WaveDispRel/WaveNumber to a new module changed gfortran inlining/FMA
  contraction and drifted seastate_wavemod5 outputs by 1 ulp (caught by the
  byte-identity gate, confirmed by bisect).
- Grid z levels and the steady current profile are captured post-init by
  WaveKinKernel_CaptureGridSeeds (pure copies).

New module SeaSt_WaveKinKernel.f90 also carries private verbatim copies of
the COSH/SINH kinematics helpers so the future per-column kernel compiles
in one unit with them, mirroring Waves.f90's current codegen.

Gate: seastate + glue regression subsets match the baseline inventory; all
19 SeaState module outputs byte-identical to the pre-change snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGPxtJHRToYcregeghmrvn
… shared by mode 0 and mode 1

VariousWaves_Init no longer assembles/IFFTs the per-point volume kinematics
itself: WaveKinKernel_ComputeColumns (module Waves, PUBLIC) computes first-
order dynamic pressure / velocity / acceleration (+MacCamy-Fuchs scaling,
+steady current, above-SWL/below-seabed zeroing, periodic last-step wrap)
for an arbitrary rectangle of grid columns, writing into caller-provided
column-local arrays. Mode 0 calls it over the full grid; mode 1 block
population (upcoming) will call it per block — one code path, so mode 1
results are bit-identical to mode 0 by construction.

Generation seeds (wave numbers, intrinsic frequencies, MCF coefficients,
direction cosines, grid coordinates, current profile) are captured inside
VariousWaves_Init from the exact values it computes, into the block store
(mode 1) or a routine-local store (mode 0). The full-size per-point
frequency-domain temporaries (7-10 complex arrays of NStepWave2 x NPoints)
are gone — the kernel uses per-column 1D work arrays, removing the init
memory spike.

Numerical note (user signed off): the restructured loop contracts FMAs
differently than the old code, shifting mode-0 outputs at single-precision
last-ulp scale vs dev (max ~1.0e-5 of channel RMS, 7/11 seastate cases;
all regression tests pass). Verified NOT a transcription bug: expressions
are verbatim, seeds are exact captures, and hosting the kernel in a
separate module vs in Waves.f90 makes no difference. Accepted in exchange
for a single shared generation path; the binding equivalence bar is now
mode 1 == mode 0 bit-for-bit.

The interim SeaSt_WaveKinKernel.f90 module (added in the previous commit)
is removed — the kernel must live in Waves.f90 anyway, and its dispersion-
helper copies are unnecessary there.

Gate: seastate subset 11/11; glue subset green except the known
pre-existing MHK_RM1_Floating_MR_Linear platform drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGPxtJHRToYcregeghmrvn
…l shared by mode 0 and mode 1

Waves2_Init no longer computes the second-order volume kinematics: the new
WaveKinKernel_AddSecondOrderColumns (module Waves2, PUBLIC) adds difference-QTF
and/or sum-QTF velocity / acceleration / dynamic-pressure corrections into
caller-provided column-local arrays for an arbitrary rectangle of grid columns.
Mode 0 (SeaState.f90) calls it over the full grid right after the first-order
fill; mode 1 block population (upcoming) will call it per block — one code
path, so mode 1 second-order results are bit-identical to mode 0 by
construction. Waves2_Init retains the input checks and the WaveElev2 surface
correction.

Seeds: Waves2_CaptureKernelSeeds copies gravity + the QTF mode flags into the
block store (mode 1) or a routine-local store (mode 0); grid coordinates are
reused from the Task-3 capture when present. SeaSt_WaveBlockStoreType gains a
Gravity field. Per the Task-2 same-compilation-unit rule, the kernel lives IN
Waves2.f90 and carries private verbatim copies of the transfer functions
(TransFuncB/D, k_nm_+/-, COSHNumOvrCOSHDen); Waves2_Init keeps the ones its
elevation routines use.

The full-size Waves2 InitOutput temporaries (WaveVel2D/S, WaveAcc2D/S,
WaveDynP2D/S — up to six extra field-size arrays at init) and the
AddArrays_4D/5D step in SeaState.f90 are gone; Waves2_InitOutputType is
removed from the registry. Additions are performed per element in the same
order and rounding as before (2*x is exact; sum term combined as T1 + 2*T2
in SiKi before the add; the wrapped last time step keeps the historical
undoubled-diff quirk).

Gate: 5MW_OC3Mnpl_DLL_WTurb_WavesIrr (+_Restart) — the discriminating
WvDiffQTF+WvSumQTF cases — have 60/61 output channels byte-identical to the
pre-change binary, including every hydro load channel driven by the volume
kinematics. Only Wave1Elev (WaveElev2 surface correction, math untouched)
shifts by 1 single-precision ulp in ~half the samples — the same
gfortran-arm64 recompilation drift documented and signed off in the Task 3
commit. OC6_phaseII is WaveMod=0 (second order trivially zero): identical.
Seastate module subset 11/11 with all outputs byte-identical; offshore glue
subset green except the known pre-existing MHK_RM1_Floating_MR_Linear
platform drift. New byte reference for the three Waves2 glue cases lives in
baselines-task4/ (workspace-local).

Also folds in the vs-build project-file removal of SeaSt_WaveKinKernel.f90
that the previous commit missed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGPxtJHRToYcregeghmrvn
…kMod=1)

The feature goes live: with WvKinBlockMod=1 the full-domain volume arrays
(WaveDynP/WaveVel/WaveAcc/WaveAccMCF) are never allocated. The XY grid is
partitioned into blocks of whole grid cells (WvKinBlockSize snapped to cells,
>=8 cells, full Z and time extent); a block is allocated and filled on the
first wave-kinematics query that lands in it, by the same two column kernels
that fill the full domain in mode 0 -- so block contents, and therefore all
outputs, are bit-identical to mode 0 by construction.

Key correction to the planned design: the volume interpolation stencil is 4
points per dimension (GridInterpSetup4D, Indx = floor(p)+[-1,0,1,2], cubic in
the interior; all four indices are read even where edge weights are zero).
Blocks therefore store the point planes [c0-1 .. c1+2] around their cell range
(three shared planes between neighbors), and the owning block is derived from
the stencil's base cell m%Indx(2,dim), which guarantees every stencil point is
in-block.

New in SeaSt_WaveField.f90 (which now uses modules Waves and Waves2 for the
kernels -- acyclic):
- WaveField_BlockStore_Init: block layout from the 4D grid params, logged.
- WaveField_EnsureBlock: double-checked populate inside
  !$OMP CRITICAL(SeaSt_BlockPop) (first-order kernel, then second-order when
  QTF terms are enabled), population/peak-resident counters, one log line per
  population, last-access stamping (eviction comes next).
- WaveField_InterpVol: the single reader of volume data. All 16 accessor read
  clusters route through it; in mode 1 it shifts a local copy of the
  interpolation misc indices to block-local and interpolates the block arrays.
- WaveField_HasMCF: replaces ALLOCATED(WaveAccMCF) guards, which are wrong in
  mode 1 where the full-domain MCF array never exists (also fixes the guard on
  the extrapolation-stretching MCF surface correction, which keyed on the
  volume array).

Mode-1 init skips the volume allocations (Initial_InitOut_Arrays), the
full-domain MCF allocation and the full-grid kernel calls (Waves.f90), and the
full-grid second-order add (SeaState.f90). Still water (WaveMod=0) and
full-field kinematics file output (WrWvKinMod=2) force mode 0 with a notice;
WaveMod=6 remains a validation error.

Gate: mode 0 untouched -- seastate module outputs 22/22 byte-identical to the
Task-3 reference, 5MW_OC3Mnpl outb data-identical to the Task-4 reference.
Mode-1 twins bit-identical to their mode-0 siblings (output diffs confined to
the timestamp header): seastate_1 (single-block AND forced 3x3 multi-block),
seastate_WaveMod7_WaveStMod2 (extrapolation stretching), hd_MCF_WaveStMod2
(MacCamy-Fuchs + stretching), 5MW_OC3Mnpl_DLL_WTurb_WavesIrr (diff+sum QTF
populated per block), 5MW_OC4Semi_WSt_WavesWN (2x2 layout, one 269 MB block
resident instead of the full domain). Regression subset green except the known
pre-existing MHK_RM1_Floating_MR_Linear platform drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGPxtJHRToYcregeghmrvn
Add WaveField_SweepBlocks: deallocates any populated on-demand wave block
that has gone longer than WvKinBlockFreeT seconds of simulation time without
an access, reclaiming its memory until the query returns. Freed blocks are
regenerated deterministically by the same seed-driven kernels as the initial
population, so eviction never changes results.

Triggers, both under the SeaSt_BlockPop critical section so eviction never
races population: (1) after each block population, since resident memory just
grew; (2) a throttled sweep on any access, at most once per max(WvKinBlockFreeT,
60 s). The fresh block's LastAccess is stamped before the population-triggered
sweep so it can never evict itself. WvKinBlockFreeT <= 0 disables freeing.

Verified: HydroDyn driver, translating monopile (WaveDisp=1) sweeping a fine
10-block grid, triangle surge -90..+90..-90 over 60 s. Eviction on (FreeT=5):
18 populations / 15 evictions with repopulation of previously-freed blocks;
eviction off (FreeT=-1): 10 populations / 0 evictions. Output byte-identical
between the two (max abs diff 0.0, all channels) — deterministic-regeneration
proof. seastate module suite 11/11 pass (mode 0 untouched by construction).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Print, in both modes at the end of SeaSt_Init: the volume grid dimensions and
component count, the full-domain volume footprint (what mode 0 allocates / mode 1
serves on demand), the eager surface-array footprint, and — in on-demand mode —
the block layout with the per-full-block size and the init-time populated count
(0, since blocks are lazy). Volume bytes are computed from the grid dimensions
(so the figure is correct in mode 1 where the arrays are never allocated); surface
bytes are summed from the allocated eager arrays via storage_size()/8*size().

Self-contained in a BLOCK construct (local decls, no new routine); output-only,
so channel results are unchanged. seastate module suite 11/11 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the "guidance added in the future" placeholder with a measured
sizing section backed by block-size x free-time sweeps on 5MW OC4-Semi
and MHK_RM1_Floating glue-code cases.

Key measured facts documented:
- Block halo (3-plane stencil overlap) makes small blocks counterproductive:
  size 50 m split the MHK footprint into 3 blocks and used ~17% MORE memory
  than the full-domain default.
- size 100-200 m gives 1 resident block at parity-to-modest savings
  (OC4-Semi: -5% peak at the 100 m default).
- WvKinBlockFreeT has no effect for station-keeping platforms (static query
  footprint never goes idle); eviction only helps a migrating query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… blocks)

WvKinBlockMod=1 blocks previously partitioned only X/Y and stored the full
Z extent per block. Extend the same constant-cell tiling machinery to Z,
using the mean Z spacing to snap the single WvKinBlockSize input to a
cube edge for all three axes. The shared first- and second-order per-column
kernels gain iPtZ0/nPtZ sub-range parameters; mode-0 callers still pass the
full Z range so mode 0 stays byte-identical, and mode 1 fills each block
through the same kernels over a Z sub-range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bookkeeping only (no FP code touched): distinguishes never-populated
blocks from evicted ones for the upcoming block VTK export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One legacy-VTK rectilinear grid per frame, one cell per block, BlockLife
cell scalar: -1 never populated, 0 evicted, (0,1] normalized time-to-evict
(pinned at 1 when eviction is disabled). Sets mvtk%nData explicitly because
vtk_dataset_rectilinear never sets it and CELL_DATA needs the cell count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Optional trailing driver-file line (absent => off); writes the block
partition VTK every N driver steps via WaveField_WriteBlockVTK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Called from WrVTK_Surfaces next to the wave-surface writer, same frame
numbering; internally a no-op unless WvKinBlockMod=1, so no new inputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the stale "Target XY edge length" phrasing (pre-Z-partition) in the
registry files, generated types, input-parser comment, openfast_io writer, and
docs appendix; bumps r-test to the matching deck-comment update (d4f6532).
Comment/doc strings only, no code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P66f1hCsQru66dxcmEwPMm
@mayankchetan mayankchetan added Type: Enhancement Module: SeaState SeaState module for wave data generation labels Jul 12, 2026

@jjonkman jjonkman 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.

Super cool that AI made this PR come together very quickly!

See a couple comments below.

In the conversation, what does "mode 1" mean in the following?:

Guards: WaveMod = 0 and WrWvKinMod = 2 force mode 0; WaveMod = 6
(externally-supplied full kinematics) with mode 1 is a fatal input error.


**NZ** sets the number of grid points in the vertical *Z*-direction from *Z* = (\ **MSL2SWL** − **Z_Depth**\ ) to *Z* = **MSL2SWL**\ . The distribution of grid points in the *Z*-direction is not uniform. It instead follows a cosine distribution: *Z*\ [\ *n*\ ] = **Z_Depth**\ (cos(\ *n*\ ·d\ *θ*\ )–1), where *n* = 0,1,…,\ **NZ**\ -1 and d\ *θ* = *π*\ /(2(\ **NZ**\ -1)). This distribution places more grid points near the free surface. **NZ** must be greater than or equal to 2.

**WvKinBlockMod** selects whether the wave-kinematics volume-data grid is precomputed over its full domain (**WvKinBlockMod** = 0, the default/current behavior) or partitioned into on-demand cube blocks (**WvKinBlockMod** = 1). When on-demand block partitioning is enabled, the wave-kinematics grid is divided into blocks partitioned in *X*, *Y*, and *Z*; each block is computed and stored only once wave kinematics are first requested at a point inside it. This reduces initialization time and memory use for domains that are much larger than the region actually queried during the simulation. Results with **WvKinBlockMod** = 1 are identical to those with **WvKinBlockMod** = 0 to within solver precision.

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.

Why is WvKinBlockMod made a switch? Are you expecting additional options in the future? If not, this could be a TRUE/FALSE flag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode 1 is referring to WvKinBlockMod=1, but you are right, I should just change that to a TRUE/FALSE flag

@RBergua

RBergua commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

This looks very cool, @mayankchetan!

Just a couple of questions based on the visualization:

  1. There seems to be a random block active. I'm not sure if this is related to the visualization or if the block is active in reality for some reason.
image
  1. Looking at the initial video frame, it seems that the wave kinematics for the mooring lines are active (SeaState-MoorDyn coupling).
image

But when the simulation advances, the blocks for the mooring lines seem to not be active anymore. Again, I'm not sure if this is related to the visualization or the actual behavior.
image

@mayankchetan

mayankchetan commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

This looks very cool, @mayankchetan!

Just a couple of questions based on the visualization:

  1. There seems to be a random block active. I'm not sure if this is related to the visualization or if the block is active in reality for some reason.
image 2. Looking at the initial video frame, it seems that the wave kinematics for the mooring lines are active (SeaState-MoorDyn coupling). image But when the simulation advances, the blocks for the mooring lines seem to not be active anymore. Again, I'm not sure if this is reated to the visualization or the actual behavior. image

So,

  1. That's the anchor location, so the block is active to provide values to MoorDyn. The SeaBed is at 200m and the SeaState domain is until 25m depth.
  2. yes! for the example we used SEASTATE WaterKin in MD. I spoke to @luwang00 about this, the mooring lines go below the seabed (because the stiffness is finite) and will not query SeaState for data:

! Check if point is below the seabed

  1. Another cool observation, in the eviction case, you can see a few blocks active even after the turbine has moved away. Its because we have outputs in SeaState at those coordinates, so the block is considered "always" active.
image

@RBergua

RBergua commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Thanks for the feedback, @mayankchetan. One small follow-up question based on my understanding of the settings used:
Assuming that the Moordyn-SeaState coupling is enabled, SeaState 3D grid is defined up to 25 m deep, and the water depth is 200 m, is SeaState providing information for the line if it's outside the 3D grid defined? This would help me understand if the anchor location can indeed receive data from SeaState or not.

@mayankchetan
mayankchetan marked this pull request as ready for review July 17, 2026 15:37
@luwang00
luwang00 requested a review from Copilot July 17, 2026 18:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an optional on-demand block-partitioning mode for SeaState wave-kinematics volume data to reduce initialization memory by lazily populating (and optionally evicting) kinematics blocks, while preserving the existing full-domain precompute mode.

Changes:

  • Introduces WvKinBlockMod/WvKinBlockSize/WvKinBlockFreeT inputs and wiring for on-demand wave-kinematics blocks in SeaState (including restart packing/unpacking support).
  • Refactors shared wave-kinematics generation into per-column kernels reused by both full-domain precompute (mode 0) and block population (mode 1), including second-order additions.
  • Adds optional wave-block VTK visualization output (SeaState driver and OpenFAST glue hook) and documents the new inputs/behavior.

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
openfast_io/openfast_io/FAST_writer.py Writes the three new SeaState spatial-discretization inputs.
openfast_io/openfast_io/FAST_reader.py Reads the three new SeaState spatial-discretization inputs.
modules/seastate/src/Waves2.txt Updates registry text to reflect removal of full-size init-time temporaries.
modules/seastate/src/Waves2_Types.f90 Removes the Waves2_InitOutputType and associated pack/copy/destroy routines.
modules/seastate/src/Waves.f90 Adds shared first-order column kernel and refactors init to capture seeds + call kernel for mode 0.
modules/seastate/src/UserWaves.f90 Avoids allocating full-domain volume arrays when block mode is active.
modules/seastate/src/SeaState.txt Adds new input fields and misc-var block-store pointer to the registry definitions.
modules/seastate/src/SeaState.f90 Wires block store ownership, guards, seed capture, second-order integration, and memory reporting.
modules/seastate/src/SeaState_Types.f90 Adds new input fields and registry pack/unpack/copy support for the block store pointer.
modules/seastate/src/SeaState_Input.f90 Parses/validates the new inputs and stores them into p%WaveField.
modules/seastate/src/SeaState_DriverCode.f90 Adds optional driver-only WrBlockVTK trailing input and writes block VTK frames.
modules/seastate/src/SeaSt_WaveField.txt Adds block/block-store type definitions and new WaveField fields for block mode.
modules/seastate/src/SeaSt_WaveField.f90 Implements block layout, population/eviction, VTK export, and unified interpolation access.
modules/seastate/src/SeaSt_WaveField_Types.f90 Adds registry-generated types and pack/unpack/copy/destroy support for blocks/store.
modules/openfast-library/src/FAST_Subs.f90 Hooks block VTK writing into existing WrVTK=2 surface output path.
LICENSE Updates the Apache-2.0 appendix boilerplate copyright placeholder line.
docs/source/user/seastate/input_files.rst Documents new inputs, sizing guidance, and VTK visualization behavior.
docs/source/user/seastate/appendix.rst Updates the SeaState input-file structure example to include new inputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread LICENSE
identification within third-party archives.

Copyright {yyyy} {name of copyright owner}
Copyright 2025 NREL

**NZ** sets the number of grid points in the vertical *Z*-direction from *Z* = (\ **MSL2SWL** − **Z_Depth**\ ) to *Z* = **MSL2SWL**\ . The distribution of grid points in the *Z*-direction is not uniform. It instead follows a cosine distribution: *Z*\ [\ *n*\ ] = **Z_Depth**\ (cos(\ *n*\ ·d\ *θ*\ )–1), where *n* = 0,1,…,\ **NZ**\ -1 and d\ *θ* = *π*\ /(2(\ **NZ**\ -1)). This distribution places more grid points near the free surface. **NZ** must be greater than or equal to 2.

**WvKinBlockMod** selects whether the wave-kinematics volume-data grid is precomputed over its full domain (**WvKinBlockMod** = 0, the default/current behavior) or partitioned into on-demand cube blocks (**WvKinBlockMod** = 1). When on-demand block partitioning is enabled, the wave-kinematics grid is divided into blocks partitioned in *X*, *Y*, and *Z*; each block is computed and stored only once wave kinematics are first requested at a point inside it. This reduces initialization time and memory use for domains that are much larger than the region actually queried during the simulation. Results with **WvKinBlockMod** = 1 are identical to those with **WvKinBlockMod** = 0 to within solver precision.
call RegUnpack(RF, OutData%WvKinBlockMod); if (RegCheckErr(RF, RoutineName)) return
call RegUnpack(RF, OutData%WvKinBlockSize); if (RegCheckErr(RF, RoutineName)) return
call RegUnpack(RF, OutData%WvKinBlockFreeT); if (RegCheckErr(RF, RoutineName)) return
if (associated(OutData%BlockStore)) deallocate(OutData%BlockStore)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Module: SeaState SeaState module for wave data generation Type: Enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants