Skip to content

Merge upstream/development (IBSEB, erf-model/ERF#3960 and #4022) into ERF-Fire - #427

Merged
hgopalan merged 52 commits into
ERF-Firefrom
merge-ibseb-into-fire
Sep 15, 2026
Merged

hgopalan merged 52 commits into
ERF-Firefrom
merge-ibseb-into-fire

Conversation

@hgopalan

Copy link
Copy Markdown
Owner

Summary

Merges upstream/development into ERF-Fire to bring the immersed-boundary surface energy balance (erf-model#3960) and its single-precision follow-up (erf-model#4022). Only those two PRs are new on development since the last sync (#419). ERF-Fire's #425 is merged in as well, so the branch is current.

Not included

The branch stops at development's erf-model#4022 merge (e44fe5e02). Development has since merged three more PRs; they come with the next sync:

Conflict resolution

  • Argument lists only. Upstream added terrain_blank to ComputeTurbulentViscosity and ComputeDiffusivityMRF (the MRF erf.pbl_ib_aware path). The fork had added Q_fire_atm. Both functions now take qheating_rates, terrain_blank and Q_fire_atm. Each has a single call site, updated to match. Every argument is a MultiFab*, so the order was checked by hand at both call sites.
  • MRF work arrays. Both the fork's fire buoyancy flux (kbfs_fire_fab) and upstream's smoothing floor (pblh_floor_fab) are kept.
  • MRF diffusivities. The fork's Scalar_v clamp and dust Schmidt scaling stay. The length scale is stored as the absolute height (pblh_corr + zib), as upstream now does.
  • Sphinx index. Lists both theory/Fire.rst and theory/ImmersedBoundarySEB.rst.

Two small commits on top

  • MRF fire w* boost on immersed columns. With erf.pbl_ib_aware, the MRF passes read an immersed column's surface scales at its first fluid cell. The fork's fire boost still read the ground surface layer (density at klo, u_star, t_star, t10av), which comes from cells inside the solid there. It now reads the same effective scales. Without erf.pbl_ib_aware those scales are copies of the ground values and the surface cell is klo, so fire runs are unchanged. No deck yet combines fire with erf.pbl_ib_aware; the fire-to-building coupling follow-up adds one.
  • Case READMEs. Two IBSEB READMEs pointed at a design note that is not in the tree. The same fix went upstream as IBSEB case READMEs: no references to a design note erf-model/ERF#4023, with an identical hunk so the next sync merges cleanly.

Test plan

  • Debug build (MPI, fire, dust, tests, assertions, bound check, no FFT), as CI builds it: no warnings; 665 unit tests pass.
  • ctest -L regression on that build: 148 of 148 pass. This includes:
    • the new IBSEB_Cube and PBL_IBAware_MRF_Smoothing gold tests;
    • PBL_IBAware_MRF_Tiling and PBL_IBAware_YSUNew_Tiling;
    • ABL_MRF_Tiling and ABL_MRF_Tiling_Smooth;
    • FireMrfThermalExcess, which exercises the fire w* boost without pbl_ib_aware;
    • every fire and dust test, and the abort tests.
  • Release build (MPI, FFT, fire, dust) of the final head, Fire GPU CI: public kernel methods, host-device ROS functions, shadow warnings #425 included: no warnings; 665 unit tests pass.
  • Single-precision compile of the three files with resolved conflicts (ERF_ComputeDiffusivityMRF.cpp, ERF_ComputeTurbulentViscosity.cpp, ERF_AdvanceDycore.cpp): clean.
  • Sphinx: 0 warnings, and both theory pages build.
  • Codespell and git diff --check on the resolved and new lines: clean. The trailing whitespace git reports comes from upstream's own gold headers and READMEs, which are left as merged.
  • GPU lint on the two MRF-side files: the same findings as upstream development, none new. The MSVC scan of the added lines is clean.
  • Case scripts rerun with the Release binary of the final head, 4 ranks each. All pass (ALL PASS):
    • SEB/FaceStorage, SEB/Shortwave, SEB/Longwave, SEB/SensibleHeat, SEB/SlabConduction, SEB/PrognosticSkin and SEB/WallFunction;
    • RegTests/ImmersedForcingTest/PBL_IBAware and RegTests/ImmersedForcingTest/PartialCells.
  • Each case's per-step report lines equal the upstream head's run, resid_max excluded. The upstream run of WallFunction was on 2 ranks; it differs only in the tenth digit of the neutral deck's 0.07 W/m2 mean, and the 4-rank run here equals the README's 4-rank reference exactly.
  • SEB/IsolatedBuilding (2 h) and SEB/BuildingSet (1 h) were not rerun on this merge. Their code is identical to upstream's, where both were rerun, and the one source change specific to this merge (the MRF fire boost) is inactive in them.

🤖 Generated with Claude Code

Harish and others added 30 commits September 3, 2026 21:55
…ance

Eight phases on faces of resolved buildings: face storage, shortwave with
ray-cast shadowing, longwave through sky/ground/building view fractions,
sensible heat through the existing immersed-forcing wall model, slab
conduction, the prognostic balance, and two canonical cases. Radiation
reaches the balance through a provider interface (prescribed, or the
two-stream column when that branch is merged), so the branch stays based
on development and carries only its own commits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…balance

erf.ibseb.enable builds, on every level, a compact list of the wall faces of
the resolved buildings: a face between a fluid cell (blanking < 0.5) and a
solid cell (>= 0.5), stored once on the rank that owns the fluid cell as
device arrays (cell, direction, side, building id, material id, area, skin
temperature, slab temperatures, view fractions, fluxes), contiguous per
local box. Building ids label the solid columns of the blanking. Nothing
evolves yet.

Output through cell-centred fields on demand: ibseb_nfaces and ibseb_tskin
in the plotfile, IBSEBState in the checkpoint, refilled into the rebuilt
list on restart so restarts do not depend on the rank count. A [IBSEB]
summary line and a per-building CSV every erf.ibseb.csv_int steps.

Regtest Exec/CanonicalTests/SEB/Storage: the ImmersedForcingTest skyscraper;
face counts per direction checked against the plotfile mask, identical on
one and four ranks (2056 faces, 51400 m2), checkpoint at step 2 and restart
to step 4 reproducing the CSV row. Sphinx: inputs table and a theory page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
plot_storage.py draws, with yt, a horizontal slice of the mask and of the
faces-per-cell field through the building and vertical slices of the mean
skin temperature through its centroid, with the mask outline, in absolute
metres (ERF plotfiles carry no unit metadata) and with the y-normal slice
swapped to (x, z).

The four sources of the balance get file, class and function documentation
in the Doxygen style of the existing code, spelling out the face convention
(fluid cell, direction, side, outward normal), the ownership and per-fab
layout of the list, the building labelling, the six-slot checkpoint field,
and which phase fills which array.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
erf.ibseb.debug now prints, in the fire module's [FIRE DEBUG] style: the
inputs and the global face counts at build, each building's footprint
columns and index box, every rank's face count with its fab ranges and
device memory, a summary with per-building rows every step, and the
checkpoint save and load of the face state.

The regtests move to Exec/CanonicalTests/SEB/Phase<n>_<name>, starting with
Phase1_Storage, so the phase order is visible in the tree; the plan, the
theory page and the README follow. The y-normal slice of plot_storage.py
gets its labels after yt's axis swap, so they read x and z.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A prescribed radiation provider (erf.ibseb.radiation = prescribed) gives the
direct-normal and horizontal diffuse irradiances and the sun vector, either
fixed (sun_mode = fixed, for analytic tests) or from the site and time
(sun_mode = solar: Spencer solar geometry, Bird direct beam, a diffuse share
of the attenuated beam), through a single copied header so the branch has
no dependency on ERF-Radiation.

Per face and per step: direct = DNI max(0, n.s) unless the ray from the
face centre toward the sun hits a building, decided by a 2D walk over the
column tops of the level (a small array replicated on every rank; the ray
only rises, so a column blocks it when the entry height is below its top);
diffuse = f_sky D + f_ground albedo_ground (DNI cos z + D) with the
placeholder view fractions (roof: sky 1; wall: sky and ground 0.5 each);
absorbed = (1 - albedo) times the sum. Called at the start of every step.
New plot fields ibseb_sw_abs and ibseb_shadow, shortwave columns in the
per-building CSV, a per-rank face dump (erf.ibseb.dump_faces_file), and the
sun in the [IBSEB DEBUG] output.

Regtest Exec/CanonicalTests/SEB/Phase2_Shortwave: a short box 40 m east of a
tall one; the shadow flag of every face matches an independent Python ray
cast, the fluxes match the formulas on every face, the tall core roof and
west wall are unshadowed with the exact incidence, the shadow on the short
box's core west wall stops at H - gap tan(elevation), one and four ranks
agree, and the solar mode gives the solstice-noon zenith at Boulder. The
embedded-boundary reader steps each edge over one cell, which the test
reads from the dump rather than assuming.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… balance

Once at initialisation every face samples a cosine-weighted hemisphere
around its outward normal (erf.ibseb.view_n_az x view_n_el rays, stratified
so the counts are view factors) through the column walk of the shadow test
made direction-aware: a rising ray is blocked by a column whose top is above
its entry height, a falling ray by a column it descends into, and reaches
the ground otherwise. The fractions of rays ending on the sky, the ground
and a building replace the phase 2 placeholders in the diffuse shortwave.

Longwave per step: LW_in = f_sky LW_sky + f_ground eps_g sigma T_g^4
+ f_bldg sigma T_skin^4, the sky term fixed (erf.ibseb.lw_down) or gray
(sky_emissivity sigma T_air^4 with the air temperature of the face's fluid
cell, through the equation of state), the building term the isothermal-
surroundings approximation; LW_net = eps (LW_in - sigma T_skin^4). No
face-to-face view factors, no radiosity. New plot fields ibseb_lw_net and
ibseb_f_sky, longwave in the CSV and the face dump, the sampling summary in
the debug output.

Regtest Exec/CanonicalTests/SEB/Phase3_Longwave on the two-box deck: the
three fractions of every face sum to one and equal an independent Python
hemisphere sampling; the tall core roof sees only sky, no roof sees the
ground, the tall west wall sees exactly half sky with its rim ledge filling
a quarter of the view just above it and little 50 m up, the short box's
core west wall sees the tall box; the longwave formulas hold on every face
for both sky modes; one and four ranks agree. Phase 2 still passes with
the sampled fractions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Per face and per step: the tangential wind of the fluid cell at half a cell
from the wall gives u* with the roughness z0_wall, the skin-to-air
potential-temperature difference (the skin temperature converted with the
cell's Exner function) gives theta* with z0h_wall, and H = rho c_p u* theta*,
positive out of the face. Walls are neutral; erf.ibseb.stability_correction
applies the surface layer's similarity functions on roofs. The latent flux
stays zero with its argument in place.

The flux enters the atmosphere as an explicit source: every face deposits
H A / (c_p V Pi) into the rho-theta equation of its fluid cell, added after
make_sources at every slow stage with atomic adds; erf.ibseb.couple_heat =
false diagnoses without applying. The immersed forcing's own surface-
temperature inputs are refused when the balance is on, since it now owns
the temperature condition at the buildings. New plot field ibseb_H, the
flux in the CSV, the dump and the debug output, and H_total_W per step.

Regtest Exec/CanonicalTests/SEB/Phase4_Sensible: a 40 m cube held at 320 K
in an 8 m/s wind at 300 K. u* and H match the formulas on every face of the
dump, H is positive and largest on the windward wall, one and four ranks
agree, and with the flux applied the extra internal energy of the air
against the diagnostic run at the same step matches the summed face flux
(the domain is a rigid closed box, so it is c_v, not c_p, that closes). A
mass-inflow, pressure-outflow variant (as the Askervein canonical) runs
with a wake warmer than the inflow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every face carries a slab of erf.ibseb.n_slab_layers uniform layers between
its skin and the building interior at T_interior, solved implicitly with
the Thomas algorithm in the form of the SLUCM branch's slab solver, with the
skin temperature as the top boundary instead of a flux (ERF_IBSEBSlab.H);
G = 2k/dz (T_skin - T_0) is the conduction into the wall, positive inward.
Unconditionally stable, up to 256 layers, advanced every step.

Materials: erf.ibseb.material_file names a CSV in the SLUCM schema (rank-0
read, broadcast), material_default and material_by_building assign them by
building id; the conductivity, heat capacity, thickness, albedo and
emissivity become per-face arrays, and the shortwave and longwave now use
the per-face optical properties. Without a file the uniform k_therm,
rho_cp, thickness, albedo and emissivity apply. New plot field ibseb_G,
G in the summary and the dump with the material columns and the top and
bottom slab layers.

Regtest Exec/CanonicalTests/SEB/Phase5_Ground: a 200 mm slab in 1 mm layers
follows the semi-infinite erfc solution for a boundary step (bottom layer
300.925 K against 300.921 K expected at 50 s) with no flux through the skin;
a light 20 mm slab reaches G = k dT / L exactly with a linear profile; two
buildings carry the concrete and timber of the CSV; the slab and its flux
restart exactly through a checkpoint. The atmosphere-derived columns differ
by about 1e-4 after a restart, which is the immersed-forcing atmosphere of
development, not the balance, and is noted in the plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every face now solves SW_abs + eps Q_ext + LW_net - H - LE - G = 0 for its
skin temperature at the end of each step (ERF_IBSEBBalance.H, lifted from
the SLUCM facet solver). The conduction term is the flux the implicit slab
step will take, G = a T - b from two trial slab steps, so the balance and
the slab agree to rounding and the slab energy changes by exactly
dt (G - G_bottom). The wall term of the incoming longwave folds into the
emission as (1 - f_bldg); the wall-function coefficient is frozen at the
wind of the step. Q_ext is an incident external flux absorbed with the
emissivity, the hook for a fire's radiation (erf.ibseb.Q_ext_uniform for
tests). The bounds and the step cap are inputs since fire exposure exceeds
the urban canopy model's 380 K. erf.ibseb.prognostic defaults to true; the
phase 2-5 decks pin it false to keep checking each term on its own.

Fixes the solar azimuth of phase 2, which was mirrored east-west (sign of
the sine term); the noon check could not see it, the sunrise deck did.

Regtest SEB/Phase6_Prognostic on a cube: residual below 1e-8 W/m2 on every
face at every step, every stored flux consistent with the skin
temperature, slab energy exact per step, closure over the run within the
summed residual, an independent Python model (own Newton, dense implicit
slab) driven by the per-step face dumps reproducing the skin temperature
to 1e-9 K, an external flux run past 380 K, a checkpoint restart, and the
sun rising over the cube at Boulder with the east wall warming first.

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

A building from a nodal height map has sliver cells with 1-20 percent
solid at the corners of the reader's one-cell ramp. The drag on a partial
cell fades with its fraction, but the wall law was applied at full rate to
any cell above 0.005 solid whose normal neighbour was exactly zero, so the
top cell of a sliver column was forced like a wall on top of an almost
free cell. In a neutral 3 m/s run on a 40 m cube at 10 m and 0.5 s a
vertical two-cell checkerboard in theta grew there over hours (10 K, 5 m/s
spikes) until the floating-point trap fired after 1.8-2.5 h, with the
surface energy balance off, a no-slip ground, the forcing outside the
substeps and a coarser small-cell threshold as well.

With erf.if_snap_partial_cells the six forcing functions place the wall
law and the surface temperature, flux and Obukhov conditions on cells at
least half solid whose normal neighbour is less than half solid, the rule
the face balance uses; cells below half solid keep the fraction-weighted
drag only. Faces passing several wall tests at once have their relaxations
averaged so the explicit substep forcing stays within its limit. Default
false: with the switch off every result is bit-identical (checked on the
phase 6 closure deck).

Regtest Exec/RegTests/ImmersedForcingTest/PartialCells: the height-map
cube for 2.64 h with the switch on stays neutral to 1e-3 K with a wake;
--reproduce runs the original selection, which traps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Exec/CanonicalTests/SEB/Phase7_IsolatedBuilding runs a 40 m concrete cube
at Boulder on the June solstice from midnight for 24 hours in a light
westerly with the prescribed clear-sky provider and a gray sky. The cube
is an exact box (eb2.geometry = box) so the immersed forcing has no
partial cells; the per-building CSV gains the sun (zenith, azimuth, DNI,
diffuse) so the day can be plotted without debug output.

The day: every face radiates below the air at night, the roof coldest at
dawn; the east wall is the first face to rise above the air after
sunrise, forty minutes before the roof; the east wall peaks at 09:39, the
roof at 13:55 at 340 K two hours after the sun, the west wall at 16:45;
the south wall is 9 K warmer than the north at 13:00; conduction turns
around at 18:10 and the slab's stored 16.5 MJ/m2 keeps the roof above the
air past 23:00. The checker asserts that sequence, the balance residual
over the day (4e-8 W/m2), the absorbed shortwave on the roof against the
clear-sky formulas integrated independently in Python (24.882 MJ/m2 both)
and the slab energy against the integrated conduction (0.01 %). The plot
script draws the skin temperature by orientation with the air, the roof
budget, the sun path, a slab Hovmoller and yt slices.

Records under findings that the sensible flux off the 340 K roof is only
10-30 W/m2 with the neutral wall function, which the next PR addresses
with the stability functions and a convective velocity scale, both
behind switches; and that the restart non-exactness of the immersed
forcing persists after erf-model erf-model#3956.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The neutral log law on the tangential wind cannot shed heat from a hot
face in calm air, which is why the day canonical's roof reached 340 K.
Two switches, both off by default so every earlier result is unchanged:

erf.ibseb.convective_velocity = deardorff puts a convective velocity
scale into the wind the wall function sees, U_eff^2 = U_tan^2 +
(beta w*)^2 (Beljaars), with w* = (g/theta H/(rho c_p) depth)^(1/3) from
the previous step's flux out of the face. The depth is the mixed layer
above a roof (z_i - z_face, floored at the building height) and the
building height for a wall; z_i follows the day by the bulk Richardson
diagnostic on the horizontal-mean profile (erf.ibseb.z_i_mode = bulk_ri),
from the surface layer's pblh at the column (pblh), or fixed (z_i).

erf.ibseb.stability_correction now iterates Dyer's similarity functions
on the roofs to convergence on the face's own Obukhov length, under-
relaxed as the surface layer's iteration is in erf-model erf-model#3486, seeded
from the ground surface layer's 2D field at the column
(erf.ibseb.obukhov_seed). Walls stay on the log law. The face's L stays
its own because a roof in a separation zone or a sunlit wall can be in
the opposite regime from the ground.

The face dump gains w_star, Olen, z_i and h_bld; the summary gains
w_star_max. Regtest SEB/Phase8_WallFunction: the cube in calm air under a
strong sun; the neutral law sheds 0.2 W/m2 from a 342 K roof and the
scale 270 W/m2; w*, the depth, u* and H follow the formulas to 1e-9; the
roofs' L is negative and consistent with u* and theta* and the corrected
log law to 1e-7; the bulk Richardson depth on a capped sounding is the
first cell above the inversion. The default path is bit-identical to the
phase 6 reference.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dary aware

erf.if_snap_partial_cells now reads the cell and face blanking snapped to
solid or fluid at half, so a height-map building becomes the staircase of
whole cells an exact box is (wall faces fully blanked, interiors damped,
nothing on the sliver cells), and it selects the point-implicit drag: the
staircase puts full-strength forcing on every wall face, whose explicit
rate at rims and corners drives the density negative within seconds on a
thin slab (the first version, which only moved the selection threshold,
survived the cube for 2.6 h but killed a 3-cell slab in two minutes).
With the switch off every result is bit-identical. The PartialCells
regtest runs the height-map cube 2.6 h clean.

erf.pbl_ib_aware (MRF and YSUNew only; MYNN, MYJ, YSU and SHOC untouched)
makes each column's surface the first fluid cell above the immersed
solid: the bulk Richardson heights, the boundary-layer depth and the K
profile are measured from it, the diffusivities vanish inside the solid,
and the surface scales of a column with solid cells are a neutral log law
at its top with erf.pbl_ib_z0, since the ground surface layer evaluates
u*, theta* and L on cells inside the building. Without immersed cells the
results are bit-identical; not supported with terrain-fitted coordinates.
Regtest ImmersedForcingTest/PBL_IBAware: with the switch the schemes are
finite everywhere and zero inside the cube, and the profile over the roof
has the shape of the ground's against the local height (MRF peaks 15 m
above each); without it MRF fills the domain with NaN and YSUNew drives
the density negative at its second step, reported not asserted.

The balance prints a cost line (per-step time of the slowest rank, faces
per rank, initialisation time) for estimating city-scale cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Exec/CanonicalTests/SEB/Phase8_BuildingSet: four buildings from a nodal
height map on a 480 m periodic domain at 10 m (a 60 m concrete slab, a
40 m brick cube east of it, two 20 m timber blocks), three materials by
building, a 3 m/s westerly, Boulder on the solstice from 05:00 for six
hours with the prescribed provider, the convective velocity scale and the
stability functions on, and the immersed forcing snapped to whole cells.

Checked: the four buildings and their materials; the residual (3e-8 W/m2
over 1444 rows); the mutual shadowing (at sunrise the cube's shadow lies
on the slab's east wall, 17 percent of its faces, clearing to 6 percent;
the 20 m blocks free of shadow by late morning); the building view
fractions of the facing walls (28 and 48 percent) against the far block's
(18); the timber roofs ending at 333 K against the concrete slab's 320 K;
w* on every sunlit face and 93 percent of the roofs unstable; the cost
line (0.4 ms per step for 157 faces per rank). The plot script draws the
per-building temperatures, the shadow fractions, a face map from above
and a yt slice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…3 through a restart

The report row gained net-longwave and sensible-flux columns after phase 1
was written; they read the air temperature and wind, which the immersed
forcing does not restart bit-for-bit (2e-5 relative, see Phase5_Ground).
The geometry, skin and slab columns stay exact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Single-precision builds (Apple Clang and SYCL, Mesh SINGLE) refused two
brace-initialised arrays in ERF_IBFaceSet.cpp that narrowed double to
float: the face-centre coordinates and the cell-centred velocity. Both
are now built from Real expressions.

The IB-aware YSUNew kernels read the surface height into a local that
seven of them never used; those locals are removed (no change in
behaviour).

codespell flagged the Fourier-number variable "Fo" in the slab solver
and its Python re-implementation, and a "tha" alias in the phase 6
check; renamed to Fourier and th_air.

Phase 6 and the PBL IB-awareness regtests pass on the rebuilt binary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
plot_buildingset.py gains three figures from the existing run output:
every face coloured by the incident direct beam at 06:00, 08:00 and
10:00 solar time (a view from the south-east and a top-down map with
roofs as squares and wall columns as bars), and the horizontal wind in
the first two cells above the ground with a vertical slice of u through
the slab and the cube from the plotfile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts in the MRF and YSUNew kernels, where development's PBLH work
(erf-model#3486) meets this branch's immersed-boundary awareness (local surface
ksurf and effective surface scales us_eff/ts_eff/qs_eff/ol_eff):

- MRF passes 2 and 4: keep development's convective/shear w* blend,
  evaluated on the column's local surface (theta_v at ksurf) with the
  effective u* and theta*, as the branch does for every other surface term.
- MRF K-profile: development's ghost-cell guard and notes, with the
  branch's k < ksurf (no diffusivity inside a solid) condition.
- YSUNew K-profile: development's rho guard, plus the branch's ksurf and
  effective Obukhov length.
- VH96 shear correction (new on development, off by default): use the
  effective u*. On columns without immersed cells it equals u*, so runs
  without erf.pbl_ib_aware are unchanged.

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

- M_PI is undefined on MSVC (C2065 in ERF_IBSEBSolar.H, which failed the
  WIN32 jobs of the previous head); the solar geometry and the reports use
  PI from ERF_Constants.H.
- The reduced face counts were `long`, which is 32-bit on Windows and has
  no ReduceLongSum overload; they are amrex::Long.
- init_ibseb() aborts on a non-uniform vertical grid (the face areas,
  heights and the ray cast take the level's constant cell sizes) and on
  erf.regrid_int > 0 (the face list is built once from the blanking);
  ibseb_report() bounds its level loop by the face sets it holds.
- Inputs.rst states the grid and regrid requirements.
- The comments describe the routines instead of the development phases.

Verified: a Debug build with MPI, no FFT, all warnings, assertions and
bound checking is warning-free and runs the 471 unit tests; the Phase 6
closure, external-flux and restart checks and the PBL_IBAware regtest pass
on 2 ranks; the six touched translation units compile in single precision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The face detection takes every solid column of the blanking for a
building, so with erf.terrain_type = ImmersedForcing the terrain would be
put under the balance as well, which the documentation says it is not.
init_ibseb() now requires erf.buildings_type = ImmersedForcing and aborts
on terrain by immersed forcing; Inputs.rst says so.

IBSEB_Cube runs the Phase6_Prognostic cube (32x32x16, 40 steps, fixed sun,
prognostic skin, slab, heat flux into the air) and compares the plotfile
with its face diagnostics against a gold; nothing in CI exercised the
balance before. Passes in a Debug build with MPI and no FFT in 57 s on
2 ranks; a run with a different albedo fails the comparison.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolves the MRF conflict with erf-model#3972 (PBL passes on the tile work box):
the immersed-boundary work arrays of MRF and YSUNew now cover the same
grown tile box as the PBLH passes, and the blanking's halo is part of the
smoothing halo check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Development merged erf-model#3972 and erf-model#3992, which put the PBL passes on a grown tile
work box; the IB-aware per-column arrays now live on that box too (merge
commit), and this registers PBL_IBAware_MRF_Tiling and
PBL_IBAware_YSUNew_Tiling in the tiling-parity harness: the 40 m cube under
a heated surface layer with a capped mixed layer, so the PBL height differs
between the roof columns and open ground. Tiled and untiled runs agree
bitwise for both schemes. The solar header takes PI from
ERF_NumericalConstants.H, as the constants split of erf-model#3994 asks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…at they test

Source/ImmersedBoundarySEB/IBSEB_DEVELOPMENT.md is a development log; it
is removed from the branch and listed in .gitignore so a local copy stays
private, and nothing in the code or the documentation refers to it.

The cases under Exec/CanonicalTests/SEB are renamed after what they test
(FaceStorage, Shortwave, Longwave, SensibleHeat, SlabConduction,
PrognosticSkin, IsolatedBuilding, WallFunction, BuildingSet) and their
READMEs, decks, scripts and the theory page no longer describe the work
as numbered phases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Harish and others added 22 commits September 13, 2026 13:35
…start of the flux, slab bound

erf.if_snap_partial_cells: with every solid cell at 1 the roof test
(t_blank < t_blank_below) could not fire, the side-wall cells carried the
wall law and the interior drag together, and the thermal wall tests
(t_blank < neighbour) could not fire either. Under the snap the roof is the
top solid cell, a cell carrying a wall law or the roof drag is not an
interior cell, and the thermal wall tests read the neighbour blanking;
with the snap off every mask is as before.

erf.pbl_ib_aware: MRF measured its PBL height from the top of the immersed
column while YSUNew stored the absolute height; both now store the absolute
height (pblh, Lturb), so every consumer of get_pblh() and the balances
z_i_mode = pblh see one convention.

The bulk Richardson depth returns heights above the domain bottom in both
branches (the fallback was ProbHi, wrong when ProbLo(2) != 0).

The sensible flux is part of the checkpointed face state (six slots of
2 + n_layers), and the initial diagnostics of a restart no longer overwrite
it, so the Deardorff convective scale restarts exactly; the WallFunction
case checks it (step-599 dumps identical to 0.0 through a checkpoint at
step 300).

SLAB_MAX_LAYERS is 32 (was 256, 8 kB of stack per GPU thread) and
n_slab_layers is checked against it.

PartialCells rerun with the new masks: 19000 steps, theta within 0.000 K,
max |w| 0.48 m/s (was 0.60), wake -0.33 m/s; PBL_IBAware, the tiling
parity tests, IBSEB_Cube, the PrognosticSkin restart and 511 unit tests
pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 6 h run with the fixed masks and the checkpointed flux: roof means
at 11:00 move by 0.3 to 0.5 K (slab 320.2, north block 332.2, cube 323.7,
far block 332.1 K), w* 0.10-0.73 m/s on the sunlit faces; every check
passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ory page

ERF_IBSEBSolar.H carried a copy of the ERF-Radiation solar geometry so the
branch had no dependency on it; with erf-model#3950 merged the five functions are
wrappers around Source/Radiation/ERF_SolarGeometry.H (same Spencer
coefficients, hour angle, zenith, azimuth and Earth-Sun distance). The
sunrise case (10800 steps, sun_mode = solar) is bitwise identical before
and after: every report line and the final face dumps.

The longwave kernel uses the shared Stefan-Boltzmann constant instead of
a local constexpr captured into the device lambda; the reserved
two_stream provider note names erf.radiation_model; a figure caption in
the theory page ran into the next paragraph (Sphinx error).

Checks from the audit notes: the seven touched units compile in single
precision; no MSVC long/M_PI patterns in the diff; the IBSEB_Cube and
tiling decks leave no unused ParmParse keys; IBSEB_Cube split into 8-cell
boxes in x and y agrees with its gold to 2e-10 (a z split is refused by
the implicit vertical diffusion guard of erf-model#3998); the Debug tree of the
merged head builds without warnings and passes 553 unit tests and the
three IBSEB CTests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…length, dead input, validation, plotfile docs

Source/ImmersedBoundarySEB has a Make.package and is wired into
Exec/Make.ERF, so the GNU Make build compiles and links again
(Exec: make COMP=llvm USE_NETCDF=FALSE builds ERF3d.llvm.TEST.MPI.ex).

With erf.pbl_ib_aware the YSUNew K-profile used the height above the
domain bottom as the mixing length; it is now the height above the
column own surface (zq_kp1 - zib) in the unstable and stable branches,
and the stable z/L ratio is formed the same way. On the heated tiling
deck the first cell above the 40 m roof drops from Kmv 3.69 to 0.86 kg/m/s;
without immersed cells nothing changes (zib = 0).

erf.ibseb.utc_offset_hours cancelled algebraically in the hour angle and
had no effect; it is removed (the sun follows time_zero_utc_s in UTC and
longitude_deg), a deck that sets it aborts, and the case decks no longer
carry it. latitude_deg, longitude_deg and day_of_year are range-checked
instead of silently clamped; Inputs.rst states the ranges.

The eight ibseb_* plotfile variables are in Plotfile3DReference.rst, with
the erf.ibseb.enable gate and the per-cell face-mean meaning.

Verified: every erf.ibseb input perturbed on a 6-step run moves the face
state (44 inputs, the skin bounds once set inside the faces range); the
three rejections abort with their messages; PBL_IBAware, both tiling
parity tests, IBSEB_Cube and 553 unit tests pass; Debug build
warning-free; Sphinx builds the two pages clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed face blanking, smoothing frame, report steps, restart width

- The five solar functions live in ERF_IBSEBSolar.H again (erf-model#4004 replaced the shared header with the CESM orbital form, which has no azimuth); bitwise identical to the removed header over 1e6 samples; the header includes what it uses.
- With erf.if_snap_partial_cells the momentum kernels take a face's blanking from the two cells it joins (solid when either is), in both builds; a face between a solid and a fluid cell is wall-normal and gets the drag toward zero, a face between two solid cells the roof or wall law of its row or the interior drag, never both; the snap off is bit-identical; the terrain kernels no longer read the switch (their wall law is weighted by the fluid fraction).
- MRF smooths the PBL height in the absolute frame (zib on before ApplyPBLHSmoothing, off after) and both MRF and YSUNew keep the smoothed height at or above the column's own floor; new gold CTest PBL_IBAware_MRF_Smoothing (the previous binary fails it, Lturb 10 percent off).
- ibseb_report takes the number of completed steps (post_timestep's nstep + 1), so the step-0 report is written once and the rows land after csv_int, 2 csv_int, ...; the case scripts and READMEs follow.
- A restart whose erf.ibseb.n_slab_layers differs from the checkpoint's aborts naming both (VisMF header width); FaceStorage runs that negative case.
- Shortwave/Longwave rank-independence checks: geometry, view, shadow and shortwave exact, atmosphere columns to 1e-9 (the dump moved from step 1 to step 2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…k slab within the 32-layer bound, reference outputs rerun

- SlabConduction's thick-slab deck used 200 layers, outside the 32-layer bound set in the first review round; it is now 32 mm in 1 mm layers over 20 s (the wave from the interior travels 4 mm and the skin side stays untouched), README and checker text follow.
- PartialCells (max |w| 0.49 m/s, wake -0.47 m/s), BuildingSet (roof means about 2 K lower with the wall-normal faces damped, every roof unstable), PrognosticSkin (step labels, 201 dumps) and FaceStorage reference outputs rerun with the new binary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… snap, checkpoint field on column blocks, column map over the built box, vector shear, slab guard

- With erf.if_snap_partial_cells the boundary solid faces take the full log-law target (the partial-cell weight 1 - t_blank had made them no-slip); PartialCells now requires that erf.if_z0 changes the flow (0.20 m/s after 400 steps; the previous binary gave exactly 0).
- IBSEBState lives on 8 x 8 column blocks up to the highest face-owning cell around the buildings, with a transfer layer (this rank's grids cut by the blocks, AllGatherBoxes, ParallelCopy) so a restart works on any rank count; init_ibseb aborts on a checkpoint of other buildings; FaceStorage restarts the 4-rank checkpoint on 1 rank and rejects a rotated height map. On the 128^3 FaceStorage level the field covers 15616 of 2097152 cells (2.1 MB against 288 MB).
- The ray cast's column map, labels and building heights cover the bounding box of the built columns; the two ray walks look a column up through ibseb::column_top(), open ground outside; the shortwave and longwave dumps are unchanged.
- The bulk Richardson depth uses |U(z) - U_1|^2 of the wind vector; slab_skin_response guards N like advance_slab_dirichlet.
- Reference outputs rerun: PartialCells (w 0.57, wake -0.41), BuildingSet (roof means 319.6/331.1/323.0/331.0 K), PrognosticSkin closure lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follows the fourth review of erf-model#3960, which left the checkpoint scaling with
the built volume and the ray cast's column map replicated at full width on
every rank.

- IBSEBState goes on 4 x 4 column blocks clipped to the k-range that owns
  faces, instead of 8 x 8 blocks running from the ground to the roof. Face-
  owning cells are the fluid cells against solid, a one-cell shell, so a
  block over the interior of a wide footprint now keeps only the layer above
  the roof while a block on a wall still spans its height.
- The field carries one slot per face of a cell, numbered in the (dir, side)
  order build() already walks, rather than a fixed six; its width is the
  largest face count on any cell of the level, which build() reproduces from
  the same blanking on restart. Three nodal fields would have been the
  obvious way to drop the slot index, but nodal block boxes share their faces
  at block boundaries and the ParallelCopy of save_state() would have two
  destinations for one node with only one of them written.
  On FaceStorage the field is now 32 boxes over 7872 cells at 18 components
  against 15616 cells at 36, a quarter of the size; n_slots is 3 there
  because three faces meet on a cell at the rim, and it is never worse than
  the six the layout used to assume.
- The slot numbering is relative, so it would shift if a cell's face list
  depended on the decomposition. It cannot -- the blanking carries
  ComputeGhostCells() + 2 ghost cells -- and build() now asserts the ghost
  layer it rests on.
- The column map of the ray cast drops from three Real arrays plus a mask to
  one int array of the highest solid cell of each column: colmax was
  redundant (a column is solid exactly when its top is not -1), h_col_top
  held the same fact again as a height, and ibseb::column_top() now builds
  the height from the index. That is 4 bytes per built column per rank
  instead of 24, and one all-reduce instead of two. The flood-fill label and
  stack are scoped so they free before the per-face uploads.
- Docs and the FaceStorage README follow the new layout, and the theory notes
  record what still scales with the built area.

FaceStorage, Shortwave and Longwave pass on 1 and 4 ranks: the checkpoint
round trip and its redistribution onto another rank count, both restart
aborts, 0/2616 shadow mismatches against the checkers' independent ray cast
at both zenith angles with the recorded 738 and 644 shadowed faces, and
0/2616 view-fraction mismatches against the independent hemisphere sampling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Surface energy balance on immersed-boundary building faces (erf.ibseb), with immersed-boundary fixes for the forcing and the MRF/YSUNew schemes
…f-model#3960

The 24 h day passes unchanged; the table now starts from the initial state (step 0) and two last digits move with the relabelled dumps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
std::max(Real, double) does not deduce when Real is float (the Mesh SINGLE CI jobs); the height is formed in double and cast, which is identical in double precision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IBSEB: single-precision build of ERF_IBFaceSet.cpp, IsolatedBuilding reference table
… into ERF-Fire

Brings the immersed-boundary surface energy balance module and its single-precision
follow-up. Conflicts were argument lists only: ComputeTurbulentViscosity and
ComputeDiffusivityMRF take upstream's terrain_blank and keep the fork's qheating_rates and
Q_fire_atm (single call site each, updated); the MRF work arrays keep both the fire buoyancy
flux and the smoothing floor; the MRF Scalar_v clamp and dust Schmidt scaling stay, with
the length scale stored as the absolute height; the Sphinx index lists both theory pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With erf.pbl_ib_aware the MRF passes read the density, u*, theta* and the surface-layer
temperature at the first fluid cell of an immersed column (ksurf) instead of the ground
surface layer's values, which come from cells inside the solid there. The fire boost of
w* (erf.pbl_mrf_fire_thermal_excess) still read the ground values; it now takes the same
effective scales. Without erf.pbl_ib_aware the effective scales are copies of the ground
ones and ksurf is the bottom cell, so fire runs are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two READMEs pointed at a plan file that is not in the tree. BuildingSet now points at the
PartialCells regression test for the negative density without the snap; IsolatedBuilding
names the two inputs that add the stability functions and the convective velocity scale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hgopalan
hgopalan merged commit 63f2884 into ERF-Fire Sep 15, 2026
34 of 78 checks passed
@hgopalan
hgopalan deleted the merge-ibseb-into-fire branch September 15, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants