Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
b8c8fa2
move consts to consts.py
mdeshotel May 5, 2026
deaa031
break intialize method into properties with setters to validate.
mdeshotel May 5, 2026
e77baf7
fix setter logic; update doc strings
mdeshotel May 13, 2026
f001e19
fix circular imports
mdeshotel May 18, 2026
ef3dc0c
updat for NHF
mdeshotel May 18, 2026
4fef5ce
remove calls to validate_config and initialize
mdeshotel May 18, 2026
1d80cb8
debugging changes
mdeshotel May 18, 2026
7d4ee6d
handle circular imports
mdeshotel May 19, 2026
7c52026
remove commented out attributes
mdeshotel May 19, 2026
3e5dc1e
use dir instead of vars
mdeshotel May 19, 2026
342e494
debugging
mdeshotel May 19, 2026
ca428da
fix regrid_opt
mdeshotel May 20, 2026
f390095
refactor initialize method
mdeshotel May 27, 2026
19d8bb7
remove excessive code comments
mdeshotel May 28, 2026
9b725fb
move init none attrs to consts.py
mdeshotel Jun 26, 2026
8e2c814
merge bmi_model refactor
mdeshotel Jul 1, 2026
2069b49
Simplify var passing within NWMv3ForcingEngineModel.run
mxkpp Apr 24, 2026
7eb4f5a
Raise error on unexpected grid_type
mxkpp Apr 27, 2026
b47e4c9
Use `time.perf_counter` instead of `time.time`
mxkpp Apr 27, 2026
8341ef8
Start encapsulating BMI model into model.py NWMv3ForcingEngineModel s…
mxkpp Apr 27, 2026
d0b6c05
Encapsulate ConfigOptions instance
mxkpp Apr 27, 2026
1173297
Encapsulate GeoMeta instance
mxkpp Apr 27, 2026
f6ebd7b
Encapsulate input forcing mod dict
mxkpp Apr 27, 2026
104d1ab
Encapsulate supp pcp mod
mxkpp Apr 27, 2026
4fdf141
Encapsulate MpiConfig
mxkpp Apr 27, 2026
246f4e2
Encapsulate OutputObj instance
mxkpp Apr 27, 2026
2abab0e
Remove unused imports
mxkpp Apr 27, 2026
5eb4779
Remove hard-coded msg control
mxkpp Apr 27, 2026
7e746b3
DRYify some AnA deltas
mxkpp Apr 27, 2026
34827a3
DRYify calls to check_program_status
mxkpp Apr 27, 2026
f5bc953
Clean up comments and docstrings
mxkpp Apr 27, 2026
584afe3
Add forcing key count assertion
mxkpp Apr 29, 2026
d65927a
Update docstrings
mxkpp Apr 29, 2026
cf65ff4
Move block using `rstFlag` to new private method
mxkpp Apr 29, 2026
6749d01
Move block for supplemental precip handling to new private method
mxkpp Apr 29, 2026
fb16a97
docstrings and type hints
mxkpp Apr 29, 2026
ff64439
Move block for AORC and NWM handling to new private method
mxkpp Apr 29, 2026
fde1d4a
Comments and type hints
mxkpp Apr 29, 2026
5b91132
Move model.py BMI variables to consts.py
mxkpp Apr 29, 2026
d757f95
Fix usage of new consts.MODEL list
mxkpp Apr 29, 2026
d041997
Comments
mxkpp Apr 29, 2026
561cb91
Add NotImplementedError for SubOutputHour
mxkpp Apr 30, 2026
422e8e9
Type hints and docstrings
mxkpp May 1, 2026
722f0ce
Rename methods
mxkpp May 1, 2026
4d45289
Fix input_forcings reference (return None conditionally)
mxkpp May 1, 2026
dc6493d
Use partials for log calls and use MPI-aware log methods
mxkpp May 2, 2026
1228f08
Run flynt -tc -ll 9999
mxkpp May 2, 2026
ab7866b
Format docstrings to reST
mxkpp May 2, 2026
0ad06fa
DRYify
mxkpp May 2, 2026
4e027d6
Add assertion
mxkpp May 2, 2026
9f4f2a7
Fix source_data_processor sets
mxkpp May 2, 2026
99f932d
Docstrings
mxkpp May 2, 2026
f58cd42
comment out not implemented error.
mdeshotel Jul 13, 2026
28454cf
Remove duplicate method
mxkpp Aug 13, 2026
2535bcc
Following refactor+rebase: update CAT-ID assignments and init calls, …
mxkpp Aug 17, 2026
800d3e8
Ruff reorder imports
mxkpp Aug 17, 2026
4451dad
Tests: update for new BMI forcing initialization method
mxkpp Aug 17, 2026
759d743
Tests: improve test data serialization/deserialization logic
mxkpp Aug 17, 2026
6e58551
Tests: bmi_model: exclude more keys
mxkpp Aug 17, 2026
c46eb32
Fix various config handling issues following refactor + rebase. Notab…
mxkpp Aug 18, 2026
8b40324
Test data: update expected json files for bmi_model and config_option…
mxkpp Aug 18, 2026
308f1d3
Bolster test utils handling of serialization and deserialization
mxkpp Aug 18, 2026
ec8d6d6
Format with ruff default line length (88)
mxkpp Aug 18, 2026
79e1082
Adjustments following review
mxkpp Aug 18, 2026
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import argparse
import os
import pathlib
import uuid

import geopandas as gpd
import netCDF4
import numpy as np
import pandas as pd
import argparse
import pathlib
import os
import uuid

gpd.options.display_precision = 16
np.set_printoptions(precision=128)
Expand Down Expand Up @@ -33,7 +34,7 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
# for orientation properties since there are issues
# with geopandas for converting crs and translating
# orientation of polygon from original dataset
hyfab_cart = gpd.read_file(hyfab_gpkg, layer='divides')
hyfab_cart = gpd.read_file(hyfab_gpkg, layer="divides")
hyfab_cart = hyfab_cart.sort_values(by=["div_id"]).reset_index(drop=True)
hyfab = hyfab_cart.to_crs("WGS84")

Expand All @@ -51,23 +52,25 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
hyfab_coords[:, 1] = false_ids

# Sort data by feature id and reset index
hyfab['element_id'] = false_ids
hyfab_cart['element_id'] = false_ids
hyfab["element_id"] = false_ids
hyfab_cart["element_id"] = false_ids

# Get element count
element_count = len(hyfab.element_id)

# find the number of nodes in first element
# based on geometry type
if (hyfab.geometry[0].geom_type == "Polygon"):
if hyfab.geometry[0].geom_type == "Polygon":
dup_df = pd.DataFrame([])
dup_df['node_x'], dup_df['node_y'] = hyfab.geometry[0].exterior.coords.xy
dup_df = dup_df.drop_duplicates(subset=['node_x', 'node_y'], keep='first')
dup_df["node_x"], dup_df["node_y"] = hyfab.geometry[0].exterior.coords.xy
dup_df = dup_df.drop_duplicates(subset=["node_x", "node_y"], keep="first")
elem_max_nodes = len(dup_df)
else:
dup_df = pd.DataFrame([])
dup_df['node_x'], dup_df['node_y'] = hyfab.geometry[0].geoms._get_geom_item(0).exterior.xy
dup_df = dup_df.drop_duplicates(subset=['node_x', 'node_y'], keep='first')
dup_df["node_x"], dup_df["node_y"] = (
hyfab.geometry[0].geoms._get_geom_item(0).exterior.xy
)
dup_df = dup_df.drop_duplicates(subset=["node_x", "node_y"], keep="first")
elem_max_nodes = len(dup_df)

# Allocate element arrays for center point calculations
Expand All @@ -84,15 +87,17 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
# based on geometry type
total_num_nodes = 0
for i in range(element_count):
if (hyfab.geometry[i].geom_type == "Polygon"):
if hyfab.geometry[i].geom_type == "Polygon":
dup_df = pd.DataFrame([])
dup_df['node_x'], dup_df['node_y'] = hyfab.geometry[i].exterior.coords.xy
dup_df = dup_df.drop_duplicates(subset=['node_x', 'node_y'], keep='first')
dup_df["node_x"], dup_df["node_y"] = hyfab.geometry[i].exterior.coords.xy
dup_df = dup_df.drop_duplicates(subset=["node_x", "node_y"], keep="first")
total_num_nodes += len(dup_df)
else:
dup_df = pd.DataFrame([])
dup_df['node_x'], dup_df['node_y'] = hyfab.geometry[i].geoms._get_geom_item(0).exterior.xy
dup_df = dup_df.drop_duplicates(subset=['node_x', 'node_y'], keep='first')
dup_df["node_x"], dup_df["node_y"] = (
hyfab.geometry[i].geoms._get_geom_item(0).exterior.xy
)
dup_df = dup_df.drop_duplicates(subset=["node_x", "node_y"], keep="first")
total_num_nodes += len(dup_df)

# assign current node id and allocate node arrays to extract
Expand All @@ -107,49 +112,60 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
# flip node coordinates based on orientation of polygons
# from the original cartesian coordinate system
for i in range(element_count):
if (hyfab.geometry[i].geom_type == "Polygon"):
if hyfab.geometry[i].geom_type == "Polygon":
dup_df = pd.DataFrame([])
dup_df['node_x'], dup_df['node_y'] = hyfab.geometry[i].exterior.coords.xy
dup_df = dup_df.drop_duplicates(subset=['node_x', 'node_y'], keep='first')
dup_df["node_x"], dup_df["node_y"] = hyfab.geometry[i].exterior.coords.xy
dup_df = dup_df.drop_duplicates(subset=["node_x", "node_y"], keep="first")
node_x = dup_df.node_x.values
node_y = dup_df.node_y.values
ccw = hyfab_cart.geometry[i].exterior.is_ccw
else:
dup_df = pd.DataFrame([])
dup_df['node_x'], dup_df['node_y'] = hyfab.geometry[i].geoms._get_geom_item(0).exterior.xy
dup_df = dup_df.drop_duplicates(subset=['node_x', 'node_y'], keep='first')
dup_df["node_x"], dup_df["node_y"] = (
hyfab.geometry[i].geoms._get_geom_item(0).exterior.xy
)
dup_df = dup_df.drop_duplicates(subset=["node_x", "node_y"], keep="first")
node_x = dup_df.node_x.values
node_y = dup_df.node_y.values
ccw = hyfab_cart.geometry[i].geoms._get_geom_item(0).exterior.is_ccw

num_nodes = len(node_x)
element_num_nodes[i] = num_nodes
if (num_nodes > elem_max_nodes):
if num_nodes > elem_max_nodes:
elem_max_nodes = num_nodes

element_x_coord[i] = hyfab.geometry[i].centroid.coords.xy[0][0]
element_y_coord[i] = hyfab.geometry[i].centroid.coords.xy[1][0]

element_elevation[i] = hyfab.elevation_mean[i]
element_slope[i] = hyfab.slope1km_mean[i]
element_slope_azmuith[i] = hyfab.aspect_circmean[i] # NHF aspect is currently in radians, may need to be converted to degrees

if (ccw):
node_x_coord[node_start:node_start + num_nodes] = np.array(node_x, dtype=np.double)
node_y_coord[node_start:node_start + num_nodes] = np.array(node_y, dtype=np.double)
# NHF aspect is currently in radians, may need to be converted to degrees
element_slope_azmuith[i] = hyfab.aspect_circmean[i]

if ccw:
node_x_coord[node_start : node_start + num_nodes] = np.array(
node_x, dtype=np.double
)
node_y_coord[node_start : node_start + num_nodes] = np.array(
node_y, dtype=np.double
)
else:
node_x_coord[node_start:node_start + num_nodes] = np.array(np.concatenate([[node_x[0]], np.flip(node_x[1:])]), dtype=np.double)
node_y_coord[node_start:node_start + num_nodes] = np.array(np.concatenate([[node_y[0]], np.flip(node_y[1:])]), dtype=np.double)
node_x_coord[node_start : node_start + num_nodes] = np.array(
np.concatenate([[node_x[0]], np.flip(node_x[1:])]), dtype=np.double
)
node_y_coord[node_start : node_start + num_nodes] = np.array(
np.concatenate([[node_y[0]], np.flip(node_y[1:])]), dtype=np.double
)
node_start += num_nodes

# Assign node data to pandas dataframe
# and calculate the duplicate nodes throughout
# the hydrofabric geometry network
node_connectivity = pd.DataFrame([])
node_connectivity['node_x'] = node_x_coord
node_connectivity['node_y'] = node_y_coord
node_connectivity["node_x"] = node_x_coord
node_connectivity["node_y"] = node_y_coord

duplicates = node_connectivity[node_connectivity.duplicated(keep='first')]
duplicates = node_connectivity[node_connectivity.duplicated(keep="first")]

# Create array to assign duplicate nodes as
# zeroes, while creating unique ids for only
Expand All @@ -158,25 +174,27 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
node_id_connectivity = np.empty(len(node_id), dtype=np.int32)
node_count = 1
for i in range(len(node_id)):
if (i in duplicates_index):
if i in duplicates_index:
node_id_connectivity[i] = 0
else:
node_id_connectivity[i] = node_count
node_count += 1

# Assign new node id network to dataframe
node_connectivity['node_id'] = node_id_connectivity
node_connectivity["node_id"] = node_id_connectivity

# calculate the node id network to include its duplicate ids
# for each instance of the node coordinates
ESMF_node_id_connectivity = node_connectivity.groupby(['node_x', 'node_y']).node_id.transform('max')
ESMF_node_id_connectivity = node_connectivity.groupby(
["node_x", "node_y"]
).node_id.transform("max")

node_connectivity['node_id_connectivity'] = ESMF_node_id_connectivity.values
node_connectivity["node_id_connectivity"] = ESMF_node_id_connectivity.values

node_connectivity_final = node_connectivity.node_id_connectivity.values

# Extract only the unique node id network and respective coordinates
node_connectivity = node_connectivity.drop_duplicates('node_id_connectivity')
node_connectivity = node_connectivity.drop_duplicates("node_id_connectivity")
node_count = len(node_connectivity)
node_x_coord_final = node_connectivity.node_x.values
node_y_coord_final = node_connectivity.node_y.values
Expand All @@ -189,7 +207,9 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
end_index = 0
for i in range(element_count):
end_index += element_num_nodes[i]
elementConn[i, 0:element_num_nodes[i]] = node_connectivity_final[start_index:end_index]
elementConn[i, 0 : element_num_nodes[i]] = node_connectivity_final[
start_index:end_index
]
start_index = end_index

out_dir = os.path.dirname(esmf_mesh_output)
Expand All @@ -203,17 +223,21 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
nc = netCDF4.Dataset(temp_path, "w", format="NETCDF4")
node_count_dim = nc.createDimension("nodeCount", node_count)
elem_count_dim = nc.createDimension("elementCount", element_count)
elem_conn_count_dim = nc.createDimension("connectionCount", len(node_connectivity_final))
elem_conn_count_dim = nc.createDimension(
"connectionCount", len(node_connectivity_final)
)
node_count_dim = nc.createDimension("coordDim", 2)
node_coords_var = nc.createVariable("nodeCoords", 'f8', ("nodeCount", "coordDim"))
node_coords_var = nc.createVariable("nodeCoords", "f8", ("nodeCount", "coordDim"))
node_coords_var.units = "degrees"
elem_id = nc.createVariable("element_id", "i4", "elementCount")
elem_id.long_name = "False 32-bit catchment IDs use for ESMF mesh generation"
elem_conn_var = nc.createVariable("elementConn", "i4", ("connectionCount"))
elem_conn_var.long_name = "Node Indices that define the element connectivity"
num_elem_conn_var = nc.createVariable("numElementConn", "i", "elementCount")
num_elem_conn_var.long_name = "Number of nodes per element"
center_coords_var = nc.createVariable("centerCoords", 'f8', ("elementCount", "coordDim"))
center_coords_var = nc.createVariable(
"centerCoords", "f8", ("elementCount", "coordDim")
)
center_coords_var.units = "degrees"
nc.gridType = "unstructured"
nc.version = "0.9"
Expand All @@ -224,7 +248,9 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
slope_elem_var = nc.createVariable("Element_Slope", "f8", ("elementCount"))
slope_elem_var.long_name = "Catchment slope"
slope_elem_var.units = "meters"
slope_azi_elem_var = nc.createVariable("Element_Slope_Azmuith", "f8", ("elementCount"))
slope_azi_elem_var = nc.createVariable(
"Element_Slope_Azmuith", "f8", ("elementCount")
)
slope_azi_elem_var.long_name = "Catchment slope azmuith angle"
slope_azi_elem_var.units = "Degrees"
hgt_elem_var[:] = element_elevation
Expand Down Expand Up @@ -258,8 +284,14 @@ def convert_hyfab_to_esmf(hyfab_gpkg: pathlib.Path, esmf_mesh_output: pathlib.Pa
def get_options():
parser = argparse.ArgumentParser()

parser.add_argument('hyfab_gpkg', type=pathlib.Path, help="Hydrofabric geopackage file pathway")
parser.add_argument("esmf_mesh_output", type=pathlib.Path, help="File pathway to save ESMF netcdf mesh file for hydrofabric")
parser.add_argument(
"hyfab_gpkg", type=pathlib.Path, help="Hydrofabric geopackage file pathway"
)
parser.add_argument(
"esmf_mesh_output",
type=pathlib.Path,
help="File pathway to save ESMF netcdf mesh file for hydrofabric",
)

return parser.parse_args()

Expand Down
Loading