Skip to content

Output along ship tracks / mooring curtains#925

Merged
JanStreffing merged 7 commits into
mainfrom
feature/tracks
Jun 3, 2026
Merged

Output along ship tracks / mooring curtains#925
JanStreffing merged 7 commits into
mainfrom
feature/tracks

Conversation

@JanStreffing

Copy link
Copy Markdown
Collaborator

I wrote FESOM 2D curtain output. It works by providing a list of lat lon in a csv file. FESOM the computes the greatcircle connections. It desifies those lines and for each subsection picks the nearest neighbour fesom nodal center. Here is a first example of an track / curtained i picked with 7 coordiantes:

/home/a/a270092/esm_tools/namelists/fesom2/tracks/test_track_core2_transect.csv
# Multi-segment transect for the FESOM-2.7 ship-track / mooring curtain
# feature (io_tracks.F90, feature/tracks branch). Same 7 vertices used by
# track_densify_demo.py to visualize the densify-and-snap pipeline.
#
# Path: subpolar N. Atlantic -> Iceland -> Nordic Seas -> Fram Strait.
# At track_resolution_km = 5.0 (default), this densifies to ~639 dense
# samples that resolve to ~152 unique CORE2 mesh nodes.
#
# Format: lon, lat
-35.0,  55.0
-30.0,  57.5
-25.0,  60.0
-20.0,  62.5
-15.0,  65.0
 -8.0,  70.0
  5.0,  78.0
grafik grafik

One year of hourly test outputs at: /work/bb1469/a270092/runtime/awiesm3-develop/test_track_11/outdata/fesom

I have only tested the output via XIOS. Not without it.

@JanStreffing JanStreffing self-assigned this May 31, 2026
@JanStreffing JanStreffing added the enhancement New feature or request label May 31, 2026
Emit a 2D (depth x along-track) NetCDF curtain of any 3D node-based
FESOM field along a user-supplied polyline, at a user-supplied cadence.
Multiple tracks can run simultaneously and write to separate files.

Config (set in namelist.io's &nml_tracks block, or via XIOS XML
overrides in context_fesom.xml for XIOS-coupled runs):

  ltracks               master switch (default off)
  track_files           semicolon-separated list of CSV paths
  track_vars            semicolon-separated tracer per track (temp|salt)
  track_names           semicolon-separated names (default track1..N)
  track_resolution_km   densification spacing (default 5 km)
  track_output_freq     shared XIOS cadence ('1h', '3h', '1d', '1mo', ...)

For each track, io_tracks_register_xios runs the pipeline:
  1. parse the CSV polyline vertices on rank 0
  2. slerp each great-circle segment at track_resolution_km
  3. MPI_MINLOC nearest-neighbour: per rank scans strictly-owned mesh
     nodes; reduce picks the global owner with tie-break by lowest rank
  4. collapse consecutive duplicate gids
  5. build a sparse per-rank list (sorted by global position)
  6. register a runtime XIOS domain (track_<name>), grid
     (grid_3d_track_<name>), field (<var>_track_<name>) and a <file>
     inside the shared "fesom_tracks" file_group

io_tracks_send packs the configured tracer at owned mesh nodes and
calls xios_send_field every timestep; XIOS gates the actual write at
freq_op. Output file pattern: <var>_track_<name>.fesom_<year>-<year>.nc
with shape (time, cell, nz).

Files:
  src/io_tracks.F90        new module
  src/io_xios.F90          xios_getvar() for the six config variables
                           plus io_tracks_register_xios() call before
                           xios_close_context_definition
  src/io_meandata.F90      &nml_tracks namelist + ltracks added to
                           &nml_general
  src/fesom_module.F90     io_tracks_send() call in the time loop
@JanStreffing JanStreffing marked this pull request as draft June 1, 2026 16:50
@JanStreffing

Copy link
Copy Markdown
Collaborator Author

Putting this on draft while reworking to be in line with tripyview logic.

@JanStreffing

Copy link
Copy Markdown
Collaborator Author

Nodal works with new logic:
grafik

Replaces the snap-to-nearest-mesh-node curtain sampler with a
geometry-aware pipeline ported from tripyview/sub_transect.py. For each
polyline sub-segment we find the mesh edges it crosses, then interpolate
each crossed edge's two endpoint values via V*(1-t)+V*t at the actual
intersection parameter t. Output samples lie on the line — no more
mesh-resolution-dependent zig-zag.

Changes:
  * src/mod_tracks_geometry.F90 (new, ~750 LOC pure Fortran). Public
    transect_t + analyse_transect: bbox + polar widening, signed-distance
    line-edge intersection, alternating up/down-section path with the
    triangle's edge_cross_dxdy used directly (1:2 LEFT, 3:4 RIGHT), and
    cumulative great-circle distance. No MPI / XIOS deps.
  * src/io_tracks.F90 rewrite around the new geometry kernel:
      - rank 0 calls gather_nod/_elem/_edge to assemble the global mesh
        once at init (same primitives io_mesh_info.F90 uses);
      - rank 0 runs analyse_transect; result broadcast to all ranks;
      - per-rank lid_n1/lid_n2 strict-own lookups built via a direct g2l
        table from myList_nod2D(1:myDim_nod2D);
      - sampling kernel: weighted contributions + weight-sum buffers
        (both MPI_Allreduced); rescale-by-weight so wet/dry edges fall
        back to the surviving endpoint instead of a half-amplitude
        partial sum; NaN where neither endpoint contributes;
      - per-level wet/dry mask via mesh%nlevels_nod2D(lid)-1;
      - round-robin m -> rank assignment for XIOS write ownership.
  * src/io_meandata.F90 / src/io_xios.F90: drop the now-meaningless
    track_resolution_km knob from the namelist binding and XML override
    block (sampling cadence is set by the mesh, not the user).
  * src/fesom_module.F90: pass mesh to io_tracks_send (needed for the
    per-level wet/dry mask).

io_xios_init's partit is now intent(inout) because the new gather path
requires it; matches the existing io_mesh_info pattern.

Verified by a 1-day CORE2 smoke run across four tracks (Atlantic->Fram
plus the three Polarstern MOSAiC ice-camp windows). Crossed-edge counts
log cleanly (242 / 187 / 60 / 24); curtains have no streaky-bottom
artifact from partial-wet edges.
@JanStreffing

Copy link
Copy Markdown
Collaborator Author

Old method vs new one:
grafikgrafik
Limitation of tripyview is that we don't do great circles. Needs more points in csv to get close. Can be improved later if desired.

@JanStreffing

Copy link
Copy Markdown
Collaborator Author

Mass transport from elements look alright.
grafik

Step 3 of the geometry-aware redesign: add support for u, v, and a
volume-transport diagnostic 'vflx' on the path-side (P-sized) XIOS
domain. Both temp/salt at edge crossings (node-based, M-sized) and u/v
at path triangles (element-based, P-sized) can now be requested in the
same track_vars list; per-track domain dispatch picks the right grid.

Sampling kernel (send_one_track_elem):
  * Reads dynamics%uv at strict-owned path elements (built via a
    global->local hash from myInd_elem2D_shrinked).
  * Rotates (u, v) from the mesh frame to geographic at each path
    centroid via vector_r2g, gated on rotated_grid .or. force_rotation.
  * Per-level wet/dry mask via mesh%nlevels(eid) - 1.
  * MPI_Allreduce on both value and contribution-count buffers; element
    ownership is unique, so the count is 0/1 and post-reduce mask sets
    NaN where no rank contributed (boundary slot or dry).

vflx formula matches tripyview's calc_transect_Xtransp do_nveccs=True
(sub_transect.py:1371-1382):
    Vflx_cell = (u * |path_dy| * nvec_x  +  v * |path_dx| * nvec_y) * helem

Cross-checked against tripyview on the same year-1 monthly-mean u/v from
this run: tripyview reports +149.27 Sv mean ACC throughflow across a
2-waypoint Drake Passage line; io_tracks vflx 3-hourly aggregated to
year-1 mean = +150.2 Sv (agreement < 1 Sv). The earlier formula
"u*dy + v*dx" without the section-normal projection gave -20 Sv, both
wrong sign and wrong magnitude; that was a real bug in this commit's
predecessor, now fixed.

Other changes:
  * track_t extended with path-side state: P, path_ei, path_dx/dy,
    path_lon/lat, path_nvec_x/y, lid_elem, nL_p, my_p, i_index_p.
  * track_t carries a var_kind flag (1 = node, 2 = elem) plus uv_comp
    (1 = u, 2 = v, 3 = vflx) to select the sampler and output domain.
  * broadcast_transect_min now also distributes path_ei, path_dx/dy,
    path_centroid_xy and path_nvec_cs for element-based tracks.
  * register_xios_objects_for_track dispatches: node-based ->
    track_<name> domain at M cells; element-based -> path_<name>
    domain at P cells; ni_glo and i_index sized accordingly.
  * io_tracks_send threads dynamics%uv through; fesom_module updated
    accordingly.
  * 'u', 'v', 'vflx' accepted as track_vars values alongside temp/salt.

Tested with a 7-track config (temp/u/v on a transect plus 3 mosaic temp
plus vflx on Drake) across CORE2 LR. Element-based u/v curtain shows
the expected EGC/NwAC signatures on the Atlantic->Fram section. Year-1
Drake ACC = +150 Sv with the right 30-day spin-up plateau.

Performance: the path-side machinery doubles per-track broadcast cost
(~negligible, <100 kB extra) and adds one extra MPI_Allreduce per send.
Node-based temp/salt tracks pay nothing for the new code.
@JanStreffing

JanStreffing commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

Status of the geometry-aware redesign

What works

  • Pure-Fortran geometry kernel mod_tracks_geometry.F90, ported from tripyview/sub_transect.py (bbox + polar widening, signed-distance line-edge intersection, alternating triangle path, cumulative great-circle distance). Uses FESOM's in-memory mesh%edge_cross_dxdy(4, edge2D) directly as tripyview's edge_dxdy_l/_r, no reconstruction.
  • Rank-0 gather of global topology via gather_edge/gather_elem/gather_nod, broadcast of the small transect_t to all ranks. Per-rank strict-own lookups (global → 1..myDim_*) for nodes (myList_nod2D(1:myDim_nod2D)) and elements (myInd_elem2D_shrinked).
  • Node-based sampling at edge crossings (temp, salt): per-edge V·(1−t)+V·t with weighted Allreduce; lone-endpoint wet/dry edges fall back to the surviving value instead of a biased partial sum; per-level nlevels_nod2D mask; NaN where neither endpoint contributes.
  • Element-based sampling at path triangles (u, v, vflx, hflx, sflx): velocities rotated to geographic via vector_r2g at each centroid; vflx = (u·|dy|·nvec_x + v·|dx|·nvec_y)·dz matches tripyview's calc_transect_Xtransp do_nveccs=True formula bit-for-bit. hflx/sflx multiply vflx by element-averaged T/S and the standard ρ₀·cp / ρ₀/1000 constants.
  • XIOS register dispatches on var kind: node-based → track_<name> domain at M cells; element-based → path_<name> at P cells. One fesom_tracks file_group, one file per track.
  • Cross-check vs tripyview on identical monthly-mean u/v: io_tracks 1-year mean = +150.4 Sv, tripyview = +149.3 Sv at the same Drake CSV. 2-year mean = +151.0 Sv (cold-start CORE2 LR). Spatial structure (deep-jet core, weak westward returns) matches expectations.
  • 7-track smoke configs work cleanly: 4 node-based temp (CORE2 Atlantic→Fram transect + 3 MOSAiC ice-camp windows) + 2 element-based u/v on the transect + 1 vflx on Drake.
  • All cleanup from the round-2 review folded in: weight-rescaled wet/dry, per-level nz_wet mask, MPI-safe par_ex instead of abort, vec_autorotate-gated rotation at the path centroid, polar bbox widening per tripyview.

What doesn't (yet)

  • Variable list is hardcoded: temp, salt, u, v, vflx, hflx, sflx. Adding any other field (e.g., ssh, w, bolus_*, passive tracers) needs an explicit case branch + the right Fortran array reference. A generic tracer:N mode would unlock any FESOM tracer index in ~20 LOC.
  • Sub-segments are straight in lat-lon, not great-circle (inherited from tripyview's planar line equation a·lon + b·lat + c = 0). Identical to the rhumb line for constant-lat sections, identical to the geodesic for constant-lon, but diverges meaningfully for long two-waypoint high-latitude sections. Densely sampled CSVs (e.g. MOSAiC ~220 waypoints) are unaffected. Fix is a spherical line-equation swap (~60 LOC) gated by a track_geodesic flag — open item, not blocking.
  • track_resolution_km was removed — sampling cadence is mesh-driven now.
  • insert_landpts for inserting NaN slots between consecutive boundary cuts is currently a stub; the within-segment ghost-slot logic in add_upsection/add_downsection covers the common case, but a transect that exits and re-enters the ocean repeatedly might miss a NaN gap. Easy to add when needed.
  • No standalone Fortran unit harness — validation today is via running the real model and diffing against tripyview on the same NetCDF u/v output. The plan called for a 1e-10 geometry diff against tripyview; not yet wired up.

Extend the element-based path-side sampler with two derived transport
diagnostics:

  * hflx -> per-cell heat transport in PW, as
      vflx_cell * T_at_element * rho0 * cp * 1e-15
  * sflx -> per-cell salinity transport in kg/s, as
      vflx_cell * S_at_element * rho0 / 1000

where vflx_cell is the same volume-transport formula already used by
the vflx variant (matching tripyview's do_nveccs=True). T/S at the
element centroid are averaged from the three vertex node values
(tracers%data(1) or (2)); halo node tracers are kept current by FESOM's
exchange, so the average works for any path element this rank strict-
owns regardless of which side of the partition its corner nodes lie on.

Wired through:
  * tracers passed into send_one_track_elem alongside dynamics/mesh
  * 'hflx', 'sflx' parsed as var_kind=2 with uv_comp=4, 5
  * tracer_idx=1 for hflx, 2 for sflx; same vector_r2g rotation and
    wet/dry mask as vflx
  * supported track_vars list updated in the parser error message

No extra MPI traffic vs vflx (one Allreduce per send). A single Drake
Passage CSV with track_vars='vflx;hflx;sflx' simultaneously gives
volume, heat and salinity transport at the same path.
@JanStreffing JanStreffing marked this pull request as ready for review June 3, 2026 07:44
…pts stub

Add or tighten subroutine headers across io_tracks.F90 and
mod_tracks_geometry.F90 following the convention that lines up with
MODFLOW / CESM / JULES / fortran-lang style guides:

  * every routine carries a short purpose line
  * public-API routines get the longer treatment with units, invariants,
    and side effects
  * internal helpers with self-documenting argument names drop the
    in/out arg listings -- the signature already says it

Remove the insert_landpts stub from mod_tracks_geometry. It was a port
target for tripyview's _do_insert_landpts (synthetic NaN slots between
two consecutive boundary edges when a polyline crosses a continent) but
was always a no-op return. The within-segment boundary case is handled
by push_ghost at build_path time; the cross-continent case is not
needed for any test transect we run. Left a one-line comment in
analyse_transect noting the missing feature.

Drop the v1-narrative leftover in the io_tracks module header.
@JanStreffing JanStreffing marked this pull request as draft June 3, 2026 12:11
For multi-segment polylines, the vflx / hflx / sflx formula needs the
local sub-segment normal at each path step, not the whole-transect
normal. Previously analyse_transect overwrote path_nvec_cs with a
single n_vec_tot for every path step, which gave correct results for
single-sub-segment polylines (Drake matched tripyview within < 1 Sv on
1-year monthly mean) but produced per-cell errors on bent polylines.

Four coordinated changes match tripyview's sub_transect.py line-for-line:

  * calc_csect_vec gains the conditional flip from tripyview:437-440
    -- going N (dy > 0) or W (dx < 0) gets the clockwise rotation,
    everything else the counter-clockwise rotation. seg%n_vec was
    previously computed unconditionally then never read.
  * subseg_t gains a path_nvec_cs(2, P) buffer.
  * build_path allocates that buffer and fills every path step inside
    the sub-segment with the sub-segment's own n_vec, matching
    tripyview:855-857.
  * concat_subtransects vstacks the per-sub-segment buffers into
    transect%path_nvec_cs, matching tripyview:386, 402.
  * analyse_transect drops the n_vec_tot overwrite at the end.

The whole-transect sign-flip on path_dx / path_dy (gated on n_vec_tot)
stays, matching tripyview:875. Diagnosis and validation plan in
runtime/fesom-2.7/meltpond_diag/path_nvec_cs_bug_analysis.md.
@JanStreffing JanStreffing marked this pull request as ready for review June 3, 2026 12:21
@JanStreffing

Copy link
Copy Markdown
Collaborator Author

@patrickscholz Ready for review.

@JanStreffing JanStreffing merged commit ef3f8a6 into main Jun 3, 2026
20 checks passed
@JanStreffing JanStreffing changed the title Output along ship trackes / mooring curtains Output along ship tracks / mooring curtains Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants