-
Notifications
You must be signed in to change notification settings - Fork 341
New tools: query_paramfile and set_paramfile #3397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 85 commits
Commits
Show all changes
127 commits
Select commit
Hold shift + click to select a range
743098a
Add query_paramfile tool.
samsrabin 0e2e1e3
query_paramfile: Accept comma-separated variable list.
samsrabin 72a27f7
query_paramfile: Print PFT name next to PFT-specific params.
samsrabin 56aa742
query_paramfile: Align PFT-specific values.
samsrabin 8e70046
query_paramfile: Add optional -p/--pft option.
samsrabin debbe36
Rename query_parameters/ to param_utils/.
samsrabin e1f29b2
query_paramfile: Add unit tests of get_arguments().
samsrabin 71f2aff
query_paramfile: Add unit tests of print_values().
samsrabin 99baf01
query_paramfile: Add simple system test; failing.
samsrabin bf39325
query_paramfile: Fix call w/o -p/--pft.
samsrabin 35bac74
query_paramfile: Add more system tests; failing.
samsrabin b2f08bd
query_paramfile: Always get pft_names if on paramfile.
samsrabin 6429f00
query_parameters: Add docstrings.
samsrabin 29ff74d
query_paramfile: Move some parser stuff to paramfile_shared.
samsrabin 2a178bc
query_paramfile: Split comma-separated lists in parse_args().
samsrabin 958aaf2
Add set_paramfile tool and tests. So far just parses some args.
samsrabin 337a072
set_paramfile: Can now copy a file.
samsrabin cea3f12
set_paramfile: Use -v/--variables to only include some vars.
samsrabin 93f29e2
set_paramfile: Can now change params. Tested w/ scalars.
samsrabin 22bdf2d
set_paramfile: Include just some PFTs with -p/--pft.
samsrabin 3aeb040
set_paramfile: Can now change 1-d parameters.
samsrabin a148838
query_paramfile: Fix call for PFT param w/o -p/--pft.
samsrabin bedf08d
Update _paramfile tool help descriptions.
samsrabin f0ba3e0
set_paramfile: Check that new value has right ndim.
samsrabin 73d0a01
set_paramfile sys tests: Check dtype didn't change.
samsrabin fed3ba4
set_paramfile: Add is_integer(). Tested but not yet used.
samsrabin 3056c36
set_paramfile: Test changing scalar int param. Failing.
samsrabin 552f979
set_paramfile: Can now change scalar int params.
samsrabin b1c0c75
Turns out set_paramfile works for 1d int params too.
samsrabin 6872d2b
set_paramfile: Don't mask or scale when reading parameter file.
samsrabin f37c301
test_sys_set_paramfile: Check that fill val didn't change.
samsrabin a8d90d6
set_paramfile: Avoid calling is_integer().
samsrabin 5602a7d
test_sys_set_paramfile: Test setting scalar double NaN using 'nan'
samsrabin 89f0c42
set_paramfile: Don't allow setting NaN if param doesn't have FillValue.
samsrabin 152ac60
set_paramfile: Don't allow setting NaN in integer params.
samsrabin cb8fadf
Resolve or delete some testing TODOs.
samsrabin 492bcea
set_paramfile: Require --drop-other-pfts to actually trim the PFT list.
samsrabin 14bb2f8
set_paramfile: Can now change just a few PFT values.
samsrabin f42d816
set_paramfile: Can now change just one PFT without touching others.
samsrabin 7eb17f3
Reformat with black.
samsrabin 5462b0e
Add previous commit to .git-blame-ignore-revs.
samsrabin 187a8e4
Satisfy pylint.
samsrabin 4e9d2fe
query_paramfiles: Use space-separated variables to match set_paramfile.
samsrabin c4d6ac6
query_paramfile now prints all variables if none specified.
samsrabin 4019a5a
query_paramfile: Handle multi-dim vars.
samsrabin ae22703
set_paramfile sys tests: Explicitly check that a 'nan' is saved as th…
samsrabin 79059bc
set_paramfile: Fix a comment.
samsrabin 65daa85
set_paramfile now preserves non-NaN _FillValue.
samsrabin 9f5e4b4
Fix subset_data when selecting just 1 pft.
samsrabin a6011fb
set_paramfile: Explain nan in help text.
samsrabin 042f5f4
netcdf_utils: Add are_xr_dataarrays_identical().
samsrabin 9552803
Use are_xr_dataarrays_identical() in set_paramfile system tests. Fail…
samsrabin 24ba0de
are_xr_dataarrays_identical(): NaNs equal; ignore source/original_shape.
samsrabin ee70147
set_paramfile: Don't add fill value to parameters without one.
samsrabin 952a355
are_xr_dataarrays_identical(): Refactor for pylint.
samsrabin 895a8df
are_xr_dataarrays_identical(): Don't ignore anything by default.
samsrabin 5492fff
set_paramfile: Add save_paramfile().
samsrabin 31894b1
set_paramfile: Refactor to reduce branching.
samsrabin 8e3e382
set_paramfile: Improve error when assigning fill for param w/o one.
samsrabin b9b9420
save_paramfile(): test saving an integer parameter with a fill value
samsrabin fce7287
set_paramfile(): Improve text about integer fill values.
samsrabin 62d7711
Reformat with black.
samsrabin 8246f54
Add previous commit to .git-blame-ignore-revs.
samsrabin eac7e81
Satisfy pylint.
samsrabin ac849e6
save_paramfile: Save calling command to history.
samsrabin f1856ab
set_paramfile: Functionize _convert_to_output_dtype().
samsrabin bc2f068
set_paramfile: Improve/test error when given float for integer param.
samsrabin 0207b46
Move are_dicts_identical_nansequal() to netcdf_utils.
samsrabin f36f3f9
Add SETPARAMFILE SystemTest.
samsrabin 49ad0f7
Reformat with black.
samsrabin 4465b0f
Add prevous commit to .git-blame-ignore-revs.
samsrabin 2eb293c
Merge remote-tracking branch 'escomp/b4b-dev' into query-paramfile
samsrabin 5f4ca9c
set_paramfile: Small refactor.
samsrabin 3211033
query/set_paramfile: Improve help text.
samsrabin 7ff67d6
query/set_paramfile: Update docstrings.
samsrabin f36f1d3
test_sys_set_paramfile: Delete some print()s.
samsrabin be8191d
Improve netcdf_utils and testing.
samsrabin 86dd02c
set_paramfile: Simplify _replace_nans_with_fill().
samsrabin 9040f4a
set_paramfile: Avoid using ds_in for robustness.
samsrabin af2d7dd
set_paramfile: Functionize apply_new_value_to_parameter().
samsrabin 8163648
set_paramfile: chg now optional kw arg if just used in err msg.
samsrabin e49e793
set_paramfile: Improve a comment.
samsrabin 2032633
set_paramfile: Delete questionably-useful, confusing -v/--variables.
samsrabin 6c80312
test_unit_netcdf_utils: Satisfy pylint.
samsrabin e339339
Add docs for paramfile tools.
samsrabin 5b5c40c
Suppress crop cold temp msgs when generate_crop_gdds true.
samsrabin 14a27f0
Move log() and error() from generate_gdds_functions to cropcal_utils.
samsrabin 444fa54
Add logging in GDD generation.
samsrabin 8b5c90d
cropcal_utils log(), error(): Prepend datetime string.
samsrabin 2132750
Reformat with black.
samsrabin 97e2bfe
Add previous commit to .git-blame-ignore-revs.
samsrabin 4113ece
Refactoring to satisfy pylint.
samsrabin ebef8c7
Move 3 functions from cropcal_utils.
samsrabin c057113
ctsm_logging: Add error_type option to error().
samsrabin 191e119
Try adding caller name to log() and error() msgs.
samsrabin e277c92
Finalize caller name in log()/error() msgs.
samsrabin 037ae1b
ctsm_logging: Functionize _compose_log_msg().
samsrabin dcb39b4
Add unit tests of log() and error().
samsrabin 75c6794
Initial add of a unit test for endrun
ekluzek 4053c0a
Add more tests for more of the options
ekluzek 4dc49a9
Move subgrid setup and teardown to setup and teardown methods, add ex…
ekluzek 1586175
Continue the endrun call even if subgrid_level is bad so that the err…
ekluzek 6c38d62
Change to the new expected behavior where the error isn't about the s…
ekluzek 7d25eba
Update to using shr_abort_abort as talked about in #3417
ekluzek a13dcdb
Update to using shr_abort_abort as talked about in #3417
ekluzek 33a8e00
Add testing for bad point in the pt_context which fails because of su…
ekluzek ef54137
Add handling in case the input point is bad to end_run_pt_context
ekluzek 33907d7
Add more error checking if a bad point is input to get_global_index, …
ekluzek 7df48b9
Allow passing file and line to endrun, output it through shr_log_errM…
ekluzek 986fe31
Add some tests for sending in file and line and one with just file to…
ekluzek 9a6bf53
Correct data type of bad_point to logical
ekluzek fc884eb
Replace most uses of npcropmin/max with is_prognostic_crop().
samsrabin 8ca7af5
Avoid using npcropmin/max for looping outside pftconMod.
samsrabin 0dfa907
Add get_crop_n_from_veg_type().
samsrabin 0901503
Add get_veg_type_from_crop_n().
samsrabin c77a405
Add pftconMod public var num_cfts_possible.
samsrabin ff4ead6
npcropmin/max are now private to pftconMod.
samsrabin 8920ed7
CNFUNMod: Improve readability of a line.
samsrabin c20859f
is_prognostic_crop(): Improve note.
samsrabin d685e6f
Change the directory name for the new endrun tests
ekluzek 5ac7e3b
Change the names from the specific subroutine endrun to abortutils th…
ekluzek 854a364
Move the descriptions of test subroutines to just after the subroutin…
ekluzek 5b40496
Reformat with black.
samsrabin a6e2309
Add previous commit to .git-blame-ignore-revs.
samsrabin 22a56e5
Small change for pylint.
samsrabin 1a879d9
set_paramfile: Test & document changing 1-d non-pft param.
samsrabin 73303fb
Merge remote-tracking branch 'escomp/b4b-dev' into query-paramfile
samsrabin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| """ | ||
| CTSM-specific test that first runs the set_paramfile tool and then ensures that CTSM does not fail | ||
| using the just-generated parameter file | ||
| """ | ||
|
|
||
| import os | ||
| import sys | ||
| import logging | ||
| import re | ||
| from CIME.SystemTests.system_tests_common import SystemTestsCommon | ||
|
|
||
| # In case we need to import set_paramfile later | ||
| _CTSM_PYTHON = os.path.join( | ||
| os.path.dirname(os.path.realpath(__file__)), os.pardir, os.pardir, "python" | ||
| ) | ||
| sys.path.insert(1, _CTSM_PYTHON) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class SETPARAMFILE(SystemTestsCommon): | ||
| def __init__(self, case): | ||
| """ | ||
| initialize an object interface to the SMS system test | ||
| """ | ||
| SystemTestsCommon.__init__(self, case) | ||
|
|
||
| # Create out-of-the-box lnd_in to obtain paramfile | ||
| case.create_namelists(component="lnd") | ||
|
|
||
| # Find the paramfile to modify | ||
| lnd_in_path = os.path.join(self._get_caseroot(), "CaseDocs", "lnd_in") | ||
| self._paramfile_in = None | ||
| with open(lnd_in_path, "r", encoding="utf-8") as lnd_in: | ||
| for line in lnd_in: | ||
| paramfile_in = re.match(r" *paramfile *= *'(.*)'", line) | ||
| if paramfile_in: | ||
| self._paramfile_in = paramfile_in.group(1) | ||
| break | ||
| if not self._paramfile_in: | ||
| raise RuntimeError(f"paramfile not found in {lnd_in_path}") | ||
|
|
||
| # Get the output file | ||
| self.paramfile_out = os.path.join(self._get_caseroot(), "paramfile.nc") | ||
|
|
||
| # Define set_paramfile command | ||
| self.set_paramfile_cmd = [ | ||
| "set_paramfile", | ||
| "-i", | ||
| self._paramfile_in, | ||
| "-o", | ||
| self.paramfile_out, | ||
| # Change two parameters for one PFT | ||
| "-p", | ||
| "needleleaf_deciduous_boreal_tree", | ||
| "rswf_min=0.35", | ||
| "rswf_max=0.7", | ||
| ] | ||
|
|
||
| def build_phase(self, sharedlib_only=False, model_only=False): | ||
| """ | ||
| Run set_paramfile and then build the model | ||
| """ | ||
|
|
||
| # Run set_paramfile. | ||
| # build_phase gets called twice: | ||
| # - once with sharedlib_only = True and | ||
| # - once with model_only = True | ||
| # Because we only need set_paramfile run once, we only do it for the sharedlib_only call. | ||
| # We could also check for the existence of the set_paramfile outputs, but that might lead to | ||
| # a situation where the user expects set_paramfile to be called but it's not. Better to run | ||
| # unnecessarily (e.g., if you fixed some FORTRAN code and just need to rebuild). | ||
| if sharedlib_only: | ||
| self._run_set_paramfile() | ||
|
|
||
| # Do the build | ||
| self.build_indv(sharedlib_only=sharedlib_only, model_only=model_only) | ||
|
|
||
| def _run_set_paramfile(self): | ||
| """ | ||
| Run set_paramfile | ||
| """ | ||
| # Import set_paramfile. Do it here rather than at top because otherwise the import will | ||
| # be attempted even during RUN phase. | ||
| # pylint: disable=wrong-import-position,import-outside-toplevel | ||
| from ctsm.param_utils.set_paramfile import main as set_paramfile | ||
|
|
||
| # Run set_paramfile | ||
| sys.argv = self.set_paramfile_cmd | ||
| set_paramfile() | ||
|
|
||
| # Append | ||
| user_nl_clm_path = os.path.join(self._get_caseroot(), "user_nl_clm") | ||
| with open(user_nl_clm_path, "a", encoding="utf-8") as user_nl_clm: | ||
| user_nl_clm.write(f"paramfile = '{self.paramfile_out}'\n") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,4 @@ Using CLM tools | |
| creating-domain-files.rst | ||
| observational-sites-datasets.rst | ||
| cprnc.rst | ||
| paramfile-tools.md | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
|
|
||
| # Tools for working with parameter files | ||
|
|
||
| This guide describes the features and usage of the `query_paramfile` and `set_paramfile` tools, located in `tools/param_utils/`. These utilities help users inspect and modify CLM parameter files. | ||
|
|
||
| Note that you need to have the `ctsm_pylib` conda environment activated to use these tools. See Sect. :numref:`using-ctsm-pylib` for more information. | ||
|
|
||
| ## `query_paramfile` | ||
| **Purpose:** Print the values of one or more parameters from a CTSM parameter file (NetCDF format). | ||
|
|
||
| **Features:** | ||
| - Print values for specified parameters or all. | ||
| - Optionally filter output by Plant Functional Types (PFTs) for PFT-specific parameters. | ||
|
|
||
| For more information, do `tools/param_utils/query_paramfile --help`. | ||
|
|
||
|
|
||
| ### Example usage | ||
|
|
||
| Print all variables in a parameter file: | ||
| ```bash | ||
| tools/param_utils/query_paramfile -i paramfile.nc | ||
| ``` | ||
|
|
||
| Print specific variables: | ||
| ```bash | ||
| tools/param_utils/query_paramfile -i paramfile.nc jmaxha jmaxhd | ||
| ``` | ||
|
|
||
| Print values for specific PFTs: | ||
| ```bash | ||
| tools/param_utils/query_paramfile -i paramfile.nc -p needleleaf_evergreen_temperate_tree,c4_grass medlynintercept medlynslope | ||
| ``` | ||
|
|
||
| ## `set_paramfile` | ||
| **Purpose:** Change values of one or more parameters in a CTSM parameter file (NetCDF format). | ||
|
|
||
| **Features:** | ||
| - Modify parameter values for all or selected PFTs. | ||
| - Optionally drop PFTs not specified. | ||
| - Set parameter values to fill (missing) values using `nan`. | ||
| - Ensures safe file handling and checks for argument validity. | ||
|
|
||
| Note that the output file must not already exist. | ||
|
|
||
| For more information, do `tools/param_utils/set_paramfile --help`. | ||
|
|
||
| ### Example usage | ||
|
|
||
| Change a parameter for all PFTs: | ||
| ```bash | ||
| tools/param_utils/set_paramfile -i paramfile.nc -o output.nc jmaxha=51000 | ||
| ``` | ||
|
|
||
| Change a parameter for specific PFTs: | ||
| ```bash | ||
| tools/param_utils/set_paramfile -i paramfile.nc -o output.nc -p needleleaf_evergreen_temperate_tree,c4_grass medlynintercept=99.9,100.1 medlynslope=2.99,1.99 | ||
| ``` | ||
|
|
||
| Set a parameter to the fill value: | ||
| ```bash | ||
| tools/param_utils/set_paramfile -i paramfile.nc -o output.nc -p needleleaf_evergreen_temperate_tree,c4_grass fleafcn=nan,nan | ||
| ``` | ||
samsrabin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| """ | ||
| Helper functions for working with netCDF files | ||
| """ | ||
|
|
||
| import numpy as np | ||
| import xarray as xr | ||
| from netCDF4 import Dataset # pylint: disable=no-name-in-module | ||
|
|
||
|
|
||
| def _is_dtype_nan_capable(ndarray: np.ndarray): | ||
| """ | ||
| Given a numpy array, return True if it's capable of taking a NaN | ||
| """ | ||
| try: | ||
| np.isnan(ndarray) | ||
| return True | ||
| except TypeError: | ||
| return False | ||
|
|
||
|
|
||
| def _are_dicts_identical_nansequal(dict0: dict, dict1: dict, keys_to_ignore=None): | ||
| """ | ||
| Compare two dictionaries, considering NaNs to be equal. Don't be strict here about types; if | ||
| they can be coerced to comparable types and then they match, return True. | ||
| """ | ||
| # pylint: disable=too-many-return-statements | ||
|
|
||
| if keys_to_ignore is None: | ||
| keys_to_ignore = [] | ||
| keys_to_ignore = np.array(keys_to_ignore) | ||
|
|
||
| if len(dict0) != len(dict1): | ||
| return False | ||
| for key, value0 in dict0.items(): | ||
| if key in keys_to_ignore: | ||
| continue | ||
| if key not in dict1: | ||
| return False | ||
| value1 = dict1[key] | ||
|
|
||
| # Coerce to numpy arrays to simplify comparison code | ||
| value0 = np.array(value0) | ||
| value1 = np.array(value1) | ||
|
|
||
| # Compare, only asking to check equal NaNs if both are capable of taking NaN values | ||
| both_are_nan_capable = _is_dtype_nan_capable(value0) and _is_dtype_nan_capable(value1) | ||
| if not np.array_equal(value0, value1, equal_nan=both_are_nan_capable): | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def get_netcdf_format(file_path): | ||
| """ | ||
| Get format of netCDF file | ||
| """ | ||
| with Dataset(file_path, "r") as netcdf_file: | ||
| netcdf_format = netcdf_file.data_model | ||
| return netcdf_format | ||
|
|
||
|
|
||
| def _is_dataarray_metadata_identical(da0: xr.DataArray, da1: xr.DataArray, keys_to_ignore=None): | ||
| """ | ||
| Check whether two DataArrays have identical-enough metadata | ||
| """ | ||
|
|
||
| # Check data type | ||
| if da0.dtype != da1.dtype: | ||
| return False | ||
|
|
||
| # Check encoding | ||
| if not _are_dicts_identical_nansequal( | ||
| da0.encoding, da1.encoding, keys_to_ignore=keys_to_ignore | ||
| ): | ||
| return False | ||
|
|
||
| # Check attributes | ||
| if not _are_dicts_identical_nansequal(da0.attrs, da1.attrs): | ||
| return False | ||
|
|
||
| # Check name | ||
| if da0.name != da1.name: | ||
| return False | ||
|
|
||
| # Check dims | ||
| if da0.dims != da1.dims: | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def _is_dataarray_data_identical(da0: xr.DataArray, da1: xr.DataArray): | ||
| """ | ||
| Check whether two DataArrays have identical data | ||
| """ | ||
| # pylint: disable=too-many-return-statements | ||
|
|
||
| # Check sizes | ||
| if da0.sizes != da1.sizes: | ||
| return False | ||
|
|
||
| # Check coordinates | ||
| if bool(da0.coords) or bool(da1.coords): | ||
| if not bool(da0.coords) or not bool(da1.coords): | ||
| return False | ||
| if not da0.coords.equals(da1.coords): | ||
| return False | ||
|
|
||
| # Check values ("The array's data converted to numpy.ndarray") | ||
| if not np.array_equal(da0.values, da1.values): | ||
| # Try-except to avoid TypeError from putting NaN-incapable dtypes through | ||
| # np.array_equal(..., equal_nan=True) | ||
| try: | ||
| if not np.array_equal(da0.values, da1.values, equal_nan=True): | ||
| return False | ||
| except TypeError: | ||
| return False | ||
|
|
||
| # Check data ("The DataArray's data as an array. The underlying array type (e.g. dask, sparse, | ||
| # pint) is preserved.") | ||
| da0_data_type = type(da0.data) | ||
| if not isinstance(da1.data, da0_data_type): | ||
| return False | ||
| if not isinstance(da0.data, np.ndarray): | ||
| raise NotImplementedError(f"Add support for comparing two objects of type {da0_data_type}") | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def are_xr_dataarrays_identical(da0: xr.DataArray, da1: xr.DataArray, keys_to_ignore=None): | ||
| """ | ||
| Comprehensively check whether two DataArrays are identical | ||
| """ | ||
| if not _is_dataarray_metadata_identical(da0, da1, keys_to_ignore=keys_to_ignore): | ||
| return False | ||
|
|
||
| if not _is_dataarray_data_identical(da0, da1): | ||
| return False | ||
|
|
||
| # Fallback to however xarray defines equality, in case we missed something above | ||
| return da0.equals(da1) |
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.