Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6b29b53
Python tools are now smart about longitude format.
samsrabin Mar 20, 2025
e5bbb14
subset_data: Don't require --lon-type if lons are unambiguous.
samsrabin Apr 3, 2025
cb06cda
subset_data: Fix help text for --lon[1,2]? args.
samsrabin Apr 4, 2025
4c004e7
Fix handling of longitude ranges that cross Prime Meridian.
samsrabin Apr 4, 2025
f107e09
Fix convert_lon_0to360().
samsrabin Apr 4, 2025
e649b0a
subset_data: Refer to ESCOMP/CTSM#2017 in lon1>=lon2 error msg.
samsrabin Apr 4, 2025
ade5baf
subset_data: Improve lon1>=lon2 error msg and test.
samsrabin Apr 4, 2025
95ae84a
Fix submodule versions.
samsrabin Apr 4, 2025
73c399e
Fix verbiage re: International Date Line vs. Prime Meridian.
samsrabin Apr 4, 2025
8b905d4
subset_data: Functionize adding --lon-type arg.
samsrabin Apr 4, 2025
251e389
Reformat with black.
samsrabin Apr 4, 2025
ef5db45
Add previous commit to .git-blame-ignore-revs.
samsrabin Apr 4, 2025
4483a90
Resolve a pylint line-too-long complaint.
samsrabin Apr 4, 2025
293ac54
Check longitudes in RegionalCase.
samsrabin Apr 4, 2025
27d45c9
Check RegionalCase in test_unit_subset_data; fix exposed bugs.
samsrabin Apr 4, 2025
3860a60
Undo some unneeded changes to subset_data args.
samsrabin Apr 4, 2025
59863ad
Restore plon_type. As original, but don't convert.
samsrabin Apr 4, 2025
7ca9ad0
Restore plon_type tests.
samsrabin Apr 4, 2025
06577f8
Remove an unneeded print().
samsrabin Apr 4, 2025
73058a4
test_sys_mesh_modifier: Set self._lon_type to 360.
samsrabin Apr 4, 2025
e65a59a
User's Guide: Fix a heading.
samsrabin Apr 4, 2025
00ef05a
User's Guide: Improve formatting of a note.
samsrabin Apr 4, 2025
c64bc2e
User's Guide, subset_data: Add info about --lon-type.
samsrabin Apr 4, 2025
8cabe07
Merge remote-tracking branch 'escomp/b4b-dev' into fix-python-tools-l…
samsrabin Apr 15, 2025
52a300e
Python: Add Longitude class.
samsrabin Apr 15, 2025
62001d5
Improve erroring around longitude type detection.
samsrabin Apr 16, 2025
cc7e951
Replace International Date Line language with 180th Meridian.
samsrabin Apr 16, 2025
e9add42
Add "Must be unambiguous" comment at pt_parser --lon.
samsrabin Apr 16, 2025
8ec5013
Reformat with black.
samsrabin Apr 16, 2025
b4b4c53
Add previous commit to .git-blame-ignore-revs.
samsrabin Apr 16, 2025
4148342
Resolve pylint complaints.
samsrabin Apr 16, 2025
b97904f
Merge branch 'b4b-dev' into fix-python-tools-longitude-format
samsrabin Apr 16, 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
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ cf433215b58ba8776ec5edfb0b0d80c0836ed3a0
16d57ff37859b34dab005693e3085d64e2bcd95a
e8fc526e0d7818d45f171488c78392c4ff63902a
cdf40d265cc82775607a1bf25f5f527bacc97405
251e389b361ba673b508e07d04ddcc06b2681989
7 changes: 1 addition & 6 deletions python/ctsm/args_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import logging
import argparse

from ctsm.config_utils import lon_range_0_to_360

logger = logging.getLogger(__name__)


Expand All @@ -34,9 +32,7 @@ def plat_type(plat):
def plon_type(plon):
"""
Function to define lon type for the parser and
convert negative longitudes to 0-360 and
raise error if lon is not between -180 and 360.

Args:
plon (str): longitude
Raises:
Expand All @@ -49,5 +45,4 @@ def plon_type(plon):
raise argparse.ArgumentTypeError(
"ERROR: Longitude should be between 0 and 360 or -180 and 180."
)
plon_out = lon_range_0_to_360(plon_float)
return plon_out
return plon_float
55 changes: 43 additions & 12 deletions python/ctsm/config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,57 @@
_CONFIG_UNSET = "UNSET"


def lon_range_0_to_360(lon_in):
def convert_lon_0to360(lon_in):
Comment thread
samsrabin marked this conversation as resolved.
Outdated
"""
Description
-----------
Restrict longitude to 0 to 360 when given as -180 to 180.
Convert a longitude from [-180, 180] format (i.e., centered around Prime Meridian) to [0, 360]
format (i.e., centered around International Date Line).
Comment thread
samsrabin marked this conversation as resolved.
Outdated
"""
if -180 <= lon_in < 0:
raise NotImplementedError(
"A negative longitude suggests you input longitudes in the range [-180, 0)---"
"i.e., centered around the Prime Meridian. This code requires longitudes in the "
"range [0, 360)---i.e., starting at the International Date Line."
)
if not (0 <= lon_in <= 360 or lon_in is None):
errmsg = "lon_in needs to be in the range 0 to 360"
abort(errmsg)
lon_out = lon_in
if not -180 <= lon_in <= 180:
raise ValueError(f"lon_in needs to be in the range [-180, 180]: {lon_in}")
lon_out = lon_in % 360
logger.info(
"Converting longitude from [-180, 180] to [0, 360]: %s to %s",
str(lon_in),
str(lon_out),
)

return lon_out


def convert_lons_if_needed(lon_1, lon_2, lon_type):
"""
Description
-----------
Given two longitudes, if their type is 180 (i.e., between -180 and 180), convert them to 0-360
"""
if lon_type == 180:
lon_1 = convert_lon_0to360(lon_1)
lon_2 = convert_lon_0to360(lon_2)
elif lon_type != 360:
raise ValueError(f"lon_type must be either 180 or 360, not {lon_type}")
return lon_1, lon_2


def check_lon1_lt_lon2(lon1, lon2, lon_type):
"""
Description
-----------
Given two longitudes, check that lon1 is < lon2. Useful for avoiding CTSM Issue #2017, but note
that to use this function properly for that purpose, you need to have already converted
longitudes from lon_type 180 to 360.
"""
if lon1 < lon2:
return

msg = f"--lon1 ({lon1}) must be < --lon2 ({lon2})\n"
msg += "See CTSM issue #2017: https://github.com/ESCOMP/CTSM/issues/2017"
if lon_type == 180:
msg = "After converting to --lon-type 360, " + msg
raise ValueError(msg)


def get_config_value(
config,
section,
Expand Down
3 changes: 1 addition & 2 deletions python/ctsm/crop_calendars/cropcal_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,7 @@ def safer_timeslice(ds_in, time_slice, time_var="time"):

def lon_idl2pm(lons_in, fail_silently=False):
Comment thread
samsrabin marked this conversation as resolved.
Outdated
"""
Convert a longitude axis that's -180 to 180 around the international date line to one that's 0
to 360 around the prime meridian.
Convert a longitude axis that's -180 to 180 to one that's 0 to 360

- If you pass in a Dataset or DataArray, the "lon" coordinates will be changed. Otherwise, it
assumes you're passing in numeric data.
Expand Down
20 changes: 19 additions & 1 deletion python/ctsm/modify_input_files/fsurdat_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,13 @@ def read_cfg_required_basic_opts(config, section, cfg_path):
file_path=cfg_path,
convert_to_type=float,
)
lon_type = get_config_value(
config=config,
section=section,
item="lon_type",
file_path=cfg_path,
convert_to_type=int,
)

landmask_file = get_config_value(
config=config,
Expand All @@ -513,7 +520,16 @@ def read_cfg_required_basic_opts(config, section, cfg_path):
lon_dimname = get_config_value(
config=config, section=section, item="lon_dimname", file_path=cfg_path, can_be_unset=True
)
return (lnd_lat_1, lnd_lat_2, lnd_lon_1, lnd_lon_2, landmask_file, lat_dimname, lon_dimname)
return (
lnd_lat_1,
lnd_lat_2,
lnd_lon_1,
lnd_lon_2,
landmask_file,
lat_dimname,
lon_dimname,
lon_type,
)


def fsurdat_modifier(parser):
Expand Down Expand Up @@ -568,6 +584,7 @@ def fsurdat_modifier(parser):
landmask_file,
lat_dimname,
lon_dimname,
lon_type,
) = read_cfg_required_basic_opts(config, section, cfg_path)
# Create ModifyFsurdat object
modify_fsurdat = ModifyFsurdat.init_from_file(
Expand All @@ -579,6 +596,7 @@ def fsurdat_modifier(parser):
landmask_file,
lat_dimname,
lon_dimname,
lon_type,
)

# Read control information about the optional sections
Expand Down
3 changes: 2 additions & 1 deletion python/ctsm/modify_input_files/mesh_mask_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ def mesh_mask_modifier(cfg_path):
lon_varname = get_config_value(
config=config, section=section, item="lon_varname", file_path=cfg_path
)
lon_type = get_config_value(config=config, section=section, item="lon_type", file_path=cfg_path)

# Create ModifyMeshMask object
modify_mesh_mask = ModifyMeshMask.init_from_file(
mesh_mask_in, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname
mesh_mask_in, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname, lon_type
)

# If output file exists, abort before starting work
Expand Down
26 changes: 19 additions & 7 deletions python/ctsm/modify_input_files/modify_fsurdat.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from ctsm.utils import abort, update_metadata
from ctsm.git_utils import get_ctsm_git_short_hash
from ctsm.config_utils import lon_range_0_to_360
from ctsm.config_utils import convert_lons_if_needed

logger = logging.getLogger(__name__)

Expand All @@ -26,7 +26,7 @@ class ModifyFsurdat:
"""

def __init__(
self, my_data, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname
self, my_data, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname, lon_type
):

self.numurbl = 3 # Number of urban density types
Expand All @@ -36,13 +36,15 @@ def __init__(
else:
abort("numurbl is not a dimension on the input surface dataset file and needs to be")

self.lon_type = lon_type
self.rectangle = self._get_rectangle(
lon_1=lon_1,
lon_2=lon_2,
lat_1=lat_1,
lat_2=lat_2,
longxy=self.file.LONGXY,
latixy=self.file.LATIXY,
lon_type=self.lon_type,
)

if landmask_file is not None:
Expand Down Expand Up @@ -72,23 +74,33 @@ def __init__(

@classmethod
def init_from_file(
cls, fsurdat_in, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname
cls,
fsurdat_in,
lon_1,
lon_2,
lat_1,
lat_2,
landmask_file,
lat_dimname,
lon_dimname,
lon_type,
):
"""Initialize a ModifyFsurdat object from file fsurdat_in"""
logger.info("Opening fsurdat_in file to be modified: %s", fsurdat_in)
my_file = xr.open_dataset(fsurdat_in)
return cls(my_file, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname)
return cls(
my_file, lon_1, lon_2, lat_1, lat_2, landmask_file, lat_dimname, lon_dimname, lon_type
)

@staticmethod
def _get_rectangle(lon_1, lon_2, lat_1, lat_2, longxy, latixy):
def _get_rectangle(lon_1, lon_2, lat_1, lat_2, longxy, latixy, lon_type):
"""
Description
-----------
"""

# ensure that lon ranges 0-360 in case user entered -180 to 180
lon_1 = lon_range_0_to_360(lon_1)
lon_2 = lon_range_0_to_360(lon_2)
lon_1, lon_2 = convert_lons_if_needed(lon_1, lon_2, lon_type)

# determine the rectangle(s)
# TODO This is not really "nearest" for the edges but isel didn't work
Expand Down
19 changes: 12 additions & 7 deletions python/ctsm/modify_input_files/modify_mesh_mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import xarray as xr

from ctsm.utils import abort
from ctsm.config_utils import lon_range_0_to_360
from ctsm.config_utils import convert_lon_0to360

logger = logging.getLogger(__name__)

Expand All @@ -29,7 +29,9 @@ class ModifyMeshMask:
# /glade/work/slevis/git/mksurfdata_toolchain/tools/modify_input_files ...
# ... /islas_examples/modify_fsurdat/fill_indian_ocean/
# Read mod_lnd_props here only for consistency checks
def __init__(self, my_data, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname):
def __init__(
self, my_data, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname, lon_type
):

self.file = my_data

Expand All @@ -46,6 +48,7 @@ def __init__(self, my_data, landmask_file, lat_dimname, lon_dimname, lat_varname
self.lonvar = self._landmask_file[lon_varname][..., :]
self.lsmlat = self._landmask_file.dims[lat_dimname]
self.lsmlon = self._landmask_file.dims[lon_dimname]
self.lon_type = lon_type

lonvar_first = self.lonvar[..., 0].data.max()
lonvar_last = self.lonvar[..., -1].data.max()
Expand Down Expand Up @@ -75,12 +78,14 @@ def __init__(self, my_data, landmask_file, lat_dimname, lon_dimname, lat_varname

@classmethod
def init_from_file(
cls, file_in, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname
cls, file_in, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname, lon_type
):
"""Initialize a ModifyMeshMask object from file_in"""
logger.info("Opening file to be modified: %s", file_in)
my_file = xr.open_dataset(file_in)
return cls(my_file, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname)
return cls(
my_file, landmask_file, lat_dimname, lon_dimname, lat_varname, lon_varname, lon_type
)

def set_mesh_mask(self, var):
"""
Expand Down Expand Up @@ -134,13 +139,13 @@ def set_mesh_mask(self, var):
+ f"{len(self.latvar.sizes)}"
)
abort(errmsg)
# ensure lon range of 0-360 rather than -180 to 180
lonvar_scalar = lon_range_0_to_360(lonvar_scalar)
# lon and lat from the mesh file
lat_mesh = float(self.file["centerCoords"][ncount, 1])
lon_mesh = float(self.file["centerCoords"][ncount, 0])
# ensure lon range of 0-360 rather than -180 to 180
lon_mesh = lon_range_0_to_360(lon_mesh)
if self.lon_type == 180:
lonvar_scalar = convert_lon_0to360(lonvar_scalar)
lon_mesh = convert_lon_0to360(lon_mesh)
Comment thread
samsrabin marked this conversation as resolved.
Outdated

errmsg = (
"Must be equal: "
Expand Down
38 changes: 15 additions & 23 deletions python/ctsm/site_and_regional/regional_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ctsm.site_and_regional.mesh_type import MeshType
from ctsm.utils import add_tag_to_filename
from ctsm.utils import abort
from ctsm.config_utils import check_lon1_lt_lon2

logger = logging.getLogger(__name__)

Expand All @@ -36,6 +37,8 @@ class RegionalCase(BaseCase):
first (bottom) longitude of a region.
lon2 : float
second (top) longitude of a region.
lon_type : int
180 if longitudes are in [-180, 180], 360 if they're in [0, 360]
reg_name: str -- default = None
Region's name
create_domain : bool
Expand Down Expand Up @@ -64,9 +67,6 @@ class RegionalCase(BaseCase):
check_region_bounds
Check for the regional bounds

check_region_lons
Check for the regional lons

check_region_lats
Check for the regional lats

Expand Down Expand Up @@ -116,6 +116,7 @@ def __init__(
create_user_mods,
overwrite,
)

self.lat1 = lat1
self.lat2 = lat2
self.lon1 = lon1
Expand Down Expand Up @@ -146,27 +147,18 @@ def check_region_bounds(self):
"""
Check for the regional bounds
"""
self.check_region_lons()
self.check_region_lats()

def check_region_lons(self):
"""
Check for the regional lon bounds
"""
if self.lon1 >= self.lon2:
err_msg = """
\n
ERROR: lon1 is bigger than lon2.
lon1 points to the westernmost longitude of the region. {}
lon2 points to the easternmost longitude of the region. {}
Please make sure lon1 is smaller than lon2.

Please note that if longitude in -180-0, the code automatically
convert it to 0-360.
""".format(
self.lon1, self.lon2
# If you're calling this, lat/lon bounds need to have been provided
if any(x is None for x in [self.lon1, self.lon2, self.lat1, self.lat2]):
raise argparse.ArgumentTypeError(
"Latitude and longitude bounds must be provided and not None.\n"
+ f" lon1: {self.lon1}\n"
+ f" lon2: {self.lon2}\n"
+ f" lat1: {self.lat1}\n"
+ f" lat2: {self.lat2}"
)
raise argparse.ArgumentTypeError(err_msg)
# By now, you need to have already converted to longitude [0, 360]
check_lon1_lt_lon2(self.lon1, self.lon2, 360)
self.check_region_lats()

def check_region_lats(self):
"""
Expand Down
Loading