Skip to content

Commit 38f05f9

Browse files
committed
Add unit tests
- Test coverage for get_time_sliced_dataset(), _add_time_series_filepath_attr(), check_values(), and get_xarray_datasets()`
1 parent f0e5426 commit 38f05f9

File tree

6 files changed

+417
-64
lines changed

6 files changed

+417
-64
lines changed

e3sm_diags/driver/utils/dataset_xr.py

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,6 @@ def get_time_sliced_dataset(self, var: str, time_slice: TimeSlice) -> xr.Dataset
360360
361361
These variables can either be from the test data or reference data.
362362
363-
TODO: Add unit test for this method.
364-
365363
Parameters
366364
----------
367365
var : str
@@ -381,34 +379,31 @@ def get_time_sliced_dataset(self, var: str, time_slice: TimeSlice) -> xr.Dataset
381379
if not isinstance(self.var, str) or self.var == "":
382380
raise ValueError("The `var` argument is not a valid string.")
383381

384-
ds = self._get_full_dataset(var)
385-
ds = self._apply_time_slice_to_dataset(ds, time_slice)
382+
filepath = self._get_filepath_with_params()
386383

387-
try:
388-
filepath = self._get_filepath_with_params()
384+
if filepath is None:
385+
raise RuntimeError(
386+
f"Unable to get file path for {self.data_type} dataset. "
387+
f"For time slicing, please ensure that "
388+
f"{'ref_file' if self.data_type == 'ref' else 'test_file'} parameter is set."
389+
)
389390

390-
if filepath:
391-
self.parameter._add_filepath_attr(self.data_type, filepath)
391+
if not os.path.exists(filepath):
392+
raise RuntimeError(f"File not found: {filepath}")
392393

393-
except Exception as e:
394-
logger.warning(f"Failed to store absolute file path: {e}")
394+
self.parameter._add_filepath_attr(self.data_type, filepath)
395+
396+
ds = self._get_full_dataset()
397+
ds = self._apply_time_slice_to_dataset(ds, time_slice)
395398

396399
return ds
397400

398-
def _get_full_dataset(self, var: str) -> xr.Dataset:
401+
def _get_full_dataset(self) -> xr.Dataset:
399402
"""Get the full dataset without any time averaging for time slicing.
400403
401404
This function uses the dataset's file path parameters to directly open
402405
the raw data file for time slicing operations.
403406
404-
TODO: Add unit test for this method.
405-
FIXME: The `var` parameter is currently unused.
406-
407-
Parameters
408-
----------
409-
var : str
410-
The key of the variable.
411-
412407
Returns
413408
-------
414409
xr.Dataset
@@ -419,31 +414,20 @@ def _get_full_dataset(self, var: str) -> xr.Dataset:
419414
RuntimeError
420415
If unable to get the full dataset or file not found.
421416
"""
422-
filepath = self._get_filepath_with_params()
423-
424-
if filepath is None:
425-
raise RuntimeError(
426-
f"Unable to get file path for {self.data_type} dataset. "
427-
f"For time slicing, please ensure that "
428-
f"{'ref_file' if self.data_type == 'ref' else 'test_file'} parameter is set."
429-
)
430-
431-
if not os.path.exists(filepath):
432-
raise RuntimeError(f"File not found: {filepath}")
417+
filepath = getattr(self.parameter, f"{self.data_type}_data_file_path")
433418

434419
logger.info(f"Opening full dataset from: {filepath}")
435420

436421
try:
437-
ds = xr.open_dataset(filepath, decode_times=True)
422+
ds = xc.open_dataset(filepath, add_bounds=["X", "Y", "T"])
438423

439424
logger.info(
440425
f"Successfully opened dataset with time dimension size: {ds.sizes.get('time', 'N/A')}"
441426
)
442-
443-
return ds
444-
# FIXME: This except block is too broad; it should catch specific exceptions.
445-
except Exception as e:
427+
except (FileNotFoundError, OSError, ValueError) as e:
446428
raise RuntimeError(f"Failed to open dataset {filepath}: {e}") from e
429+
else:
430+
return ds
447431

448432
def _get_filepath_with_params(self) -> str | None:
449433
"""Get the filepath using parameters.
@@ -483,14 +467,10 @@ def _apply_time_slice_to_dataset(
483467
xr.Dataset
484468
The dataset with time slice applied.
485469
"""
486-
# FIXME: Use xcdat.get_dim_keys to find time dimension.
487-
time_dim = None
488-
489-
for dim in ds.dims:
490-
if str(dim).lower() in ["time", "t"]:
491-
time_dim = dim
492-
493-
break
470+
try:
471+
time_dim = xc.get_dim_keys(ds, axis="T")
472+
except (ValueError, KeyError):
473+
time_dim = None
494474

495475
if time_dim is None:
496476
logger.warning(
@@ -499,9 +479,15 @@ def _apply_time_slice_to_dataset(
499479

500480
return ds
501481

502-
# Single index selection
503482
index = int(time_slice)
504-
ds_sliced = ds.isel({time_dim: index})
483+
484+
try:
485+
ds_sliced = ds.isel({time_dim: index})
486+
except IndexError as e:
487+
raise IndexError(
488+
f"Time slice index {index} is out of bounds for time dimension "
489+
f"of size {ds.sizes[time_dim]}."
490+
) from e
505491

506492
logger.info(
507493
f"Applied time slice '{time_slice}' to dataset. "
@@ -562,7 +548,7 @@ def get_climo_dataset(self, var: str, season: ClimoFreq) -> xr.Dataset:
562548
ds_climo = climo(ds, self.var, season).to_dataset()
563549
ds_climo = ds_climo.bounds.add_missing_bounds(axes=["X", "Y"])
564550

565-
self.parameter._add_time_series_file_path_attr(self.data_type, ds)
551+
self.parameter._add_time_series_filepath_attr(self.data_type, ds)
566552

567553
return ds_climo
568554

e3sm_diags/parameter/core_parameter.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ def _run_diag(self) -> list[Any]:
412412

413413
return results
414414

415-
def _add_time_series_file_path_attr(
415+
def _add_time_series_filepath_attr(
416416
self,
417417
data_type: Literal["test", "ref"],
418418
ds: xr.Dataset,
@@ -434,9 +434,9 @@ def _add_time_series_file_path_attr(
434434
if data_type not in {"test", "ref"}:
435435
raise ValueError("data_type must be either 'test' or 'ref'.")
436436

437-
file_path_attr = f"{data_type}_data_file_path"
437+
filepath_attr = f"{data_type}_data_file_path"
438438

439-
setattr(self, file_path_attr, getattr(ds, "file_path", "Unknown"))
439+
setattr(self, filepath_attr, getattr(ds, "file_path", "Unknown"))
440440

441441
def _add_filepath_attr(
442442
self,
@@ -462,14 +462,14 @@ def _add_filepath_attr(
462462
if data_type not in {"test", "ref"}:
463463
raise ValueError("data_type must be either 'test' or 'ref'.")
464464

465-
file_path_attr = f"{data_type}_data_file_path"
465+
filepath_attr = f"{data_type}_data_file_path"
466466

467467
if not filepath:
468468
raise ValueError(
469469
"Filepath must be provided for the climatology or time-slice data."
470470
)
471471

472-
setattr(self, file_path_attr, os.path.abspath(filepath))
472+
setattr(self, filepath_attr, os.path.abspath(filepath))
473473

474474
def _get_time_selection_to_use(
475475
self, require_one: bool = True
@@ -479,8 +479,6 @@ def _get_time_selection_to_use(
479479
480480
If ``time_slices`` are specified, they take precedence over ``seasons``.
481481
482-
TODO: Add unit tests for this method.
483-
484482
Parameters
485483
----------
486484
require_one : bool, optional

examples/ex8-native-grid-visualization/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This example demonstrates the **native grid visualization** feature introduced i
1212
## Key Features
1313

1414
The native grid visualization capability:
15+
1516
- Supports various native grid formats (cubed-sphere, unstructured, etc.)
1617
- Eliminates artifacts introduced by regridding
1718
- Enables comparison of models with different native grids
@@ -28,10 +29,12 @@ The native grid visualization capability:
2829
## Data Requirements
2930

3031
This example uses test data located at LCRC:
32+
3133
- Data path: `/lcrc/group/e3sm/public_html/e3sm_diags_test_data/native_grid`
3234
- Grid files: `/lcrc/group/e3sm/diagnostics/grids/`
3335

3436
For your own data, ensure you have:
37+
3538
1. Model output files on native grid
3639
2. Corresponding grid files in UGRID format
3740

@@ -79,6 +82,7 @@ e3sm_diags lat_lon_native \
7982
## Configuration File
8083

8184
The `diags.cfg` file allows you to customize:
85+
8286
- Variables to plot (e.g., TGCLDLWP)
8387
- Regions of interest
8488
- Colormap settings
@@ -87,6 +91,7 @@ The `diags.cfg` file allows you to customize:
8791
## Expected Output
8892

8993
The diagnostic will generate:
94+
9095
- Native grid visualizations for specified variables
9196
- Test model plot
9297
- Reference model plot
@@ -97,14 +102,15 @@ Results will be saved in: `/lcrc/group/e3sm/public_html/diagnostic_output/$USER/
97102

98103
## Notes
99104

100-
- Native grid visualization requires UXarray library (included in E3SM Unified environment)
105+
- Native grid visualization requires the UXarray library, which is included as a dependency of E3SM diagnostics and the E3SM Unified environment.
101106
- Grid files must be in UGRID format
102107
- This example uses `time_slices` for snapshot analysis; you can also use `seasons` for climatology
103108
- For model-only runs (no reference data), set `model_only = True` and omit ref_grid_file
104109

105110
## Differences from Regular lat_lon Set
106111

107112
Unlike the standard `lat_lon` set which regrids data to a regular lat-lon grid:
113+
108114
- `lat_lon_native` preserves the original grid structure
109115
- No interpolation artifacts
110116
- Better representation of native grid features
@@ -113,5 +119,6 @@ Unlike the standard `lat_lon` set which regrids data to a regular lat-lon grid:
113119
## More Information
114120

115121
For more details, see:
122+
116123
- [E3SM Diags Documentation](https://e3sm-project.github.io/e3sm_diags)
117124
- [UXarray Documentation](https://uxarray.readthedocs.io/)

0 commit comments

Comments
 (0)