Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1ed8370
read unstructured elmer-xios grid
ClemensSchannwell Oct 20, 2025
1e6dff8
add elmer_xios type
ClemensSchannwell Oct 20, 2025
7890ea8
add elmer-xios-mesh option
ClemensSchannwell Oct 20, 2025
31384f8
allow output writing for unstructured grids
ClemensSchannwell Oct 20, 2025
cbb2c5c
restructure code for reading surface geometry from elmer-xios output
ClemensSchannwell Oct 23, 2025
e6a2fc2
clean up print statements
ClemensSchannwell Oct 23, 2025
f5aea8b
Merge branch 'main' into feature_greenland
BenjaminRodenberg Nov 6, 2025
1833131
Fix formatting
BenjaminRodenberg Nov 6, 2025
3cb21e9
avoid using surface topography from elmer
ClemensSchannwell Nov 11, 2025
4be61a8
adjust runtime that it matches with elmer
ClemensSchannwell Nov 11, 2025
db825d1
fix merge conflicts
ClemensSchannwell Nov 11, 2025
29350ad
fix formatting
ClemensSchannwell Nov 11, 2025
dcea3f7
Rename (for safer merge).
BenjaminRodenberg Nov 14, 2025
ea7e28c
Merge branch 'main' into feature_greenland
BenjaminRodenberg Nov 14, 2025
1428593
Add minimal EBFMCouplingConfig class.
BenjaminRodenberg Nov 25, 2025
07f050f
Merge branch 'main' into minimal-coupling-configuration
BenjaminRodenberg Nov 25, 2025
e163957
Introduce GridConfig and general refactoring.
BenjaminRodenberg Nov 25, 2025
b6c1b0b
Simplify and check for shading.
BenjaminRodenberg Nov 25, 2025
2cf7b25
Add pointers to #11.
BenjaminRodenberg Nov 25, 2025
0b14bf3
Merge branch 'main' into feature_greenland
BenjaminRodenberg Nov 25, 2025
3b37aee
Merge branch 'minimal-coupling-configuration' into feature_greenland
BenjaminRodenberg Nov 25, 2025
9b044b9
Apply some changes suggested in review.
BenjaminRodenberg Nov 25, 2025
c404755
Merge branch 'main' into feature_greenland
BenjaminRodenberg Nov 26, 2025
f222ff9
Check for property dem_file instead of grid type.
BenjaminRodenberg Nov 26, 2025
a859699
Set dem_file=None by default.
BenjaminRodenberg Nov 26, 2025
c495e6e
Rename XIOS_CUSTOM to ELMERXIOS.
BenjaminRodenberg Dec 2, 2025
631b8f6
Merge branch 'main' into feature_greenland
BenjaminRodenberg Dec 2, 2025
2101e63
Change default value.
BenjaminRodenberg Dec 2, 2025
01229e7
Store structured/unstructured in grid.
BenjaminRodenberg Dec 2, 2025
e7d2807
Reduce code duplication.
BenjaminRodenberg Dec 2, 2025
db2c652
Reduce code duplication and restructure.
BenjaminRodenberg Dec 2, 2025
48f8008
Cleanup.
BenjaminRodenberg Dec 2, 2025
67f59f5
Add Changelog entry.
BenjaminRodenberg Dec 3, 2025
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ SPDX-License-Identifier: CC-BY-4.0

# develop

* Support new input mesh format. EBFM now accepts Elmer/Ice mesh file for xy-coordinates and separate unstructured NetCDF elevation file obtained from XIOS. To use this feature please provide `--elmer-mesh` together with the new option `--netcdf-mesh-unstructured`. See https://github.com/EBFMorg/EBFM/pull/12.
* Use `setuptools_scm` as backend for `--version` information. See https://github.com/EBFMorg/EBFM/pull/46.
* Remove `pathlib` from requirements, because this can lead to a bug. https://github.com/EBFMorg/EBFM/pull/48
* Remove `pathlib` from requirements, because this can lead to a bug. https://github.com/EBFMorg/EBFM/pull/48.

# v0.1.0

Expand Down
1 change: 1 addition & 0 deletions src/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ def exchange_elmer_ice(self, put_data: Dict[str, np.array]) -> Dict[str, np.arra
received_data = {}
for field_name, field in self.target_fields.items():
if field_name in elmer_fields:
logger.debug(f"EBFM: {field.component_name}, {field.grid_name}, {field.name}")
role = self.interface.get_field_role(field.component_name, field.grid_name, field.name)
assert (
role == yac.ExchangeType.TARGET
Expand Down
46 changes: 38 additions & 8 deletions src/ebfm/INIT.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from netCDF4 import Dataset, num2date

from pathlib import Path
from reader import read_elmer_mesh, read_dem
from reader import read_elmer_mesh, read_dem, read_dem_xios

from elmer.mesh import Mesh
from ebfm.config import GridConfig
Expand Down Expand Up @@ -153,8 +153,14 @@ def init_constants():

def init_grid(grid, io, config: GridConfig):
grid["is_partitioned"] = config.is_partitioned
Comment thread
BenjaminRodenberg marked this conversation as resolved.

if config.grid_type is GridInputType.CUSTOM: # Read grid from Elmer, elevations from BedMachine
grid["is_unstructured"] = config.is_unstructured

# Read grid from Elmer, elevations from BedMachine
if config.dem_file:
grid_input_type_supporting_dem = [GridInputType.CUSTOM, GridInputType.ELMERXIOS]
assert (
config.grid_type in grid_input_type_supporting_dem
), f"DEM file can only be specified for {grid_input_type_supporting_dem}."
if config.is_partitioned:
mesh: Mesh = read_elmer_mesh(
mesh_root=config.mesh_file,
Expand All @@ -165,15 +171,18 @@ def init_grid(grid, io, config: GridConfig):
mesh: Mesh = read_elmer_mesh(mesh_root=config.mesh_file)

grid["x"], grid["y"] = mesh.x_vertices, mesh.y_vertices
grid["z"] = read_dem(config.dem_file, grid["x"], grid["y"])
if config.grid_type is GridInputType.CUSTOM:
grid["z"] = read_dem(config.dem_file, grid["x"], grid["y"])
grid["lat"] = np.zeros_like(grid["x"]) + 75 # test values!
grid["lon"] = np.zeros_like(grid["x"]) + 320 # test values!
if config.grid_type is GridInputType.ELMERXIOS:
grid = read_dem_xios(config.dem_file, grid)
grid["mask"] = np.ones_like(grid["x"]) # treats every grid cell as glacier
grid["gpsum"] = np.sum(grid["mask"] == 1) # number of modelled grid cells
grid["slope_x"] = np.zeros_like(grid["x"]) # test values!
grid["slope_y"] = np.zeros_like(grid["x"]) # test values!
grid["lat"] = np.zeros_like(grid["x"]) + 75 # test values!
grid["lon"] = np.zeros_like(grid["x"]) + 320 # test values!
grid["slope_beta"] = np.zeros_like(grid["x"]) # test values!
grid["slope_gamma"] = np.zeros_like(grid["x"]) # test values!
grid["mask"] = np.ones_like(grid["x"]) # treats every grid cell as glacier
grid["gpsum"] = np.sum(grid["mask"] == 1) # number of modelled grid cells
grid["mesh"] = mesh
grid["has_shading"] = False # TODO: see https://github.com/EBFMorg/EBFM/issues/11
# TODO later add slope
Expand Down Expand Up @@ -300,12 +309,33 @@ def init_grid(grid, io, config: GridConfig):
grid["slope_gamma"][(grid["slope_x"] > 0) & (grid["slope_y"] == 0)] = np.pi / 2
grid["slope_gamma"][(grid["slope_x"] < 0) & (grid["slope_y"] == 0)] = -np.pi / 2
grid["slope_gamma"] = -grid["slope_gamma"]
elif grid["input_type"] is GridInputType.ELMER_XIOS:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?

It's generally a good idea to make sure that at least one of the cases in this chain of if-elif-statements is entered. I always append a final else: raise Exception(f"Cannot handle unexpected {grid["input_type"]=}"). This will make sure that the program exits.

grid = read_elmer_xios_grid(grid=grid, gridfile=config.dem_file)
grid["gpsum"] = grid["z"].shape[0]
grid["mask"] = (grid["h"] > 1.0) * 1.0
grid["x"] = np.zeros_like(grid["z"])
grid["slope_beta"] = np.zeros_like(grid["x"]) # test values!
grid["slope_gamma"] = np.zeros_like(grid["x"]) # test values!
else:
raise ValueError(f"Unsupported grid input type {config.grid_type} specified in configuration.")

return grid


def read_elmer_xios_grid(grid, gridfile: Path):
import netCDF4 as nc

print(gridfile)
with nc.Dataset(gridfile) as file:
grid["lat"] = file["mesh2D_node_x"][:]
grid["lon"] = file["mesh2D_node_y"][:]
grid["x"] = file["x"][:]
grid["y"] = file["y"][:]
grid["z"] = np.squeeze(file["zs"][:].data)
grid["h"] = np.squeeze(file["h"][:].data)
return grid


def read_MATLAB_grid(gridfile: Path):
"""
Provides grid information by reading from a .mat file or allowing user input.
Expand Down
73 changes: 41 additions & 32 deletions src/ebfm/LOOP_write_to_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,16 @@ def save_netCDF_file():
# Create NetCDF file
nc_filepath = os.path.join(io["outdir"], "model_output.nc")
io["nc_file"] = Dataset(nc_filepath, "w", format="NETCDF4")

# Define dimensions
io["nc_file"].createDimension("time", None) # Unlimited time dimension
io["nc_file"].createDimension("y", grid["x_2D"].shape[0]) # 2D grid rows
io["nc_file"].createDimension("x", grid["x_2D"].shape[1]) # 2D grid columns

if grid["is_unstructured"]:
# Define dimensions
io["nc_file"].createDimension("y", grid["lat"].shape[0]) # 2D grid rows
else: # structured grid
# Define dimensions
io["nc_file"].createDimension("y", grid["x_2D"].shape[0]) # 2D grid rows
io["nc_file"].createDimension("x", grid["x_2D"].shape[1]) # 2D grid columns

io["nc_file"].createDimension("nl", grid["nl"]) # Vertical layers for `sub` variables

# Define standard output variables
Expand All @@ -154,35 +159,39 @@ def save_netCDF_file():
var_units = entry[1]
var_desc = entry[3]

# Check if variable is a `sub` variable
if varname.startswith("sub"):
# Define variable as 4D: (time, y, x, nl)
nc_var = io["nc_file"].createVariable(
varname,
np.float32,
("time", "y", "x", "nl"),
zlib=True,
complevel=4,
fill_value=-9999.0, # Fill missing values
chunksizes=(
1,
grid["x_2D"].shape[0],
grid["x_2D"].shape[1],
grid["nl"],
),
)
dimensions: tuple
chunksizes: tuple

if grid["is_unstructured"]:
# Check if variable is a `sub` variable
if varname.startswith("sub"):
# Define variable as 3D: (time, y, nl)
dimensions = ("time", "y", "nl")
chunksizes = (1, grid["lat"].shape[0], grid["nl"])
else:
# Define variable as 2D: (time, y)
dimensions = ("time", "y")
chunksizes = (1, grid["lat"].shape[0])
else:
# Define variable as 3D: (time, y, x)
nc_var = io["nc_file"].createVariable(
varname,
np.float32,
("time", "y", "x"),
zlib=True,
complevel=4,
fill_value=-9999.0,
chunksizes=(1, grid["x_2D"].shape[0], grid["x_2D"].shape[1]),
)

# Check if variable is a `sub` variable
if varname.startswith("sub"):
# Define variable as 4D: (time, y, x, nl)
dimensions = ("time", "y", "x", "nl")
chunksizes = (1, grid["x_2D"].shape[0], grid["x_2D"].shape[1], grid["nl"])
else:
# Define variable as 3D: (time, y, x)
dimensions = ("time", "y", "x")
chunksizes = (1, grid["x_2D"].shape[0], grid["x_2D"].shape[1])

nc_var = io["nc_file"].createVariable(
varname=varname,
datatype=np.float32,
dimensions=dimensions,
zlib=True,
complevel=4,
fill_value=-9999.0, # Fill missing values
chunksizes=chunksizes,
)
# Assign metadata
nc_var.units = var_units
nc_var.description = var_desc
Expand Down
8 changes: 7 additions & 1 deletion src/ebfm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ class GridConfig:

grid_type: GridInputType # Name of the grid used in coupling
mesh_file: Path # Path to the grid file
dem_file: Path # Path to the DEM file (only relevant for CUSTOM grid type)
dem_file: Path = None # Path to the DEM file (only relevant for CUSTOM grid type)
Comment thread
BenjaminRodenberg marked this conversation as resolved.
is_partitioned: bool # Whether the grid is partitioned
is_unstructured: bool = False # Whether the grid is unstructured
partition_id: int # Partition ID (only relevant if is_partitioned is True)

def __init__(self, args: Namespace):
Expand Down Expand Up @@ -103,6 +104,11 @@ def __init__(self, args: Namespace):
self.grid_type = GridInputType.CUSTOM
self.mesh_file = args.elmer_mesh
self.dem_file = args.netcdf_mesh
elif args.netcdf_mesh_unstructured and args.elmer_mesh:
self.grid_type = GridInputType.ELMERXIOS
self.mesh_file = args.elmer_mesh
self.dem_file = args.netcdf_mesh_unstructured
Comment thread
BenjaminRodenberg marked this conversation as resolved.
self.is_unstructured = True
elif args.elmer_mesh:
self.grid_type = GridInputType.ELMER
self.mesh_file = args.elmer_mesh
Expand Down
14 changes: 11 additions & 3 deletions src/ebfm/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@


class GridInputType(Enum):
MATLAB = "matlab" # .mat grid file with elevation
CUSTOM = "custom" # Elmer/Ice mesh file for xy-coordinates and separate NetCDF elevation file
ELMER = "elmer" # Elmer/Ice mesh file with elevation in z-coordinate
# .mat grid file with elevation
MATLAB = "matlab"

# Elmer/Ice mesh file for xy-coordinates and separate NetCDF elevation file
CUSTOM = "custom"

# Elmer/Ice mesh file for xy-coordinates and separate unstructured NetCDF elevation file obtained from XIOS
ELMERXIOS = "elmerxios"

# Elmer/Ice mesh file with elevation in z-coordinate
ELMER = "elmer"
24 changes: 15 additions & 9 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def extract_active_coupling_features(args: argparse.Namespace) -> List[str]:


def main():
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)

parser.add_argument(
"--version",
Expand Down Expand Up @@ -126,20 +126,27 @@ def main():
" If --netcdf-mesh is provided elevations will be read from the given NetCDF mesh file.",
)

input_group.add_argument(
"--netcdf-mesh-unstructured",
type=Path,
help="Path to the unstructured NetCDF mesh file. Optional if using --elmer-mesh."
" If --netcdf-mesh is provided elevations will be read from the given NetCDF mesh file.",
)

time_group = parser.add_argument_group("time configuration")

time_group.add_argument(
"--start-time",
type=str,
help="Start time of the simulation in format 'DD-Mon-YYYY HH:MM', e.g., '01-Jan-1979 00:00'.",
help="Start time of the simulation in format 'DD-Mon-YYYY HH:MM'",
default="1-Jan-1979 00:00",
)

time_group.add_argument(
"--end-time",
type=str,
help="End time of the simulation in format 'DD-Mon-YYYY HH:MM', e.g., '02-Jan-1979 09:00'.",
default="2-Jan-1979 09:00",
help="End time of the simulation in format 'DD-Mon-YYYY HH:MM'",
default="2-Jan-1979 00:00",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did another merge of main into this PR. The default values are quite arbitrary. So I'm fine to just change the default to the value you are using if this saves you some typing.

You can also use the new options to provide different values.

)

time_group.add_argument(
Expand Down Expand Up @@ -297,11 +304,10 @@ def main():

# Write output to files (only in uncoupled run and for unpartitioned grid)
if not grid["is_partitioned"] and not coupler.has_coupling:
assert (
grid_config.grid_type is GridInputType.MATLAB
), "Output writing currently only implemented for MATLAB input grids."
io, OUTFILE = LOOP_write_to_file.main(OUTFILE, io, OUT, grid, t, time, C)
pass
if grid_config.grid_type is GridInputType.MATLAB:
io, OUTFILE = LOOP_write_to_file.main(OUTFILE, io, OUT, grid, t, time, C)
else:
logger.warning("Skipping writing output to file for Elmer input grids.")
elif grid["is_partitioned"] or coupler.has_coupling:
logger.warning("Skipping writing output to file for coupled or partitioned runs.")
else:
Expand Down
22 changes: 22 additions & 0 deletions src/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ def read_elmer_mesh(mesh_root: Path, is_partitioned: bool = False, partition_id:
)


def read_dem_xios(dem_file: Path, grid: dict):
"""Read digital elevation model (DEM) file.
Args:
dem_file (Path): Path to the DEM NetCDF file.
grid (dict): grid-related parameters
Returns:
grid (dict): dictionary containing grid-related parameters
"""
assert dem_file.is_file(), f"DEM file {dem_file} does not exist."

import netCDF4

nc = netCDF4.Dataset(dem_file)
assert (
np.squeeze(nc["x"][:]).shape == grid["x"].shape
), "Surface mesh and Elmer mesh do not have the same number of vertices"
grid["z"] = np.squeeze(nc["zs"][:]).data
grid["lat"] = np.squeeze(nc["mesh2D_node_x"][:]).data
grid["lon"] = np.squeeze(nc["mesh2D_node_y"][:]).data
return grid


def read_dem(dem_file: Path, xs: NDArray[np.float64], ys: NDArray[np.float64]):
"""Read digital elevation model (DEM) file.
Args:
Expand Down