Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ add_library(wtm
src/update_effective_storativity.cpp
)
target_compile_options(wtm PRIVATE -Wfloat-conversion -Wall -Wextra -pedantic -Wshadow)
target_link_libraries(wtm PUBLIC richdem OpenMP::OpenMP_CXX fmt::fmt PkgConfig::PETSC )
target_link_libraries(wtm PUBLIC richdem OpenMP::OpenMP_CXX fmt::fmt PkgConfig::PETSC)
target_compile_features(wtm PUBLIC cxx_std_20)

add_executable(wtm.x
Expand Down
5 changes: 5 additions & 0 deletions src/CreateSNES.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ void InitialiseSNES(AppCtx& user_context, Parameters& params) {
DMSetUp(user_context.da);

user_context.make_global_vectors();
user_context.make_local_vectors();

DMSetApplicationContext(user_context.da, &user_context);
SNESSetDM(user_context.snes, user_context.da);

// Anderson mixing converges reliably without a Jacobian for this nonlinear problem.
// m=1 (1 history vector) is sufficient and avoids the instability seen with m>1.
// Override with -snes_type or -snes_anderson_m at runtime if needed.
SNESSetType(user_context.snes, SNESANDERSON);
SNESSetFromOptions(user_context.snes);
}
15 changes: 13 additions & 2 deletions src/CreateSNES.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ struct AppCtx {
Vec mask = nullptr;
Vec topo_vec = nullptr;
Vec rech_vec = nullptr;
Vec T_vec = nullptr;
Vec porosity_vec = nullptr;
Vec starting_wtd = nullptr;

// Local ghost vectors for fields accessed at neighbor indices in FormFunctionLocal
Vec topo_local = nullptr;
Vec fdepth_local = nullptr;
Vec ksat_local = nullptr;
Vec T_local = nullptr; // scratch: 1/T, computed over ghost range each F eval

// Extract global vectors from DM; then duplicate for remaining
// vectors that are the same types
void make_global_vectors() {
Expand All @@ -33,10 +38,16 @@ struct AppCtx {
VecDuplicate(x, &mask);
VecDuplicate(x, &topo_vec);
VecDuplicate(x, &rech_vec);
VecDuplicate(x, &T_vec);
VecDuplicate(x, &porosity_vec);
VecDuplicate(x, &starting_wtd);
}

void make_local_vectors() {
DMCreateLocalVector(da, &topo_local);
DMCreateLocalVector(da, &fdepth_local);
DMCreateLocalVector(da, &ksat_local);
DMCreateLocalVector(da, &T_local);
}
};

void InitialiseSNES(AppCtx& user_context, Parameters& params);
35 changes: 32 additions & 3 deletions src/DMDA_array_pack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,39 @@ void populate_DMDA_array_pack(AppCtx& user_context, ArrayPack& arp, DMDA_Array_P
for (auto i = xs; i < xs + xm; i++) {
dmdapack.cellsize_EW_squared[j][i] = arp.cellsize_e_w_metres[j] * arp.cellsize_e_w_metres[j];
dmdapack.mask[j][i] = arp.land_mask(i, j);
dmdapack.fdepth_vec[j][i] = arp.fdepth(i, j);
dmdapack.ksat_vec[j][i] = arp.ksat(i, j);
dmdapack.topo_vec[j][i] = arp.topo(i, j);
dmdapack.porosity_vec[j][i] = arp.porosity(i, j);
}
}
}

// Populate the global topo/fdepth/ksat vecs from arp and scatter to local ghost vectors.
// Must be called while these global vecs are NOT under DMDAVecGetArray (i.e., before or after
// DMDA_Array_Pack holds them — which it does NOT, by design).
void scatter_static_fields(AppCtx& user_context, ArrayPack& arp) {
const auto [xs, ys, xm, ym] = get_corners(user_context.da);
PetscScalar **topo_arr, **fdepth_arr, **ksat_arr;

DMDAVecGetArray(user_context.da, user_context.topo_vec, &topo_arr);
DMDAVecGetArray(user_context.da, user_context.fdepth_vec, &fdepth_arr);
DMDAVecGetArray(user_context.da, user_context.ksat_vec, &ksat_arr);
for (auto j = ys; j < ys + ym; j++) {
for (auto i = xs; i < xs + xm; i++) {
topo_arr[j][i] = arp.topo(i, j);
fdepth_arr[j][i] = arp.fdepth(i, j);
ksat_arr[j][i] = arp.ksat(i, j);
}
}
DMDAVecRestoreArray(user_context.da, user_context.topo_vec, &topo_arr);
DMDAVecRestoreArray(user_context.da, user_context.fdepth_vec, &fdepth_arr);
DMDAVecRestoreArray(user_context.da, user_context.ksat_vec, &ksat_arr);

// The DMDA's internal PetscSF is shared across all GlobalToLocal operations on the same DM.
// Overlapping Begin calls (Begin A, Begin B, End A, End B) confuse the SF state machine;
// each pair must be completed sequentially.
DMGlobalToLocalBegin(user_context.da, user_context.topo_vec, INSERT_VALUES, user_context.topo_local);
DMGlobalToLocalEnd(user_context.da, user_context.topo_vec, INSERT_VALUES, user_context.topo_local);
DMGlobalToLocalBegin(user_context.da, user_context.fdepth_vec, INSERT_VALUES, user_context.fdepth_local);
DMGlobalToLocalEnd(user_context.da, user_context.fdepth_vec, INSERT_VALUES, user_context.fdepth_local);
DMGlobalToLocalBegin(user_context.da, user_context.ksat_vec, INSERT_VALUES, user_context.ksat_local);
DMGlobalToLocalEnd(user_context.da, user_context.ksat_vec, INSERT_VALUES, user_context.ksat_local);
}
17 changes: 5 additions & 12 deletions src/DMDA_array_pack.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,23 @@
struct DMDA_Array_Pack {
PetscScalar** x = nullptr;
PetscScalar** cellsize_EW_squared = nullptr;
PetscScalar** fdepth_vec = nullptr;
PetscScalar** ksat_vec = nullptr;
PetscScalar** mask = nullptr;
PetscScalar** topo_vec = nullptr;
PetscScalar** rech_vec = nullptr;
PetscScalar** T_vec = nullptr;
PetscScalar** porosity_vec = nullptr;
PetscScalar** starting_wtd = nullptr;
const AppCtx* context = nullptr;

// topo_vec, fdepth_vec, ksat_vec are intentionally NOT held here.
// They are scattered to AppCtx local ghost vectors before each solve so that
// FormFunctionLocal can safely access neighbor indices across MPI boundaries.

DMDA_Array_Pack(const AppCtx& user) {
assert(!context); // Make sure we're not already initialized
context = &user;
DMDAVecGetArray(user.da, user.x, &x);
DMDAVecGetArray(user.da, user.cellsize_EW_squared, &cellsize_EW_squared);
DMDAVecGetArray(user.da, user.fdepth_vec, &fdepth_vec);
DMDAVecGetArray(user.da, user.ksat_vec, &ksat_vec);
DMDAVecGetArray(user.da, user.mask, &mask);
DMDAVecGetArray(user.da, user.topo_vec, &topo_vec);
DMDAVecGetArray(user.da, user.rech_vec, &rech_vec);
DMDAVecGetArray(user.da, user.T_vec, &T_vec);
DMDAVecGetArray(user.da, user.porosity_vec, &porosity_vec);
DMDAVecGetArray(user.da, user.starting_wtd, &starting_wtd);
}
Expand All @@ -32,16 +28,13 @@ struct DMDA_Array_Pack {
assert(context); // Make sure we are already initialized
DMDAVecRestoreArray(context->da, context->x, &x);
DMDAVecRestoreArray(context->da, context->cellsize_EW_squared, &cellsize_EW_squared);
DMDAVecRestoreArray(context->da, context->fdepth_vec, &fdepth_vec);
DMDAVecRestoreArray(context->da, context->ksat_vec, &ksat_vec);
DMDAVecRestoreArray(context->da, context->mask, &mask);
DMDAVecRestoreArray(context->da, context->topo_vec, &topo_vec);
DMDAVecRestoreArray(context->da, context->rech_vec, &rech_vec);
DMDAVecRestoreArray(context->da, context->T_vec, &T_vec);
DMDAVecRestoreArray(context->da, context->porosity_vec, &porosity_vec);
DMDAVecRestoreArray(context->da, context->starting_wtd, &starting_wtd);
context = nullptr;
}
};

void populate_DMDA_array_pack(AppCtx& user_context, ArrayPack& arp, DMDA_Array_Pack& dmdapack);
void scatter_static_fields(AppCtx& user_context, ArrayPack& arp);
27 changes: 21 additions & 6 deletions src/WTM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,13 @@ void update(
if ((params.cycles_done % params.cycles_to_save) == 0) {
// Save the output every "cycles_to_save" iterations, under a new filename
// so we can compare how the water table has changed through time.
arp.wtd.setNoData(-9999);
arp.wtd.saveGDAL(fmt::format("{}{:09}.tif", params.outfile_prefix, params.cycles_done));
// wtd is fully assembled on all ranks by FanDarcyGroundwater::update; rank 0 writes.
PetscMPIInt rank;
MPI_Comm_rank(PETSC_COMM_WORLD, &rank);
if (rank == 0) {
arp.wtd.setNoData(-9999);
arp.wtd.saveGDAL(fmt::format("{}{:09}.tif", params.outfile_prefix, params.cycles_done));
}
}

arp.wtd_old = arp.wtd; // These are used to see how much change occurs
Expand Down Expand Up @@ -228,9 +233,13 @@ void finalise(Parameters& params, ArrayPack& arp, AppCtx& user_context) {
std::ofstream textfile(params.textfilename, std::ios_base::app);

textfile << "p done with processing" << std::endl;
// save the final answer for water table depth.
arp.wtd.setNoData(-9999);
arp.wtd.saveGDAL(fmt::format("{}{:09}.tif", params.outfile_prefix, params.cycles_done));
// Save the final answer. wtd is assembled on all ranks; only rank 0 writes to avoid conflicts.
PetscMPIInt rank;
MPI_Comm_rank(PETSC_COMM_WORLD, &rank);
if (rank == 0) {
arp.wtd.setNoData(-9999);
arp.wtd.saveGDAL(fmt::format("{}{:09}.tif", params.outfile_prefix, params.cycles_done));
}

textfile.close();

Expand All @@ -244,9 +253,12 @@ void finalise(Parameters& params, ArrayPack& arp, AppCtx& user_context) {
VecDestroy(&user_context.mask);
VecDestroy(&user_context.topo_vec);
VecDestroy(&user_context.rech_vec);
VecDestroy(&user_context.T_vec);
VecDestroy(&user_context.porosity_vec);
VecDestroy(&user_context.starting_wtd);
VecDestroy(&user_context.topo_local);
VecDestroy(&user_context.fdepth_local);
VecDestroy(&user_context.ksat_local);
VecDestroy(&user_context.T_local);
}

int main(int argc, char** argv) {
Expand All @@ -269,6 +281,9 @@ int main(int argc, char** argv) {

DMDA_Array_Pack dmdapack(user_context); // this needs to come after initialise
populate_DMDA_array_pack(user_context, arp, dmdapack);
// Scatter topo/fdepth/ksat to local ghost vectors. These global vecs are not held by
// dmdapack so there is no GetArray lock conflict.
scatter_static_fields(user_context, arp);

run(params, arp, user_context, dmdapack);

Expand Down
42 changes: 30 additions & 12 deletions src/transient_groundwater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ int update(Parameters& params, ArrayPack& arp, AppCtx& user_context, DMDA_Array_
}
}

// Assemble the full wtd field across all MPI ranks. Each rank has updated
// only its owned cells; non-owned entries are left at their previous values.
// Build a buffer with owned cells non-zero and everything else zeroed, then
// sum across ranks so that every rank ends up with the complete correct field.
{
const int total = params.ncells_x * params.ncells_y;
std::vector<double> owned_only(total, 0.0);
for (int j = ys; j < ys + ym; j++)
for (int i = xs; i < xs + xm; i++)
owned_only[j * params.ncells_x + i] = arp.wtd(i, j);
MPI_Allreduce(MPI_IN_PLACE, owned_only.data(), total, MPI_DOUBLE, MPI_SUM, PETSC_COMM_WORLD);
for (int j = 0; j < params.ncells_y; j++)
for (int i = 0; i < params.ncells_x; i++)
arp.wtd(i, j) = owned_only[j * params.ncells_x + i];
}

return 0;
}

Expand Down Expand Up @@ -230,21 +246,23 @@ static PetscErrorCode FormFunctionLocal(DMDALocalInfo* info, PetscScalar** x, Pe
**my_porosity;

/*
Compute function over the locally owned part of the grid
*/
Compute function over the locally owned part of the grid.
topo/fdepth/ksat/T use local ghost vectors so neighbor accesses [j][i±1] are valid under MPI.
*/
PetscCall(DMDAVecGetArray(da, user_context->mask, &my_mask));
PetscCall(DMDAVecGetArray(da, user_context->cellsize_EW_squared, &cellsize_ew_sq));
PetscCall(DMDAVecGetArray(da, user_context->fdepth_vec, &my_fdepth));
PetscCall(DMDAVecGetArray(da, user_context->ksat_vec, &my_ksat));
PetscCall(DMDAVecGetArray(da, user_context->topo_vec, &my_topo));
PetscCall(DMDAVecGetArray(da, user_context->fdepth_local, &my_fdepth));
PetscCall(DMDAVecGetArray(da, user_context->ksat_local, &my_ksat));
PetscCall(DMDAVecGetArray(da, user_context->topo_local, &my_topo));
PetscCall(DMDAVecGetArray(da, user_context->rech_vec, &my_rech));
PetscCall(DMDAVecGetArray(da, user_context->T_vec, &my_T));
PetscCall(DMDAVecGetArray(da, user_context->T_local, &my_T));
PetscCall(DMDAVecGetArray(da, user_context->porosity_vec, &my_porosity));
PetscCall(DMDAVecGetArray(da, user_context->starting_wtd, &my_starting_wtd));

// Compute 1/T over the full ghost range so neighbor lookups in the owned-range loop below are valid.
#pragma omp parallel for default(none) shared(info, my_T, x, my_topo, my_fdepth, my_ksat) collapse(2)
for (auto j = info->ys; j < info->ys + info->ym; j++) {
for (auto i = info->xs; i < info->xs + info->xm; i++) {
for (auto j = info->gys; j < info->gys + info->gym; j++) {
for (auto i = info->gxs; i < info->gxs + info->gxm; i++) {
my_T[j][i] = 1. / depthIntegratedTransmissivity(x[j][i] - my_topo[j][i], my_fdepth[j][i], my_ksat[j][i]);
}
}
Expand Down Expand Up @@ -283,11 +301,11 @@ static PetscErrorCode FormFunctionLocal(DMDALocalInfo* info, PetscScalar** x, Pe

PetscCall(DMDAVecRestoreArray(da, user_context->mask, &my_mask));
PetscCall(DMDAVecRestoreArray(da, user_context->cellsize_EW_squared, &cellsize_ew_sq));
PetscCall(DMDAVecRestoreArray(da, user_context->fdepth_vec, &my_fdepth));
PetscCall(DMDAVecRestoreArray(da, user_context->ksat_vec, &my_ksat));
PetscCall(DMDAVecRestoreArray(da, user_context->topo_vec, &my_topo));
PetscCall(DMDAVecRestoreArray(da, user_context->fdepth_local, &my_fdepth));
PetscCall(DMDAVecRestoreArray(da, user_context->ksat_local, &my_ksat));
PetscCall(DMDAVecRestoreArray(da, user_context->topo_local, &my_topo));
PetscCall(DMDAVecRestoreArray(da, user_context->rech_vec, &my_rech));
PetscCall(DMDAVecRestoreArray(da, user_context->T_vec, &my_T));
PetscCall(DMDAVecRestoreArray(da, user_context->T_local, &my_T));
PetscCall(DMDAVecRestoreArray(da, user_context->porosity_vec, &my_porosity));
PetscCall(DMDAVecRestoreArray(da, user_context->starting_wtd, &my_starting_wtd));

Expand Down
79 changes: 79 additions & 0 deletions tests/ghost_cell/check_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
Compare 1-process and 2-process WTM output TIFs.

With the ghost-cell fix both runs must agree at all interior cells.
Without it the two halves of the domain are hydrologically decoupled at the
MPI processor boundary (x = NX//2), producing a clearly visible (~O(1) m) error.
"""

import sys
import os
import glob
import numpy as np
import rasterio

NX = 12


def last_tif(prefix, outdir):
pattern = os.path.join(outdir, f"{prefix}*.tif")
tifs = sorted(glob.glob(pattern))
if not tifs:
raise FileNotFoundError(f"No TIF files matching {pattern}")
return tifs[-1]


def load(path):
with rasterio.open(path) as src:
data = src.read(1).astype(np.float64)
return data


def main():
dir1p = "out_1p"
dir2p = "out_2p"

f1 = last_tif("out_", dir1p)
f2 = last_tif("out_", dir2p)
print(f"1-process output : {f1}")
print(f"2-process output : {f2}")

w1 = load(f1)
w2 = load(f2)

if w1.shape != w2.shape:
print(f"FAIL: shape mismatch {w1.shape} vs {w2.shape}", file=sys.stderr)
sys.exit(1)

diff = np.abs(w1 - w2)

# Interior land cells only (exclude ocean edges at row 0, row NY-1, col 0, col NX-1)
interior = diff[1:-1, 1:-1]

max_diff = interior.max()
mean_diff = interior.mean()
boundary_diff = diff[1:-1, NX // 2 - 1 : NX // 2 + 1].max()

print(f"Max |Δwtd| interior : {max_diff:.6f} m")
print(f"Mean |Δwtd| interior : {mean_diff:.6f} m")
print(f"Max |Δwtd| at MPI bound : {boundary_diff:.6f} m")

# Threshold: numerical summation-order differences are O(1e-10) m.
# The ghost-cell bug produces O(1) m errors near the boundary.
TOLERANCE = 1e-4 # generous; anything above ~0.01 m signals the bug

if max_diff > TOLERANCE:
print(
f"\nFAIL: max difference {max_diff:.4f} m exceeds tolerance {TOLERANCE} m.\n"
"Ghost-cell error is present — the MPI boundary suppresses inter-rank flux.",
file=sys.stderr,
)
sys.exit(1)
else:
print(f"\nPASS: 1-process and 2-process outputs agree to within {TOLERANCE} m.")
sys.exit(0)


if __name__ == "__main__":
main()
Loading
Loading