From 9b4012973a5e71f5ad22c116f4d0d0a6ca48fc91 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 24 Apr 2025 15:04:40 -0500 Subject: [PATCH 1/9] Plotting based on filter_get_bins --- openmc_plotter/plotmodel.py | 91 ++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index a339fd2..9c66887 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -44,7 +44,8 @@ openmc.CellFilter, openmc.DistribcellFilter, openmc.CellInstanceFilter, - openmc.MeshFilter) + openmc.MeshFilter, + openmc.MeshMaterialFilter) _PRODUCTIONS = ('delayed-nu-fission', 'prompt-nu-fission', 'nu-fission', 'nu-scatter', 'H1-production', 'H2-production', @@ -672,6 +673,13 @@ def create_tally_image(self, view: Optional[PlotView] = None): nuclides, view) return image + (units_out,) + + elif tally.contains_filter(openmc.MeshMaterialFilter): + image = self._create_tally_filter_image( + tally, tally_value, openmc.MeshMaterialFilter, scores, nuclides, view) + return image + (units_out,) + + elif contains_distribcell or contains_cellinstance: if tally_value == 'rel_err': mean_data = self._create_distribcell_image( @@ -944,6 +952,87 @@ def _do_op(array, tally_value, ax=0): return image_data, None, data_min, data_max + def _create_tally_filter_image( + self, tally: openmc.Tally, tally_value: TallyValueType, filter_class, + scores: Tuple[str], nuclides: Tuple[str], view: PlotView = None + ): + # some variables used throughout + if view is None: + view = self.currentView + + def _do_op(array, tally_value, ax=0): + if tally_value == 'mean': + return np.sum(array, axis=ax) + elif tally_value == 'std_dev': + return np.sqrt(np.sum(array**2, axis=ax)) + + # start with reshaped data + data = tally.get_reshaped_data(tally_value) + + # move mesh axes to the end of the filters + filter_idx = [type(filter) for filter in tally.filters].index(filter_class) + data = np.moveaxis(data, filter_idx, -1) + + # sum over the rest of the tally filters + for tally_filter in tally.filters: + if type(tally_filter) is filter_class: + continue + + selected_bins = self.appliedFilters[tally_filter] + if selected_bins: + # sum filter data for the selected bins + data = data[np.array(selected_bins)].sum(axis=0) + else: + # if the filter is completely unselected, + # set all of its data to zero and remove the axis + data[:] = 0.0 + data = _do_op(data, tally_value) + + # filter by selected nuclides + if not nuclides: + data = 0.0 + + selected_nuclides = [] + for idx, nuclide in enumerate(tally.nuclides): + if nuclide in nuclides: + selected_nuclides.append(idx) + data = _do_op(data[np.array(selected_nuclides)], tally_value) + + # filter by selected scores + if not scores: + data = 0.0 + + selected_scores = [] + for idx, score in enumerate(tally.scores): + if score in scores: + selected_scores.append(idx) + data = _do_op(data[np.array(selected_scores)], tally_value) + + # Get mesh bins from openmc.lib + filter = tally.find_filter(filter_class) + filter_cpp = openmc.lib.filters[filter.id] + + if view is None: + view = self.currentView + + bins = filter_cpp.get_plot_bins( + origin=view.origin, + width=(view.width, view.height), + basis=view.basis, + pixels=(view.h_res, view.v_res), + ) + + # set image data + image_data = np.full_like(self.ids, np.nan, dtype=float) + mask = (bins >= 0) + image_data[mask] = data[bins[mask]] + + # get dataset's min/max + data_min = np.min(data) + data_max = np.max(data) + + return image_data, None, data_min, data_max + @property def cell_ids(self): return self.ids_map[:, :, 0] From ec4e01538e235ccdedfbd6634ee3413c04ed0bad Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 1 Feb 2026 22:40:31 -0600 Subject: [PATCH 2/9] Utilize raster_plot instead of id_map and property_map --- openmc_plotter/plotmodel.py | 78 +++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index 9c66887..0d67240 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -123,15 +123,23 @@ class PlotWorker(QObject): def generate_maps(self, work_item: PlotWorkItem): try: params = work_item.view_params - view_param = ViewParam(params["origin"], params["width"], - params["height"], params["h_res"]) - view_param.h_res = params["h_res"] - view_param.v_res = params["v_res"] - view_param.basis = params["basis"] - view_param.level = params["level"] - view_param.color_overlaps = params["color_overlaps"] - ids_map = openmc.lib.id_map(view_param) - properties = openmc.lib.property_map(view_param) + + # Determine if we need filter bins for MeshMaterialFilter tally + filter_cpp = None + if params.get("filter_id") is not None: + filter_cpp = openmc.lib.filters[params["filter_id"]] + + # Single call replaces id_map + property_map + get_plot_bins + ids_map, properties = openmc.lib.raster_plot( + origin=params["origin"], + width=(params["width"], params["height"]), + basis=params["basis"], + pixels=(params["h_res"], params["v_res"]), + color_overlaps=params["color_overlaps"], + level=params["level"], + filter=filter_cpp, + ) + self.finished.emit(work_item.view_params, ids_map, properties) except Exception as exc: self.error.emit(str(exc)) @@ -474,8 +482,22 @@ def view_params_payload(self, view: "PlotView"): "basis": str(vp.basis), "level": int(vp.level), "color_overlaps": bool(vp.color_overlaps), + "filter_id": self.get_active_mesh_material_filter_id(view), } + def get_active_mesh_material_filter_id(self, view: "PlotView") -> Optional[int]: + """Return the filter ID if displaying a MeshMaterialFilter tally, else None.""" + if self._statepoint is None: + return None + if not view.tallyDataVisible or view.selectedTally is None: + return None + + tally = self._statepoint.tallies[view.selectedTally] + if tally.contains_filter(openmc.MeshMaterialFilter): + filter = tally.find_filter(openmc.MeshMaterialFilter) + return filter.id + return None + def can_reuse_maps(self, view: "PlotView"): if self.ids_map is None or self.properties is None: return False @@ -496,8 +518,21 @@ def makePlot(self, view: Optional["PlotView"] = None, if ids_map is None or properties is None: if (self.currentView.view_params != view.view_params) or \ (self.ids_map is None) or (self.properties is None): - self.ids_map = openmc.lib.id_map(view.view_params) - self.properties = openmc.lib.property_map(view.view_params) + # Determine if we need filter bins for MeshMaterialFilter tally + filter_cpp = None + filter_id = self.get_active_mesh_material_filter_id(view) + if filter_id is not None: + filter_cpp = openmc.lib.filters[filter_id] + + self.ids_map, self.properties = openmc.lib.raster_plot( + origin=view.origin, + width=(view.width, view.height), + basis=view.basis, + pixels=(view.h_res, view.v_res), + color_overlaps=view.color_overlaps, + level=view.level, + filter=filter_cpp, + ) self.map_view_params = self.view_params_payload(view) else: self.ids_map = ids_map @@ -1008,19 +1043,14 @@ def _do_op(array, tally_value, ax=0): selected_scores.append(idx) data = _do_op(data[np.array(selected_scores)], tally_value) - # Get mesh bins from openmc.lib - filter = tally.find_filter(filter_class) - filter_cpp = openmc.lib.filters[filter.id] - - if view is None: - view = self.currentView - - bins = filter_cpp.get_plot_bins( - origin=view.origin, - width=(view.width, view.height), - basis=view.basis, - pixels=(view.h_res, view.v_res), - ) + # Extract filter bins from ids_map (computed during raster_plot call) + # ids_map has shape (v_res, h_res, 4) when filter was included + if self.ids_map.shape[2] < 4: + raise RuntimeError( + "Filter bins not available. Ensure raster_plot was called with " + "the appropriate filter for MeshMaterialFilter tallies." + ) + bins = self.ids_map[:, :, 3] # set image data image_data = np.full_like(self.ids, np.nan, dtype=float) From 071bfeb3b9b07f83c5e5e68c69f5a95a71bbf1fe Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 1 Feb 2026 22:55:54 -0600 Subject: [PATCH 3/9] Rename to geom_data / property_data --- openmc_plotter/main_window.py | 6 ++-- openmc_plotter/plotgui.py | 6 ++-- openmc_plotter/plotmodel.py | 66 ++++++++++++++++------------------- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/openmc_plotter/main_window.py b/openmc_plotter/main_window.py index 2ae8e20..0bc738f 100755 --- a/openmc_plotter/main_window.py +++ b/openmc_plotter/main_window.py @@ -1224,7 +1224,7 @@ def requestPlotUpdate(self, view=None): view_params = self.model.view_params_payload(view_snapshot) self.plot_manager.set_latest_view_params(view_params) self.plot_manager.clear_pending() - self.model.makePlot(view_snapshot, self.model.ids_map, self.model.properties) + self.model.makePlot(view_snapshot, self.model.geom_data, self.model.property_data) self.resetModels() self.showCurrentView() if not self.plot_manager.is_busy: @@ -1244,10 +1244,10 @@ def _on_plot_started(self): def _on_plot_queued(self): self.plotIm.showUpdatingOverlay("Generating Plot... (update queued)") - def _on_plot_finished(self, view_snapshot, view_params, ids_map, properties): + def _on_plot_finished(self, view_snapshot, view_params, geom_data, property_data): if view_params != self.plot_manager.latest_view_params: return - self.model.makePlot(view_snapshot, ids_map, properties) + self.model.makePlot(view_snapshot, geom_data, property_data) self.resetModels() self.showCurrentView() diff --git a/openmc_plotter/plotgui.py b/openmc_plotter/plotgui.py index b04b1b6..5375bb0 100644 --- a/openmc_plotter/plotgui.py +++ b/openmc_plotter/plotgui.py @@ -255,8 +255,8 @@ def getIDinfo(self, event): and 0 <= xPos and xPos < self.model.currentView.h_res: id = self.model.ids[yPos, xPos] instance = self.model.instances[yPos, xPos] - temp = "{:g}".format(self.model.properties[yPos, xPos, 0]) - density = "{:g}".format(self.model.properties[yPos, xPos, 1]) + temp = "{:g}".format(self.model.property_data[yPos, xPos, 0]) + density = "{:g}".format(self.model.property_data[yPos, xPos, 1]) else: id = _NOT_FOUND instance = _NOT_FOUND @@ -579,7 +579,7 @@ def updatePixmap(self): norm = SymLogNorm( 1E-10) if cv.color_scale_log[cv.colorby] else None - data = self.model.properties[:, :, idx] + data = self.model.property_data[:, :, idx] self.image = self.figure.subplots().imshow(data, cmap=cmap, norm=norm, diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index 0d67240..9d9079f 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -130,7 +130,7 @@ def generate_maps(self, work_item: PlotWorkItem): filter_cpp = openmc.lib.filters[params["filter_id"]] # Single call replaces id_map + property_map + get_plot_bins - ids_map, properties = openmc.lib.raster_plot( + geom_data, property_data = openmc.lib.raster_plot( origin=params["origin"], width=(params["width"], params["height"]), basis=params["basis"], @@ -140,7 +140,7 @@ def generate_maps(self, work_item: PlotWorkItem): filter=filter_cpp, ) - self.finished.emit(work_item.view_params, ids_map, properties) + self.finished.emit(work_item.view_params, geom_data, property_data) except Exception as exc: self.error.emit(str(exc)) @@ -233,12 +233,12 @@ def _start_next(self): self.work_requested.emit(work_item) @Slot(object, object, object) - def _on_worker_finished(self, view_params, ids_map, properties): + def _on_worker_finished(self, view_params, geom_data, property_data): request = self._in_flight_request self._in_flight_request = None if request is not None: self.plot_finished.emit(request.view_snapshot, - view_params, ids_map, properties) + view_params, geom_data, property_data) if self._pending_request is not None: self._start_next() else: @@ -275,10 +275,10 @@ class PlotModel: Dictionary mapping material IDs to openmc.Material instances ids : NumPy int array (v_res, h_res, 1) Mapping of plot coordinates to cell/material ID by pixel - ids_map : NumPy int32 array (v_res, h_res, 3) - Mapping of cell and material ids - properties : Numpy float array (v_res, h_res, 3) - Mapping of cell temperatures and material densities + geom_data : NumPy int32 array (v_res, h_res, 3) or (v_res, h_res, 4) + Geometry data with cell IDs, instances, material IDs, and optionally filter bins + property_data : Numpy float array (v_res, h_res, 2) + Property data with cell temperatures and material densities image : NumPy int array (v_res, h_res, 3) The current RGB image data statepoint : StatePointModel @@ -319,9 +319,9 @@ def __init__(self, use_settings_pkl, model_path, default_res): # Cell/Material ID by coordinates self.ids = None - # Return values from id_map and property_map - self.ids_map = None - self.properties = None + # Return values from raster_plot + self.geom_data = None + self.property_data = None self.map_view_params = None self.version = __version__ @@ -499,32 +499,28 @@ def get_active_mesh_material_filter_id(self, view: "PlotView") -> Optional[int]: return None def can_reuse_maps(self, view: "PlotView"): - if self.ids_map is None or self.properties is None: + if self.geom_data is None or self.property_data is None: return False return self.map_view_params == self.view_params_payload(view) def makePlot(self, view: Optional["PlotView"] = None, - ids_map=None, properties=None): - """ Generate new plot image from active view settings - - Creates corresponding .xml files from user-chosen settings. - Runs OpenMC in plot mode to generate new plot image. - """ + geom_data=None, property_data=None): + """Generate new plot image from active view settings""" if view is None: view = self.activeView # update/call maps under 2 circumstances - # 1. this is the intial plot (ids_map/properties are None) + # 1. this is the intial plot (geom_data/property_data are None) # 2. The active (desired) view differs from the current view parameters - if ids_map is None or properties is None: + if geom_data is None or property_data is None: if (self.currentView.view_params != view.view_params) or \ - (self.ids_map is None) or (self.properties is None): + (self.geom_data is None) or (self.property_data is None): # Determine if we need filter bins for MeshMaterialFilter tally filter_cpp = None filter_id = self.get_active_mesh_material_filter_id(view) if filter_id is not None: filter_cpp = openmc.lib.filters[filter_id] - self.ids_map, self.properties = openmc.lib.raster_plot( + self.geom_data, self.property_data = openmc.lib.raster_plot( origin=view.origin, width=(view.width, view.height), basis=view.basis, @@ -535,8 +531,8 @@ def makePlot(self, view: Optional["PlotView"] = None, ) self.map_view_params = self.view_params_payload(view) else: - self.ids_map = ids_map - self.properties = properties + self.geom_data = geom_data + self.property_data = property_data self.map_view_params = self.view_params_payload(view) # update current view @@ -584,15 +580,15 @@ def makePlot(self, view: Optional["PlotView"] = None, # tally data self.tally_data = None - self.properties[self.properties < 0.0] = np.nan + self.property_data[self.property_data < 0.0] = np.nan - self.temperatures = self.properties[..., _PROPERTY_INDICES['temperature']] - self.densities = self.properties[..., _PROPERTY_INDICES['density']] + self.temperatures = self.property_data[..., _PROPERTY_INDICES['temperature']] + self.densities = self.property_data[..., _PROPERTY_INDICES['density']] minmax = {} for prop in _MODEL_PROPERTIES: idx = _PROPERTY_INDICES[prop] - prop_data = self.properties[:, :, idx] + prop_data = self.property_data[:, :, idx] minmax[prop] = (np.min(np.nan_to_num(prop_data)), np.max(np.nan_to_num(prop_data))) @@ -1043,14 +1039,14 @@ def _do_op(array, tally_value, ax=0): selected_scores.append(idx) data = _do_op(data[np.array(selected_scores)], tally_value) - # Extract filter bins from ids_map (computed during raster_plot call) - # ids_map has shape (v_res, h_res, 4) when filter was included - if self.ids_map.shape[2] < 4: + # Extract filter bins from geom_data (computed during raster_plot call) + # geom_data has shape (v_res, h_res, 4) when filter was included + if self.geom_data.shape[2] < 4: raise RuntimeError( "Filter bins not available. Ensure raster_plot was called with " "the appropriate filter for MeshMaterialFilter tallies." ) - bins = self.ids_map[:, :, 3] + bins = self.geom_data[:, :, 3] # set image data image_data = np.full_like(self.ids, np.nan, dtype=float) @@ -1065,15 +1061,15 @@ def _do_op(array, tally_value, ax=0): @property def cell_ids(self): - return self.ids_map[:, :, 0] + return self.geom_data[:, :, 0] @property def instances(self): - return self.ids_map[:, :, 1] + return self.geom_data[:, :, 1] @property def mat_ids(self): - return self.ids_map[:, :, 2] + return self.geom_data[:, :, 2] class ViewParam(openmc.lib.plot._PlotBase): From 0840fe1f4b857d1e54132b44c214713ab11db7c9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 14 Feb 2026 11:01:43 -0600 Subject: [PATCH 4/9] Rename to slice_plot --- openmc_plotter/plotmodel.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index 9d9079f..91c8e12 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -130,7 +130,7 @@ def generate_maps(self, work_item: PlotWorkItem): filter_cpp = openmc.lib.filters[params["filter_id"]] # Single call replaces id_map + property_map + get_plot_bins - geom_data, property_data = openmc.lib.raster_plot( + geom_data, property_data = openmc.lib.slice_plot( origin=params["origin"], width=(params["width"], params["height"]), basis=params["basis"], @@ -319,7 +319,7 @@ def __init__(self, use_settings_pkl, model_path, default_res): # Cell/Material ID by coordinates self.ids = None - # Return values from raster_plot + # Return values from slice_plot self.geom_data = None self.property_data = None self.map_view_params = None @@ -520,7 +520,7 @@ def makePlot(self, view: Optional["PlotView"] = None, if filter_id is not None: filter_cpp = openmc.lib.filters[filter_id] - self.geom_data, self.property_data = openmc.lib.raster_plot( + self.geom_data, self.property_data = openmc.lib.slice_plot( origin=view.origin, width=(view.width, view.height), basis=view.basis, @@ -1039,11 +1039,11 @@ def _do_op(array, tally_value, ax=0): selected_scores.append(idx) data = _do_op(data[np.array(selected_scores)], tally_value) - # Extract filter bins from geom_data (computed during raster_plot call) + # Extract filter bins from geom_data (computed during slice_plot call) # geom_data has shape (v_res, h_res, 4) when filter was included if self.geom_data.shape[2] < 4: raise RuntimeError( - "Filter bins not available. Ensure raster_plot was called with " + "Filter bins not available. Ensure slice_plot was called with " "the appropriate filter for MeshMaterialFilter tallies." ) bins = self.geom_data[:, :, 3] From cc317fa4d31362122a8f24621c4ea1c4accf7b70 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Jun 2026 17:33:37 -0500 Subject: [PATCH 5/9] Local replacement for _PlotBase since it's removed in OpenMC --- openmc_plotter/plotmodel.py | 236 +++++++++++++++++++++++++++++++++++- 1 file changed, 234 insertions(+), 2 deletions(-) diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index 91c8e12..b470fae 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -2,7 +2,7 @@ from ast import literal_eval from collections import defaultdict import copy -from ctypes import c_int32, c_char_p +from ctypes import c_int32, c_char_p, Structure, c_int, c_size_t, c_bool, c_double from dataclasses import dataclass import hashlib import itertools @@ -1072,7 +1072,238 @@ def mat_ids(self): return self.geom_data[:, :, 2] -class ViewParam(openmc.lib.plot._PlotBase): +class _Position(Structure): + """Definition of an xyz location in space with underlying c-types + + C-type Attributes + ----------------- + x : c_double + Position's x value (default: 0.0) + y : c_double + Position's y value (default: 0.0) + z : c_double + Position's z value (default: 0.0) + """ + _fields_ = [('x', c_double), + ('y', c_double), + ('z', c_double)] + + def __getitem__(self, idx): + if idx == 0: + return self.x + elif idx == 1: + return self.y + elif idx == 2: + return self.z + else: + raise IndexError(f"{idx} index is invalid for _Position") + + def __setitem__(self, idx, val): + if idx == 0: + self.x = val + elif idx == 1: + self.y = val + elif idx == 2: + self.z = val + else: + raise IndexError(f"{idx} index is invalid for _Position") + + def __repr__(self): + return f"({self.x}, {self.y}, {self.z})" + + +class _PlotBase(Structure): + """A structure defining a 2-D geometry slice with underlying c-types + + C-Type Attributes + ----------------- + origin_ : openmc.lib.plot._Position + A position defining the origin of the plot. + u_span_ : openmc.lib.plot._Position + Full-width span vector defining the plot's horizontal axis. + v_span_ : openmc.lib.plot._Position + Full-height span vector defining the plot's vertical axis. + width_ : openmc.lib.plot._Position + The width of the plot along the x, y, and z axes, respectively + basis_ : c_int + The axes basis of the plot view. + pixels_ : c_size_t[3] + The resolution of the plot in the horizontal and vertical dimensions + color_overlaps_ : c_bool + Whether to assign unique IDs (-3) to overlapping regions. + level_ : c_int + The universe level for the plot view + + Attributes + ---------- + origin : tuple or list of ndarray + Origin (center) of the plot + width : float + The horizontal dimension of the plot in geometry units (cm) + height : float + The vertical dimension of the plot in geometry units (cm) + basis : string + One of {'xy', 'xz', 'yz'} indicating the horizontal and vertical + axes of the plot. + h_res : int + The horizontal resolution of the plot in pixels + v_res : int + The vertical resolution of the plot in pixels + level : int + The universe level for the plot (default: -1 -> all universes shown) + """ + _fields_ = [('origin_', _Position), + ('u_span_', _Position), + ('v_span_', _Position), + ('width_', _Position), + ('basis_', c_int), + ('pixels_', 3*c_size_t), + ('color_overlaps_', c_bool), + ('level_', c_int)] + + def __init__(self): + self.level_ = -1 + self.basis_ = 1 + self.color_overlaps_ = False + self._update_spans() + + def _update_spans(self): + if self.basis_ == 1: + self.u_span_.x = self.width_.x + self.u_span_.y = 0.0 + self.u_span_.z = 0.0 + self.v_span_.x = 0.0 + self.v_span_.y = self.width_.y + self.v_span_.z = 0.0 + elif self.basis_ == 2: + self.u_span_.x = self.width_.x + self.u_span_.y = 0.0 + self.u_span_.z = 0.0 + self.v_span_.x = 0.0 + self.v_span_.y = 0.0 + self.v_span_.z = self.width_.y + elif self.basis_ == 3: + self.u_span_.x = 0.0 + self.u_span_.y = self.width_.x + self.u_span_.z = 0.0 + self.v_span_.x = 0.0 + self.v_span_.y = 0.0 + self.v_span_.z = self.width_.y + + @property + def origin(self): + return self.origin_ + + @origin.setter + def origin(self, origin): + self.origin_.x = origin[0] + self.origin_.y = origin[1] + self.origin_.z = origin[2] + + @property + def width(self): + return self.width_.x + + @width.setter + def width(self, width): + self.width_.x = width + self._update_spans() + + @property + def height(self): + return self.width_.y + + @height.setter + def height(self, height): + self.width_.y = height + self._update_spans() + + @property + def basis(self): + if self.basis_ == 1: + return 'xy' + elif self.basis_ == 2: + return 'xz' + elif self.basis_ == 3: + return 'yz' + + raise ValueError(f"Plot basis {self.basis_} is invalid") + + @basis.setter + def basis(self, basis): + if isinstance(basis, str): + valid_bases = ('xy', 'xz', 'yz') + basis = basis.lower() + if basis not in valid_bases: + raise ValueError(f"{basis} is not a valid plot basis.") + + if basis == 'xy': + self.basis_ = 1 + elif basis == 'xz': + self.basis_ = 2 + elif basis == 'yz': + self.basis_ = 3 + self._update_spans() + return + + if isinstance(basis, int): + valid_bases = (1, 2, 3) + if basis not in valid_bases: + raise ValueError(f"{basis} is not a valid plot basis.") + self.basis_ = basis + self._update_spans() + return + + raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") + + @property + def h_res(self): + return self.pixels_[0] + + @h_res.setter + def h_res(self, h_res): + self.pixels_[0] = h_res + + @property + def v_res(self): + return self.pixels_[1] + + @v_res.setter + def v_res(self, v_res): + self.pixels_[1] = v_res + + @property + def level(self): + return int(self.level_) + + @level.setter + def level(self, level): + self.level_ = level + + @property + def color_overlaps(self): + return self.color_overlaps_ + + @color_overlaps.setter + def color_overlaps(self, color_overlaps): + self.color_overlaps_ = color_overlaps + + def __repr__(self): + out_str = ["-----", + "Plot:", + "-----", + f"Origin: {self.origin}", + f"Width: {self.width}", + f"Height: {self.height}", + f"Basis: {self.basis}", + f"HRes: {self.h_res}", + f"VRes: {self.v_res}", + f"Color Overlaps: {self.color_overlaps}", + f"Level: {self.level}"] + return '\n'.join(out_str) + + +class ViewParam(_PlotBase): """Viewer settings that are needed for _PlotBase and are independent of all other plotter/model settings. @@ -1167,6 +1398,7 @@ def urc(self): def __eq__(self, other): return repr(self) == repr(other) + class PlotViewIndependent: """View settings for OpenMC plot, independent of the model. From 2dffc5af4555fbd98cead99a4d87408c364a2353 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Jun 2026 17:57:40 -0500 Subject: [PATCH 6/9] Refactor to remove _PlotBase and _Position --- openmc_plotter/main_window.py | 2 +- openmc_plotter/plotmodel.py | 305 ++++++---------------------------- 2 files changed, 51 insertions(+), 256 deletions(-) diff --git a/openmc_plotter/main_window.py b/openmc_plotter/main_window.py index 0bc738f..91140fe 100755 --- a/openmc_plotter/main_window.py +++ b/openmc_plotter/main_window.py @@ -684,7 +684,7 @@ def redo(self): def restoreDefault(self): if self.model.currentView != self.model.defaultView: self.model.storeCurrent() - self.model.activeView.adopt_plotbase(self.model.defaultView) + self.model.activeView.adopt_view_params(self.model.defaultView) self.geometryPanel.update() self.colorDialog.updateDialogValues() self.requestPlotUpdate() diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index b470fae..44e330f 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -2,7 +2,7 @@ from ast import literal_eval from collections import defaultdict import copy -from ctypes import c_int32, c_char_p, Structure, c_int, c_size_t, c_bool, c_double +from ctypes import c_int32, c_char_p from dataclasses import dataclass import hashlib import itertools @@ -1072,246 +1072,17 @@ def mat_ids(self): return self.geom_data[:, :, 2] -class _Position(Structure): - """Definition of an xyz location in space with underlying c-types - C-type Attributes - ----------------- - x : c_double - Position's x value (default: 0.0) - y : c_double - Position's y value (default: 0.0) - z : c_double - Position's z value (default: 0.0) - """ - _fields_ = [('x', c_double), - ('y', c_double), - ('z', c_double)] - - def __getitem__(self, idx): - if idx == 0: - return self.x - elif idx == 1: - return self.y - elif idx == 2: - return self.z - else: - raise IndexError(f"{idx} index is invalid for _Position") - - def __setitem__(self, idx, val): - if idx == 0: - self.x = val - elif idx == 1: - self.y = val - elif idx == 2: - self.z = val - else: - raise IndexError(f"{idx} index is invalid for _Position") - - def __repr__(self): - return f"({self.x}, {self.y}, {self.z})" - - -class _PlotBase(Structure): - """A structure defining a 2-D geometry slice with underlying c-types - - C-Type Attributes - ----------------- - origin_ : openmc.lib.plot._Position - A position defining the origin of the plot. - u_span_ : openmc.lib.plot._Position - Full-width span vector defining the plot's horizontal axis. - v_span_ : openmc.lib.plot._Position - Full-height span vector defining the plot's vertical axis. - width_ : openmc.lib.plot._Position - The width of the plot along the x, y, and z axes, respectively - basis_ : c_int - The axes basis of the plot view. - pixels_ : c_size_t[3] - The resolution of the plot in the horizontal and vertical dimensions - color_overlaps_ : c_bool - Whether to assign unique IDs (-3) to overlapping regions. - level_ : c_int - The universe level for the plot view - - Attributes - ---------- - origin : tuple or list of ndarray - Origin (center) of the plot - width : float - The horizontal dimension of the plot in geometry units (cm) - height : float - The vertical dimension of the plot in geometry units (cm) - basis : string - One of {'xy', 'xz', 'yz'} indicating the horizontal and vertical - axes of the plot. - h_res : int - The horizontal resolution of the plot in pixels - v_res : int - The vertical resolution of the plot in pixels - level : int - The universe level for the plot (default: -1 -> all universes shown) - """ - _fields_ = [('origin_', _Position), - ('u_span_', _Position), - ('v_span_', _Position), - ('width_', _Position), - ('basis_', c_int), - ('pixels_', 3*c_size_t), - ('color_overlaps_', c_bool), - ('level_', c_int)] - - def __init__(self): - self.level_ = -1 - self.basis_ = 1 - self.color_overlaps_ = False - self._update_spans() - - def _update_spans(self): - if self.basis_ == 1: - self.u_span_.x = self.width_.x - self.u_span_.y = 0.0 - self.u_span_.z = 0.0 - self.v_span_.x = 0.0 - self.v_span_.y = self.width_.y - self.v_span_.z = 0.0 - elif self.basis_ == 2: - self.u_span_.x = self.width_.x - self.u_span_.y = 0.0 - self.u_span_.z = 0.0 - self.v_span_.x = 0.0 - self.v_span_.y = 0.0 - self.v_span_.z = self.width_.y - elif self.basis_ == 3: - self.u_span_.x = 0.0 - self.u_span_.y = self.width_.x - self.u_span_.z = 0.0 - self.v_span_.x = 0.0 - self.v_span_.y = 0.0 - self.v_span_.z = self.width_.y - - @property - def origin(self): - return self.origin_ - - @origin.setter - def origin(self, origin): - self.origin_.x = origin[0] - self.origin_.y = origin[1] - self.origin_.z = origin[2] - - @property - def width(self): - return self.width_.x - - @width.setter - def width(self, width): - self.width_.x = width - self._update_spans() - - @property - def height(self): - return self.width_.y - - @height.setter - def height(self, height): - self.width_.y = height - self._update_spans() - - @property - def basis(self): - if self.basis_ == 1: - return 'xy' - elif self.basis_ == 2: - return 'xz' - elif self.basis_ == 3: - return 'yz' - - raise ValueError(f"Plot basis {self.basis_} is invalid") - - @basis.setter - def basis(self, basis): - if isinstance(basis, str): - valid_bases = ('xy', 'xz', 'yz') - basis = basis.lower() - if basis not in valid_bases: - raise ValueError(f"{basis} is not a valid plot basis.") - - if basis == 'xy': - self.basis_ = 1 - elif basis == 'xz': - self.basis_ = 2 - elif basis == 'yz': - self.basis_ = 3 - self._update_spans() - return - - if isinstance(basis, int): - valid_bases = (1, 2, 3) - if basis not in valid_bases: - raise ValueError(f"{basis} is not a valid plot basis.") - self.basis_ = basis - self._update_spans() - return - - raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") - - @property - def h_res(self): - return self.pixels_[0] - - @h_res.setter - def h_res(self, h_res): - self.pixels_[0] = h_res - - @property - def v_res(self): - return self.pixels_[1] - - @v_res.setter - def v_res(self, v_res): - self.pixels_[1] = v_res - - @property - def level(self): - return int(self.level_) - @level.setter - def level(self, level): - self.level_ = level - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, color_overlaps): - self.color_overlaps_ = color_overlaps - - def __repr__(self): - out_str = ["-----", - "Plot:", - "-----", - f"Origin: {self.origin}", - f"Width: {self.width}", - f"Height: {self.height}", - f"Basis: {self.basis}", - f"HRes: {self.h_res}", - f"VRes: {self.v_res}", - f"Color Overlaps: {self.color_overlaps}", - f"Level: {self.level}"] - return '\n'.join(out_str) - - -class ViewParam(_PlotBase): - """Viewer settings that are needed for _PlotBase and are independent - of all other plotter/model settings. +class ViewParam: + """View parameters defining a 2-D geometry slice. Parameters ---------- origin : 3-tuple of floats Origin (center) of plot view - width: float + width : float Width of plot view in model units height : float Height of plot view in model units @@ -1320,7 +1091,7 @@ class ViewParam(_PlotBase): Attributes ---------- - origin : 3-tuple of floats + origin : tuple of float Origin (center) of plot view width : float Width of the plot view in model units @@ -1340,36 +1111,46 @@ class ViewParam(_PlotBase): The universe level for the plot (default: -1 -> all universes shown) """ + _VALID_BASES = ('xy', 'xz', 'yz') + def __init__(self, origin=(0, 0, 0), width=10, height=10, default_res=1000): """Initialize ViewParam attributes""" - super().__init__() - - # View Parameters - self.level = -1 - self.origin = origin - self.width = width - self.height = height - self.h_res = default_res - self.v_res = default_res + self.origin = tuple(origin) + self.width = float(width) + self.height = float(height) + self.h_res = int(default_res) + self.v_res = int(default_res) self.basis = 'xy' + self.level = -1 self.color_overlaps = False + @property + def basis(self): + return self._basis + + @basis.setter + def basis(self, basis): + if basis not in self._VALID_BASES: + raise ValueError(f"'{basis}' is not a valid plot basis. " + f"Must be one of {self._VALID_BASES}.") + self._basis = basis + @property def slice_axis(self): - if self.basis == 'xy': + if self._basis == 'xy': return 2 - elif self.basis == 'yz': + elif self._basis == 'yz': return 0 else: return 1 @property def llc(self): - if self.basis == 'xy': + if self._basis == 'xy': x = self.origin[0] - self.width / 2.0 y = self.origin[1] - self.height / 2.0 z = self.origin[2] - elif self.basis == 'yz': + elif self._basis == 'yz': x = self.origin[0] y = self.origin[1] - self.width / 2.0 z = self.origin[2] - self.height / 2.0 @@ -1381,11 +1162,11 @@ def llc(self): @property def urc(self): - if self.basis == 'xy': + if self._basis == 'xy': x = self.origin[0] + self.width / 2.0 y = self.origin[1] + self.height / 2.0 z = self.origin[2] - elif self.basis == 'yz': + elif self._basis == 'yz': x = self.origin[0] y = self.origin[1] + self.width / 2.0 z = self.origin[2] + self.height / 2.0 @@ -1395,6 +1176,20 @@ def urc(self): z = self.origin[2] + self.height / 2.0 return x, y, z + def __repr__(self): + out_str = ["-----", + "Plot:", + "-----", + f"Origin: {self.origin}", + f"Width: {self.width}", + f"Height: {self.height}", + f"Basis: {self.basis}", + f"HRes: {self.h_res}", + f"VRes: {self.v_res}", + f"Color Overlaps: {self.color_overlaps}", + f"Level: {self.level}"] + return '\n'.join(out_str) + def __eq__(self, other): return repr(self) == repr(other) @@ -1553,7 +1348,7 @@ class PlotView: view_ind : PlotViewIndependent instance viewing parameters that are independent of the model view_params : ViewParam instance - view parameters necesary for _PlotBase + view parameters defining the 2-D geometry slice cells : Dict of DomainView instances Dictionary of cell view settings by ID materials : Dict of DomainView instances @@ -1563,8 +1358,8 @@ class PlotView: """ attrs = ('view_ind', 'view_params', 'cells', 'materials', 'selectedTally', 'mesh_annotations') - plotbase_attrs = ('level', 'origin', 'width', 'height', - 'h_res', 'v_res', 'basis', 'llc', 'urc', 'color_overlaps') + view_param_attrs = ('level', 'origin', 'width', 'height', + 'h_res', 'v_res', 'basis', 'llc', 'urc', 'color_overlaps', 'slice_axis') def __init__(self, origin=(0, 0, 0), width=10, height=10, restore_view=None, restore_domains=False, default_res=None): @@ -1602,7 +1397,7 @@ def __getattr__(self, name): if name not in self.__dict__: raise AttributeError('{} not in PlotView dict'.format(name)) return self.__dict__[name] - elif name in self.plotbase_attrs: + elif name in self.view_param_attrs: return getattr(self.view_params, name) else: return getattr(self.view_ind, name) @@ -1610,7 +1405,7 @@ def __getattr__(self, name): def __setattr__(self, name, value): if name in self.attrs: super().__setattr__(name, value) - elif name in self.plotbase_attrs: + elif name in self.view_param_attrs: setattr(self.view_params, name, value) else: setattr(self.view_ind, name, value) @@ -1674,7 +1469,7 @@ def getDomains(domain_type, rng) -> DomainViewDict: return DomainViewDict(defaults) - def adopt_plotbase(self, view): + def adopt_view_params(self, view): """ Applies only the geometric aspects of a view to the current view From 15ceb5415638b8bc436adea420f0b20a5d2d5e75 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Jun 2026 17:59:02 -0500 Subject: [PATCH 7/9] Update comment --- openmc_plotter/plotmodel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index 44e330f..6cb1191 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -129,7 +129,7 @@ def generate_maps(self, work_item: PlotWorkItem): if params.get("filter_id") is not None: filter_cpp = openmc.lib.filters[params["filter_id"]] - # Single call replaces id_map + property_map + get_plot_bins + # Get geometry and property data from OpenMC library geom_data, property_data = openmc.lib.slice_plot( origin=params["origin"], width=(params["width"], params["height"]), From 0140f74da7ba3bd083dd8d5cca2010f5b5b3827b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Jun 2026 19:05:35 -0500 Subject: [PATCH 8/9] Configure git safe directory --- .github/workflows/ci.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bf71b0..c65ceb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,23 +24,26 @@ jobs: env: DISPLAY: ':99.0' steps: - - - name: Apt dependencies + - name: Apt dependencies shell: bash run: | apt update apt install -y libglu1-mesa libglib2.0-0 libfontconfig1 libegl-dev libxkbcommon-x11-0 xvfb libdbus-1-3 /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - - - uses: actions/checkout@v4 - - - name: Install + + - uses: actions/checkout@v4 + + - name: Configure Git safe directory + shell: bash + run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" + + - name: Install shell: bash run: | cd ${GITHUB_WORKSPACE} pip install .[test] - - - name: Test + + - name: Test shell: bash run: | cd ${GITHUB_WORKSPACE} From 89023311d67159a6b315775aee8bf2188cb8c69f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Jun 2026 19:16:22 -0500 Subject: [PATCH 9/9] Fix ViewParam origin type --- openmc_plotter/plotmodel.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openmc_plotter/plotmodel.py b/openmc_plotter/plotmodel.py index 6cb1191..316f65d 100644 --- a/openmc_plotter/plotmodel.py +++ b/openmc_plotter/plotmodel.py @@ -1080,7 +1080,7 @@ class ViewParam: Parameters ---------- - origin : 3-tuple of floats + origin : sequence of float Origin (center) of plot view width : float Width of plot view in model units @@ -1091,7 +1091,7 @@ class ViewParam: Attributes ---------- - origin : tuple of float + origin : list of float Origin (center) of plot view width : float Width of the plot view in model units @@ -1114,8 +1114,7 @@ class ViewParam: _VALID_BASES = ('xy', 'xz', 'yz') def __init__(self, origin=(0, 0, 0), width=10, height=10, default_res=1000): - """Initialize ViewParam attributes""" - self.origin = tuple(origin) + self.origin = list(origin) self.width = float(width) self.height = float(height) self.h_res = int(default_res)