From f44e2ce5f9e8ee7e9fc6ff2d5edeb04304f436c1 Mon Sep 17 00:00:00 2001 From: Dan Lipsa Date: Wed, 25 Feb 2026 16:38:50 -0500 Subject: [PATCH 1/5] Add EAMExtract --- src/e3sm_quickview/plugins/eam_projection.py | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/e3sm_quickview/plugins/eam_projection.py b/src/e3sm_quickview/plugins/eam_projection.py index 8e866b0..38c1a3c 100644 --- a/src/e3sm_quickview/plugins/eam_projection.py +++ b/src/e3sm_quickview/plugins/eam_projection.py @@ -419,6 +419,69 @@ def RequestData(self, request, inInfo, outInfo): """ ) +@smproperty.xml( + """ + + + + + """ +) +class EAMExtract(VTKPythonAlgorithmBase): + def __init__(self): + super().__init__( + nInputPorts=1, nOutputPorts=1, outputType="vtkUnstructuredGrid" + ) + self.longrange = [-180.0, 180.0] + self.latrange = [-90.0, 90.0] + + def SetLongitudeRange(self, min, max): + if self.longrange[0] != min or self.longrange[1] != max: + self.longrange = [min, max] + self.Modified() + + def SetLatitudeRange(self, min, max): + if self.latrange[0] != min or self.latrange[1] != max: + self.latrange = [min, max] + self.Modified() + + def RequestData(self, request, inInfo, outInfo): + inData = self.GetInputData(inInfo, 0, 0) + outData = self.GetOutputData(outInfo, 0) + if self.longrange == [-180.0, 180] and self.latrange == [-90, 90]: + outData.ShallowCopy(inData) + timeLog.End() + return 1 + + box = vtkPVBox() + box.SetReferenceBounds( + self.longrange[0], + self.longrange[1], + self.latrange[0], + self.latrange[1], + -1.0, + 1.0, + ) + box.SetUseReferenceBounds(True) + extract = vtkPVClipDataSet() + extract.SetClipFunction(box) + extract.InsideOutOn() + extract.ExactBoxClipOn() + extract.SetInputData(inData) + extract.Update() + + outData.ShallowCopy(extract.GetOutput()) + timeLog.End() + return 1 + + + class EAMCenterMeridian(VTKPythonAlgorithmBase): def __init__(self): super().__init__( From 187cc9a8e4b37f9473159f1a412a19c380e07969 Mon Sep 17 00:00:00 2001 From: Dan Lipsa Date: Fri, 20 Feb 2026 07:45:20 -0500 Subject: [PATCH 2/5] EAMCenterMeridian can center on specified meridian. --- src/e3sm_quickview/plugins/eam_projection.py | 101 ++++++++++--------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/src/e3sm_quickview/plugins/eam_projection.py b/src/e3sm_quickview/plugins/eam_projection.py index 38c1a3c..62faa08 100644 --- a/src/e3sm_quickview/plugins/eam_projection.py +++ b/src/e3sm_quickview/plugins/eam_projection.py @@ -1,7 +1,10 @@ from paraview.simple import * from paraview.util.vtkAlgorithm import * from vtkmodules.numpy_interface import dataset_adapter as dsa -from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonCore import ( + vtkPoints, + vtkIdList, +) from vtkmodules.vtkCommonDataModel import ( vtkPolyData, vtkCellArray, @@ -52,7 +55,6 @@ def ProcessPoint(point, radius): z = rho * math.cos(math.radians(phi)) return [x, y, z] - @smproxy.filter() @smproperty.input(name="Input") @smdomain.datatype( @@ -408,16 +410,7 @@ def RequestData(self, request, inInfo, outInfo): @smproxy.filter() @smproperty.input(name="Input") @smdomain.datatype( - dataTypes=["vtkPolyData", "vtkUnstructuredGrid"], composite_data_supported=False -) -@smproperty.xml( - """ - - - """ + dataTypes=["vtkPolyData"], composite_data_supported=False ) @smproperty.xml( """ @@ -456,7 +449,6 @@ def RequestData(self, request, inInfo, outInfo): outData = self.GetOutputData(outInfo, 0) if self.longrange == [-180.0, 180] and self.latrange == [-90, 90]: outData.ShallowCopy(inData) - timeLog.End() return 1 box = vtkPVBox() @@ -477,68 +469,87 @@ def RequestData(self, request, inInfo, outInfo): extract.Update() outData.ShallowCopy(extract.GetOutput()) - timeLog.End() return 1 +@smproxy.filter() +@smproperty.input(name="Input") +@smproperty.xml( + """ + + + + Sets the central meridian. + Commonly used central meridians (longitudes) (- represents West, + represents East, + 0 is Greenwitch prime meridian): + - 0 (Prime Meridian): Standard "Western" view. + - -90/-100: Centered on North America. + - 100/110: Centered on Asia. + - -150/-160: Centered on the Pacific Ocean. + - 20: Often used to center Europe and Africa. + + + """ +) +@smdomain.datatype( + dataTypes=["vtkPolyData", "vtkUnstructuredGrid"], composite_data_supported=False +) class EAMCenterMeridian(VTKPythonAlgorithmBase): + '''Cuts an unstructured grid and re-arranges the pieces such that + the specified meridian is in the middle. Note that the mesh is + specified with bounds [0, 360], but the meridian is specified in the more + common bounds [-180, 180]. + ''' def __init__(self): super().__init__( nInputPorts=1, nOutputPorts=1, outputType="vtkUnstructuredGrid" ) - self.project = 0 - self.cmeridian = 0.0 - - def SetCentralMeridian(self, meridian): - if self.cmeridian != meridian: - self.cmeridian = meridian - self.Modified() + # common values: + self._center_meridian = 0 + + def SetMeridian(self, center_meridian_): + ''' + Specifies the central meridian (longitude in the middle of the map) + ''' + self._center_meridian = center_meridian_ + self.Modified() def RequestData(self, request, inInfo, outInfo): inData = self.GetInputData(inInfo, 0, 0) outData = self.GetOutputData(outInfo, 0) - if self.cmeridian == 0: - outData.ShallowCopy(inData) - return 1 + cut_meridian = self._center_meridian + 180 - split = (self.cmeridian - 180) if self.cmeridian > 0 else (self.cmeridian + 180) - - planeL = vtkPlane() - planeL.SetOrigin([split, 0.0, 0.0]) - planeL.SetNormal([-1, 0, 0]) + plane = vtkPlane() + plane.SetOrigin([cut_meridian, 0.0, 0.0]) + plane.SetNormal([-1, 0, 0]) + # vtkClipPolyData hangs clipL = vtkTableBasedClipDataSet() - clipL.SetClipFunction(planeL) + clipL.SetClipFunction(plane) clipL.SetInputData(inData) clipL.Update() - planeR = vtkPlane() - planeR.SetOrigin([split, 0.0, 0.0]) - planeR.SetNormal([1, 0, 0]) + plane.SetNormal([1, 0, 0]) clipR = vtkTableBasedClipDataSet() - clipR.SetClipFunction(planeR) + clipR.SetClipFunction(plane) clipR.SetInputData(inData) clipR.Update() transFunc = vtkTransform() - transFunc.Translate(360, 0, 0) + transFunc.Translate(-360, 0, 0) transform = vtkTransformFilter() - transform.SetInputData(clipL.GetOutput()) + transform.SetInputData(clipR.GetOutput()) transform.SetTransform(transFunc) transform.Update() append = vtkAppendFilter() - append.AddInputData(clipR.GetOutput()) + append.AddInputData(clipL.GetOutput()) append.AddInputData(transform.GetOutput()) append.Update() + outData.ShallowCopy(append.GetOutput()) - transFunc = vtkTransform() - transFunc.Translate(-(180 + split), 0, 0) - transform = vtkTransformFilter() - transform.SetInputData(append.GetOutput()) - transform.SetTransform(transFunc) - transform.Update() - - outData.ShallowCopy(transform.GetOutput()) return 1 From 39edee94ab85218f6ad8de1cef109c790bae8b08 Mon Sep 17 00:00:00 2001 From: Dan Lipsa Date: Wed, 25 Feb 2026 16:43:38 -0500 Subject: [PATCH 3/5] Split EAMTransformAndExtract into EAMCenterMeridian and EAMExtract --- src/e3sm_quickview/pipeline.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/e3sm_quickview/pipeline.py b/src/e3sm_quickview/pipeline.py index f7cb479..d475144 100644 --- a/src/e3sm_quickview/pipeline.py +++ b/src/e3sm_quickview/pipeline.py @@ -209,8 +209,11 @@ def Update(self, data_file, conn_file, force_reload=False): ) # Step 1: Extract and transform atmospheric data - atmos_extract = EAMTransformAndExtract( # noqa: F821 - registrationName="AtmosExtract", Input=self.data + atmos_center = EAMCenterMeridian( + registrationName="AtmosCenter", Input=self.data + ) + atmos_extract = EAMExtract( # noqa: F821 + registrationName="AtmosExtract", Input=atmos_center ) atmos_extract.LongitudeRange = [-180.0, 180.0] atmos_extract.LatitudeRange = [-90.0, 90.0] @@ -224,7 +227,11 @@ def Update(self, data_file, conn_file, force_reload=False): atmos_proj.Projection = self.projection atmos_proj.Translate = 0 atmos_proj.UpdatePipeline() - self.moveextents = atmos_proj.GetDataInformation().GetBounds() + + atmos_polydata = ExtractSurface( + registrationName="AtmosPolyData", Input=atmos_proj + ) + self.moveextents = atmos_polydata.GetDataInformation().GetBounds() # Step 3: Load and process continent outlines if self.globe is None: @@ -270,7 +277,7 @@ def Update(self, data_file, conn_file, force_reload=False): grid_proj.UpdatePipeline() # Step 8: Cache all projected views for rendering - self.views["atmosphere_data"] = OutputPort(atmos_proj, 0) + self.views["atmosphere_data"] = OutputPort(atmos_polydata, 0) self.views["continents"] = OutputPort(cont_proj, 0) self.views["grid_lines"] = OutputPort(grid_proj, 0) From f38b98115a8a2da71f9ce0cec7c1f503853dad8c Mon Sep 17 00:00:00 2001 From: Dan Lipsa Date: Wed, 11 Mar 2026 11:34:51 -0400 Subject: [PATCH 4/5] Add ForceFloatPoints to EAMSliceSource Points are created from variables in the connectivity file: double grid_corner_lat(grid_size, grid_corners) ; double grid_corner_lon(grid_size, grid_corners) ; --- src/e3sm_quickview/plugins/eam_reader.py | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/e3sm_quickview/plugins/eam_reader.py b/src/e3sm_quickview/plugins/eam_reader.py index 37bf7b1..2c60339 100644 --- a/src/e3sm_quickview/plugins/eam_reader.py +++ b/src/e3sm_quickview/plugins/eam_reader.py @@ -221,6 +221,20 @@ def _markmodified(*args, **kwars): """ ) +@smproperty.xml( + """ + + + + If True, the points of the dataset will be float, otherwise they will be float or double depending + on the type of corner_lat and corner_lon variables in the connectivity file. + + + """ +) class EAMSliceSource(VTKPythonAlgorithmBase): def __init__(self): VTKPythonAlgorithmBase.__init__( @@ -230,6 +244,7 @@ def __init__(self): self._DataFileName = None self._ConnFileName = None + self._ForceFloatPoints = True self._dirty = False # Variables for dimension sliders @@ -448,7 +463,12 @@ def _build_geometry(self, meshdata): lat = meshdata[latdim][:].data.flatten() lon = meshdata[londim][:].data.flatten() - coords = np.empty((len(lat), 3), dtype=np.float64) + + if self._ForceFloatPoints: + points_type = np.float32 + else: + points_type = np.float64 if lat.dtype == np.float64 else np.float32 + coords = np.empty((len(lat), 3), dtype=points_type) coords[:, 0] = lon coords[:, 1] = lat coords[:, 2] = 0.0 @@ -579,6 +599,11 @@ def SetConnFileName(self, fname): self._populate_variable_metadata() self.Modified() + def SetForceFloatPoints(self, forceFloatPoints): + if self._ForceFloatPoints != forceFloatPoints: + self._ForceFloatPoints = forceFloatPoints + self.Modified() + def SetSlicing(self, slice_str): # Parse JSON string containing dimension slices and update self._slices From 098433039766c7321fa2ac177aeb4e5b029b0346 Mon Sep 17 00:00:00 2001 From: Dan Lipsa Date: Wed, 18 Mar 2026 08:58:04 -0400 Subject: [PATCH 5/5] Cache output for CenterMeridian, reuse the mesh and existing arrays --- src/e3sm_quickview/plugins/eam_projection.py | 110 +++++++++++++------ 1 file changed, 78 insertions(+), 32 deletions(-) diff --git a/src/e3sm_quickview/plugins/eam_projection.py b/src/e3sm_quickview/plugins/eam_projection.py index 62faa08..e60cc16 100644 --- a/src/e3sm_quickview/plugins/eam_projection.py +++ b/src/e3sm_quickview/plugins/eam_projection.py @@ -11,7 +11,10 @@ vtkPlane, ) from vtkmodules.vtkCommonTransforms import vtkTransform -from vtkmodules.vtkFiltersCore import vtkAppendFilter +from vtkmodules.vtkFiltersCore import ( + vtkAppendFilter, + vtkGenerateIds, +) from vtkmodules.vtkFiltersGeneral import ( vtkTransformFilter, vtkTableBasedClipDataSet, @@ -510,6 +513,7 @@ def __init__(self): ) # common values: self._center_meridian = 0 + self._cached_output = None def SetMeridian(self, center_meridian_): ''' @@ -518,38 +522,80 @@ def SetMeridian(self, center_meridian_): self._center_meridian = center_meridian_ self.Modified() + def GetMeridian(self): + ''' + Returns the central meridian + ''' + return self._center_meridian + def RequestData(self, request, inInfo, outInfo): inData = self.GetInputData(inInfo, 0, 0) - outData = self.GetOutputData(outInfo, 0) - - cut_meridian = self._center_meridian + 180 - - plane = vtkPlane() - plane.SetOrigin([cut_meridian, 0.0, 0.0]) - plane.SetNormal([-1, 0, 0]) - # vtkClipPolyData hangs - clipL = vtkTableBasedClipDataSet() - clipL.SetClipFunction(plane) - clipL.SetInputData(inData) - clipL.Update() - - plane.SetNormal([1, 0, 0]) - clipR = vtkTableBasedClipDataSet() - clipR.SetClipFunction(plane) - clipR.SetInputData(inData) - clipR.Update() - - transFunc = vtkTransform() - transFunc.Translate(-360, 0, 0) - transform = vtkTransformFilter() - transform.SetInputData(clipR.GetOutput()) - transform.SetTransform(transFunc) - transform.Update() - - append = vtkAppendFilter() - append.AddInputData(clipL.GetOutput()) - append.AddInputData(transform.GetOutput()) - append.Update() - outData.ShallowCopy(append.GetOutput()) + inPoints = inData.GetPoints() + inCellArray = inData.GetCells() + outData = self.GetOutputData(outInfo, 0) + if self._cached_output and self._cached_output.GetMTime() > inPoints.GetMTime() and \ + self._cached_output.GetMTime() > inCellArray.GetMTime(): + # only scalars have been added or removed + cached_cell_data = self._cached_output.GetCellData() + + in_cell_data = inData.GetCellData() + + outData.ShallowCopy(self._cached_output) + out_cell_data = outData.GetCellData() + + out_cell_data.Initialize() + for i in range(in_cell_data.GetNumberOfArrays()): + in_array = in_cell_data.GetArray(i) + cached_array = cached_cell_data.GetArray(in_array.GetName()) + if cached_array and cached_array.GetMTime() >= in_array.GetMTime(): + # this scalar has been seen before + # simply add a reference in the outData + out_cell_data.AddArray(in_array) + else: + # this scalar is new + # we have to fill in the additional cells resulted from the clip + out_array = in_array.NewInstance() + array0 = cached_cell_data.GetArray(0) + out_array.SetNumberOfComponents(array0.GetNumberOfComponents()) + out_array.SetNumberOfTuples(array0.GetNumberOfTuples()) + out_array.SetName(in_array.GetName()) + out_cell_data.AddArray(out_array) + outData.cell_data[out_array.GetName()] = inData.cell_data[i][self._cached_output.cell_data['PedigreeIds']] + else: + generate_ids = vtkGenerateIds() + generate_ids.SetInputData(inData) + generate_ids.PointIdsOff() + generate_ids.SetCellIdsArrayName("PedigreeIds") + + cut_meridian = self._center_meridian + 180 + plane = vtkPlane() + plane.SetOrigin([cut_meridian, 0.0, 0.0]) + plane.SetNormal([-1, 0, 0]) + # vtkClipPolyData hangs + clipL = vtkTableBasedClipDataSet() + clipL.SetClipFunction(plane) + clipL.SetInputConnection(generate_ids.GetOutputPort()) + clipL.Update() + + plane.SetNormal([1, 0, 0]) + clipR = vtkTableBasedClipDataSet() + clipR.SetClipFunction(plane) + clipR.SetInputConnection(generate_ids.GetOutputPort()) + clipR.Update() + + transFunc = vtkTransform() + transFunc.Translate(-360, 0, 0) + transform = vtkTransformFilter() + transform.SetInputData(clipR.GetOutput()) + transform.SetTransform(transFunc) + transform.Update() + + append = vtkAppendFilter() + append.AddInputData(clipL.GetOutput()) + append.AddInputData(transform.GetOutput()) + append.Update() + outData.ShallowCopy(append.GetOutput()) + self._cached_output = outData.NewInstance() + self._cached_output.ShallowCopy(outData) return 1