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
Original file line number Diff line number Diff line change
Expand Up @@ -11353,11 +11353,11 @@ def check_regrid_status(
[force_count, wrf_hydro_geo_meta.ny_local_elem], np.float32
)
elif config_options.grid_type == "hydrofabric":
input_forcings.regridded_forcings1 = np.empty(
[force_count, wrf_hydro_geo_meta.ny_local], np.float32
input_forcings.regridded_forcings1 = np.full(
[force_count, wrf_hydro_geo_meta.ny_local], np.nan,dtype=np.float32 #NOTE changed to np.full to be deterministic for unit tests.
)
input_forcings.regridded_forcings2 = np.empty(
[force_count, wrf_hydro_geo_meta.ny_local], np.float32
input_forcings.regridded_forcings2 = np.full(
[force_count, wrf_hydro_geo_meta.ny_local], np.nan,dtype=np.float32 #NOTE changed to np.full to be deterministic for unit tests.
)

if mpi_config.rank == 0:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,12 @@ def initDict(ConfigOptions, GeoMetaWrfHydro):
)
elif ConfigOptions.grid_type == "hydrofabric":
# Initialize the local final grid of values
InputDict[supp_pcp_key].final_supp_precip = np.empty(
[GeoMetaWrfHydro.ny_local], np.float64
# NOTE changed from np.empty to np.full for determinism of test data.
InputDict[supp_pcp_key].final_supp_precip = np.full(
[GeoMetaWrfHydro.ny_local], np.nan, dtype=np.float64
)
InputDict[supp_pcp_key].regridded_mask = np.empty(
[GeoMetaWrfHydro.ny_local], np.float32
InputDict[supp_pcp_key].regridded_mask = np.full(
[GeoMetaWrfHydro.ny_local], np.nan, dtype=np.float32
)

InputDict[supp_pcp_key].userCycleOffset = ConfigOptions.supp_input_offsets[
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ testpaths =
tests/esmf_regrid
tests/geomod
tests/input_forcing
tests/ana
tests/bmi_model
92 changes: 39 additions & 53 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,16 @@ The test suite is organized into the following modules:
- **`esmf_regrid/`** - Tests for ESMF regridding functionality
- **`geomod/`** - Tests for geomod components
- **`input_forcing/`** - Tests for input forcing data processing
- **`supp_precip/`** - Tests for supp precip data processing
- **`bmi_model/`** - Tests for the BMI model lifecycle
- **`config_options/`** - Tests config options
- **`test_utils.py`** - Shared test utilities and fixtures
- **`conftest.py`** - Pytest configuration and shared fixtures

## Prerequisites
### Setup requirements:
1. Create the forcing config.yml files using RTE.
2. Enter the RTE devcontainer.

### Required Dependencies

The test suite requires Python 3.11 or higher. Install the package with test dependencies inside of the `dev container`:

```bash
# From the repository root directory
pip install -e ".[develop]"
```

Or install pytest directly inside of the `dev container`:

```bash
pip install pytest
```

### Additional Requirements

Ensure all main package dependencies are installed inside of the `dev container` (this typically should happen when the `dev container` is built):

```bash
pip install -e .
```
## Prerequisite Steps
1. Clone the nwm-rte repository
2. Build a Docker image using nwm-rte.
3. Enter a Dev Container using nwm-rte.

## Running Tests

Expand All @@ -91,9 +70,21 @@ Multiple processors: ( cd src/ngen-forcing && mpirun -n 2 pytest tests/geomod)
Single processor: ( cd src/ngen-forcing && pytest tests/input_forcing)
Multiple processors: ( cd src/ngen-forcing && mpirun -n 2 pytest tests/input_forcing)

# Supp precip tests
Single processor: ( cd src/ngen-forcing && pytest tests/supp_precip)
Multiple processors: ( cd src/ngen-forcing && mpirun -n 2 pytest tests/supp_precip)

# Analysis and Assimilation tests
Single processor: ( cd src/ngen-forcing && pytest tests/ana )
Multiple processors: ( cd src/ngen-forcing && mpirun -n 2 pytest tests/ana )

# BMI model tests
Single processor: ( cd src/ngen-forcing && pytest tests/bmi_model)
Multiple processors: ( cd src/ngen-forcing && mpirun -n 2 pytest tests/bmi_model)

# config options tests
Single processor: ( cd src/ngen-forcing && pytest tests/config_options)
Multiple processors: ( cd src/ngen-forcing && mpirun -n 2 pytest tests/config_options)
```

Create new test output data (creates expected outputs for subsequent tests)
Expand All @@ -110,16 +101,33 @@ Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/input_forcing)
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/input_forcing)

# Supp precip tests
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/supp_precip)
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/supp_precip)

# Analysis and Assimilation tests
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/ana )
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/ana )

# BMI model tests
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/bmi_model)
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/bmi_model)

# config_options tests
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/config_options)
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/config_options)
```

In the rare case where you want to create new `expected` data and run the tests using `old` variable names use the following for `Input Forcing Tests`:
```bash
# Input forcing tests
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/input_forcing --map_old_to_new_var_names False)
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/input_forcing --map_old_to_new_var_names False)

# Supp precip tests
Single processor: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true pytest tests/supp_precip --map_old_to_new_var_names False)
Multiple processors: ( cd src/ngen-forcing && FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA=true mpirun -n 2 pytest tests/supp_precip --map_old_to_new_var_names False)

```
## Test Configuration

Expand All @@ -130,34 +138,12 @@ The test suite is configured via `pytest.ini` at the repository root:
- **Verbosity**: Full trace with verbose output (`-vv`)
- **Test paths**: Pre-configured to discover tests in `esmf_regrid`, `geomod`, `input_forcing`, and `bmi_model`


## Test Data

Test data is stored in the `test_data/` directory. Tests may reference files from this location for input data and expected results validation.

## Writing New Tests
## Writing New Tests or Updating Expected Results Files

When adding new tests:

1. Place test files in the appropriate subdirectory
2. Name test files with the `test_*.py` prefix
3. Name test functions with the `test_*` prefix
4. Use fixtures from `conftest.py` for common setup
5. Place test data files in `test_data/` with descriptive names

Example test structure:

```python
import pytest

def test_my_feature():
"""Test description."""
# Arrange
input_data = ...

# Act
result = function_under_test(input_data)

# Assert
assert result == expected_output
```
When adding new tests, use the OS env var `FORCING_PYTEST_WRITE_TEST_EXPECTED_DATA`
to have your new test automatically write new "expected" data to `tests/test_data/expected_results/`,
then commit those files to the repository. See above for example calls.
49 changes: 49 additions & 0 deletions tests/ana/test_ana.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import importlib.util
import logging
import os

import pytest

### Load import tests.test_utils as test_utils, referring explicitly to its path.
### This explicit load is necessary since March 2026 versions of ngen which introduced /ngen-app/ngen/extern/topoflow-glacier/tests
spec = importlib.util.spec_from_file_location(
"tests.test_utils", os.path.abspath("tests/test_utils.py")
)
test_utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_utils)

consts = test_utils.test_consts
configs = test_utils.test_config_classes


TEST_CONFIGS = [
configs.TestConfig_AnA(
config_file=consts.FORECAST_FORCING_CONFIG_FILE__ANA_CONUS,
keys_to_check=(),
keys_to_exclude=consts.KEYS_TO_EXCLUDE,
grid_type=consts.GRID_TYPE,
test_file_name_prefix="ana_standard_conus",
),
]


@pytest.mark.parametrize("bmi_forcing_fixture_ana", TEST_CONFIGS, indirect=True)
def test_input_forcing(
bmi_forcing_fixture_ana: test_utils.BMIForcingFixture_AnA, # pyright: ignore
) -> None:
"""Pytest function for testing Analysis and Assimilation."""
### Total number of timesteps needs to be at least 3, since the 1st and 2nd behaves differently than the others,
### e.g. see `if config_options.current_output_step == 1` throughout the code and the regridded_forcings1 vs regridded_forcings2 weighting.
total_timesteps = 3

fixt = bmi_forcing_fixture_ana
# fixt.after_intitialization_check() # NOTE this is disabled because with -n 2, there are attrs initialized to arbitrary values.
for i in range(total_timesteps):
logging.info("Starting bmi_model.update()...")
fixt.bmi_model.update()
fixt.after_bmi_model_update(
current_output_step=i + 1,
)
logging.info("Starting bmi_model.finalize()...")
fixt.bmi_model.finalize()
fixt.after_finalize()
41 changes: 17 additions & 24 deletions tests/bmi_model/test_bmi_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,30 @@
test_utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_utils)

consts = test_utils.test_consts
configs = test_utils.test_config_classes

### This disables a LOG call which was causing a crash at ioMod.py: LOG.debug(f"Wgrib2 command: {Wgrib2Cmd}", True)
os.environ["MFE_SILENT"] = "true"

TEST_CONFIGS = [
configs.TestConfig_BmiModel(
config_file=consts.RETRO_FORCING_CONFIG_FILE__AORC_CONUS,
keys_to_check=(),
keys_to_exclude=tuple(
set(consts.KEYS_TO_EXCLUDE) | {"d_program_init", "geogrid", "scratch_dir"}
),
grid_type=consts.GRID_TYPE,
test_file_name_prefix="bmi_model",
),
]

RETRO_FORCING_CONFIG_FILE__AORC_CONUS = (
"/workspaces/nwm-rte/src/ngen-forcing/tests/test_data/configs/aorc_config.yml"
)
COMPOSITE_KEYS_TO_CHECK = ()
GRID_TYPE = "hydrofabric" # ["gridded","hydrofabric","unstructured"]
### Drop non-deterministic values (random IDs, timestamps, hashed paths)
KEYS_TO_EXCLUDE = ("uid64", "d_program_init", "geogrid", "scratch_dir")


@pytest.mark.parametrize(
"bmi_forcing_fixture_bmi_model",
[
(
RETRO_FORCING_CONFIG_FILE__AORC_CONUS,
COMPOSITE_KEYS_TO_CHECK,
KEYS_TO_EXCLUDE,
GRID_TYPE,
)
],
indirect=True,
)

@pytest.mark.parametrize("bmi_forcing_fixture_bmi_model", TEST_CONFIGS, indirect=True)
def test_bmi_model(
bmi_forcing_fixture_bmi_model: test_utils.BMIForcingFixture_BmiModel, # pyright: ignore
) -> None:
"""Pytest function for testing BMI model functionality."""
### Total number of timesteps needs to be at least 2, since the 1st one behaves differently than the others, e.g. see `if config_options.current_output_step == 1` throughout the code.
### Total number of timesteps needs to be at least 3, since the 1st and 2nd behaves differently than the others,
### e.g. see `if config_options.current_output_step == 1` throughout the code and the regridded_forcings1 vs regridded_forcings2 weighting.
total_timesteps = 3

fixt = bmi_forcing_fixture_bmi_model
Expand Down
56 changes: 56 additions & 0 deletions tests/config_options/test_config_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import importlib.util
import os

import pytest

### Load import tests.test_utils as test_utils, referring explicitly to its path.
### This explicit load is necessary since March 2026 versions of ngen which introduced /ngen-app/ngen/extern/topoflow-glacier/tests
spec = importlib.util.spec_from_file_location(
"tests.test_utils", os.path.abspath("tests/test_utils.py")
)
test_utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_utils)

consts = test_utils.test_consts
configs = test_utils.test_config_classes

TEST_FILE_NAME_PREFIX = "config_options"

TEST_CONFIGS = [
configs.TestConfig_ConfigOptions(
config_file=consts.RETRO_FORCING_CONFIG_FILE__AORC_CONUS,
keys_to_check=consts.COMPOSITE_KEYS_TO_CHECK,
keys_to_exclude=tuple(
set(consts.KEYS_TO_EXCLUDE) | {"d_program_init", "geogrid", "scratch_dir"}
),
grid_type=consts.GRID_TYPE,
test_file_name_prefix=TEST_FILE_NAME_PREFIX,
),
]


@pytest.mark.parametrize(
"bmi_forcing_fixture_configoptions", TEST_CONFIGS, indirect=True
)
def test_geomod(
bmi_forcing_fixture_configoptions: test_utils.BMIForcingFixture_GeoMod, # pyright: ignore
) -> None:
"""Pytest function for testing ConfigOptions functionality."""
### Total number of timesteps needs to be at least 3, since the 1st and 2nd behaves differently than the others,
### e.g. see `if config_options.current_output_step == 1` throughout the code and the regridded_forcings1 vs regridded_forcings2 weighting.
total_timesteps = 3

fixt = bmi_forcing_fixture_configoptions
if len(fixt.input_forcing_mod) != 1:
raise ValueError(
f"Expected 1 key for input_forcing_mod, got {len(fixt.input_forcing_mod)}: {list(fixt.input_forcing_mod.keys())}"
)

fixt.after_intitialization_check()
for i in range(total_timesteps):
fixt.bmi_model.update()
fixt.after_bmi_model_update(
current_output_step=i + 1,
)
fixt.bmi_model.finalize()
fixt.after_finalize()
Loading