Skip to content

Commit 4dac13f

Browse files
Merge pull request #161 from rurata/main
merging devel to Main 01/13/2026
2 parents da48a77 + 1c23995 commit 4dac13f

File tree

31 files changed

+3316
-1722
lines changed

31 files changed

+3316
-1722
lines changed

.github/workflows/marscalendar_test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ on:
2020
- 'bin/MarsCalendar.py'
2121
- 'tests/test_marscalendar.py'
2222
- '.github/workflows/marscalendar_test.yml'
23+
# - 'AmesCAP/amescap/Script_utils.py'
24+
# - 'AmesCAP/amescap/Ncdf_wrapper.py'
25+
# - 'AmesCAP/amescap/FV3_utils.py'
26+
# - 'AmesCAP/amescap/Spectral_utils.py'
2327
jobs:
2428
test:
2529
# Run on multiple OS and Python versions for comprehensive testing

amescap/FV3_utils.py

Lines changed: 66 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -956,84 +956,12 @@ def cart_to_azimut_TR(u, v, mode="from"):
956956
np.sqrt(u**2 + v**2))
957957

958958

959-
def sfc_area_deg(lon1, lon2, lat1, lat2, R=3390000.):
960-
"""
961-
Return the surface between two sets of latitudes/longitudes::
962-
963-
S = int[R^2 dlon cos(lat) dlat] _____lat2
964-
\ \
965-
\____\lat1
966-
lon1 lon2
967-
:param lon1: longitude from set 1 [°]
968-
:type lon1: float
969-
:param lon2: longitude from set 2 [°]
970-
:type lon2: float
971-
:param lat1: latitude from set 1 [°]
972-
:type lat1: float
973-
:param lat2: longitude from set 2 [°]
974-
:type lat2: float
975-
:param R: planetary radius [m]
976-
:type R: int
977-
978-
.. note::
979-
qLon and Lat define the corners of the area not the grid cell center.
980-
"""
981-
982-
lat1 *= np.pi/180
983-
lat2 *= np.pi/180
984-
lon1 *= np.pi/180
985-
lon2 *= np.pi/180
986-
return ((R**2)
987-
* np.abs(lon1 - lon2)
988-
* np.abs(np.sin(lat1) - np.sin(lat2)))
989-
990-
991-
def area_meridional_cells_deg(lat_c, dlon, dlat, normalize=False, R=3390000.):
992-
"""
993-
Return area of invidual cells for a meridional band of thickness
994-
``dlon`` where ``S = int[R^2 dlon cos(lat) dlat]`` with
995-
``sin(a)-sin(b) = 2 cos((a+b)/2)sin((a+b)/2)``
996-
so ``S = 2 R^2 dlon 2cos(lat)sin(dlat/2)``::
997-
998-
_________lat + dlat/2
999-
\ lat \ ^
1000-
\lon + \ | dlat
1001-
\________\lat - dlat/2 v
1002-
lon - dlon/2 lon + dlon/2
1003-
<------>
1004-
dlon
1005-
1006-
:param lat_c: latitude of cell center [°]
1007-
:type lat_c: float
1008-
:param dlon: cell angular width [°]
1009-
:type dlon: float
1010-
:param dlat: cell angular height [°]
1011-
:type dlat: float
1012-
:param R: planetary radius [m]
1013-
:type R: float
1014-
:param normalize: if True, the sum of the output elements = 1
1015-
:type normalize: bool
1016-
:return: ``S`` areas of the cells, same size as ``lat_c`` in [m2]
1017-
or normalized by the total area
1018-
"""
1019-
1020-
# Initialize
1021-
area_tot = 1.
1022-
# Compute total area in a longitude band extending from
1023-
# ``lat[0] - dlat/2`` to ``lat_c[-1] + dlat/2``
1024-
if normalize:
1025-
area_tot = sfc_area_deg(-dlon/2, dlon/2, (lat_c[0] - dlat/2),
1026-
(lat_c[-1] + dlat/2), R)
1027-
# Now convert to radians
1028-
lat_c = lat_c*np.pi/180
1029-
dlon *= np.pi/180
1030-
dlat *= np.pi/180
1031-
return (2. * R**2 * dlon * np.cos(lat_c) * np.sin(dlat/2.) / area_tot)
1032-
1033-
1034959
def area_weights_deg(var_shape, lat_c, axis = -2):
1035960
"""
1036-
Return weights for averaging the variable.
961+
Returns weights scaled so that np.mean(var*W) gives an area-weighted
962+
average. This works because grid cells near the poles have smaller
963+
areas than those at the equator, so they should contribute less to
964+
a global average.
1037965
1038966
Expected dimensions are:
1039967
@@ -1048,12 +976,11 @@ def area_weights_deg(var_shape, lat_c, axis = -2):
1048976
may be truncated on either end (e.g., ``lat = [-20 ..., 0... 50]``)
1049977
but must be continuous.
1050978
1051-
:param var_shape: variable shape
979+
:param var_shape: the shape/dimensions of your data array
1052980
:type var_shape: tuple
1053-
:param lat_c: latitude of cell centers [°]
981+
:param lat_c: latitude cell centers in degrees [°]
1054982
:type lat_c: float
1055-
:param axis: position of the latitude axis for 2D and higher
1056-
dimensional arrays. The default is the SECOND TO LAST dimension
983+
:param axis: which dimension contains latitude, default: 2nd-to-last
1057984
:type axis: int
1058985
:return: ``W`` weights for the variable ready for standard
1059986
averaging as ``np.mean(var*W)`` [condensed form] or
@@ -1083,33 +1010,75 @@ def area_weights_deg(var_shape, lat_c, axis = -2):
10831010
``np.average(var, weights=W, axis = X)`` to average over a
10841011
specific axis.
10851012
"""
1086-
1013+
# Check if either the lat array or var shape is essentially
1014+
# scalar (single value)
1015+
# np.atleast_1d() ensures the input is treated as at least a 1D
1016+
# array for checking its length
10871017
if (len(np.atleast_1d(lat_c)) == 1 or
10881018
len(np.atleast_1d(var_shape)) == 1):
1089-
# If variable or lat is a scalar, do nothing
1019+
# If either is scalar, returns an array of 1s matching the var
1020+
# shape (no weighting needed since there's nothing to weight across)
10901021
return np.ones(var_shape)
10911022
else:
1092-
# Then lat has at least 2 elements
1023+
# Calculates lat spacing by taking the difference between the
1024+
# first two latitude points. Assumes uniform grid spacing
10931025
dlat = lat_c[1]-lat_c[0]
1094-
# Calculate cell areas. Since it is normalized, we can use
1095-
# ``dlon = 1`` and ``R = 1`` without changing the result. Note
1096-
# that ``sum(A) = (A1 + A2 + ... An) = 1``
1097-
A = area_meridional_cells_deg(lat_c, 1, dlat, normalize = True, R = 1)
1026+
1027+
# Calculate cell areas
1028+
# Use dlon = 1 (arbitrary lon spacing and planet
1029+
# radius because normalization makes the absolute values
1030+
# irrelevant), then normalize
1031+
R = 1.
1032+
dlon = 1.
1033+
1034+
# Compute total surface area for normalization
1035+
lon1_deg = -dlon/2
1036+
lon2_deg = dlon/2
1037+
lat1_deg = lat_c[0] - dlat/2
1038+
lat2_deg = lat_c[-1] + dlat/2
1039+
1040+
# Convert to radians for area calculation
1041+
lat1_rad = lat1_deg * np.pi/180
1042+
lat2_rad = lat2_deg * np.pi/180
1043+
lon1_rad = lon1_deg * np.pi/180
1044+
lon2_rad = lon2_deg * np.pi/180
1045+
1046+
area_tot = ((R**2)
1047+
* np.abs(lon1_rad - lon2_rad)
1048+
* np.abs(np.sin(lat1_rad) - np.sin(lat2_rad)))
1049+
1050+
# Convert to radians
1051+
lat_c_rad = lat_c * np.pi/180
1052+
dlon_rad = dlon * np.pi/180
1053+
dlat_rad = dlat * np.pi/180
1054+
1055+
# Calculate normalized areas. Areas sum to 1
1056+
A = (2. * R**2 * dlon_rad * np.cos(lat_c_rad) *
1057+
np.sin(dlat_rad/2.) / area_tot)
10981058

1059+
# Check if var is 1D (just lat values)
10991060
if len(var_shape) == 1:
1100-
# Variable is a 1D array of size = [lat]. Easiest case
1101-
# since ``(w1 + w2 + ...wn) = sum(A) = 1`` and
1102-
# ``N = len(lat)``
1061+
# For 1D case: multiply normalized areas by the number of
1062+
# lat points. Creates weights where sum(W) = len(lat_c),
1063+
# allowing np.mean(var*W) to give the area-weighted average
11031064
W = A * len(lat_c)
11041065
else:
1105-
# Generate the appropriate shape for the area ``A``,
1106-
# e.g., [time, lev, lat, lon] > [1, 1, lat, 1]
1107-
# In this case,`` N = time * lev * lat * lon`` and
1108-
# ``(w1 + w2 + ...wn) = time * lev * lon * sum(A)``,
1109-
# therefore ``N / (w1 + w2 + ...wn) = lat``
1066+
# For multidimensional data: create a list of 1s with
1067+
# length matching the number of dims in the var
1068+
# (e.g., [1, 1, 1, 1] for 4D data).
11101069
reshape_shape = [1 for i in range(0, len(var_shape))]
1070+
# Set the lat dim to the actual number of lat points
1071+
# (e.g., [1, 1, lat, 1] for 4D data where lat = third dim)
11111072
reshape_shape[axis] = len(lat_c)
1073+
# Reshape the 1D area array to match the var's
1074+
# dimensionality (broadcasting-ready shape), then multiply
1075+
# by the number of lat points. Allows the weights to
1076+
# broadcast correctly across all other dims.
11121077
W = A.reshape(reshape_shape)*len(lat_c)
1078+
1079+
# Expand the weights to full var shape by multiplying with an
1080+
# array of 1s. Creates the final weight array that can be
1081+
# directly multiplied with other data for area-weighted avg.
11131082
return W*np.ones(var_shape)
11141083

11151084

@@ -1738,7 +1707,8 @@ def get_trend_2D(VAR, LON, LAT, type_trend="wmean"):
17381707
# dimensions
17391708

17401709
# Flatten array (``[10, 36, lat, lon]`` -> ``[360, lat, lon]``)
1741-
nflatten = int(np.prod(var_shape[:-2]))
1710+
prod_result = np.prod(var_shape[:-2])
1711+
nflatten = int(prod_result.item()) if hasattr(prod_result, 'item') else int(prod_result)
17421712
reshape_flat = np.append(nflatten, var_shape[-2:])
17431713
VAR = VAR.reshape(reshape_flat)
17441714

0 commit comments

Comments
 (0)