Skip to content

FF: Curl/Cartesian wake fixes and AWAE VTK wake-plane output - #3368

Open
andrew-platt wants to merge 61 commits into
OpenFAST:rc-5.0.1from
andrew-platt:b/FF_wake_start_noise
Open

FF: Curl/Cartesian wake fixes and AWAE VTK wake-plane output#3368
andrew-platt wants to merge 61 commits into
OpenFAST:rc-5.0.1from
andrew-platt:b/FF_wake_start_noise

Conversation

@andrew-platt

Copy link
Copy Markdown
Collaborator

Ready to merge

Feature or improvement description

This PR addresses multiple bugs and adds VTK wake-plane visualization capabilities for FAST.Farm's Curl/Cartesian wake models in the AWAE and WakeDynamics modules.

Bug fixes:

  • Fix missing Vx_wake2 initialization at T=0 for Curl/Cartesian wake models — the firstPass branch in WD_CalcOutput computed y%Vx_wake (polar) but never populated y%Vx_wake2, and the unconditional taper block overwrote it with zeros (update the solution before the convergence criteria control flow #32 taper fix also merged).
  • Fix edges of wake planes not being updated in Cartesian/Curled models (loop limits were one point short).
  • Fix uninitialized allocatable arrays in AWAE/WD.
  • Initialize values for Cartesian/Curl planes 0+1 in WakeDynamics.
  • Fix wake plane VTK output missing z contribution.
  • Fix smooth tapering from numDFull to numDBuff for curled wake.

New VTK wake-plane visualization:

  • Extract VTK wake-plane output routines into a new AWAE_vtk module (AWAE_vtk.f90), replacing inline routines in AWAE_CalcOutput.
  • Add wake-plane wireframe VTK output (Write_Planes_WireFrame, PlaneCorners helper).
  • Add per-plane VTK data outputs (Write_Planes_Data, Write_WakePlane_Data_File).
  • Add .vtk.series file writers for wireframes, wake-plane data, and disturbed-wind slices for ParaView time-series loading.
  • Move wake visualization VTK files into a subdirectory.
  • Rename VTK root paths and add separate wake VTK root.

Other changes:

  • Update MaxNumPlanes calculation for Cartesian/Curl models with larger safety factors.
  • Update FAST.Farm plan document to Rev26.
  • Add documentation for FAST.Farm wake extent and buffering.
  • Update .gitignore for VS Code and Docker build configs.

Impacted areas of the software

  • modules/awae/ — New AWAE_vtk.f90 module; changes to AWAE.f90, AWAE_IO.f90, AWAE_Registry.txt, AWAE_Types.f90, CMakeLists.txt
  • modules/wakedynamics/WakeDynamics.f90 (initialization, taper, edge update fixes)
  • glue-codes/fast-farm/FAST_Farm_Subs.f90 (MaxNumPlanes calculation)
  • docs/ — FAST.Farm plan (Rev26), wake extent documentation
  • vs-build/modules/AWAE.vfproj updated for new source file

Additional supporting information

The Curl and Cartesian wake models had several initialization and update issues that caused incorrect wake deficit values at startup and incorrect edge values during simulation. The VTK output refactoring moves wake visualization code into a dedicated module for maintainability and adds per-plane data output and ParaView .vtk.series index files for easier post-processing.

Generative AI usage

Substantial portions of the VTK output code and bug fix analysis were developed with AI assistance:

This PR description was generated with AI assistance:

Test results, if applicable

No test result changes. No r-test updates required.

  • r-test branch merging not required

andrew-platt and others added 22 commits May 12, 2026 13:07
Add Write_Planes_WireFrame and PlaneCorners helper to emit each
active wake plane's outline in the global frame using vtk_lines.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This is not complete, but contains some of the info.
The .vtk.series file is not working as expected yet.
The edge points on the wake planes were not getting updated.  The loop limits were one point short.
New guidance for calculating with larger safety factors, and different calculation for catesian+curl than polar.

Co-authored-by: Regis Thedin <registhedin@gmail.com>
WD: fix smooth tapering from `numDFull` to `numDBuff` for curled wake
- Create modules/awae/src/AWAE_vtk.f90 containing module AWAE_vtk with
  all wake-plane VTK output routines: PlaneAxes, PlaneCorners,
  Write_Planes_WireFrame, Write_Planes_Data, Write_WakePlane_Data_File,
  Write_WakePlane_Series, and Write_NullPlane.

- Remove those routines from AWAE_CalcOutput's internal contains block
  and convert them to standalone module subroutines with explicit
  arguments (p, u, m, n, t, Tstr) replacing host association.

- Move the per-step Write_WakePlane_Series call from AWAE_CalcOutput to
  AWAE_End, writing series files once at shutdown instead of every output
  step.  Merge the former two-routine pattern (Write_WakePlane_Series +
  Write_AllWakePlane_Series) into a single Write_WakePlane_Series(p, m).

- Extract the inline null-plane block into Write_NullPlane subroutine.

- Remove unused variables: FmtStr argument, PlaneNum, fileExists,
  rootDir from the former Write_WakePlane_Series.

- Add AWAE_vtk.f90 to CMakeLists.txt and vs-build project; add
  'use AWAE_vtk' to module AWAE.

Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com>
…s files

Add a new routine Write_WireFrame_Series(p) to AWAE_vtk that writes one
ParaView .vtk.series index file per turbine for the WakePlanesWireFrame
VTK outputs. Called once at shutdown from AWAE_End alongside the existing
Write_WakePlane_Series call.

Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com>
Add Write_DisWind_Series to AWAE_vtk module to generate ParaView
.vtk.series index files for Low-resolution disturbed-wind XY, YZ,
and XZ output slices.  The routine is called from AWAE_End for each
valid slice, matching the existing pattern for wake-plane and
wireframe series files.

Co-authored-by: GitHub Copilot <noreply@github.com>
…dels

In WD_CalcOutput, the firstPass branch computed y%Vx_wake (polar) via
NearWakeCorrection but never populated y%Vx_wake2 for Cartesian/Curl models.
Additionally, the unconditional Cartesian/Curl taper block overwrote y%Vx_wake2
with xd%Vx_wake2 (still zeros) on firstPass, discarding any values set earlier.

Changes:
- Add Axisymmetric2CartesianVx conversion in the firstPass block of WD_CalcOutput
  to populate y%Vx_wake2 for Curl/Cartesian models.
- Guard the Cartesian/Curl taper block with (.not. OtherState%firstPass) so it
  does not overwrite the firstPass initialization.
- Add Mod_Wake guard and NearWakeCorrection call in InitStatesWithInputs for
  Cartesian/Curl state initialization.
- Remove unused GridTol parameter in AWAE.f90.

Co-authored-by: andrew-platt <andy.platt@nrel.gov>
andrew-platt and others added 7 commits July 29, 2026 21:25
Replace the adjacent-only OOB merge loop with a collect-then-merge
approach that checks all pairs of out-of-bounds planes for proximity.

- Collect OOB plane indices into a scratch array (O(N) scan).
- All-pairs proximity check among OOB planes only (O(nOOB^2), typically
  nOOB is very small).
- Work backwards from highest index so ShiftWakePlanesDown does not
  invalidate lower-index merge targets.
- Add AdjustOobIndices subroutine to maintain the OOB index list after
  each merge (remove entry, decrement indices above the dropped plane).

This handles non-sequential OOB planes that arise when plane passing
is allowed (e.g. drones or variable wind conditions).

Co-authored-by: GitHub Copilot <copilot@github.com>
Co-authored-by: Claude (Anthropic) <claude@anthropic.com>
Create modules/wakedynamics/tests/ following the same pattern used by
InflowWind and the other modules (a wakedynamics_utest.F90 driver plus
one file per test suite, registered in unit_tests/CMakeLists.txt as the
wakedynamics_utest target).

Move the two existing WD_TEST_* subroutines out of WakeDynamics.f90 and
into proper testdrive unit tests:
- WD_TEST_AddVelocityCurl -> test_addvelocitycurl.F90
  (test_add_velocity_curl)
- WD_TEST_Axi2Cart -> test_axisymmetric2cartesian.F90
  (test_axisymmetric2cartesian_vel)

These were previously dead code: public subroutines using print/STOP
for assertions, never invoked from anywhere in the codebase. They are
now real, runnable tests using testdrive's check(), matching the
convention already used by inflowwind_utest, aerodyn_utest, etc.

Since AddVelocityCurl and Axisymmetric2CartesianVel are private module
procedures, expose them directly as public (replacing the WD_TEST_*
public declarations) so the new test suites can call them, following
the same pattern used by IfW_FlowField's public CalcAccel routines.

Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ly out-of-bounds

WD_UpdateStates declared its local `oobIdx` array as `0:MaxNumPlanes-1`
(size MaxNumPlanes), but the OOB-collection loop can legitimately need
to record up to `MaxNumPlanes` out-of-bounds plane indices (one entry
per existing plane, if every plane simultaneously falls outside
p%LowResBounds). This wrote one element past the end of the array.

Fix: widen `oobIdx` to `0:MaxNumPlanes` (one extra unused slot).

While adding a test that forces every plane out of bounds at once
(exercising multiple simultaneous plane merges), a second, related bug
surfaced in the OOB-pair merge loop: after AdjustOobIndices() shrinks
nOOB and removes the merged entry, `iOOB` was never re-clamped, so a
later loop pass could index oobIdx() with a stale value beyond the
now-smaller nOOB, feeding a bad plane index into MergeWakePlanes /
ShiftWakePlanesDown and causing an out-of-bounds write there too.
Fixed by clamping `iOOB = min(iOOB, nOOB)` after AdjustOobIndices().

Add modules/wakedynamics/tests/test_wd_oobidx.F90: a regression test
that initializes WakeDynamics (Polar wake model) with a small
MaxNumPlanes, grows the plane count to the cap under steady inflow,
then tightens p%LowResBounds so every existing plane is simultaneously
out of bounds, and asserts WD_UpdateStates completes without error.
Confirmed this test crashes with a stack array-bounds error before
each of the two fixes above, and passes after both.

Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Previously, once a turbine's tracked wake-plane count reached
p%MaxNumPlanes, WD_UpdateStates re-issued the "exceeded the allowed
number" ErrID_Warn on every subsequent timestep for the rest of the
simulation (the clamp condition recurs every call once the plane count
saturates at the cap), flooding logs with an unbounded number of
identical warnings instead of a single, one-time notice.

Add OtherState%MaxPlanesWarned (WakeDynamics_Registry.txt), initialized
to .false. in WD_Init alongside the existing OtherState%firstPass
one-time flag. WD_UpdateStates now gates the SetErrStat(ErrID_Warn, ...)
call behind this flag and sets it once the warning has fired; the
NumPlanes clamp itself remains unconditional every call. Since each
turbine has its own WD module instance (and thus its own OtherState),
a single scalar flag naturally yields "once per turbine" without
needing per-turbine indexing.

Add modules/wakedynamics/tests/test_wd_maxplanes_warning.F90: drives
WD_UpdateStates well past a small MaxNumPlanes and asserts the warning
message appears exactly once across all calls, while NumPlanes stays
clamped at the cap throughout.

Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@andrew-platt andrew-platt added the ai assisted AI written with strong human guidance. label Aug 25, 2026
rthedin and others added 22 commits August 26, 2026 13:13
The high-res search requested NumDT*n_high_low-1 time slices, but the largest
high-res index actually read by AWAE_UpdateStates is (NumDT-1)*n_high_low
(i_hl is forced to 0 on the final low-res step), i.e. (NumDT-1)*n_high_low+1
slices.

For n_high_low=4 this over-requested by 2, so a run whose LES data exactly
covered TMax was rejected for missing two directories it never reads. For
n_high_low=1 -- legal, since DT_High need only divide DT_Low -- it
under-requested by one, which is harmless today but becomes an out-of-bounds
read once the directory index is looked up from a table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
amrex_find_subvols_c compared the directory index suffix against DirStartIndex
as a string. AMReX pads plotfile indices to a *minimum* width and widens beyond
it as needed, so once a run passes 99999 steps a six-digit index sorts before a
five-digit one: "100030" < "27150". Every such directory was silently skipped
and the run failed with a misleading count mismatch pointing at data that was
present on disk.

Parse the suffix to an integer and compare numerically. While here, harden the
extraction: anchor the prefix match at position 0, require the suffix be all
digits, and use stoll behind a try/catch. Previously a sibling directory such as
ffboxes_1_00010_bak threw std::invalid_argument across an extern "C" boundary,
terminating the process with no error message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReadWindAMReX formatted the directory index with a fixed-width I<DirIndexLen>
edit descriptor, where DirIndexLen comes from len_trim(DirStartIndex). AMReX
treats that width as a minimum and widens it as the step counter grows, so a run
starting at "27150" and reaching index 100030 wrote '*****' and failed to open
the directory.

Format with I0 and left-pad to DirIndexLen, so the width matches
amrex::Concatenate for any index magnitude.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add DirIndexLow(:) and DirIndexHigh(:) to AWAE_ParameterType, to hold the
plotfile directory index for each low- and high-resolution time step. These
replace the constant-stride assumption encoded in DirIndexDeltaLow/High, which
cannot represent LES output written with a varying solver time step.

Unused in this commit; populated and consumed in the following ones. The old
scalars are left in place so this commit builds standalone.

AWAE_Types.f90 regenerated with -DGENERATE_TYPES=ON.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… stride

amrex_find_subvols_c scanned the sub-volume directories, read each plotfile
header, validated that its time was a multiple of dt -- and then threw the
result away, returning only a first index and a single stride. That stride was
then required to be uniform across every directory.

The stride is an artifact of how the LES advanced its solver, not a property of
the data. AMR-Wind run with time.initial_dt = 0.025 until delay_time, then
fixed_dt = 0.05, writes boxes every 0.1 s throughout -- uniform in time to 1e-10
s -- but the index stride steps from 4 to 2 at the transition, and the run was
rejected.

Return the full time step -> directory index table instead. Each directory is
assigned the step round((header_time - start_time)/dt) and must land within
tolerance of it; a step claimed by no directory or by two is fatal. This
subsumes the old fmod filter, the count check and the stride check, and is
strictly stronger than all three: missing files and duplicates are now detected
exactly, and the residual test still skips deliberately decimated output.

The matching tolerance is max(1e-6, 1e-9*(|t0|+|t|)). The error being absorbed
is the drift a solver accumulates by summing its time step, which grows with
absolute simulated time -- a precursor restarted at t = 3e4 s carries far more
of it than one starting at zero -- so a fixed absolute tolerance is the wrong
shape. The floor preserves the previous behavior for runs starting near t = 0.

High-resolution sub-volumes are now compared to sub-volume 1 element by element
rather than by stride alone, which catches a sub-volume whose whole sequence is
offset from its siblings -- previously accepted, and silently supplying each
turbine a different instant in time.

ReadWindAMReX still builds names arithmetically in this commit; it is switched
to the table next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ting it

ReadWindAMReX rebuilt the directory name at every read as
DirStartNum + Delta*n, which is only correct when the LES wrote its plotfiles at
a constant step index stride. Read the index out of the table built during
initialization instead.

This is what makes variable-time-step LES output work end to end: with the old
arithmetic, data whose stride changed part way through would resolve to a
directory holding a *different* simulation time -- for a 4->2 stride change 1000
steps in, step 1001 wants t+100.1 s but the formula lands on t+100.2 s, and the
error grows from there. Silently, with no failure.

Also guard the lookup: an out-of-range step is fatal rather than clamped, since
clamping would freeze the inflow for the tail of a run instead of reporting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DirStartNum, DirIndexDeltaLow and DirIndexDeltaHigh existed only to reconstruct
directory names arithmetically. With the index table in place they have no
remaining consumer.

AWAE_Types.f90 regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five tests covering the behavior the preceding commits changed:

  variable_stride    stride 4 -> 2 with a uniform 0.1 s output interval, using
                     header times that carry the drift seen in real data
  wide_index         five-digit start index followed by six-digit indices
  missing_step       a gap must name the step, not just a count
  duplicate_step     two directories on the same time must be rejected
  out_of_tolerance   a near miss must be reported as such

variable_stride and wide_index fail on rc-5.0.1 and pass here; the other three
cover conditions the old code either mis-reported or accepted silently.

Fixtures are copies of the existing plotfile directories with only the Header
time and level_steps rewritten -- 130 KB total. The binary Level_0 data is never
opened by amrex_find_subvols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes to the search, both cost-only -- the directories selected are
identical.

Walk candidates in ascending index order and stop at the first one past the
requested window. Simulation time rises with the step counter, so nothing after
it can qualify. Previously the search read a header for every directory matching
the prefix no matter how short the FAST.Farm run: a 100 s window over a
precursor that had written 2200 low-res boxes read all 2200.

Read the plotfile Header directly instead of constructing a PlotFileData, which
also opens Level_0/Cell_H and builds a DistributionMapping to obtain a number
already present in the text. Grid validation is unchanged and still runs on
every directory -- it just takes its inputs from a cheaper source. The text
parse is validated against amrex_read_header_c on the start directory before
being trusted, and falls back to it on any parse failure, so a plotfile whose
layout does not match is handled rather than mis-read.

Measured on a 65-turbine dataset: the low-res search for a 100 s window went
from ~7.6 minutes to 0.85 s.

No OpenMP. The plan allowed for parallelising the scan, but after these two
changes the measurement no longer justifies the complexity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The AMReX section stated that the directory for time step n is
DirStartNum + n * Delta_index. That is no longer how the reader works, and
describing a constant stride as part of the file-naming contract is what led a
user to conclude their valid variable-time-step data was malformed.

Replace with the actual rule -- directories are matched by the simulation time
in their Header -- and state explicitly that the index stride may vary. Document
the matching tolerance, the exactly-one-directory-per-time-step requirement, and
the cross-sub-volume time consistency check.

No input file changes: DirStartIndex keeps its meaning and no field is added,
removed, or reinterpreted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
amrex_read_data_c called PlotFileData::get(level, varname) once per velocity
component, which re-reads the whole level each time. The cost of a read is set by
the number of boxes rather than the volume of data, so three passes cost three
times as much.

Read every component at once with get(level) and index the component off the
Array4 instead.

Measured against a low-resolution sub-volume written with 93456 boxes (a
528x472x24 grid at chunk_size_vec = 8 8 1): 63 s -> 16.5 s. On the
high-resolution boxes, where per-box overhead dominates a 48 KB array, 55 ms ->
7 ms; that is the one that matters in aggregate, since FAST.Farm reads
NumTurbines * (n_high_low + 1) high-resolution boxes per low-resolution step
against a single low-resolution one.

Output is unchanged: dumping both readers over a real 68 MB low-resolution box
and a high-resolution box gives byte-identical results.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReadWindAMReX is where the 0-based directory index table is consumed and where
the index suffix is formatted, and neither had direct coverage. Four cases:

  lookup        step 1 of table [0, 6, 12] reads directory 00006, checked
                against a direct read of that directory rather than just the
                absence of an error
  out_of_range  a step past the table is fatal and names the available range;
                it must never be clamped, which would silently freeze the
                inflow for the rest of the run
  wide_index    DirIndexLen of five with index 100000 widens the suffix to six
                digits and reads the right box (the subvolwide fixtures)
  no_table      an unpopulated table is reported as such rather than looked
                up as zeros

Writing wide_index showed that pointing amrex_read_data at a directory that
does not exist aborts the process rather than returning an error: amrex::Error
calls abort() instead of throwing, so the try/catch in amrex_read_data_c never
fires. That is pre-existing and left alone here; the test was reworked to read
an existing fixture instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With AMREX_READER=ON and BUILD_TESTING=ON the amrex_reader_utest target failed
to link:

    ld: crt1.o: in function `_start': undefined reference to `main'

Both of its sources are Fortran and its entry point is a Fortran program, but
awaelib transitively pulls in the C++ awaelib_c, so CMake resolves the link
language to CXX and the C++ driver's crt1.o then looks for a C main. Pin the
linker language to Fortran and pull in the C++ runtime explicitly with -cxxlib,
which is what the Fortran driver needs to resolve the AMReX and awaelib_c
symbols.

Every other unit test target in this file links with the Fortran driver
already; this one only differed because of the C++ dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-cxxlib is an Intel Fortran option; gfortran rejects it, so any GCC build with
AMREX_READER=ON and BUILD_TESTING=ON failed at link. It is also unnecessary:
with the link language pinned to Fortran the target links cleanly with ifx
without it, which is how glue-codes/fast-farm handles the same awaelib ->
awaelib_c C++ dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…laimed

The scan walked candidates in ascending index order and stopped at the first
directory whose header time fell past the requested window, on the assumption
that time rises with the step counter. Within one run it does. But leftovers
from an earlier run with a different time step share the index base and carry
later times on lower indices, so a single stale directory could end the walk
before the valid data that sorts after it -- and the run then failed with a
spurious "no sub-volume directory was found for time step N" pointing the
user at data that was on disk. A killed run restarted with a smaller
initial_dt is exactly this case.

Keep the early stop, but only once every step has been claimed. While any
step is still missing, index order is not trusted to imply time order and the
walk continues; a missing step is reported only after the full scan. Clean
directories keep the same cost as before.

The new subvolstale fixture has such a leftover (index 00002 at t = 5.0)
ahead of the directories holding steps 1 and 2. It fails on the previous
commit with the message above and passes here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…range

The text-parse fast path accepted any ncomp >= 1, and amrex_read_data_c had no
component check at all, so on the fast path only the starting directory was
ever validated for ncomp == 3. A directory with fewer components then reached
fab(i, j, k, ivar) with ivar past the FAB's component count: an out-of-bounds
read yielding garbage velocities instead of an error. The text parser now
returns false for anything but three components (which sends that directory
to amrex_read_header_c, where it is reported), and amrex_read_data_c checks
nComp() before reading.

Also:

- The parser cross-checks level_steps against the directory index it was
  asked about; a mismatch means the name does not describe the contents, so
  fall back to the authoritative reader rather than trust the text.
- parse_dir_index deliberately uses stoll to cope with wide indices, but the
  result was narrowed to int unchecked. Both narrowing sites now fail with a
  clear message past the 32-bit range instead of wrapping.
- The comment claiming the parser is safe to call from several threads is
  dropped; the AMReX read path is serial and nothing here relies on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
amrex_find_subvols set ErrStat = ErrID_None only inside the ENABLE_AMREX_LIB
branch, so the #else branch called SetErrStat on undefined intent(out) dummies
(SetErrStat does ErrStat = MAX(ErrStat, ...)). The two older shims never
initialized them in either branch. Initialize in all three before the #ifdef.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The text-parse fast path only turns on when it agrees with amrex_read_header_c
on the starting directory, and none of the existing fixtures satisfy that:
they are hand-built subsets whose Header domain box (256x256x128) is much
larger than the box array in Cell_H (3x4x5), so every test took the slow path
and the parser had no coverage. Real Kynema/AMR-Wind sub-volume output is
tiled -- one FAB spanning the domain box -- and the fast path does engage on
it in production, which is where the measured speedup came from.

Add a fixture with that real layout: three consecutive high-resolution
snapshots as written by the sampler (headers, single FAB, three masked
velocity components, 0.1 s apart), with the FAB payload replaced by a known
field u = 1+i, v = 2+j, w = 3+k so the read can be checked exactly rather than
for plausibility. Expose the parser to Fortran as amrex_parse_header_text and
add four tests: the search and the read on the tiled fixture, agreement
between the text parser and amrex_read_header on it, and the disagreement on
the hand-built fixture that explains why those fall back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The AMReX section said every step 0..NumDT-1 needs a directory. That is the
low-resolution count; each high-resolution domain needs steps 0 through
(NumDT-1)*(DT_Low/DT_High). Also state that directories between required
steps are ignored, so the LES may write sub-volumes more often than FAST.Farm
reads them, provided the FAST.Farm step is a multiple of the output interval.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes for AMReX data handling for FAST.Farm
Follow-up to #33. Four issues found reviewing that work.

Match the sub-volume prefix on the entry name, not the whole path. The scan
was anchored with rfind(prefix, 0), but directory_iterator over "." hands
back entries as "./name", so a WindDirPrefix carrying no directory of its
own matched nothing beyond the starting directory and the run died with
"no sub-volume directory was found for time step 1" with every directory
present. Not reachable through FAST.Farm, which prepends PriPath (at least
"./") to a relative WindDirPrefix, but it is reachable for any direct
caller of amrex_find_subvols -- the unit tests among them.

Check the grid in amrex_read_data against the caller's array. The reader
writes the destination by grid index and had no idea how big it was, so a
plotfile describing a larger grid wrote past the end of it. The sub-volume
search does check this at initialization, but it may have taken the Header
text fast path, which reads the domain box while the read takes the union
of the box array -- the two agree on real sampler output and are only
verified to agree on the starting directory. Also require the boxes to
tile the grid: they are disjoint, so summing their volumes settles it, and
anything less leaves part of the destination holding the previous step.

Count claimed steps instead of rescanning the table for each directory
past the window. The scan stops at the first such directory once the table
is complete, so this cost nothing on a good dataset -- but when a step is
genuinely missing it never stops, making the walk O(directories x steps)
on exactly the path that was supposed to report the problem quickly.

Say which tolerance a near miss was measured against. The tolerance grows
with simulated time, so the value quoted in the message was not quite the
one applied to the directory being reported.

Also soften the "exactly one directory per step" claim in the docs and the
routine comment: the walk stops early once the table is complete, so a
stale duplicate sorting after the first directory past the window is never
read. The check is best effort, not exhaustive.

Tests: two added, both fail without the corresponding fix. All 21 AMReX
reader unit tests pass, as do the other six utest suites, and FAST.Farm
links.

Assisted-by: Claude Code:claude-opus-5[1m] [Bash] [WebFetch]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants