diff --git a/src/e3sm_quickview/app.py b/src/e3sm_quickview/app.py index 6b63d46..334fa30 100644 --- a/src/e3sm_quickview/app.py +++ b/src/e3sm_quickview/app.py @@ -562,10 +562,12 @@ async def data_loading_open(self, simulation, connectivity): # Initialize dynamic index variables for each dimension for dim_name in available_tracks: index_var = f"{dim_name}_idx" - if "time" in index_var: - self.state[index_var] = 50 - else: - self.state[index_var] = 0 + default_idx = 50 if "time" in index_var else 0 + # Clamp to the dimension: an index past the end is rejected + # by the reader, and most files have far fewer than 50 steps. + self.state[index_var] = min( + default_idx, self.source.dimensions[dim_name].size - 1 + ) self.state.change(index_var)( partial(self._on_slicing_change, dim_name, index_var) ) diff --git a/src/e3sm_quickview/plugins/eam_projection.py b/src/e3sm_quickview/plugins/eam_projection.py index a6fc9b8..e282dd8 100644 --- a/src/e3sm_quickview/plugins/eam_projection.py +++ b/src/e3sm_quickview/plugins/eam_projection.py @@ -133,40 +133,6 @@ def ProcessPoint(point, radius): return [x, y, z] -# Slice plans keyed on the PedigreeIds array identity. Pedigree permutations -# from vtkTableBasedClipDataSet are long-run-monotonic (typically runs of -# thousands of +1-stepped indices), so we can replace fancy indexing with a -# list of slice copies and reduce the per-tick cost substantially. -_pedigree_slice_plan_cache = {} - - -def _get_pedigree_slice_plan(pedigree_vtk): - """Return (starts, ends, pid_np) for the pedigree permutation. - - The plan represents pedigree as a sequence of runs where each run i maps - output[starts[i]:ends[i]] ← input[pid_np[starts[i]]:pid_np[starts[i]]+len]. - Cached by (id, MTime) of the pedigree VTK array — vtk_to_numpy returns - a fresh ndarray each call, so keying on ndarray identity would miss. - """ - key = (id(pedigree_vtk), pedigree_vtk.GetMTime()) - entry = _pedigree_slice_plan_cache.get(key) - if entry is not None: - return entry - - pid_np = numpy_support.vtk_to_numpy(pedigree_vtk) - diff = np.diff(pid_np.astype(np.int64, copy=False)) - breaks = np.flatnonzero(diff != 1) - starts = np.empty(len(breaks) + 1, dtype=np.int64) - starts[0] = 0 - starts[1:] = breaks + 1 - ends = np.empty_like(starts) - ends[:-1] = starts[1:] - ends[-1] = len(pid_np) - entry = (starts, ends, pid_np) - _pedigree_slice_plan_cache[key] = entry - return entry - - def add_cell_arrays(inData, outData, cached_output): """ Adds arrays not modified in inData to outData. @@ -175,10 +141,11 @@ def add_cell_arrays(inData, outData, cached_output): is different than the number of values in the arrays already processed through the pipeline. - The indexed copy is done in-place into a pre-allocated output buffer - using a cached slice plan over the pedigree permutation — roughly 2x - faster than fancy numpy indexing for the clip-induced permutations we - see here. + A single fancy-index gather does this. An earlier version walked the + permutation as a list of monotonic run slices, assuming the runs were + thousands of entries long. Measured against the permutations this pipeline + actually produces — mean run 55-110 — that loop is 8-16x *slower* than one + numpy gather, because the per-run Python overhead dominates. """ pedigreeIds = cached_output.cell_data["PedigreeIds"] if pedigreeIds is None: @@ -186,8 +153,7 @@ def add_cell_arrays(inData, outData, cached_output): return pedigree_vtk = cached_output.GetCellData().GetArray("PedigreeIds") - with _perf.timed("add_cell_arrays.slice_plan"): - starts, ends, pid_np = _get_pedigree_slice_plan(pedigree_vtk) + pid_np = numpy_support.vtk_to_numpy(pedigree_vtk) cached_cell_data = cached_output.GetCellData() in_cell_data = inData.GetCellData() @@ -214,9 +180,7 @@ def add_cell_arrays(inData, outData, cached_output): in_np = numpy_support.vtk_to_numpy(in_array) out_np = numpy_support.vtk_to_numpy(out_array) - for s, e in zip(starts, ends): - src_off = int(pid_np[s]) - out_np[s:e] = in_np[src_off : src_off + (e - s)] + out_np[...] = in_np[pid_np] out_array.Modified() diff --git a/src/e3sm_quickview/plugins/eam_reader.py b/src/e3sm_quickview/plugins/eam_reader.py index 8ea4791..5646fdf 100644 --- a/src/e3sm_quickview/plugins/eam_reader.py +++ b/src/e3sm_quickview/plugins/eam_reader.py @@ -665,7 +665,15 @@ def SetSlicing(self, slice_str): if invalid_slices: print_error(f"Invalid slice indices: {', '.join(invalid_slices)}") - else: + + # Mark modified whenever a *valid* slice changed, even if some + # other dimension in the same request was out of range. The + # valid values have already been stored, so skipping Modified() + # would leave the reader's state ahead of its output -- and + # because the caller resends the whole slicing dict every time, + # one stale out-of-range index would otherwise suppress every + # later change. + if self._changed_dims: self.Modified() except (json.JSONDecodeError, ValueError) as e: