Skip to content

Commit 10c80e5

Browse files
EliEli
authored andcommitted
Fixed some precision things in vgrid that were causing edges to be duplicated. Added description of vgrid formats to SKILLS.md because they frequently cause confusion.
1 parent 402a23c commit 10c80e5

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

SKILLS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Vertical Grid
2+
3+
## vgrid.in Format vs schimpy Internal Representation (ivcor=1, LSC2)
4+
5+
### On-disk format (vgrid.in, version 5.10)
6+
7+
```
8+
1 ← ivcor (1 = LSC2 localized sigma)
9+
nvrt ← max number of vertical levels across all nodes
10+
kbp(1) kbp(2) ... kbp(n_nodes) ← 1-based bottom level index per node
11+
1 sigma(1,1) sigma(1,2) ... ← level 1 values for all nodes (one row per level)
12+
2 sigma(2,1) sigma(2,2) ...
13+
...
14+
nvrt sigma(nvrt,1) sigma(nvrt,2)...
15+
```
16+
17+
- **Sigma range**: −1 (bottom) to 0 (surface), monotonically increasing with level index.
18+
- **Level ordering**: level 1 is the deepest possible; level `nvrt` is the surface.
19+
- **Per-node bottom**: `kbp(i)` (1-based) is the first valid level for node i. Levels below `kbp` are padded with **−9.0** (sentinel for "not applicable").
20+
- **Right-justified**: valid sigma values occupy levels `kbp` through `nvrt`; everything to the left is −9.0.
21+
- **Output precision**: `%14.6f` — only 6 decimal places. Consecutive sigma values that differ by less than 5×10⁻⁷ will round to the same value and cause SCHISM to abort ("Weird side (3)").
22+
23+
### schimpy internal representation (`SchismLocalVerticalMesh`)
24+
25+
- **Left-justified**: `sigma[i, 0:Ni]` holds the `Ni` valid values; `sigma[i, Ni:]` is `NaN`.
26+
- **kbps is 0-based**: `kbps[i] = nvrt − Ni`. File writes `kbps + 1`.
27+
- **Same sigma convention**: values are in [−1, 0], bottom-first to surface-last within the valid slice.
28+
- **Sentinel mapping**: on read, −9.0 → NaN. On write, NaN positions are filled with −9.0.
29+
30+
### Pipeline internal convention (lsc2_v2.py)
31+
32+
- **Sigma in [0, 1]**: 0 = surface, 1 = bottom. Left-justified: `sigma[i, 0] = 0`, `sigma[i, Ni-1] = 1`.
33+
- **Conversion to SCHISM format**: negate (`-sigma`) then `flip_sigma` (reverses non-NaN slice). Result: [−1, ..., 0, NaN, ...] — bottom-first, matching `SchismLocalVerticalMesh` expectations.
34+
- **Gotcha**: `flip_sigma` reverses the non-NaN portion. If the input is a palindrome (e.g., `[0, -1, 0]`), the flip is a no-op and the malformed array passes through unchanged. This happens when dry nodes (depth ≤ 0) have uninitialized interface values that clip to symmetric patterns.
35+
36+
### Key invariants
37+
38+
1. After the full pipeline, each node's sigma must be **strictly monotonically increasing** when written to disk.
39+
2. Monotonicity must survive the `%14.6f` rounding — consecutive sigma must differ by at least ~1×10⁻⁶.
40+
3. The z-coordinate check in SCHISM is at **side midpoints** (averaging two nodes), so both nodes of every edge must individually be monotone.

schimpy/lsc2_v2.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,44 @@ def fix_sigma_pileups(
575575
"depth_m": D,
576576
})
577577

578+
# ------------------------------------------------------------------
579+
# Final pass: snap sigma to the output precision used by the vgrid
580+
# writer (6 decimal places, i.e. %14.6f). Without this, two sigma
581+
# values that differ by less than 5e-7 survive the TOL=1e-10 check
582+
# above but become identical after rounding in vgrid.in, which causes
583+
# SCHISM to abort with "Weird side (3)".
584+
# ------------------------------------------------------------------
585+
OUTPUT_DECIMALS = 6
586+
for i in range(n):
587+
Ni = int(NL[i])
588+
if Ni < 2:
589+
continue
590+
s = np.round(S[i, :Ni], OUTPUT_DECIMALS)
591+
# Ensure endpoints are exact
592+
s[0] = 0.0
593+
s[-1] = 1.0
594+
# Remove consecutive duplicates introduced by rounding
595+
keep = np.r_[True, np.diff(s) > 0.0]
596+
# Always keep the last entry (surface = 1.0)
597+
keep[-1] = True
598+
if not keep.all():
599+
s = s[keep]
600+
S[i, :] = np.nan
601+
S[i, :len(s)] = s
602+
NL[i] = len(s)
603+
logs.append({
604+
"node": i,
605+
"x": float(mesh.nodes[i, 0]) if have_xy else np.nan,
606+
"y": float(mesh.nodes[i, 1]) if have_xy else np.nan,
607+
"Ni_before": Ni,
608+
"Ni_after": len(s),
609+
"dupes_removed": int(Ni - len(s)),
610+
"bottom_drops": 0,
611+
"dz1_after_m": np.nan,
612+
"dz2_after_m": np.nan,
613+
"depth_m": float(depth[i]),
614+
})
615+
578616
df = pd.DataFrame(logs, columns=[
579617
"node","x","y","Ni_before","Ni_after",
580618
"dupes_removed","bottom_drops","dz1_after_m","dz2_after_m","depth_m"
@@ -1333,7 +1371,14 @@ def _dbg_levels(tag):
13331371
# Finalization: single forward guard + exact endpoints (+ strict nudge)
13341372
for i in range(n):
13351373
Ni = int(Nlevels[i]); D = depth[i]
1336-
if Ni < 2 or D <= 0.0:
1374+
if Ni < 2:
1375+
continue
1376+
1377+
# Dry / non-positive-depth nodes: assign uniform degenerate sigma
1378+
if D <= 0.0:
1379+
D_eff = max(D, 1e-6)
1380+
for k in range(Ni):
1381+
h[i, k] = D_eff * k / (Ni - 1)
13371382
continue
13381383

13391384
# Surface exactly 0

0 commit comments

Comments
 (0)