Skip to content

Commit a6c5fd5

Browse files
authored
Merge pull request #134 from paulromano/mesh-plotting
Generalize mesh tally plotting
2 parents 484466b + deae21a commit a6c5fd5

6 files changed

Lines changed: 108 additions & 99 deletions

File tree

openmc_plotter/docks.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ def updateNuclides(self):
670670
self.model.appliedNuclides = tuple(applied_nuclides)
671671

672672
if 'total' in applied_nuclides:
673-
self.model.appliedNuclides = ['total',]
673+
self.model.appliedNuclides = ('total',)
674674
for nuclide, nuclide_box in self.nuclide_map.items():
675675
if nuclide != 'total':
676676
nuclide_box.setFlags(QtCore.Qt.ItemIsUserCheckable)
@@ -826,6 +826,11 @@ def __init__(self, model, main_window, field, colormaps=None):
826826
zero_connector = partial(main_window.toggleTallyMaskZero)
827827
self.maskZeroBox.stateChanged.connect(zero_connector)
828828

829+
# Volume normalization check box
830+
self.volumeNormBox = QCheckBox()
831+
volume_connector = partial(main_window.toggleTallyVolumeNorm)
832+
self.volumeNormBox.stateChanged.connect(volume_connector)
833+
829834
# Clip data to min/max check box
830835
self.clipDataBox = QCheckBox()
831836
clip_connector = partial(main_window.toggleTallyDataClip)
@@ -849,6 +854,7 @@ def __init__(self, model, main_window, field, colormaps=None):
849854
self.layout.addRow("Log Scale: ", self.scaleBox)
850855
self.layout.addRow("Clip Data: ", self.clipDataBox)
851856
self.layout.addRow("Mask Zeros: ", self.maskZeroBox)
857+
self.layout.addRow("Volume normalize: ", self.volumeNormBox)
852858
self.layout.addRow("Contours: ", self.contoursBox)
853859
self.layout.addRow("Contour Levels:", self.contourLevelsLine)
854860
self.setLayout(self.layout)
@@ -881,6 +887,10 @@ def updateMaskZeros(self):
881887
cv = self.model.currentView
882888
self.maskZeroBox.setChecked(cv.tallyMaskZeroValues)
883889

890+
def updateVolumeNorm(self):
891+
cv = self.model.currentView
892+
self.volumeNormBox.setChecked(cv.tallyVolumeNorm)
893+
884894
def updateDataClip(self):
885895
cv = self.model.currentView
886896
self.clipDataBox.setChecked(cv.clipTallyData)
@@ -900,6 +910,7 @@ def update(self):
900910

901911
self.updateMinMax()
902912
self.updateMaskZeros()
913+
self.updateVolumeNorm()
903914
self.updateDataClip()
904915
self.updateDataIndicator()
905916
self.updateTallyContours()

openmc_plotter/main_window.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ def openStatePoint(self):
550550
msg_box.exec()
551551
return
552552
filename, ext = QFileDialog.getOpenFileName(self, "Open StatePoint",
553-
".", "statepoint*.h5")
553+
".", "*.h5")
554554
if filename:
555555
try:
556556
self.model.openStatePoint(filename)
@@ -951,6 +951,10 @@ def toggleTallyMaskZero(self, state):
951951
av = self.model.activeView
952952
av.tallyMaskZeroValues = bool(state)
953953

954+
def toggleTallyVolumeNorm(self, state):
955+
av = self.model.activeView
956+
av.tallyVolumeNorm = bool(state)
957+
954958
def editTallyAlpha(self, value, apply=False):
955959
av = self.model.activeView
956960
av.tallyDataAlpha = value

openmc_plotter/plotgui.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import numpy as np
1515

1616
from .plot_colors import rgb_normalize, invert_rgb
17-
from .plotmodel import DomainDelegate
17+
from .plotmodel import DomainDelegate, PlotModel
1818
from .plotmodel import _NOT_FOUND, _VOID_REGION, _OVERLAP, _MODEL_PROPERTIES
1919
from .scientific_spin_box import ScientificDoubleSpinBox
2020
from .custom_widgets import HorizontalLine
@@ -23,7 +23,7 @@
2323

2424
class PlotImage(FigureCanvas):
2525

26-
def __init__(self, model, parent, main_window):
26+
def __init__(self, model: PlotModel, parent, main_window):
2727

2828
self.figure = Figure(dpi=main_window.logicalDpiX())
2929
super().__init__(self.figure)
@@ -339,8 +339,8 @@ def mouseReleaseEvent(self, event):
339339

340340
def wheelEvent(self, event):
341341

342-
if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier:
343-
numDegrees = event.delta() / 8
342+
if event.angleDelta() and event.modifiers() == QtCore.Qt.ShiftModifier:
343+
numDegrees = event.angleDelta() / 8
344344

345345
if 24 < self.main_window.zoom + numDegrees < 5001:
346346
self.main_window.editZoom(self.main_window.zoom + numDegrees)

openmc_plotter/plotmodel.py

Lines changed: 67 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
from __future__ import annotations
12
from ast import literal_eval
23
from collections import defaultdict
34
import copy
45
import hashlib
56
import itertools
6-
import os
7-
from pathlib import Path
87
import pickle
98
import threading
9+
from typing import Literal, Tuple, Optional
1010

1111
from PySide6.QtWidgets import QItemDelegate, QColorDialog, QLineEdit, QMessageBox
1212
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QSize, QEvent
@@ -28,9 +28,14 @@
2828
_MODEL_PROPERTIES = ('temperature', 'density')
2929
_PROPERTY_INDICES = {'temperature': 0, 'density': 1}
3030

31-
_REACTION_UNITS = 'Reactions per Source Particle'
32-
_PRODUCTION_UNITS = 'Particles Produced per Source Particle'
33-
_ENERGY_UNITS = 'eV per Source Particle'
31+
_REACTION_UNITS = 'reactions/source'
32+
_PRODUCTION_UNITS = 'particles/source'
33+
_ENERGY_UNITS = 'eV/source'
34+
35+
_REACTION_UNITS_VOL = 'reactions/cm³/source'
36+
_PRODUCTION_UNITS_VOL = 'particles/cm³/source'
37+
_ENERGY_UNITS_VOL = 'eV/cm³/source'
38+
3439

3540
_SPATIAL_FILTERS = (openmc.UniverseFilter,
3641
openmc.MaterialFilter,
@@ -42,24 +47,33 @@
4247
_PRODUCTIONS = ('delayed-nu-fission', 'prompt-nu-fission', 'nu-fission',
4348
'nu-scatter', 'H1-production', 'H2-production',
4449
'H3-production', 'He3-production', 'He4-production')
50+
_ENERGY_SCORES = {'heating', 'heating-local', 'kappa-fission',
51+
'fission-q-prompt', 'fission-q-recoverable',
52+
'damage-energy'}
4553

4654
_SCORE_UNITS = {p: _PRODUCTION_UNITS for p in _PRODUCTIONS}
47-
_SCORE_UNITS['flux'] = 'Particle-cm/Particle'
48-
_SCORE_UNITS['current'] = 'Particles per source Particle'
49-
_SCORE_UNITS['events'] = 'Events per Source Particle'
50-
_SCORE_UNITS['inverse-velocity'] = 'Particle-seconds per Source Particle'
51-
_SCORE_UNITS['heating'] = _ENERGY_UNITS
52-
_SCORE_UNITS['heating-local'] = _ENERGY_UNITS
53-
_SCORE_UNITS['kappa-fission'] = _ENERGY_UNITS
54-
_SCORE_UNITS['fission-q-prompt'] = _ENERGY_UNITS
55-
_SCORE_UNITS['fission-q-recoverable'] = _ENERGY_UNITS
56-
_SCORE_UNITS['decay-rate'] = 'Seconds^-1'
57-
_SCORE_UNITS['damage-energy'] = _ENERGY_UNITS
55+
_SCORE_UNITS['flux'] = 'particle-cm/source'
56+
_SCORE_UNITS['current'] = 'particle/source'
57+
_SCORE_UNITS['events'] = 'events/source'
58+
_SCORE_UNITS['inverse-velocity'] = 'particle-s/source'
59+
_SCORE_UNITS['decay-rate'] = 'particle/s/source'
60+
_SCORE_UNITS.update({s: _ENERGY_UNITS for s in _ENERGY_SCORES})
61+
62+
_SCORE_UNITS_VOL = {p: _PRODUCTION_UNITS_VOL for p in _PRODUCTIONS}
63+
_SCORE_UNITS_VOL['flux'] = 'particle/cm²/source'
64+
_SCORE_UNITS_VOL['current'] = 'particle/cm³/source'
65+
_SCORE_UNITS_VOL['events'] = 'events/cm³/source'
66+
_SCORE_UNITS_VOL['inverse-velocity'] = 'particle-s/cm³/source'
67+
_SCORE_UNITS_VOL['decay-rate'] = 'particle/s/cm³/source'
68+
_SCORE_UNITS.update({s: _ENERGY_UNITS_VOL for s in _ENERGY_SCORES})
69+
5870

5971
_TALLY_VALUES = {'Mean': 'mean',
6072
'Std. Dev.': 'std_dev',
6173
'Rel. Error': 'rel_err'}
6274

75+
TallyValueType = Literal['mean', 'std_dev', 'rel_err']
76+
6377

6478
def hash_file(path):
6579
# return the md5 hash of a file
@@ -382,11 +396,11 @@ def storeCurrent(self):
382396
""" Add current view to previousViews list """
383397
self.previousViews.append(copy.deepcopy(self.currentView))
384398

385-
def create_tally_image(self, view=None):
399+
def create_tally_image(self, view: Optional[PlotView] = None):
386400
"""
387401
Parameters
388402
----------
389-
view :
403+
view : PlotView
390404
View used to set bounds of the tally data
391405
392406
Returns
@@ -436,6 +450,10 @@ def create_tally_image(self, view=None):
436450
contains_cellinstance = tally.contains_filter(openmc.CellInstanceFilter)
437451

438452
if tally.contains_filter(openmc.MeshFilter):
453+
# Check for volume normalization in order to change units
454+
if view.tallyVolumeNorm:
455+
units_out = _SCORE_UNITS_VOL.get(scores[0], _REACTION_UNITS_VOL)
456+
439457
if tally_value == 'rel_err':
440458
# get both the std. dev. data and mean data
441459
# to create the relative error data
@@ -635,7 +653,10 @@ def _create_distribcell_image(self, tally, tally_value, scores, nuclides, cellin
635653

636654
return image_data, None, data_min, data_max
637655

638-
def _create_tally_mesh_image(self, tally, tally_value, scores, nuclides, view=None):
656+
def _create_tally_mesh_image(
657+
self, tally: openmc.Tally, tally_value: TallyValueType,
658+
scores: Tuple[str], nuclides: Tuple[str], view: PlotView = None
659+
):
639660
# some variables used throughout
640661
if view is None:
641662
view = self.currentView
@@ -652,57 +673,10 @@ def _do_op(array, tally_value, ax=0):
652673
# start with reshaped data
653674
data = tally.get_reshaped_data(tally_value)
654675

655-
# determine basis indices
656-
if view.basis == 'xy':
657-
h_ind = 0
658-
v_ind = 1
659-
ax = 2
660-
elif view.basis == 'yz':
661-
h_ind = 1
662-
v_ind = 2
663-
ax = 0
664-
else:
665-
h_ind = 0
666-
v_ind = 2
667-
ax = 1
668-
669-
# adjust corners of the mesh for a translation
670-
# applied to the mesh filter
671-
lower_left = mesh.lower_left
672-
upper_right = mesh.upper_right
673-
width = mesh.width
674-
dimension = mesh.dimension
675-
if hasattr(mesh_filter, 'translation') and mesh_filter.translation is not None:
676-
lower_left += mesh_filter.translation
677-
upper_right += mesh_filter.translation
678-
679-
# For 2D meshes, add an extra z dimension
680-
if len(mesh.dimension) == 2:
681-
lower_left = np.hstack((lower_left, -1e50))
682-
upper_right = np.hstack((upper_right, 1e50))
683-
width = np.hstack((width, 2e50))
684-
dimension = np.hstack((dimension, 1))
685-
686-
# reduce data to the visible slice of the mesh values
687-
k = int((view.origin[ax] - lower_left[ax]) // width[ax])
688-
689-
# setup slice
690-
data_slice = [None, None, None]
691-
data_slice[h_ind] = slice(dimension[h_ind])
692-
data_slice[v_ind] = slice(dimension[v_ind])
693-
data_slice[ax] = k
694-
695-
if k < 0 or k > dimension[ax]:
696-
return (None, None, None, None)
697-
698676
# move mesh axes to the end of the filters
699677
filter_idx = [type(filter) for filter in tally.filters].index(openmc.MeshFilter)
700678
data = np.moveaxis(data, filter_idx, -1)
701679

702-
# reshape data (with zyx ordering for mesh data)
703-
data = data.reshape(data.shape[:-1] + tuple(dimension[::-1]))
704-
data = data[..., data_slice[2], data_slice[1], data_slice[0]]
705-
706680
# sum over the rest of the tally filters
707681
for tally_filter in tally.filters:
708682
if type(tally_filter) == openmc.MeshFilter:
@@ -738,18 +712,36 @@ def _do_op(array, tally_value, ax=0):
738712
selected_scores.append(idx)
739713
data = _do_op(data[np.array(selected_scores)], tally_value)
740714

715+
# Account for mesh filter translation
716+
if mesh_filter.translation is not None:
717+
t = mesh_filter.translation
718+
origin = (view.origin[0] - t[0], view.origin[1] - t[1], view.origin[2] - t[2])
719+
else:
720+
origin = view.origin
721+
722+
# Get mesh bins from openmc.lib
723+
mesh_cpp = openmc.lib.meshes[mesh.id]
724+
mesh_bins = mesh_cpp.get_plot_bins(
725+
origin=origin,
726+
width=(view.width, view.height),
727+
basis=view.basis,
728+
pixels=(view.h_res, view.v_res),
729+
)
730+
731+
# Apply volume normalization
732+
if view.tallyVolumeNorm:
733+
data /= mesh_cpp.volumes
734+
735+
# set image data
736+
image_data = np.full_like(self.ids, np.nan, dtype=float)
737+
mask = (mesh_bins >= 0)
738+
image_data[mask] = data[mesh_bins[mask]]
739+
741740
# get dataset's min/max
742741
data_min = np.min(data)
743742
data_max = np.max(data)
744743

745-
# set image data, reverse y-axis
746-
image_data = data[::-1, ...]
747-
748-
# return data extents (in cm) for the tally
749-
extents = [lower_left[h_ind], upper_right[h_ind],
750-
lower_left[v_ind], upper_right[v_ind]]
751-
752-
return image_data, extents, data_min, data_max
744+
return image_data, None, data_min, data_max
753745

754746
@property
755747
def cell_ids(self):
@@ -939,6 +931,7 @@ def __init__(self):
939931
self.tallyDataMax = np.inf
940932
self.tallyDataLogScale = False
941933
self.tallyMaskZeroValues = False
934+
self.tallyVolumeNorm = False
942935
self.clipTallyData = False
943936
self.tallyValue = "Mean"
944937
self.tallyContours = False

openmc_plotter/tools.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,33 +168,36 @@ def populate(self):
168168
mesh = mesh_filter.mesh
169169
assert(mesh.n_dimension == 3)
170170

171-
llc = mesh.lower_left
171+
bbox = mesh.bounding_box
172+
173+
llc = bbox.lower_left
172174
self.xminBox.setValue(llc[0])
173175
self.yminBox.setValue(llc[1])
174176
self.zminBox.setValue(llc[2])
175177

176-
urc = mesh.upper_right
178+
urc = bbox.upper_right
177179
self.xmaxBox.setValue(urc[0])
178180
self.ymaxBox.setValue(urc[1])
179181
self.zmaxBox.setValue(urc[2])
180182

181-
dims = mesh.dimension
182-
self.xResBox.setValue(dims[0])
183-
self.yResBox.setValue(dims[1])
184-
self.zResBox.setValue(dims[2])
185-
186183
bounds_msg = "Using MeshFilter to set bounds automatically."
187184
for box in self.bounds_spin_boxes:
188185
box.setEnabled(False)
189186
box.setToolTip(bounds_msg)
190187

191-
resolution_msg = "Using MeshFilter to set resolution automatically."
192-
self.xResBox.setEnabled(False)
193-
self.xResBox.setToolTip(resolution_msg)
194-
self.yResBox.setEnabled(False)
195-
self.yResBox.setToolTip(resolution_msg)
196-
self.zResBox.setEnabled(False)
197-
self.zResBox.setToolTip(resolution_msg)
188+
dims = mesh.dimension
189+
if len(dims) == 3:
190+
self.xResBox.setValue(dims[0])
191+
self.yResBox.setValue(dims[1])
192+
self.zResBox.setValue(dims[2])
193+
194+
resolution_msg = "Using MeshFilter to set resolution automatically."
195+
self.xResBox.setEnabled(False)
196+
self.xResBox.setToolTip(resolution_msg)
197+
self.yResBox.setEnabled(False)
198+
self.yResBox.setToolTip(resolution_msg)
199+
self.zResBox.setEnabled(False)
200+
self.zResBox.setToolTip(resolution_msg)
198201

199202
else:
200203
# initialize using the bounds of the current view
@@ -214,14 +217,12 @@ def populate(self):
214217

215218
def export_data(self):
216219
# cache current and active views
217-
cv = self.model.currentView
218220
av = self.model.activeView
219221
try:
220222
# export the tally data
221223
self._export_data()
222224
finally:
223-
#always reset to the original view
224-
self.model.currentView = cv
225+
# always reset to the original view
225226
self.model.activeView = av
226227
self.model.makePlot()
227228

0 commit comments

Comments
 (0)