From 9ae9efd79a1f756a5bc3b79655514b2c3360c928 Mon Sep 17 00:00:00 2001 From: Berk Geveci Date: Mon, 20 Jul 2026 11:44:44 -0400 Subject: [PATCH] fix(crop): crop variable field by absolute lat/lon range The variable-data crop filter (EAMExtract) trimmed by amounts measured from the global extent (range_to_trim) but applied them to the data's own bounds, while the base map (EAMTransformAndExtract) clipped at absolute lat/lon. The two frames only agree for global data, so a regional mesh (e.g. lat 55-85N) had its field mis-cropped while the map cropped correctly. Give EAMExtract absolute LongitudeRange/LatitudeRange and mask cell centers directly, in the same [-180,180] x [-90,90] frame as EAMTransformAndExtract. Drop range_to_trim; DataReader.crop now passes the raw [min, max] ranges. Both map and field now share one convention and crop consistently for any data extent. --- src/e3sm_quickview/pipeline.py | 18 ++--- src/e3sm_quickview/plugins/eam_projection.py | 70 ++++++++++---------- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/e3sm_quickview/pipeline.py b/src/e3sm_quickview/pipeline.py index 99be6e9..d2f6cf3 100644 --- a/src/e3sm_quickview/pipeline.py +++ b/src/e3sm_quickview/pipeline.py @@ -7,16 +7,6 @@ from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper -def range_to_trim(range, max): - """ - Convert an range interval contained in [-180, 180] (such as [-170, 160]) - into an interval specifying how much to trim (for our example: [10, 20]). - 'max' is 180 for longitude or 90 for latitude - """ - (min_range, max_range) = range - return [min_range + max, max - max_range] - - def load_plugins(): try: plugin_dir = Path(__file__).with_name("plugins") @@ -151,8 +141,8 @@ def __init__(self, projection="Mollweide"): ) self._crop = simple.EAMExtract( Input=self.center_meridian, - TrimLongitude=[0, 0], - TrimLatitude=[0, 0], + LongitudeRange=[-180, 180], + LatitudeRange=[-90, 90], ) self.proj = simple.EAMProject( # noqa: F821 Input=self._crop, @@ -250,8 +240,8 @@ def update(self, time=0.0): self.geometry.UpdatePipeline(time) def crop(self, longitude_min_max, latitude_min_max): - self._crop.TrimLongitude = range_to_trim(longitude_min_max, 180) - self._crop.TrimLatitude = range_to_trim(latitude_min_max, 90) + self._crop.LongitudeRange = longitude_min_max + self._crop.LatitudeRange = latitude_min_max class EAMVisSource: diff --git a/src/e3sm_quickview/plugins/eam_projection.py b/src/e3sm_quickview/plugins/eam_projection.py index 90853ce..cbeb34e 100644 --- a/src/e3sm_quickview/plugins/eam_projection.py +++ b/src/e3sm_quickview/plugins/eam_projection.py @@ -596,15 +596,15 @@ def RequestData(self, request, inInfo, outInfo): @smdomain.datatype(dataTypes=["vtkPolyData"], composite_data_supported=False) @smproperty.xml( """ - + default_values="-180 180"> - + default_values="-90 90"> """ ) @@ -613,44 +613,44 @@ def __init__(self): super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkUnstructuredGrid" ) - self.trim_lon = [0, 0] - self.trim_lat = [0, 0] + self.lon_range = [-180.0, 180.0] + self.lat_range = [-90.0, 90.0] self.cached_cell_centers = None self._cached_output = None - self._last_was_trimmed = False + self._last_was_cropped = False - def SetTrimLongitude(self, left, right): - if left < 0 or left > 360 or right < 0 or right > 360 or left > (360 - right): + def SetLongitudeRange(self, min, max): + if min < -180 or max > 180 or min > max: print_error( - f"SetTrimLongitude called with invalid parameters: {left=}, {right=}" + f"SetLongitudeRange called with invalid parameters: {min=}, {max=}" ) return - if self.trim_lon[0] != left or self.trim_lon[1] != right: - self.trim_lon = [left, right] + if self.lon_range[0] != min or self.lon_range[1] != max: + self.lon_range = [min, max] self.Modified() - def SetTrimLatitude(self, left, right): - if left < 0 or left > 180 or right < 0 or right > 180 or left > (180 - right): + def SetLatitudeRange(self, min, max): + if min < -90 or max > 90 or min > max: print_error( - f"SetTrimLatitude called with invalid parameters: {left=}, {right=}" + f"SetLatitudeRange called with invalid parameters: {min=}, {max=}" ) return - if self.trim_lat[0] != left or self.trim_lat[1] != right: - self.trim_lat = [left, right] + if self.lat_range[0] != min or self.lat_range[1] != max: + self.lat_range = [min, max] self.Modified() def RequestData(self, request, inInfo, outInfo): with _perf.timed("extract.RequestData"): inData = self.GetInputData(inInfo, 0, 0) outData = self.GetOutputData(outInfo, 0) - if self.trim_lon == [0, 0] and self.trim_lat == [0, 0]: + if self.lon_range == [-180.0, 180.0] and self.lat_range == [-90.0, 90.0]: outData.ShallowCopy(inData) # Only invalidate the shared points when transitioning *out* of a - # trimmed state — the original code did it unconditionally, which + # cropped state — the original code did it unconditionally, which # defeated EAMProject's cache on every pipeline update. - if self._last_was_trimmed: + if self._last_was_cropped: outData.GetPoints().Modified() - self._last_was_trimmed = False + self._last_was_cropped = False return 1 if self.cached_cell_centers and self.cached_cell_centers.GetMTime() >= max( @@ -699,20 +699,20 @@ def RequestData(self, request, inInfo, outInfo): cell_types.DeepCopy(outData.GetCellTypesArray()) outData.SetCells(cell_types, cells) - # compute the new bounds by trimming the inData bounds - bounds = list(inData.GetBounds()) - bounds[0] = bounds[0] + self.trim_lon[0] - bounds[1] = bounds[1] - self.trim_lon[1] - bounds[2] = bounds[2] + self.trim_lat[0] - bounds[3] = bounds[3] - self.trim_lat[1] + # Crop against absolute lon/lat ranges, in the same + # [-180, 180] x [-90, 90] frame as EAMTransformAndExtract, + # so the result is independent of the data's own extent + # (regional meshes no longer get mis-cropped). + lon_min, lon_max = self.lon_range + lat_min, lat_max = self.lat_range - # add HIDDENCELL based on bounds + # add HIDDENCELL based on ranges with _perf.timed("extract.ghost_mask"): outside_mask = ( - (cc[:, 0] < bounds[0]) - | (cc[:, 0] > bounds[1]) - | (cc[:, 1] < bounds[2]) - | (cc[:, 1] > bounds[3]) + (cc[:, 0] < lon_min) + | (cc[:, 0] > lon_max) + | (cc[:, 1] < lat_min) + | (cc[:, 1] > lat_max) ) # Create ghost array (0 = visible, HIDDENCELL = invisible) ghost_np = np.where( @@ -728,7 +728,7 @@ def RequestData(self, request, inInfo, outInfo): self._cached_output = outData.NewInstance() self._cached_output.ShallowCopy(outData) - self._last_was_trimmed = True + self._last_was_cropped = True return 1