├── .github └── workflows │ ├── ci.yml │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── openmc_plotter ├── __init__.py ├── __main__.py ├── assets │ ├── openmc_logo.png │ └── splash.png ├── custom_widgets.py ├── docks.py ├── main_window.py ├── overlays.py ├── plot_colors.py ├── plotgui.py ├── plotmodel.py ├── scientific_spin_box.py ├── statepointmodel.py └── tools.py ├── pyproject.toml ├── pytest.ini ├── screenshots ├── atr.png ├── beavrs.png ├── beavrs_zoomed.png ├── color_dialog.png ├── dagmc.png ├── pincell_tally.png ├── shortcuts.png └── source-sites.png ├── setup.py └── tests ├── __init__.py ├── conftest.py └── setup_test ├── geometry.xml ├── materials.xml ├── ref.png ├── ref1.png ├── settings.xml ├── test.pltvw ├── test.py └── test1.pltvw /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # allows us to run workflows manually 5 | workflow_dispatch: 6 | 7 | pull_request: 8 | branches: 9 | - develop 10 | - master 11 | push: 12 | branches: 13 | - develop 14 | - master 15 | 16 | env: 17 | OMP_NUM_THREADS: 2 18 | QT_QPA_PLATFORM: offscreen 19 | 20 | jobs: 21 | ci: 22 | runs-on: ubuntu-latest 23 | container: openmc/openmc:develop 24 | env: 25 | DISPLAY: ':99.0' 26 | steps: 27 | - 28 | name: Apt dependencies 29 | shell: bash 30 | run: | 31 | apt update 32 | apt install -y libglu1-mesa libglib2.0-0 libfontconfig1 libegl-dev libxkbcommon-x11-0 xvfb libdbus-1-3 33 | /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 34 | - 35 | uses: actions/checkout@v4 36 | - 37 | name: Install 38 | shell: bash 39 | run: | 40 | cd ${GITHUB_WORKSPACE} 41 | pip install .[test] 42 | - 43 | name: Test 44 | shell: bash 45 | run: | 46 | cd ${GITHUB_WORKSPACE} 47 | pytest -v tests 48 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | dist: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: hynek/build-and-inspect-python-package@v2 16 | 17 | publish: 18 | needs: [dist] 19 | environment: pypi 20 | permissions: 21 | id-token: write 22 | runs-on: ubuntu-latest 23 | if: github.event_name == 'release' && github.event.action == 'published' 24 | 25 | steps: 26 | - uses: actions/download-artifact@v4 27 | with: 28 | name: Packages 29 | path: dist 30 | 31 | - uses: pypa/gh-action-pypi-publish@release/v1 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | __pycache__ 3 | plot_settings.pkl 4 | *.egg-info 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2021 UChicago Argonne, LLC and OpenMC contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://img.shields.io/pypi/v/openmc-plotter?color=brightgreen&label=pypi&logo=grebrightgreenen&logoColor=green)](https://pypi.org/project/openmc-plotter/) 2 | 3 | # OpenMC Plot Explorer 4 | 5 | ## Description 6 | 7 | A graphical interface for visualizing and navigating OpenMC models. 8 | 9 | Originally created by @landonjmitchell at the University of Chicago ([original 10 | repository](https://github.com/landonjmitchell/openmc-plotgui)). 11 | 12 | ![beavrs](./screenshots/beavrs.png) 13 | 14 | ![beavrs_zoom](./screenshots/beavrs_zoomed.png) 15 | 16 | ## Dependencies 17 | 18 | OpenMC, Matplotlib, NumPy, PySide6 19 | 20 | ## Installation 21 | 22 | This package is available from PyPI and conda-forge. To install from PyPI run 23 | the following command from the terminal: 24 | ```console 25 | python -m pip install openmc-plotter 26 | ``` 27 | 28 | Alternatively, conda can be used to install the plotter (recommended option for 29 | conda enviroments): 30 | ```console 31 | conda install -c conda-forge openmc-plotter 32 | ``` 33 | 34 | ## Usage 35 | 36 | From a directory containing an OpenMC model run: 37 | 38 | ```console 39 | $ openmc-plotter 40 | ``` 41 | 42 | or simply run 43 | 44 | ```console 45 | $ openmc-plotter 46 | ``` 47 | 48 | from the directory containing the model. 49 | 50 | Once the viewer has opened, press `?` to view a variety of keyboard shortcuts. 51 | 52 | ![shortcuts](./screenshots/shortcuts.png) 53 | 54 | ## Batch Mode 55 | 56 | Plot view (`.pltvw`) files generated in interactive sessions (see [Saving and Exporting](#saving-and-exporting)) 57 | can be used to generate images from the command line without opening the plotter GUI. 58 | 59 | ```console 60 | $ openmc-plotter -b view1.pltvw view1.pltvw view1.pltvw 61 | ``` 62 | ## Troubleshooting 63 | 64 | Updates to the plotter can result in stale references to deprecated features. For example, it's possible that something like the following may appear after an update: 65 | 66 | ``` 67 | AttributeError: 'MainWindow' object has no attribute 'shortcutOverlay' 68 | ``` 69 | 70 | To address this, the application settings can be cleared, the plotter can be started with the `--clear-config` (or `-c`) to reset the application's settings cache on startup. 71 | 72 | ```bash 73 | $ openmc-plotter --clear-cache 74 | ``` 75 | 76 | ## Features 77 | 78 | ### Tally Visualization 79 | 80 | The plotter also provides the ability to view tallies with spatial filters (mesh 81 | filter, cell filter, etc.). After loading a statepoint file from the "Edit" 82 | menu, tallies can be displayed on top of the geometry. 83 | 84 | ![tally](./screenshots/pincell_tally.png) 85 | 86 | Filters, scores, and nuclides on the tally can be enabled/disabled to isolate data. 87 | 88 | ### Color Manipulation 89 | 90 | Cell and material colors can be customized in the color dialog menu. Overlap 91 | coloring can be enabled as well to debug problems in the geometry definition. 92 | 93 | ![colors](./screenshots/color_dialog.png) 94 | 95 | ### DAGMC Geometry Visualization 96 | 97 | The plotter can also present the CAD-based tesellation geometry enabled by the 98 | [Direct Accelerated Geometry Monte Carlo](https://svalinn.github.io/DAGMC/) 99 | (DAGMC) toolkit. Below is the cross section of a tokamake model generated using [paramak](https://paramak.readthedocs.io/en/main/): 100 | 101 | ![dagmc](./screenshots/dagmc.png) 102 | 103 | As well as a DAGMC model of the Advanced Test Reactor (ATR): 104 | 105 | ![atr](./screenshots/atr.png) 106 | 107 | ### Saving and Exporting 108 | 109 | - Any image displayed in the plotter can be saved in any format supported by the 110 | user's Matplotlib installation. 111 | 112 | - Tally and geometry data (material/cell IDs) can be exported to a VTK file under "File->Export" 113 | 114 | ### Source Site Plotting 115 | 116 | Source locations from an externally defined source can be visualized in the plotter to verify 117 | source definitions. These source sites are gathered as generated by the transport model. A tolerance 118 | can be provided to filter out source sites that are too far from the slice plane, otherwise source 119 | locations are projected onto the slice plane. 120 | 121 | ![Source plotting](./screenshots/source-sites.png) 122 | 123 | # Options/Functionality 124 | 125 | ## Menu Bar: 126 | 127 | - File→Save Image As... : Save an image file of the current plot. 128 | - File→Save View Settings... : Save a .pltvw pickle file containing the current plot settings. 129 | - File→Open View Settings... : Open and load a .pltvw pickle file containing a previously saved view. 130 | - File→Quit : Quit the application. 131 | 132 | - Edit→Apply Changes : Apply any un-applied plot setting changes, and reload plot image. 133 | - Edit→Undo : Undo last applied plot settings changes, and reload plot image. 134 | - Edit→Redo : Redo last applied plot settings changes, and reload plot image. 135 | - Edit→Restore Default Settings : Restore to default plot settings and reload plot image. 136 | 137 | - Edit→Basis→xy : Change plot basis to xy, apply changes, and reload plot. 138 | - Edit→Basis→xz : Change plot basis to xz, apply changes, and reload plot. 139 | - Edit→Basis→yz : Change plot basis to yz, apply changes, and reload plot. 140 | - Edit→Color By→Cell : Change plot settings to be colored by cell, apply changes, and reload plot. 141 | - Edit→Color By→Material : Change plot settings to be colored by material, apply changes, and reload plot. 142 | - Edit→Enable Masking : Enable/Disable masking, apply changes, and reload plot. 143 | - Edit→Enable Highlighting : Enable/Disable highlighting, apply changes, and reload plot. 144 | - Edit→Enable Overlap Coloring : Enable/Disable display of geometry overlaps, apply changes, and reload plot. 145 | 146 | - View→Hide[Show] Dock : Hide/Show Dock. 147 | - View→Zoom... : Open dialog to input new zoom value. 148 | 149 | - Window→Main Window : Activate, bring main window to front. 150 | - Window→Color Options : [Open], activate, bring color options dialog to front. 151 | 152 | ## Dock: 153 | 154 | ### Origin: 155 | - X, Y, Z : Set the active plot origin to the values entered for each dimension. 156 | 157 | ### Options: 158 | 159 | - Width : Set the width of the active plot in plot units. 160 | - Height : Set the height of the active plot in plot units. 161 | - Basis : Set the basis of the active plot. 162 | - Color By : Select how the active plot is colored. 163 | - Color Options... : Open the color options dialog. 164 | 165 | ### Resolution: 166 | 167 | - Fixed Aspect Ratio : Check to prevent plot image stretching/warping. 168 | - Pixel Width: Set width in pixels of active plot. 169 | - Pixel Height : Set height in pixels of active plot. 170 | 171 | ### Other: 172 | 173 | - Apply Changes : Apply changes made to active plot, reload plot image. 174 | - Zoom : Set zoom level of plot image. 175 | 176 | ## Plot Image 177 | 178 | ### Actions: 179 | 180 | - Mouse Hover : Display plot coordinates in bottom-right of status bar. Display cell/material ID and name (if any) in bottom-left of status bar. 181 | 182 | - Left Mouse Button Drag : Crop active plot to selection, apply changes, and reload plot image. 183 | - Shift + Left Mouse Button Drag : De-crop active plot so that the current plot dimensions fit within selected area, apply changes, and reload plot image. 184 | - Note: To cancel selection, reduce selection size to less than 10 pixels in 185 | either dimension and release. Active plot Origin, width, and height values 186 | will be returned to current plot settings. 187 | 188 | - Double-Click Left Mouse Button : Set origin to point clicked, apply changes, and reload plot image. 189 | - Shift + Scroll : Increase/Decrease zoom level of plot image. 190 | 191 | - Right-Click on plot background → activate context menu: 192 | - Edit Background Color... : Select a new color for plot background, apply changes, and reload plot image. 193 | 194 | See menu bar for other context menu options. 195 | - Right-Click on plot overlap region → activate context menu: 196 | - Edit Overlap Color... : Select a new color for overlap regions, apply changes, and reload plot image. 197 | 198 | - Right-click on plot cell/material : Activate context menu: 199 | - Displays cell/material ID and name (if defined). 200 | - Edit Cell/Material Color... : Select a new color for the selected cell/material, apply changes, and reload plot image. 201 | - Mask Cell/Material : Mask/Unmask selected cell/material, apply changes, and reload plot image. 202 | - Highlight Cell/Material : Highlight/Unhighlight selected cell/material, apply changes, and reload plot image. 203 | 204 | 205 | See menu bar for other context menu options. 206 | 207 | ## Color Options Dialog 208 | 209 | ### General Tab: 210 | 211 | - Masking : Enable/Disable masking on active plot. 212 | - Mask Color : Select color of masked components on active plot. 213 | - Highlighting : Enable/Disable highlighting on active plot. Enabling 214 | highlighting will disable custom cell/material color selection. 215 | - Highlight Color : Select overlay color of non-highlighted cells/materials. 216 | - Highlight Alpha : Set alpha transparency level of non-highlighted color overlay. 217 | - Highlight Seed : Select seed for randomized colorization of cells/materials when highlighting is enabled. 218 | - Background Color : Select color of plot background for active plot. 219 | - Show Overlaps : Display overlap regions on the plot. 220 | - Overlap Color : Customize the displayed color of overlap regions. 221 | - Color Plot By : Select how the active plot is to be colored. 222 | 223 | ### Cells/Materials Tabs: 224 | 225 | - Double-click Name field to edit cell/material name. Edited names will not be reflected in .xml files. 226 | - Double-click Color field to select a color for the cell/material in the active plot. 227 | - Double-click SVG/RBG field to enter a new color for the cell/material. May 228 | be entered as SVG color, or RGB value (with or without parentheses). 229 | - Right-click Color or SVG/RGB field to clear. This will reset the color to the default value. 230 | - Click Mask field to mask/unmask cell/material in active plot. 231 | - Click Highlight field to highlight/unhighlight cell/material in active plot. 232 | - Apply Changes : Apply changes made to active plot, reload plot image. 233 | - Close : Close the color options dialog. 234 | 235 | **Note: Fields appear dynamically based on whether Masking/Highlighting are enabled or disabled.** 236 | 237 | ### On Open: 238 | 239 | Application windows are restored to their previous locations and sizes. If 240 | the .xml files match those of the previous sessions, the plot model will be 241 | restored to its previous state. 242 | 243 | ### On Close: 244 | Application status, including window size and location, will be saved. The 245 | current state of the plot model, including current plot and up to 10 246 | previous/subsequent plots (i.e. for undo/redo) will be saved. Active plot 247 | changes that have not been applied will be lost. 248 | 249 | ## Developer Notes 250 | 251 | ### Structure 252 | 253 | - openmc-plotter: primary executable. contains the major program logic used to interact with the application. 254 | - plotmodel.py: contains the underlying data structure of the plot model and application state. 255 | - plotgui.py: contains the bulk of the graphical elements of the application. 256 | - overlays.py: contains screen overlays seen in the GUI. Just keyboard shortcuts for now. 257 | - plot_colors.py: module with convenience functions for generating and modifying RGB/RGBA colors 258 | - assets: directory containing icons and images for the application 259 | 260 | ### Terminology 261 | 262 | #### Plot Image 263 | The plot slice image in the central area of the application. 264 | 265 | #### Active Plot 266 | Plot settings that are changed as dock and color dialog fields are changed. Not necessarily reflected in the plot image. 267 | 268 | #### Current Plot 269 | Plot settings currently displayed in the plot image. 270 | 271 | Applying changes causes the active plot to become the current plot, and a new plot image to be generated. 272 | -------------------------------------------------------------------------------- /openmc_plotter/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.4.1' 2 | -------------------------------------------------------------------------------- /openmc_plotter/__main__.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from pathlib import Path 3 | from threading import Thread 4 | import os 5 | import signal 6 | import sys 7 | 8 | from PySide6 import QtCore, QtGui 9 | from PySide6.QtWidgets import QApplication, QSplashScreen 10 | 11 | from . import __version__ 12 | from .main_window import MainWindow, _openmcReload 13 | 14 | 15 | def main(): 16 | ap = ArgumentParser(description='OpenMC Plotter GUI') 17 | version_str = f'OpenMC Plotter Version: {__version__}' 18 | ap.add_argument('-v', '--version', action='version', version=version_str, 19 | help='Display version info.') 20 | ap.add_argument('-e','--ignore-settings', action='store_false', 21 | help='Ignore plot_settings.pkl file if present.') 22 | ap.add_argument('-s', '--threads', type=int, default=None, 23 | help='If present, number of threads used to generate plots.') 24 | ap.add_argument('-r', '--resolution', type=int, default=None, 25 | help='Default number of pixels in each direction') 26 | ap.add_argument('model_path', nargs='?', default=os.curdir, 27 | help='Location of model XML file or a directory containing ' 28 | 'XML files (default is current dir)') 29 | ap.add_argument('-b', '--batch-mode', nargs='+', default=False, 30 | help='View files used to generate plots in batch mode') 31 | ap.add_argument('-c', '--clear-config', action='store_true', default=False, 32 | help='Clear the Qt application configuration settings') 33 | 34 | args = ap.parse_args() 35 | 36 | run_app(args) 37 | 38 | 39 | def run_app(user_args): 40 | path_icon = str(Path(__file__).parent / 'assets' / 'openmc_logo.png') 41 | path_splash = str(Path(__file__).parent / 'assets' / 'splash.png') 42 | 43 | app = QApplication(sys.argv) 44 | app.setOrganizationName("OpenMC") 45 | app.setOrganizationDomain("openmc.org") 46 | app.setApplicationName("OpenMC Plot Explorer") 47 | app.setWindowIcon(QtGui.QIcon(path_icon)) 48 | app.setAttribute(QtCore.Qt.AA_DontShowIconsInMenus, True) 49 | 50 | if user_args.clear_config: 51 | settings = QtCore.QSettings() 52 | settings.clear() 53 | 54 | splash_pix = QtGui.QPixmap(path_splash) 55 | splash = QSplashScreen(splash_pix, QtCore.Qt.WindowStaysOnTopHint) 56 | splash.setMask(splash_pix.mask()) 57 | if not user_args.batch_mode: 58 | splash.show() 59 | app.processEvents() 60 | splash.setMask(splash_pix.mask()) 61 | splash.showMessage("Loading Model...", 62 | QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) 63 | app.processEvents() 64 | # load OpenMC model on another thread 65 | openmc_args = {'threads': user_args.threads, 'model_path': user_args.model_path} 66 | loader_thread = Thread(target=_openmcReload, kwargs=openmc_args) 67 | loader_thread.start() 68 | # while thread is working, process app events 69 | while loader_thread.is_alive(): 70 | app.processEvents() 71 | 72 | splash.clearMessage() 73 | splash.showMessage("Starting GUI...", 74 | QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) 75 | app.processEvents() 76 | 77 | font_metric = QtGui.QFontMetrics(app.font()) 78 | screen_size = app.primaryScreen().size() 79 | mainWindow = MainWindow(font_metric, screen_size, user_args.model_path, 80 | user_args.threads, user_args.resolution) 81 | # connect splashscreen to main window, close when main window opens 82 | mainWindow.loadGui(use_settings_pkl=user_args.ignore_settings) 83 | 84 | splash.close() 85 | 86 | if user_args.batch_mode: 87 | for view_file in user_args.batch_mode: 88 | mainWindow.saveBatchImage(view_file) 89 | mainWindow.close() 90 | sys.exit() 91 | else: 92 | mainWindow.show() 93 | 94 | # connect interrupt signal to close call 95 | signal.signal(signal.SIGINT, lambda *args: mainWindow.close()) 96 | # create timer that interrupts the Qt event loop 97 | # to check for a signal 98 | timer = QtCore.QTimer() 99 | timer.start(500) 100 | timer.timeout.connect(lambda: None) 101 | 102 | sys.exit(app.exec()) 103 | 104 | if __name__ == '__main__': 105 | main() 106 | -------------------------------------------------------------------------------- /openmc_plotter/assets/openmc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/openmc_plotter/assets/openmc_logo.png -------------------------------------------------------------------------------- /openmc_plotter/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/openmc_plotter/assets/splash.png -------------------------------------------------------------------------------- /openmc_plotter/custom_widgets.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import warnings 3 | 4 | from PySide6 import QtWidgets, QtCore 5 | from PySide6.QtWidgets import QFrame 6 | 7 | 8 | class HorizontalLine(QFrame): 9 | """ 10 | Custom divider widget used in several layouts as a marker between 11 | different sections. 12 | """ 13 | def __init__(self): 14 | super().__init__() 15 | self.setFrameShape(QFrame.HLine) 16 | self.setFrameShadow(QFrame.Sunken) 17 | 18 | 19 | class Expander(QtWidgets.QWidget): 20 | """ 21 | A containing widget that can have a title and be collapsed or expanded 22 | inside of its frame. 23 | """ 24 | def __init__(self, title='', parent=None, layout=None, animationDuration=100): 25 | super().__init__(parent) 26 | 27 | self.layout_set = False 28 | 29 | self.animationDuration = animationDuration 30 | self.toggleAnimation = QtCore.QParallelAnimationGroup() 31 | self.contentArea = QtWidgets.QScrollArea() 32 | self.headerLine = QtWidgets.QFrame() 33 | self.toggleButton = QtWidgets.QToolButton() 34 | self.mainLayout = QtWidgets.QGridLayout() 35 | 36 | toggleButton = self.toggleButton 37 | toggleButton.setStyleSheet("QToolButton { border: none; }") 38 | toggleButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon) 39 | toggleButton.setArrowType(QtCore.Qt.RightArrow) 40 | toggleButton.setText(title) 41 | toggleButton.setCheckable(True) 42 | toggleButton.setChecked(False) 43 | 44 | headerLine = self.headerLine 45 | headerLine.setFrameShape(QtWidgets.QFrame.HLine) 46 | headerLine.setFrameShadow(QtWidgets.QFrame.Sunken) 47 | headerLine.setVisible(False) 48 | headerLine.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 49 | 50 | self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }") 51 | self.contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) 52 | # start collapsed by default 53 | self.contentArea.setMaximumHeight(0) 54 | self.contentArea.setMinimumHeight(0) 55 | 56 | # let the entire widget grow and shrink with its content 57 | toggleAnimation = self.toggleAnimation 58 | toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self, b"minimumHeight")) 59 | toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self, b"maximumHeight")) 60 | toggleAnimation.addAnimation(QtCore.QPropertyAnimation(self.contentArea, b"maximumHeight")) 61 | # don't waste space 62 | mainLayout = self.mainLayout 63 | mainLayout.setVerticalSpacing(0) 64 | mainLayout.setContentsMargins(0, 0, 0, 0) 65 | row = 0 66 | mainLayout.addWidget(self.toggleButton, row, 0, 1, 1, QtCore.Qt.AlignLeft) 67 | mainLayout.addWidget(self.headerLine, row, 2, 1, 1) 68 | row = 1 69 | mainLayout.addWidget(self.contentArea, row, 0, 1, 3) 70 | self.setLayout(self.mainLayout) 71 | 72 | def start_animation(checked): 73 | arrow_type = QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow 74 | toggleButton.setArrowType(arrow_type) 75 | 76 | if toggleButton.isChecked() != checked: 77 | toggleButton.setChecked(checked) 78 | 79 | if not self.layout_set: 80 | warnings.warn("No layout set for expanding widget") 81 | return 82 | 83 | direction = QtCore.QAbstractAnimation.Forward if checked else QtCore.QAbstractAnimation.Backward 84 | toggleAnimation.setDirection(direction) 85 | toggleAnimation.start() 86 | 87 | self.toggleButton.clicked.connect(start_animation) 88 | 89 | # make animation accessible as callable attributes 90 | self.expand = partial(start_animation, True) 91 | self.collapse = partial(start_animation, False) 92 | 93 | if layout is not None: 94 | self.setContentLayout(layout) 95 | 96 | def setContentLayout(self, contentLayout): 97 | self.layout_set = True 98 | self.contentArea.destroy() 99 | self.contentArea.setLayout(contentLayout) 100 | collapsedHeight = self.sizeHint().height() - self.contentArea.maximumHeight() 101 | contentHeight = contentLayout.sizeHint().height() 102 | for i in range(self.toggleAnimation.animationCount() - 1): 103 | expandAnimation = self.toggleAnimation.animationAt(i) 104 | expandAnimation.setDuration(self.animationDuration) 105 | expandAnimation.setStartValue(collapsedHeight) 106 | expandAnimation.setEndValue(collapsedHeight + contentHeight) 107 | contentAnimation = self.toggleAnimation.animationAt(self.toggleAnimation.animationCount() - 1) 108 | contentAnimation.setDuration(self.animationDuration) 109 | contentAnimation.setStartValue(0) 110 | contentAnimation.setEndValue(contentHeight) 111 | -------------------------------------------------------------------------------- /openmc_plotter/docks.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from collections.abc import Iterable 3 | from collections import defaultdict 4 | 5 | from PySide6 import QtCore 6 | from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QVBoxLayout, 7 | QGroupBox, QFormLayout, QLabel, QLineEdit, 8 | QComboBox, QSpinBox, QDoubleSpinBox, QSizePolicy, 9 | QCheckBox, QDockWidget, QScrollArea, QListWidget, 10 | QListWidgetItem, QTreeWidget, QTreeWidgetItem) 11 | import matplotlib.pyplot as plt 12 | import numpy as np 13 | import openmc 14 | 15 | from .custom_widgets import HorizontalLine, Expander 16 | from .scientific_spin_box import ScientificDoubleSpinBox 17 | from .plotmodel import (_SCORE_UNITS, _TALLY_VALUES, 18 | _REACTION_UNITS, _SPATIAL_FILTERS) 19 | 20 | 21 | class PlotterDock(QDockWidget): 22 | """ 23 | Dock widget with common settings for the plotting application 24 | """ 25 | 26 | def __init__(self, model, font_metric, parent=None): 27 | super().__init__(parent) 28 | 29 | self.model = model 30 | self.font_metric = font_metric 31 | self.main_window = parent 32 | 33 | self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 34 | 35 | 36 | class MeshAnnotationDock(PlotterDock): 37 | """Dock for mesh annotation options""" 38 | 39 | def __init__(self, model, font_metric, parent=None): 40 | super().__init__(model, font_metric, parent) 41 | 42 | self.treeLayout = QVBoxLayout() 43 | self.meshTree = QTreeWidget() 44 | self.treeExpander = Expander("Meshes:", layout=self.treeLayout) 45 | self.treeExpander.expand() # start with meshes expanded 46 | 47 | self.meshTree.setColumnCount(1) 48 | 49 | self.mesh_items = [] 50 | for mesh_id in self.model.cpp_mesh_ids(): 51 | mesh_item = QTreeWidgetItem(self.meshTree, (f'Mesh {mesh_id}',)) 52 | mesh_item.setFlags(mesh_item.flags() | QtCore.Qt.ItemIsUserCheckable) 53 | mesh_item.setCheckState(0, QtCore.Qt.Unchecked) 54 | self.mesh_items.append((mesh_id, mesh_item)) 55 | self.meshTree.addTopLevelItem(mesh_item) 56 | 57 | self.meshTree.setHeaderHidden(True) 58 | 59 | # Create submit button 60 | self.applyButton = QPushButton("Apply Changes") 61 | # Mac bug fix 62 | self.applyButton.setMinimumHeight(self.font_metric.height() * 1.6) 63 | self.applyButton.clicked.connect(self.main_window.applyChanges) 64 | 65 | label = QLabel("Mesh Annotations") 66 | self.treeLayout.addWidget(label) 67 | self.treeLayout.addWidget(self.meshTree) 68 | self.treeLayout.addWidget(HorizontalLine()) 69 | self.treeLayout.addWidget(self.applyButton) 70 | 71 | self.optionsWidget = QWidget() 72 | self.optionsWidget.setLayout(self.treeLayout) 73 | self.setWidget(self.optionsWidget) 74 | 75 | def get_checked_meshes(self): 76 | return [id for id, item in self.mesh_items if item.checkState(0) == QtCore.Qt.Checked] 77 | 78 | def update(self): 79 | pass 80 | 81 | def resizeEvent(self, event): 82 | self.main_window.resizeEvent(event) 83 | 84 | hideEvent = showEvent = moveEvent = resizeEvent 85 | 86 | 87 | class DomainDock(PlotterDock): 88 | """ 89 | Domain options dock 90 | """ 91 | 92 | def __init__(self, model, font_metric, parent=None): 93 | super().__init__(model, font_metric, parent) 94 | 95 | self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea) 96 | 97 | # Create Controls 98 | self._createOriginBox() 99 | self._createOptionsBox() 100 | self._createResolutionBox() 101 | 102 | # Create submit button 103 | self.applyButton = QPushButton("Apply Changes") 104 | # Mac bug fix 105 | self.applyButton.setMinimumHeight(self.font_metric.height() * 1.6) 106 | self.applyButton.clicked.connect(self.main_window.applyChanges) 107 | 108 | # Create Zoom box 109 | self.zoomBox = QSpinBox() 110 | self.zoomBox.setSuffix(' %') 111 | self.zoomBox.setRange(25, 2000) 112 | self.zoomBox.setValue(100) 113 | self.zoomBox.setSingleStep(25) 114 | self.zoomBox.valueChanged.connect(self.main_window.editZoom) 115 | self.zoomLayout = QHBoxLayout() 116 | self.zoomLayout.addWidget(QLabel('Zoom:')) 117 | self.zoomLayout.addWidget(self.zoomBox) 118 | self.zoomLayout.setContentsMargins(0, 0, 0, 0) 119 | self.zoomWidget = QWidget() 120 | self.zoomWidget.setLayout(self.zoomLayout) 121 | 122 | # Create Layout 123 | self.dockLayout = QVBoxLayout() 124 | self.dockLayout.addWidget(QLabel("Geometry/Properties")) 125 | self.dockLayout.addWidget(HorizontalLine()) 126 | self.dockLayout.addWidget(self.originGroupBox) 127 | self.dockLayout.addWidget(self.optionsGroupBox) 128 | self.dockLayout.addWidget(self.resGroupBox) 129 | self.dockLayout.addWidget(HorizontalLine()) 130 | self.dockLayout.addWidget(self.zoomWidget) 131 | self.dockLayout.addWidget(HorizontalLine()) 132 | self.dockLayout.addStretch() 133 | self.dockLayout.addWidget(self.applyButton) 134 | self.dockLayout.addWidget(HorizontalLine()) 135 | 136 | self.optionsWidget = QWidget() 137 | self.optionsWidget.setLayout(self.dockLayout) 138 | self.setWidget(self.optionsWidget) 139 | 140 | def _createOriginBox(self): 141 | 142 | # X Origin 143 | self.xOrBox = QDoubleSpinBox() 144 | self.xOrBox.setDecimals(9) 145 | self.xOrBox.setRange(-99999, 99999) 146 | xbox_connector = partial(self.main_window.editSingleOrigin, 147 | dimension=0) 148 | self.xOrBox.valueChanged.connect(xbox_connector) 149 | 150 | # Y Origin 151 | self.yOrBox = QDoubleSpinBox() 152 | self.yOrBox.setDecimals(9) 153 | self.yOrBox.setRange(-99999, 99999) 154 | ybox_connector = partial(self.main_window.editSingleOrigin, 155 | dimension=1) 156 | self.yOrBox.valueChanged.connect(ybox_connector) 157 | 158 | # Z Origin 159 | self.zOrBox = QDoubleSpinBox() 160 | self.zOrBox.setDecimals(9) 161 | self.zOrBox.setRange(-99999, 99999) 162 | zbox_connector = partial(self.main_window.editSingleOrigin, 163 | dimension=2) 164 | self.zOrBox.valueChanged.connect(zbox_connector) 165 | 166 | # Origin Form Layout 167 | self.orLayout = QFormLayout() 168 | self.orLayout.addRow('X:', self.xOrBox) 169 | self.orLayout.addRow('Y:', self.yOrBox) 170 | self.orLayout.addRow('Z:', self.zOrBox) 171 | self.orLayout.setLabelAlignment(QtCore.Qt.AlignLeft) 172 | self.orLayout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) 173 | 174 | # Origin Group Box 175 | self.originGroupBox = QGroupBox('Origin') 176 | self.originGroupBox.setLayout(self.orLayout) 177 | 178 | def _createOptionsBox(self): 179 | 180 | # Width 181 | self.widthBox = QDoubleSpinBox(self) 182 | self.widthBox.setRange(.1, 99999) 183 | self.widthBox.setDecimals(9) 184 | self.widthBox.valueChanged.connect(self.main_window.editWidth) 185 | 186 | # Height 187 | self.heightBox = QDoubleSpinBox(self) 188 | self.heightBox.setRange(.1, 99999) 189 | self.heightBox.setDecimals(9) 190 | self.heightBox.valueChanged.connect(self.main_window.editHeight) 191 | 192 | # ColorBy 193 | self.colorbyBox = QComboBox(self) 194 | self.colorbyBox.addItem("material") 195 | self.colorbyBox.addItem("cell") 196 | self.colorbyBox.addItem("temperature") 197 | self.colorbyBox.addItem("density") 198 | self.colorbyBox.currentTextChanged[str].connect( 199 | self.main_window.editColorBy) 200 | 201 | # Universe level (applies to cell coloring only) 202 | self.universeLevelBox = QComboBox(self) 203 | self.universeLevelBox.addItem('all') 204 | for i in range(self.model.max_universe_levels): 205 | self.universeLevelBox.addItem(str(i)) 206 | self.universeLevelBox.currentTextChanged[str].connect( 207 | self.main_window.editUniverseLevel) 208 | 209 | # Alpha 210 | self.domainAlphaBox = QDoubleSpinBox(self) 211 | self.domainAlphaBox.setValue(self.model.activeView.domainAlpha) 212 | self.domainAlphaBox.setSingleStep(0.05) 213 | self.domainAlphaBox.setDecimals(2) 214 | self.domainAlphaBox.setRange(0.0, 1.0) 215 | self.domainAlphaBox.valueChanged.connect( 216 | self.main_window.editPlotAlpha) 217 | 218 | # Visibility 219 | self.visibilityBox = QCheckBox(self) 220 | self.visibilityBox.stateChanged.connect( 221 | self.main_window.editPlotVisibility) 222 | 223 | # Outlines 224 | self.outlinesBox = QCheckBox(self) 225 | self.outlinesBox.stateChanged.connect(self.main_window.toggleOutlines) 226 | 227 | # Basis 228 | self.basisBox = QComboBox(self) 229 | self.basisBox.addItem("xy") 230 | self.basisBox.addItem("xz") 231 | self.basisBox.addItem("yz") 232 | self.basisBox.currentTextChanged.connect(self.main_window.editBasis) 233 | 234 | # Advanced Color Options 235 | self.colorOptionsButton = QPushButton('Color Options...') 236 | self.colorOptionsButton.setMinimumHeight( 237 | self.font_metric.height() * 1.6) 238 | self.colorOptionsButton.clicked.connect( 239 | self.main_window.showColorDialog) 240 | 241 | # Options Form Layout 242 | self.opLayout = QFormLayout() 243 | self.opLayout.addRow('Width:', self.widthBox) 244 | self.opLayout.addRow('Height:', self.heightBox) 245 | self.opLayout.addRow('Basis:', self.basisBox) 246 | self.opLayout.addRow('Color By:', self.colorbyBox) 247 | self.opLayout.addRow('Universe Level:', self.universeLevelBox) 248 | self.opLayout.addRow('Plot alpha:', self.domainAlphaBox) 249 | self.opLayout.addRow('Visible:', self.visibilityBox) 250 | self.opLayout.addRow('Outlines:', self.outlinesBox) 251 | self.opLayout.addRow(self.colorOptionsButton) 252 | self.opLayout.setLabelAlignment(QtCore.Qt.AlignLeft) 253 | self.opLayout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) 254 | 255 | # Options Group Box 256 | self.optionsGroupBox = QGroupBox('Options') 257 | self.optionsGroupBox.setLayout(self.opLayout) 258 | 259 | def _createResolutionBox(self): 260 | 261 | # Horizontal Resolution 262 | self.hResBox = QSpinBox(self) 263 | self.hResBox.setRange(1, 99999) 264 | self.hResBox.setSingleStep(25) 265 | self.hResBox.setSuffix(' px') 266 | self.hResBox.valueChanged.connect(self.main_window.editHRes) 267 | 268 | # Vertical Resolution 269 | self.vResLabel = QLabel('Pixel Height:') 270 | self.vResBox = QSpinBox(self) 271 | self.vResBox.setRange(1, 99999) 272 | self.vResBox.setSingleStep(25) 273 | self.vResBox.setSuffix(' px') 274 | self.vResBox.valueChanged.connect(self.main_window.editVRes) 275 | 276 | # Ratio checkbox 277 | self.ratioCheck = QCheckBox("Fixed Aspect Ratio", self) 278 | self.ratioCheck.stateChanged.connect(self.main_window.toggleAspectLock) 279 | 280 | # Resolution Form Layout 281 | self.resLayout = QFormLayout() 282 | self.resLayout.addRow(self.ratioCheck) 283 | self.resLayout.addRow('Pixel Width:', self.hResBox) 284 | self.resLayout.addRow(self.vResLabel, self.vResBox) 285 | self.resLayout.setLabelAlignment(QtCore.Qt.AlignLeft) 286 | self.resLayout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) 287 | 288 | # Resolution Group Box 289 | self.resGroupBox = QGroupBox("Resolution") 290 | self.resGroupBox.setLayout(self.resLayout) 291 | 292 | def updateDock(self): 293 | self.updateOrigin() 294 | self.updateWidth() 295 | self.updateHeight() 296 | self.updateColorBy() 297 | self.updateUniverseLevel() 298 | self.updatePlotAlpha() 299 | self.updatePlotVisibility() 300 | self.updateOutlines() 301 | self.updateBasis() 302 | self.updateAspectLock() 303 | self.updateHRes() 304 | self.updateVRes() 305 | 306 | def updateOrigin(self): 307 | self.xOrBox.setValue(self.model.activeView.origin[0]) 308 | self.yOrBox.setValue(self.model.activeView.origin[1]) 309 | self.zOrBox.setValue(self.model.activeView.origin[2]) 310 | 311 | def updateWidth(self): 312 | self.widthBox.setValue(self.model.activeView.width) 313 | 314 | def updateHeight(self): 315 | self.heightBox.setValue(self.model.activeView.height) 316 | 317 | def updateColorBy(self): 318 | self.colorbyBox.setCurrentText(self.model.activeView.colorby) 319 | if self.model.activeView.colorby != 'cell': 320 | self.universeLevelBox.setEnabled(False) 321 | else: 322 | self.universeLevelBox.setEnabled(True) 323 | 324 | def updateUniverseLevel(self): 325 | self.universeLevelBox.setCurrentIndex(self.model.activeView.level + 1) 326 | 327 | def updatePlotAlpha(self): 328 | self.domainAlphaBox.setValue(self.model.activeView.domainAlpha) 329 | 330 | def updatePlotVisibility(self): 331 | self.visibilityBox.setChecked(self.model.activeView.domainVisible) 332 | 333 | def updateOutlines(self): 334 | self.outlinesBox.setChecked(self.model.activeView.outlines) 335 | 336 | def updateBasis(self): 337 | self.basisBox.setCurrentText(self.model.activeView.basis) 338 | 339 | def updateAspectLock(self): 340 | aspect_lock = bool(self.model.activeView.aspectLock) 341 | self.ratioCheck.setChecked(aspect_lock) 342 | self.vResBox.setDisabled(aspect_lock) 343 | self.vResLabel.setDisabled(aspect_lock) 344 | 345 | def updateHRes(self): 346 | self.hResBox.setValue(self.model.activeView.h_res) 347 | 348 | def updateVRes(self): 349 | self.vResBox.setValue(self.model.activeView.v_res) 350 | 351 | def revertToCurrent(self): 352 | cv = self.model.currentView 353 | 354 | self.xOrBox.setValue(cv.origin[0]) 355 | self.yOrBox.setValue(cv.origin[1]) 356 | self.zOrBox.setValue(cv.origin[2]) 357 | 358 | self.widthBox.setValue(cv.width) 359 | self.heightBox.setValue(cv.height) 360 | 361 | def resizeEvent(self, event): 362 | self.main_window.resizeEvent(event) 363 | 364 | hideEvent = showEvent = moveEvent = resizeEvent 365 | 366 | 367 | class TallyDock(PlotterDock): 368 | 369 | def __init__(self, model, font_metric, parent=None): 370 | super().__init__(model, font_metric, parent) 371 | 372 | self.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) 373 | 374 | # Dock maps for tally information 375 | self.tally_map = {} 376 | self.filter_map = {} 377 | self.score_map = {} 378 | self.nuclide_map = {} 379 | 380 | # Tally selector 381 | self.tallySelectorLayout = QFormLayout() 382 | self.tallySelector = QComboBox(self) 383 | self.tallySelector.currentTextChanged[str].connect( 384 | self.main_window.editSelectedTally) 385 | self.tallySelectorLayout.addRow(self.tallySelector) 386 | self.tallySelectorLayout.setLabelAlignment(QtCore.Qt.AlignLeft) 387 | self.tallySelectorLayout.setFieldGrowthPolicy( 388 | QFormLayout.AllNonFixedFieldsGrow) 389 | 390 | # Add selector to its own box 391 | self.tallyGroupBox = QGroupBox('Selected Tally') 392 | self.tallyGroupBox.setLayout(self.tallySelectorLayout) 393 | 394 | # Create submit button 395 | self.applyButton = QPushButton("Apply Changes") 396 | self.applyButton.setMinimumHeight(self.font_metric.height() * 1.6) 397 | self.applyButton.clicked.connect(self.main_window.applyChanges) 398 | 399 | # Color options section 400 | self.tallyColorForm = ColorForm(self.model, self.main_window, 'tally') 401 | self.scoresGroupBox = Expander(title="Scores:") 402 | self.scoresListWidget = QListWidget() 403 | self.scoresListWidget.itemChanged.connect(self.updateScores) 404 | self.nuclidesListWidget = QListWidget() 405 | 406 | # Main layout 407 | self.dockLayout = QVBoxLayout() 408 | self.dockLayout.addWidget(QLabel("Tallies")) 409 | self.dockLayout.addWidget(HorizontalLine()) 410 | self.dockLayout.addWidget(self.tallyGroupBox) 411 | self.dockLayout.addStretch() 412 | self.dockLayout.addWidget(HorizontalLine()) 413 | self.dockLayout.addWidget(self.tallyColorForm) 414 | self.dockLayout.addWidget(HorizontalLine()) 415 | self.dockLayout.addWidget(self.applyButton) 416 | 417 | # Create widget for dock and apply main layout 418 | self.scroll = QScrollArea() 419 | self.scroll.setWidgetResizable(True) 420 | self.widget = QWidget() 421 | self.widget.setLayout(self.dockLayout) 422 | self.scroll.setWidget(self.widget) 423 | self.setWidget(self.scroll) 424 | 425 | def _createFilterTree(self, spatial_filters): 426 | av = self.model.activeView 427 | tally = self.model.statepoint.tallies[av.selectedTally] 428 | filters = tally.filters 429 | 430 | # create a tree for the filters 431 | self.treeLayout = QVBoxLayout() 432 | self.filterTree = QTreeWidget() 433 | self.treeLayout.addWidget(self.filterTree) 434 | self.treeExpander = Expander("Filters:", layout=self.treeLayout) 435 | self.treeExpander.expand() # start with filters expanded 436 | 437 | header = QTreeWidgetItem(["Filters"]) 438 | self.filterTree.setHeaderItem(header) 439 | header.setHidden(True) 440 | #self.filterTree.setItemHidden(header, True) 441 | self.filterTree.setColumnCount(1) 442 | 443 | self.filter_map = {} 444 | self.bin_map = {} 445 | 446 | for tally_filter in filters: 447 | filter_label = str(type(tally_filter)).split(".")[-1][:-2] 448 | filter_item = QTreeWidgetItem(self.filterTree, (filter_label,)) 449 | self.filter_map[tally_filter] = filter_item 450 | 451 | # make checkable 452 | if not spatial_filters: 453 | filter_item.setFlags(QtCore.Qt.ItemIsUserCheckable) 454 | filter_item.setToolTip( 455 | 0, "Only tallies with spatial filters are viewable.") 456 | else: 457 | filter_item.setFlags( 458 | filter_item.flags() | QtCore.Qt.ItemIsUserCheckable | 459 | QtCore.Qt.ItemIsAutoTristate) 460 | filter_item.setCheckState(0, QtCore.Qt.Unchecked) 461 | 462 | # all mesh bins are selected by default and not shown in the dock 463 | if isinstance(tally_filter, openmc.MeshFilter): 464 | filter_item.setCheckState(0, QtCore.Qt.Checked) 465 | filter_item.setFlags(QtCore.Qt.ItemIsUserCheckable) 466 | filter_item.setToolTip( 467 | 0, "All Mesh bins are selected automatically") 468 | continue 469 | 470 | def _bin_sort_val(bin): 471 | if isinstance(bin, Iterable): 472 | if all([isinstance(val, float) for val in bin]): 473 | return np.sum(bin) 474 | else: 475 | return tuple(bin) 476 | else: 477 | return bin 478 | 479 | if isinstance(tally_filter, openmc.EnergyFunctionFilter): 480 | bins = [0] 481 | else: 482 | bins = tally_filter.bins 483 | 484 | for bin in sorted(bins, key=_bin_sort_val): 485 | item = QTreeWidgetItem(filter_item, [str(bin),]) 486 | if not spatial_filters: 487 | item.setFlags(QtCore.Qt.ItemIsUserCheckable) 488 | item.setToolTip( 489 | 0, "Only tallies with spatial filters are viewable.") 490 | else: 491 | item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) 492 | item.setCheckState(0, QtCore.Qt.Unchecked) 493 | 494 | bin = bin if not isinstance(bin, Iterable) else tuple(bin) 495 | self.bin_map[tally_filter, bin] = item 496 | 497 | # start with all filters selected if spatial filters are present 498 | if spatial_filters: 499 | filter_item.setCheckState(0, QtCore.Qt.Checked) 500 | 501 | def selectFromModel(self): 502 | cv = self.model.currentView 503 | self.selectedTally(cv.selectedTally) 504 | 505 | def selectTally(self, tally_label=None): 506 | # using active view to populate tally options live 507 | av = self.model.activeView 508 | 509 | # reset form layout 510 | for i in reversed(range(self.tallySelectorLayout.count())): 511 | self.tallySelectorLayout.itemAt(i).widget().setParent(None) 512 | 513 | # always re-add the tally selector to the layout 514 | self.tallySelectorLayout.addRow(self.tallySelector) 515 | self.tallySelectorLayout.addRow(HorizontalLine()) 516 | 517 | if tally_label is None or tally_label == "None" or tally_label == "": 518 | av.selectedTally = None 519 | self.score_map = None 520 | self.nuclide_map = None 521 | self.filter_map = None 522 | av.tallyValue = "Mean" 523 | else: 524 | # get the tally 525 | tally = self.model.statepoint.tallies[av.selectedTally] 526 | 527 | # populate filters 528 | filter_types = {type(f) for f in tally.filters} 529 | spatial_filters = bool(filter_types.intersection(_SPATIAL_FILTERS)) 530 | 531 | if not spatial_filters: 532 | self.filter_description = QLabel("(No Spatial Filters)") 533 | self.tallySelectorLayout.addRow(self.filter_description) 534 | 535 | self._createFilterTree(spatial_filters) 536 | 537 | self.tallySelectorLayout.addRow(self.treeExpander) 538 | self.tallySelectorLayout.addRow(HorizontalLine()) 539 | 540 | # value selection 541 | self.tallySelectorLayout.addRow(QLabel("Value:")) 542 | self.valueBox = QComboBox(self) 543 | self.values = tuple(_TALLY_VALUES.keys()) 544 | for value in self.values: 545 | self.valueBox.addItem(value) 546 | self.tallySelectorLayout.addRow(self.valueBox) 547 | self.valueBox.currentTextChanged[str].connect( 548 | self.main_window.editTallyValue) 549 | self.updateTallyValue() 550 | 551 | if not spatial_filters: 552 | self.valueBox.setEnabled(False) 553 | self.valueBox.setToolTip( 554 | "Only tallies with spatial filters are viewable.") 555 | 556 | # scores 557 | self.score_map = {} 558 | self.scoresListWidget.clear() 559 | 560 | for score in tally.scores: 561 | ql = QListWidgetItem() 562 | ql.setText(score) 563 | ql.setCheckState(QtCore.Qt.Unchecked) 564 | if not spatial_filters: 565 | ql.setFlags(QtCore.Qt.ItemIsUserCheckable) 566 | else: 567 | ql.setFlags(ql.flags() | QtCore.Qt.ItemIsUserCheckable) 568 | ql.setFlags(ql.flags() & ~QtCore.Qt.ItemIsSelectable) 569 | self.score_map[score] = ql 570 | self.scoresListWidget.addItem(ql) 571 | 572 | # select the first score item by default 573 | for item in self.score_map.values(): 574 | item.setCheckState(QtCore.Qt.Checked) 575 | break 576 | self.updateScores() 577 | 578 | self.scoresGroupBoxLayout = QVBoxLayout() 579 | self.scoresGroupBoxLayout.addWidget(self.scoresListWidget) 580 | self.scoresGroupBox = Expander( 581 | "Scores:", layout=self.scoresGroupBoxLayout) 582 | self.tallySelectorLayout.addRow(self.scoresGroupBox) 583 | 584 | # nuclides 585 | self.nuclide_map = {} 586 | self.nuclide_map.clear() 587 | self.nuclidesListWidget.clear() 588 | 589 | sorted_nuclides = sorted(tally.nuclides) 590 | # always put total at the top 591 | if 'total' in sorted_nuclides: 592 | idx = sorted_nuclides.index('total') 593 | sorted_nuclides.insert(0, sorted_nuclides.pop(idx)) 594 | 595 | for nuclide in sorted_nuclides: 596 | ql = QListWidgetItem() 597 | ql.setText(nuclide.capitalize()) 598 | ql.setCheckState(QtCore.Qt.Unchecked) 599 | if not spatial_filters: 600 | ql.setFlags(QtCore.Qt.ItemIsUserCheckable) 601 | else: 602 | ql.setFlags(ql.flags() | QtCore.Qt.ItemIsUserCheckable) 603 | ql.setFlags(ql.flags() & ~QtCore.Qt.ItemIsSelectable) 604 | self.nuclide_map[nuclide] = ql 605 | self.nuclidesListWidget.addItem(ql) 606 | 607 | # select the first nuclide item by default 608 | for item in self.nuclide_map.values(): 609 | item.setCheckState(QtCore.Qt.Checked) 610 | break 611 | self.updateNuclides() 612 | 613 | self.nuclidesGroupBoxLayout = QVBoxLayout() 614 | self.nuclidesGroupBoxLayout.addWidget(self.nuclidesListWidget) 615 | self.nuclidesGroupBox = Expander( 616 | "Nuclides:", layout=self.nuclidesGroupBoxLayout) 617 | self.tallySelectorLayout.addRow(self.nuclidesGroupBox) 618 | 619 | def updateMinMax(self): 620 | self.tallyColorForm.updateMinMax() 621 | 622 | def updateTallyValue(self): 623 | cv = self.model.currentView 624 | idx = self.valueBox.findText(cv.tallyValue) 625 | self.valueBox.setCurrentIndex(idx) 626 | 627 | def updateSelectedTally(self): 628 | cv = self.model.currentView 629 | idx = 0 630 | if cv.selectedTally: 631 | idx = self.tallySelector.findData(cv.selectedTally) 632 | self.tallySelector.setCurrentIndex(idx) 633 | 634 | def updateFilters(self): 635 | # if the filters header is checked, uncheck all bins and return 636 | applied_filters = defaultdict(tuple) 637 | for f, f_item in self.filter_map.items(): 638 | if type(f) == openmc.MeshFilter: 639 | continue 640 | 641 | filter_checked = f_item.checkState(0) 642 | if filter_checked == QtCore.Qt.Unchecked: 643 | for i in range(f_item.childCount()): 644 | bin_item = f_item.child(i) 645 | bin_item.setCheckState(0, QtCore.Qt.Unchecked) 646 | applied_filters[f] = tuple() 647 | elif filter_checked == QtCore.Qt.Checked: 648 | if isinstance(f, openmc.EnergyFunctionFilter): 649 | applied_filters[f] = (0,) 650 | else: 651 | for i in range(f_item.childCount()): 652 | bin_item = f_item.child(i) 653 | bin_item.setCheckState(0, QtCore.Qt.Checked) 654 | applied_filters[f] = tuple(range(f_item.childCount())) 655 | elif filter_checked == QtCore.Qt.PartiallyChecked: 656 | selected_bins = [] 657 | if isinstance(f, openmc.EnergyFunctionFilter): 658 | bins = [0] 659 | else: 660 | bins = f.bins 661 | for idx, b in enumerate(bins): 662 | b = b if not isinstance(b, Iterable) else tuple(b) 663 | bin_checked = self.bin_map[(f, b)].checkState(0) 664 | if bin_checked == QtCore.Qt.Checked: 665 | selected_bins.append(idx) 666 | applied_filters[f] = tuple(selected_bins) 667 | 668 | self.model.appliedFilters = applied_filters 669 | 670 | def updateScores(self): 671 | applied_scores = [] 672 | for score, score_box in self.score_map.items(): 673 | if score_box.checkState() == QtCore.Qt.CheckState.Checked: 674 | applied_scores.append(score) 675 | self.model.appliedScores = tuple(applied_scores) 676 | 677 | with QtCore.QSignalBlocker(self.scoresListWidget): 678 | if not applied_scores: 679 | # if no scores are selected, enable all scores again 680 | for score, score_box in self.score_map.items(): 681 | score_box.setFlags(QtCore.Qt.ItemIsUserCheckable | 682 | QtCore.Qt.ItemIsEnabled | 683 | QtCore.Qt.ItemIsSelectable) 684 | else: 685 | # get units of applied scores 686 | selected_units = _SCORE_UNITS.get( 687 | applied_scores[0], _REACTION_UNITS) 688 | # disable scores with incompatible units 689 | for score, score_box in self.score_map.items(): 690 | sunits = _SCORE_UNITS.get(score, _REACTION_UNITS) 691 | if sunits != selected_units: 692 | score_box.setFlags(QtCore.Qt.ItemIsUserCheckable) 693 | score_box.setToolTip( 694 | "Score is incompatible with currently selected scores") 695 | else: 696 | score_box.setFlags(score_box.flags() | 697 | QtCore.Qt.ItemIsUserCheckable) 698 | score_box.setFlags(score_box.flags() & 699 | ~QtCore.Qt.ItemIsSelectable) 700 | 701 | def updateNuclides(self): 702 | applied_nuclides = [] 703 | for nuclide, nuclide_box in self.nuclide_map.items(): 704 | if nuclide_box.checkState() == QtCore.Qt.CheckState.Checked: 705 | applied_nuclides.append(nuclide) 706 | self.model.appliedNuclides = tuple(applied_nuclides) 707 | 708 | if 'total' in applied_nuclides: 709 | self.model.appliedNuclides = ('total',) 710 | for nuclide, nuclide_box in self.nuclide_map.items(): 711 | if nuclide != 'total': 712 | nuclide_box.setFlags(QtCore.Qt.ItemIsUserCheckable) 713 | nuclide_box.setToolTip( 714 | "De-select 'total' to enable other nuclides") 715 | elif not applied_nuclides: 716 | # if no nuclides are selected, enable all nuclides again 717 | for nuclide, nuclide_box in self.nuclide_map.items(): 718 | empty_item = QListWidgetItem() 719 | nuclide_box.setFlags(empty_item.flags() | 720 | QtCore.Qt.ItemIsUserCheckable) 721 | nuclide_box.setFlags(empty_item.flags() & 722 | ~QtCore.Qt.ItemIsSelectable) 723 | 724 | def updateModel(self): 725 | self.updateFilters() 726 | self.updateScores() 727 | self.updateNuclides() 728 | 729 | def update(self): 730 | 731 | # update the color form 732 | self.tallyColorForm.update() 733 | 734 | if self.model.statepoint: 735 | self.tallySelector.clear() 736 | self.tallySelector.setEnabled(True) 737 | self.tallySelector.addItem("None") 738 | for idx, tally in enumerate(self.model.statepoint.tallies.values()): 739 | if tally.name == "": 740 | self.tallySelector.addItem( 741 | f'Tally {tally.id}', userData=tally.id) 742 | else: 743 | self.tallySelector.addItem( 744 | f'Tally {tally.id} "{tally.name}"', userData=tally.id) 745 | self.tally_map[idx] = tally 746 | self.updateSelectedTally() 747 | self.updateMinMax() 748 | else: 749 | self.tallySelector.clear() 750 | self.tallySelector.setDisabled(True) 751 | 752 | 753 | class ColorForm(QWidget): 754 | """ 755 | Class for handling a field with a colormap, alpha, and visibility 756 | 757 | Attributes 758 | ---------- 759 | 760 | model : PlotModel 761 | The model instance used when updating information on the form. 762 | colormapBox : QComboBox 763 | Holds the string of the matplotlib colorbar being used 764 | visibilityBox : QCheckBox 765 | Indicator for whether or not the field should be visible 766 | alphaBox : QDoubleSpinBox 767 | Holds the alpha value for the displayed field data 768 | colormapBox : QComboBox 769 | Selector for colormap 770 | dataIndicatorCheckBox : QCheckBox 771 | Inidcates whether or not the data indicator will appear on the colorbar 772 | userMinMaxBox : QCheckBox 773 | Indicates whether or not the user defined values in the min and max 774 | will be used to set the bounds of the colorbar. 775 | maxBox : ScientificDoubleSpinBox 776 | Max value of the colorbar. If the userMinMaxBox is checked, this will be 777 | the user's input. If the userMinMaxBox is not checked, this box will 778 | hold the max value of the visible data. 779 | minBox : ScientificDoubleSpinBox 780 | Min value of the colorbar. If the userMinMaxBox is checked, this will be 781 | the user's input. If the userMinMaxBox is not checked, this box will 782 | hold the max value of the visible data. 783 | scaleBox : QCheckBox 784 | Indicates whether or not the data is displayed on a log or linear 785 | scale 786 | maskZeroBox : QCheckBox 787 | Indicates whether or not values equal to zero are displayed 788 | clipDataBox : QCheckBox 789 | Indicates whether or not values outside the min/max are displayed 790 | contoursBox : QCheckBox 791 | Inidicates whether or not data is displayed as contours 792 | contourLevelsLine : QLineEdit 793 | Controls the contours of the data. If this line contains a single 794 | integer, that number of levels is used to display the data. If a 795 | comma-separated set of values is entered, those values will be used as 796 | levels in the contour plot. 797 | """ 798 | 799 | def __init__(self, model, main_window, field, colormaps=None): 800 | super().__init__() 801 | 802 | self.model = model 803 | self.main_window = main_window 804 | self.field = field 805 | 806 | self.layout = QFormLayout() 807 | 808 | # Visibility check box 809 | self.visibilityBox = QCheckBox() 810 | visible_connector = partial(main_window.toggleTallyVisibility) 811 | self.visibilityBox.stateChanged.connect(visible_connector) 812 | 813 | # Alpha value 814 | self.alphaBox = QDoubleSpinBox() 815 | self.alphaBox.setDecimals(2) 816 | self.alphaBox.setRange(0, 1) 817 | self.alphaBox.setSingleStep(0.05) 818 | alpha_connector = partial(main_window.editTallyAlpha) 819 | self.alphaBox.valueChanged.connect(alpha_connector) 820 | 821 | # Color map selector 822 | self.colormapBox = QComboBox() 823 | if colormaps is None: 824 | colormaps = sorted( 825 | m for m in plt.colormaps() if not m.endswith("_r")) 826 | for colormap in colormaps: 827 | self.colormapBox.addItem(colormap) 828 | cmap_connector = partial(main_window.editTallyDataColormap) 829 | self.colormapBox.currentTextChanged[str].connect(cmap_connector) 830 | 831 | # Color map reverse 832 | self.reverseCmapBox = QCheckBox() 833 | reverse_connector = partial(main_window.toggleReverseCmap) 834 | self.reverseCmapBox.stateChanged.connect(reverse_connector) 835 | 836 | # Data indicator line check box 837 | self.dataIndicatorCheckBox = QCheckBox() 838 | data_indicator_connector = partial( 839 | main_window.toggleTallyDataIndicator) 840 | self.dataIndicatorCheckBox.stateChanged.connect( 841 | data_indicator_connector) 842 | 843 | # User specified min/max check box 844 | self.userMinMaxBox = QCheckBox() 845 | minmax_connector = partial(main_window.toggleTallyDataUserMinMax) 846 | self.userMinMaxBox.stateChanged.connect(minmax_connector) 847 | 848 | # Data min spin box 849 | self.minBox = ScientificDoubleSpinBox() 850 | self.minBox.setMinimum(0.0) 851 | min_connector = partial(main_window.editTallyDataMin) 852 | self.minBox.valueChanged.connect(min_connector) 853 | 854 | # Data max spin box 855 | self.maxBox = ScientificDoubleSpinBox() 856 | self.maxBox.setMinimum(0.0) 857 | max_connector = partial(main_window.editTallyDataMax) 858 | self.maxBox.valueChanged.connect(max_connector) 859 | 860 | # Linear/Log scaling check box 861 | self.scaleBox = QCheckBox() 862 | scale_connector = partial(main_window.toggleTallyLogScale) 863 | self.scaleBox.stateChanged.connect(scale_connector) 864 | 865 | # Masking of zero values check box 866 | self.maskZeroBox = QCheckBox() 867 | zero_connector = partial(main_window.toggleTallyMaskZero) 868 | self.maskZeroBox.stateChanged.connect(zero_connector) 869 | 870 | # Volume normalization check box 871 | self.volumeNormBox = QCheckBox() 872 | volume_connector = partial(main_window.toggleTallyVolumeNorm) 873 | self.volumeNormBox.stateChanged.connect(volume_connector) 874 | 875 | # Clip data to min/max check box 876 | self.clipDataBox = QCheckBox() 877 | clip_connector = partial(main_window.toggleTallyDataClip) 878 | self.clipDataBox.stateChanged.connect(clip_connector) 879 | 880 | # Display data as contour plot check box 881 | self.contoursBox = QCheckBox() 882 | self.contoursBox.stateChanged.connect(main_window.toggleTallyContours) 883 | self.contourLevelsLine = QLineEdit() 884 | self.contourLevelsLine.textChanged.connect( 885 | main_window.editTallyContourLevels) 886 | 887 | # Organize widgets on layout 888 | self.layout.addRow("Visible:", self.visibilityBox) 889 | self.layout.addRow("Alpha: ", self.alphaBox) 890 | self.layout.addRow("Colormap: ", self.colormapBox) 891 | self.layout.addRow("Reverse colormap: ", self.reverseCmapBox) 892 | self.layout.addRow("Data Indicator: ", self.dataIndicatorCheckBox) 893 | self.layout.addRow("Custom Min/Max: ", self.userMinMaxBox) 894 | self.layout.addRow("Min: ", self.minBox) 895 | self.layout.addRow("Max: ", self.maxBox) 896 | self.layout.addRow("Log Scale: ", self.scaleBox) 897 | self.layout.addRow("Clip Data: ", self.clipDataBox) 898 | self.layout.addRow("Mask Zeros: ", self.maskZeroBox) 899 | self.layout.addRow("Volume normalize: ", self.volumeNormBox) 900 | self.layout.addRow("Contours: ", self.contoursBox) 901 | self.layout.addRow("Contour Levels:", self.contourLevelsLine) 902 | self.setLayout(self.layout) 903 | 904 | def updateTallyContours(self): 905 | cv = self.model.currentView 906 | self.contoursBox.setChecked(cv.tallyContours) 907 | self.contourLevelsLine.setText(cv.tallyContourLevels) 908 | 909 | def updateDataIndicator(self): 910 | cv = self.model.currentView 911 | self.dataIndicatorCheckBox.setChecked(cv.tallyDataIndicator) 912 | 913 | def setMinMaxEnabled(self, enable): 914 | enable = bool(enable) 915 | self.minBox.setEnabled(enable) 916 | self.maxBox.setEnabled(enable) 917 | 918 | def updateMinMax(self): 919 | cv = self.model.currentView 920 | self.minBox.setValue(cv.tallyDataMin) 921 | self.maxBox.setValue(cv.tallyDataMax) 922 | self.setMinMaxEnabled(cv.tallyDataUserMinMax) 923 | 924 | def updateTallyVisibility(self): 925 | cv = self.model.currentView 926 | self.visibilityBox.setChecked(cv.tallyDataVisible) 927 | 928 | def updateMaskZeros(self): 929 | cv = self.model.currentView 930 | self.maskZeroBox.setChecked(cv.tallyMaskZeroValues) 931 | 932 | def updateVolumeNorm(self): 933 | cv = self.model.currentView 934 | self.volumeNormBox.setChecked(cv.tallyVolumeNorm) 935 | 936 | def updateDataClip(self): 937 | cv = self.model.currentView 938 | self.clipDataBox.setChecked(cv.clipTallyData) 939 | 940 | def update(self): 941 | cv = self.model.currentView 942 | 943 | # set colormap value in selector 944 | cmap = cv.tallyDataColormap 945 | idx = self.colormapBox.findText(cmap, QtCore.Qt.MatchFixedString) 946 | self.colormapBox.setCurrentIndex(idx) 947 | 948 | self.alphaBox.setValue(cv.tallyDataAlpha) 949 | self.visibilityBox.setChecked(cv.tallyDataVisible) 950 | self.userMinMaxBox.setChecked(cv.tallyDataUserMinMax) 951 | self.scaleBox.setChecked(cv.tallyDataLogScale) 952 | 953 | self.updateMinMax() 954 | self.updateMaskZeros() 955 | self.updateVolumeNorm() 956 | self.updateDataClip() 957 | self.updateDataIndicator() 958 | self.updateTallyContours() 959 | -------------------------------------------------------------------------------- /openmc_plotter/main_window.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from functools import partial 3 | from pathlib import Path 4 | import pickle 5 | from threading import Thread 6 | 7 | from PySide6 import QtCore, QtGui 8 | from PySide6.QtGui import QKeyEvent, QAction 9 | from PySide6.QtWidgets import (QApplication, QLabel, QSizePolicy, QMainWindow, 10 | QScrollArea, QMessageBox, QFileDialog, 11 | QColorDialog, QInputDialog, QWidget, 12 | QGestureEvent) 13 | 14 | import openmc 15 | import openmc.lib 16 | 17 | try: 18 | import vtk 19 | _HAVE_VTK = True 20 | except ImportError: 21 | _HAVE_VTK = False 22 | 23 | from .plotmodel import PlotModel, DomainTableModel, hash_model 24 | from .plotgui import PlotImage, ColorDialog 25 | from .docks import DomainDock, TallyDock, MeshAnnotationDock 26 | from .overlays import ShortcutsOverlay 27 | from .tools import ExportDataDialog, SourceSitesDialog 28 | 29 | 30 | def _openmcReload(threads=None, model_path='.'): 31 | # reset OpenMC memory, instances 32 | openmc.lib.reset() 33 | openmc.lib.finalize() 34 | # initialize geometry (for volume calculation) 35 | openmc.lib.settings.output_summary = False 36 | args = ["-c"] 37 | if threads is not None: 38 | args += ["-s", str(threads)] 39 | args.append(str(model_path)) 40 | openmc.lib.init(args) 41 | openmc.lib.settings.verbosity = 1 42 | 43 | 44 | class MainWindow(QMainWindow): 45 | def __init__(self, 46 | font=QtGui.QFontMetrics(QtGui.QFont()), 47 | screen_size=QtCore.QSize(), 48 | model_path='.', 49 | threads=None, 50 | resolution=None): 51 | super().__init__() 52 | 53 | self.screen = screen_size 54 | self.font_metric = font 55 | self.setWindowTitle('OpenMC Plot Explorer') 56 | self.model_path = Path(model_path) 57 | self.threads = threads 58 | self.default_res = resolution 59 | 60 | def loadGui(self, use_settings_pkl=True): 61 | 62 | self.pixmap = None 63 | self.zoom = 100 64 | 65 | self.loadModel(use_settings_pkl=use_settings_pkl) 66 | 67 | # Create viewing area 68 | self.frame = QScrollArea(self) 69 | cw = QWidget() 70 | self.frame.setCornerWidget(cw) 71 | self.frame.setAlignment(QtCore.Qt.AlignCenter) 72 | self.frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 73 | self.setCentralWidget(self.frame) 74 | 75 | # connect pinch gesture (OSX) 76 | self.grabGesture(QtCore.Qt.PinchGesture) 77 | 78 | # Create plot image 79 | self.plotIm = PlotImage(self.model, self.frame, self) 80 | self.plotIm.frozen = True 81 | self.frame.setWidget(self.plotIm) 82 | 83 | # Dock 84 | self.dock = DomainDock(self.model, self.font_metric, self) 85 | self.dock.setObjectName("Domain Options Dock") 86 | self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.dock) 87 | 88 | # Tally Dock 89 | self.tallyDock = TallyDock(self.model, self.font_metric, self) 90 | self.tallyDock.update() 91 | self.tallyDock.setObjectName("Tally Options Dock") 92 | self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.tallyDock) 93 | 94 | # Mesh Annotation Dock 95 | self.meshAnnotationDock = MeshAnnotationDock(self.model, self.font_metric, self) 96 | self.meshAnnotationDock.update() 97 | self.meshAnnotationDock.setObjectName("Mesh Annotation Dock") 98 | self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.meshAnnotationDock) 99 | 100 | # Color DialogtallyDock 101 | self.colorDialog = ColorDialog(self.model, self.font_metric, self) 102 | self.colorDialog.hide() 103 | 104 | # Tools 105 | self.exportDataDialog = ExportDataDialog(self.model, self.font_metric, self) 106 | self.sourceSitesDialog = SourceSitesDialog(self.model, self.font_metric, self) 107 | 108 | # Keyboard overlay 109 | self.shortcutOverlay = ShortcutsOverlay(self) 110 | self.shortcutOverlay.hide() 111 | 112 | # Restore Window Settings 113 | self.restoreWindowSettings() 114 | 115 | # Create menubar 116 | self.createMenuBar() 117 | self.updateEditMenu() 118 | 119 | # Status Bar 120 | self.coord_label = QLabel() 121 | self.statusBar().addPermanentWidget(self.coord_label) 122 | self.coord_label.hide() 123 | 124 | # Load Plot 125 | self.statusBar().showMessage('Generating Plot...') 126 | self.dock.updateDock() 127 | self.tallyDock.update() 128 | self.colorDialog.updateDialogValues() 129 | self.statusBar().showMessage('') 130 | 131 | # Timer allows GUI to render before plot finishes loading 132 | QtCore.QTimer.singleShot(0, self.showCurrentView) 133 | 134 | self.plotIm.frozen = False 135 | 136 | def event(self, event): 137 | # use pinch event to update zoom 138 | if isinstance(event, QGestureEvent): 139 | pinch = event.gesture(QtCore.Qt.PinchGesture) 140 | self.editZoom(self.zoom * pinch.scaleFactor()) 141 | if isinstance(event, QKeyEvent) and hasattr(self, "shortcutOverlay"): 142 | self.shortcutOverlay.event(event) 143 | return super().event(event) 144 | 145 | def show(self): 146 | super().show() 147 | self.plotIm._resize() 148 | 149 | def toggleShortcuts(self): 150 | if self.shortcutOverlay.isVisible(): 151 | self.shortcutOverlay.close() 152 | else: 153 | self.shortcutOverlay.move(0, 0) 154 | self.shortcutOverlay.resize(self.width(), self.height()) 155 | self.shortcutOverlay.show() 156 | 157 | # Create and update menus: 158 | def createMenuBar(self): 159 | self.mainMenu = self.menuBar() 160 | 161 | # File Menu 162 | self.reloadModelAction = QAction("&Reload model...", self) 163 | self.reloadModelAction.setShortcut("Ctrl+Shift+R") 164 | self.reloadModelAction.setToolTip("Reload current model") 165 | self.reloadModelAction.setStatusTip("Reload current model") 166 | reload_connector = partial(self.loadModel, reload=True) 167 | self.reloadModelAction.triggered.connect(reload_connector) 168 | 169 | self.saveImageAction = QAction("&Save Image As...", self) 170 | self.saveImageAction.setShortcut("Ctrl+Shift+S") 171 | self.saveImageAction.setToolTip('Save plot image') 172 | self.saveImageAction.setStatusTip('Save plot image') 173 | save_image_connector = partial(self.saveImage, filename=None) 174 | self.saveImageAction.triggered.connect(save_image_connector) 175 | 176 | self.saveViewAction = QAction("Save &View...", self) 177 | self.saveViewAction.setShortcut(QtGui.QKeySequence.Save) 178 | self.saveViewAction.setStatusTip('Save current view settings') 179 | self.saveViewAction.triggered.connect(self.saveView) 180 | 181 | self.openAction = QAction("&Open View...", self) 182 | self.openAction.setShortcut(QtGui.QKeySequence.Open) 183 | self.openAction.setToolTip('Open saved view settings') 184 | self.openAction.setStatusTip('Open saved view settings') 185 | self.openAction.triggered.connect(self.openView) 186 | 187 | self.quitAction = QAction("&Quit", self) 188 | self.quitAction.setShortcut(QtGui.QKeySequence.Quit) 189 | self.quitAction.setToolTip('Quit OpenMC Plot Explorer') 190 | self.quitAction.setStatusTip('Quit OpenMC Plot Explorer') 191 | self.quitAction.triggered.connect(self.close) 192 | 193 | self.exportDataAction = QAction('E&xport...', self) 194 | self.exportDataAction.setToolTip('Export model and tally data VTK') 195 | self.setStatusTip('Export current model and tally data to VTK') 196 | self.exportDataAction.triggered.connect(self.exportTallyData) 197 | if not _HAVE_VTK: 198 | self.exportDataAction.setEnabled(False) 199 | self.exportDataAction.setToolTip("Disabled: VTK Python module is not installed") 200 | 201 | self.fileMenu = self.mainMenu.addMenu('&File') 202 | self.fileMenu.addAction(self.reloadModelAction) 203 | self.fileMenu.addAction(self.saveImageAction) 204 | self.fileMenu.addAction(self.exportDataAction) 205 | self.fileMenu.addSeparator() 206 | self.fileMenu.addAction(self.saveViewAction) 207 | self.fileMenu.addAction(self.openAction) 208 | self.fileMenu.addSeparator() 209 | self.fileMenu.addAction(self.quitAction) 210 | 211 | # Data Menu 212 | self.openStatePointAction = QAction("&Open statepoint...", self) 213 | self.openStatePointAction.setToolTip('Open statepoint file') 214 | self.openStatePointAction.triggered.connect(self.openStatePoint) 215 | 216 | self.sourceSitesAction = QAction('&Sample source sites...', self) 217 | self.sourceSitesAction.setToolTip('Add source sites to plot') 218 | self.setStatusTip('Sample and add source sites to the plot') 219 | self.sourceSitesAction.triggered.connect(self.plotSourceSites) 220 | 221 | self.importPropertiesAction = QAction("&Import properties...", self) 222 | self.importPropertiesAction.setToolTip("Import properties") 223 | self.importPropertiesAction.triggered.connect(self.importProperties) 224 | 225 | self.dataMenu = self.mainMenu.addMenu('D&ata') 226 | self.dataMenu.addAction(self.openStatePointAction) 227 | self.dataMenu.addAction(self.sourceSitesAction) 228 | self.dataMenu.addAction(self.importPropertiesAction) 229 | self.updateDataMenu() 230 | 231 | # Edit Menu 232 | self.applyAction = QAction("&Apply Changes", self) 233 | self.applyAction.setShortcut("Ctrl+Return") 234 | self.applyAction.setToolTip('Generate new view with changes applied') 235 | self.applyAction.setStatusTip('Generate new view with changes applied') 236 | self.applyAction.triggered.connect(self.applyChanges) 237 | 238 | self.undoAction = QAction('&Undo', self) 239 | self.undoAction.setShortcut(QtGui.QKeySequence.Undo) 240 | self.undoAction.setToolTip('Undo') 241 | self.undoAction.setStatusTip('Undo last plot view change') 242 | self.undoAction.setDisabled(True) 243 | self.undoAction.triggered.connect(self.undo) 244 | 245 | self.redoAction = QAction('&Redo', self) 246 | self.redoAction.setDisabled(True) 247 | self.redoAction.setToolTip('Redo') 248 | self.redoAction.setStatusTip('Redo last plot view change') 249 | self.redoAction.setShortcut(QtGui.QKeySequence.Redo) 250 | self.redoAction.triggered.connect(self.redo) 251 | 252 | self.restoreAction = QAction("&Restore Default Plot", self) 253 | self.restoreAction.setShortcut("Ctrl+R") 254 | self.restoreAction.setToolTip('Restore to default plot view') 255 | self.restoreAction.setStatusTip('Restore to default plot view') 256 | self.restoreAction.triggered.connect(self.restoreDefault) 257 | 258 | self.editMenu = self.mainMenu.addMenu('&Edit') 259 | self.editMenu.addAction(self.applyAction) 260 | self.editMenu.addSeparator() 261 | self.editMenu.addAction(self.undoAction) 262 | self.editMenu.addAction(self.redoAction) 263 | self.editMenu.addSeparator() 264 | self.editMenu.addAction(self.restoreAction) 265 | self.editMenu.addSeparator() 266 | self.editMenu.aboutToShow.connect(self.updateEditMenu) 267 | 268 | # Edit -> Basis Menu 269 | self.xyAction = QAction('&xy ', self) 270 | self.xyAction.setCheckable(True) 271 | self.xyAction.setShortcut('Alt+X') 272 | self.xyAction.setToolTip('Change to xy basis') 273 | self.xyAction.setStatusTip('Change to xy basis') 274 | xy_connector = partial(self.editBasis, 'xy', apply=True) 275 | self.xyAction.triggered.connect(xy_connector) 276 | 277 | self.xzAction = QAction('x&z ', self) 278 | self.xzAction.setCheckable(True) 279 | self.xzAction.setShortcut('Alt+Z') 280 | self.xzAction.setToolTip('Change to xz basis') 281 | self.xzAction.setStatusTip('Change to xz basis') 282 | xz_connector = partial(self.editBasis, 'xz', apply=True) 283 | self.xzAction.triggered.connect(xz_connector) 284 | 285 | self.yzAction = QAction('&yz ', self) 286 | self.yzAction.setCheckable(True) 287 | self.yzAction.setShortcut('Alt+Y') 288 | self.yzAction.setToolTip('Change to yz basis') 289 | self.yzAction.setStatusTip('Change to yz basis') 290 | yz_connector = partial(self.editBasis, 'yz', apply=True) 291 | self.yzAction.triggered.connect(yz_connector) 292 | 293 | self.basisMenu = self.editMenu.addMenu('&Basis') 294 | self.basisMenu.addAction(self.xyAction) 295 | self.basisMenu.addAction(self.xzAction) 296 | self.basisMenu.addAction(self.yzAction) 297 | self.basisMenu.aboutToShow.connect(self.updateBasisMenu) 298 | 299 | # Edit -> Color By Menu 300 | self.cellAction = QAction('&Cell', self) 301 | self.cellAction.setCheckable(True) 302 | self.cellAction.setShortcut('Alt+C') 303 | self.cellAction.setToolTip('Color by cell') 304 | self.cellAction.setStatusTip('Color plot by cell') 305 | cell_connector = partial(self.editColorBy, 'cell', apply=True) 306 | self.cellAction.triggered.connect(cell_connector) 307 | 308 | self.materialAction = QAction('&Material', self) 309 | self.materialAction.setCheckable(True) 310 | self.materialAction.setShortcut('Alt+M') 311 | self.materialAction.setToolTip('Color by material') 312 | self.materialAction.setStatusTip('Color plot by material') 313 | material_connector = partial(self.editColorBy, 'material', apply=True) 314 | self.materialAction.triggered.connect(material_connector) 315 | 316 | self.temperatureAction = QAction('&Temperature', self) 317 | self.temperatureAction.setCheckable(True) 318 | self.temperatureAction.setShortcut('Alt+T') 319 | self.temperatureAction.setToolTip('Color by temperature') 320 | self.temperatureAction.setStatusTip('Color plot by temperature') 321 | temp_connector = partial(self.editColorBy, 'temperature', apply=True) 322 | self.temperatureAction.triggered.connect(temp_connector) 323 | 324 | self.densityAction = QAction('&Density', self) 325 | self.densityAction.setCheckable(True) 326 | self.densityAction.setShortcut('Alt+D') 327 | self.densityAction.setToolTip('Color by density') 328 | self.densityAction.setStatusTip('Color plot by density') 329 | density_connector = partial(self.editColorBy, 'density', apply=True) 330 | self.densityAction.triggered.connect(density_connector) 331 | 332 | self.colorbyMenu = self.editMenu.addMenu('&Color By') 333 | self.colorbyMenu.addAction(self.cellAction) 334 | self.colorbyMenu.addAction(self.materialAction) 335 | self.colorbyMenu.addAction(self.temperatureAction) 336 | self.colorbyMenu.addAction(self.densityAction) 337 | 338 | self.colorbyMenu.aboutToShow.connect(self.updateColorbyMenu) 339 | 340 | self.editMenu.addSeparator() 341 | 342 | # Edit -> Other Options 343 | self.maskingAction = QAction('Enable &Masking', self) 344 | self.maskingAction.setShortcut('Ctrl+M') 345 | self.maskingAction.setCheckable(True) 346 | self.maskingAction.setToolTip('Toggle masking') 347 | self.maskingAction.setStatusTip('Toggle whether masking is enabled') 348 | masking_connector = partial(self.toggleMasking, apply=True) 349 | self.maskingAction.toggled.connect(masking_connector) 350 | self.editMenu.addAction(self.maskingAction) 351 | 352 | self.highlightingAct = QAction('Enable High&lighting', self) 353 | self.highlightingAct.setShortcut('Ctrl+L') 354 | self.highlightingAct.setCheckable(True) 355 | self.highlightingAct.setToolTip('Toggle highlighting') 356 | self.highlightingAct.setStatusTip('Toggle whether ' 357 | 'highlighting is enabled') 358 | highlight_connector = partial(self.toggleHighlighting, apply=True) 359 | self.highlightingAct.toggled.connect(highlight_connector) 360 | self.editMenu.addAction(self.highlightingAct) 361 | 362 | self.overlapAct = QAction('Enable Overlap Coloring', self) 363 | self.overlapAct.setShortcut('Ctrl+P') 364 | self.overlapAct.setCheckable(True) 365 | self.overlapAct.setToolTip('Toggle overlapping regions') 366 | self.overlapAct.setStatusTip('Toggle display of overlapping ' 367 | 'regions when enabled') 368 | overlap_connector = partial(self.toggleOverlaps, apply=True) 369 | self.overlapAct.toggled.connect(overlap_connector) 370 | self.editMenu.addAction(self.overlapAct) 371 | 372 | self.outlineAct = QAction('Enable Domain Outlines', self) 373 | self.outlineAct.setShortcut('Ctrl+U') 374 | self.outlineAct.setCheckable(True) 375 | self.outlineAct.setToolTip('Display Cell/Material Boundaries') 376 | self.outlineAct.setStatusTip('Toggle display of domain ' 377 | 'outlines when enabled') 378 | outline_connector = partial(self.toggleOutlines, apply=True) 379 | self.outlineAct.toggled.connect(outline_connector) 380 | self.editMenu.addAction(self.outlineAct) 381 | 382 | # View Menu 383 | self.dockAction = QAction('Hide &Dock', self) 384 | self.dockAction.setShortcut("Ctrl+D") 385 | self.dockAction.setToolTip('Toggle dock visibility') 386 | self.dockAction.setStatusTip('Toggle dock visibility') 387 | self.dockAction.triggered.connect(self.toggleDockView) 388 | 389 | self.tallyDockAction = QAction('Tally &Dock', self) 390 | self.tallyDockAction.setShortcut("Ctrl+T") 391 | self.tallyDockAction.setToolTip('Toggle tally dock visibility') 392 | self.tallyDockAction.setStatusTip('Toggle tally dock visibility') 393 | self.tallyDockAction.triggered.connect(self.toggleTallyDockView) 394 | 395 | self.meshAnnotationDockAction = QAction('Mesh &Annotation Dock', self) 396 | self.meshAnnotationDockAction.setShortcut("Ctrl+E") 397 | self.meshAnnotationDockAction.setToolTip('Toggle mesh annotation dock visibility') 398 | self.meshAnnotationDockAction.setStatusTip('Toggle mesh annotation dock visibility') 399 | self.meshAnnotationDockAction.triggered.connect(self.toggleMeshAnnotationDockView) 400 | 401 | self.zoomAction = QAction('&Zoom...', self) 402 | self.zoomAction.setShortcut('Alt+Shift+Z') 403 | self.zoomAction.setToolTip('Edit zoom factor') 404 | self.zoomAction.setStatusTip('Edit zoom factor') 405 | self.zoomAction.triggered.connect(self.editZoomAct) 406 | 407 | self.viewMenu = self.mainMenu.addMenu('&View') 408 | self.viewMenu.addAction(self.dockAction) 409 | self.viewMenu.addAction(self.tallyDockAction) 410 | self.viewMenu.addAction(self.meshAnnotationDockAction) 411 | self.viewMenu.addSeparator() 412 | self.viewMenu.addAction(self.zoomAction) 413 | self.viewMenu.aboutToShow.connect(self.updateViewMenu) 414 | 415 | # Window Menu 416 | self.mainWindowAction = QAction('&Main Window', self) 417 | self.mainWindowAction.setCheckable(True) 418 | self.mainWindowAction.setToolTip('Bring main window to front') 419 | self.mainWindowAction.setStatusTip('Bring main window to front') 420 | self.mainWindowAction.triggered.connect(self.showMainWindow) 421 | 422 | self.colorDialogAction = QAction('Color &Options', self) 423 | self.colorDialogAction.setCheckable(True) 424 | self.colorDialogAction.setToolTip('Bring Color Dialog to front') 425 | self.colorDialogAction.setStatusTip('Bring Color Dialog to front') 426 | self.colorDialogAction.triggered.connect(self.showColorDialog) 427 | 428 | # Keyboard Shortcuts Overlay 429 | self.keyboardShortcutsAction = QAction("&Keyboard Shortcuts...", self) 430 | self.keyboardShortcutsAction.setShortcut("?") 431 | self.keyboardShortcutsAction.setToolTip("Display Keyboard Shortcuts") 432 | self.keyboardShortcutsAction.setStatusTip("Display Keyboard Shortcuts") 433 | self.keyboardShortcutsAction.triggered.connect(self.toggleShortcuts) 434 | 435 | self.windowMenu = self.mainMenu.addMenu('&Window') 436 | self.windowMenu.addAction(self.mainWindowAction) 437 | self.windowMenu.addAction(self.colorDialogAction) 438 | self.windowMenu.addAction(self.keyboardShortcutsAction) 439 | self.windowMenu.aboutToShow.connect(self.updateWindowMenu) 440 | 441 | def updateEditMenu(self): 442 | changed = self.model.currentView != self.model.defaultView 443 | self.restoreAction.setDisabled(not changed) 444 | 445 | self.maskingAction.setChecked(self.model.currentView.masking) 446 | self.highlightingAct.setChecked(self.model.currentView.highlighting) 447 | self.outlineAct.setChecked(self.model.currentView.outlines) 448 | self.overlapAct.setChecked(self.model.currentView.color_overlaps) 449 | 450 | num_previous_views = len(self.model.previousViews) 451 | self.undoAction.setText('&Undo ({})'.format(num_previous_views)) 452 | num_subsequent_views = len(self.model.subsequentViews) 453 | self.redoAction.setText('&Redo ({})'.format(num_subsequent_views)) 454 | 455 | def updateBasisMenu(self): 456 | self.xyAction.setChecked(self.model.currentView.basis == 'xy') 457 | self.xzAction.setChecked(self.model.currentView.basis == 'xz') 458 | self.yzAction.setChecked(self.model.currentView.basis == 'yz') 459 | 460 | def updateColorbyMenu(self): 461 | cv = self.model.currentView 462 | self.cellAction.setChecked(cv.colorby == 'cell') 463 | self.materialAction.setChecked(cv.colorby == 'material') 464 | self.temperatureAction.setChecked(cv.colorby == 'temperature') 465 | self.densityAction.setChecked(cv.colorby == 'density') 466 | 467 | def updateViewMenu(self): 468 | if self.dock.isVisible(): 469 | self.dockAction.setText('Hide &Dock') 470 | else: 471 | self.dockAction.setText('Show &Dock') 472 | 473 | def updateWindowMenu(self): 474 | self.colorDialogAction.setChecked(self.colorDialog.isActiveWindow()) 475 | self.mainWindowAction.setChecked(self.isActiveWindow()) 476 | 477 | def saveBatchImage(self, view_file): 478 | """ 479 | Loads a view in the GUI and generates an image 480 | 481 | Parameters 482 | ---------- 483 | view_file : str or pathlib.Path 484 | The path to a view file that is compatible with the loaded model. 485 | """ 486 | # store the 487 | cv = self.model.currentView 488 | # load the view from file 489 | self.loadViewFile(view_file) 490 | self.plotIm.saveImage(view_file.replace('.pltvw', '')) 491 | 492 | # Menu and shared methods 493 | def loadModel(self, reload=False, use_settings_pkl=True): 494 | if reload: 495 | self.resetModels() 496 | else: 497 | self.model = PlotModel(use_settings_pkl, self.model_path, self.default_res) 498 | 499 | # update plot and model settings 500 | self.updateRelativeBases() 501 | 502 | self.cellsModel = DomainTableModel(self.model.activeView.cells) 503 | self.materialsModel = DomainTableModel(self.model.activeView.materials) 504 | 505 | openmc_args = {'threads': self.threads, 'model_path': self.model_path} 506 | 507 | if reload: 508 | loader_thread = Thread(target=_openmcReload, kwargs=openmc_args) 509 | loader_thread.start() 510 | while loader_thread.is_alive(): 511 | self.statusBar().showMessage("Reloading model...") 512 | QApplication.processEvents() 513 | 514 | self.plotIm.model = self.model 515 | self.applyChanges() 516 | 517 | def saveImage(self, filename=None): 518 | if filename is None: 519 | filename, ext = QFileDialog.getSaveFileName(self, 520 | "Save Plot Image", 521 | "untitled", 522 | "Images (*.png)") 523 | if filename: 524 | self.plotIm.saveImage(filename) 525 | self.statusBar().showMessage('Plot Image Saved', 5000) 526 | 527 | def saveView(self): 528 | filename, ext = QFileDialog.getSaveFileName(self, 529 | "Save View Settings", 530 | "untitled", 531 | "View Settings (*.pltvw)") 532 | if filename: 533 | if "." not in filename: 534 | filename += ".pltvw" 535 | 536 | saved = {'version': self.model.version, 537 | 'current': self.model.currentView} 538 | with open(filename, 'wb') as file: 539 | pickle.dump(saved, file) 540 | 541 | def loadViewFile(self, filename): 542 | try: 543 | with open(filename, 'rb') as file: 544 | saved = pickle.load(file) 545 | except Exception: 546 | message = 'Error loading plot settings' 547 | saved = {'version': None, 548 | 'current': None} 549 | 550 | if saved['version'] == self.model.version: 551 | self.model.activeView = saved['current'] 552 | self.dock.updateDock() 553 | self.colorDialog.updateDialogValues() 554 | self.applyChanges() 555 | message = '{} loaded'.format(filename) 556 | else: 557 | message = 'Error loading plot settings. Incompatible model.' 558 | self.statusBar().showMessage(message, 5000) 559 | 560 | def openView(self): 561 | filename, ext = QFileDialog.getOpenFileName(self, "Open View Settings", 562 | ".", "*.pltvw") 563 | if filename: 564 | self.loadViewFile(filename) 565 | 566 | def openStatePoint(self): 567 | # check for an alread-open statepoint 568 | if self.model.statepoint: 569 | msg_box = QMessageBox() 570 | msg_box.setText("Please close the current statepoint file before " 571 | "opening a new one.") 572 | msg_box.setIcon(QMessageBox.Information) 573 | msg_box.setStandardButtons(QMessageBox.Ok) 574 | msg_box.exec() 575 | return 576 | filename, ext = QFileDialog.getOpenFileName(self, "Open StatePoint", 577 | ".", "*.h5") 578 | if filename: 579 | try: 580 | self.model.openStatePoint(filename) 581 | message = 'Opened statepoint file: {}' 582 | except (FileNotFoundError, OSError): 583 | message = 'Error opening statepoint file: {}' 584 | msg_box = QMessageBox() 585 | msg = "Could not open statepoint file: \n\n {} \n" 586 | msg_box.setText(msg.format(filename)) 587 | msg_box.setIcon(QMessageBox.Warning) 588 | msg_box.setStandardButtons(QMessageBox.Ok) 589 | msg_box.exec() 590 | finally: 591 | self.statusBar().showMessage(message.format(filename), 5000) 592 | self.updateDataMenu() 593 | self.tallyDock.update() 594 | 595 | def importProperties(self): 596 | filename, ext = QFileDialog.getOpenFileName(self, "Import properties", 597 | ".", "*.h5") 598 | if not filename: 599 | return 600 | 601 | try: 602 | openmc.lib.import_properties(filename) 603 | message = 'Imported properties: {}' 604 | except (FileNotFoundError, OSError, openmc.lib.exc.OpenMCError) as e: 605 | message = 'Error opening properties file: {}' 606 | msg_box = QMessageBox() 607 | msg_box.setText(f"Error opening properties file: \n\n {e} \n") 608 | msg_box.setIcon(QMessageBox.Warning) 609 | msg_box.setStandardButtons(QMessageBox.Ok) 610 | msg_box.exec() 611 | finally: 612 | self.statusBar().showMessage(message.format(filename), 5000) 613 | 614 | if self.model.activeView.colorby == 'temperature': 615 | self.applyChanges() 616 | 617 | def closeStatePoint(self): 618 | # remove the statepoint object and update the data menu 619 | filename = self.model.statepoint.filename 620 | self.model.statepoint = None 621 | self.model.currentView.selectedTally = None 622 | self.model.activeView.selectedTally = None 623 | 624 | msg = "Closed statepoint file {}".format(filename) 625 | self.statusBar().showMessage(msg) 626 | self.updateDataMenu() 627 | self.tallyDock.selectTally() 628 | self.tallyDock.update() 629 | self.plotIm.updatePixmap() 630 | 631 | def updateDataMenu(self): 632 | if self.model.statepoint: 633 | self.closeStatePointAction = QAction("&Close statepoint", self) 634 | self.closeStatePointAction.setToolTip("Close current statepoint") 635 | self.closeStatePointAction.triggered.connect(self.closeStatePoint) 636 | self.dataMenu.addAction(self.closeStatePointAction) 637 | elif hasattr(self, "closeStatePointAction"): 638 | self.dataMenu.removeAction(self.closeStatePointAction) 639 | 640 | 641 | def updateMeshAnnotations(self): 642 | self.model.activeView.mesh_annotations = self.meshAnnotationDock.get_checked_meshes() 643 | 644 | def plotSourceSites(self): 645 | self.sourceSitesDialog.show() 646 | self.sourceSitesDialog.raise_() 647 | self.sourceSitesDialog.activateWindow() 648 | 649 | def applyChanges(self): 650 | if self.model.activeView != self.model.currentView: 651 | self.statusBar().showMessage('Generating Plot...') 652 | QApplication.processEvents() 653 | if self.model.activeView.selectedTally is not None: 654 | self.tallyDock.updateModel() 655 | self.updateMeshAnnotations() 656 | self.model.storeCurrent() 657 | self.model.subsequentViews = [] 658 | self.plotIm.generatePixmap() 659 | self.resetModels() 660 | self.showCurrentView() 661 | self.statusBar().showMessage('') 662 | else: 663 | self.statusBar().showMessage('No changes to apply.', 3000) 664 | 665 | def undo(self): 666 | self.statusBar().showMessage('Generating Plot...') 667 | QApplication.processEvents() 668 | 669 | self.model.undo() 670 | self.resetModels() 671 | self.showCurrentView() 672 | self.dock.updateDock() 673 | self.colorDialog.updateDialogValues() 674 | 675 | if not self.model.previousViews: 676 | self.undoAction.setDisabled(True) 677 | self.redoAction.setDisabled(False) 678 | self.statusBar().showMessage('') 679 | 680 | def redo(self): 681 | self.statusBar().showMessage('Generating Plot...') 682 | QApplication.processEvents() 683 | 684 | self.model.redo() 685 | self.resetModels() 686 | self.showCurrentView() 687 | self.dock.updateDock() 688 | self.colorDialog.updateDialogValues() 689 | 690 | if not self.model.subsequentViews: 691 | self.redoAction.setDisabled(True) 692 | self.undoAction.setDisabled(False) 693 | self.statusBar().showMessage('') 694 | 695 | def restoreDefault(self): 696 | if self.model.currentView != self.model.defaultView: 697 | 698 | self.statusBar().showMessage('Generating Plot...') 699 | QApplication.processEvents() 700 | 701 | self.model.storeCurrent() 702 | self.model.activeView.adopt_plotbase(self.model.defaultView) 703 | self.plotIm.generatePixmap() 704 | self.resetModels() 705 | self.showCurrentView() 706 | self.dock.updateDock() 707 | self.colorDialog.updateDialogValues() 708 | 709 | self.model.subsequentViews = [] 710 | self.statusBar().showMessage('') 711 | 712 | def editBasis(self, basis, apply=False): 713 | self.model.activeView.basis = basis 714 | self.dock.updateBasis() 715 | if apply: 716 | self.applyChanges() 717 | 718 | def editColorBy(self, domain_kind, apply=False): 719 | self.model.activeView.colorby = domain_kind 720 | self.dock.updateColorBy() 721 | self.colorDialog.updateColorBy() 722 | if apply: 723 | self.applyChanges() 724 | 725 | def editUniverseLevel(self, level, apply=False): 726 | if level in ('all', ''): 727 | self.model.activeView.level = -1 728 | else: 729 | self.model.activeView.level = int(level) 730 | self.dock.updateUniverseLevel() 731 | self.colorDialog.updateUniverseLevel() 732 | if apply: 733 | self.applyChanges() 734 | 735 | def toggleOverlaps(self, state, apply=False): 736 | self.model.activeView.color_overlaps = bool(state) 737 | self.colorDialog.updateOverlap() 738 | if apply: 739 | self.applyChanges() 740 | 741 | def editColorMap(self, colormap_name, property_type, apply=False): 742 | self.model.activeView.colormaps[property_type] = colormap_name 743 | self.plotIm.updateColorMap(colormap_name, property_type) 744 | self.colorDialog.updateColorMaps() 745 | if apply: 746 | self.applyChanges() 747 | 748 | def editColorbarMin(self, min_val, property_type, apply=False): 749 | av = self.model.activeView 750 | current = av.user_minmax[property_type] 751 | av.user_minmax[property_type] = (min_val, current[1]) 752 | self.colorDialog.updateColorMinMax() 753 | self.plotIm.updateColorMinMax(property_type) 754 | if apply: 755 | self.applyChanges() 756 | 757 | def editColorbarMax(self, max_val, property_type, apply=False): 758 | av = self.model.activeView 759 | current = av.user_minmax[property_type] 760 | av.user_minmax[property_type] = (current[0], max_val) 761 | self.colorDialog.updateColorMinMax() 762 | self.plotIm.updateColorMinMax(property_type) 763 | if apply: 764 | self.applyChanges() 765 | 766 | def toggleColorbarScale(self, state, property, apply=False): 767 | av = self.model.activeView 768 | av.color_scale_log[property] = bool(state) 769 | # temporary, should be resolved diferently in the future 770 | cv = self.model.currentView 771 | cv.color_scale_log[property] = bool(state) 772 | self.plotIm.updateColorbarScale() 773 | if apply: 774 | self.applyChanges() 775 | 776 | def toggleUserMinMax(self, state, property): 777 | av = self.model.activeView 778 | av.use_custom_minmax[property] = bool(state) 779 | if av.user_minmax[property] == (0.0, 0.0): 780 | av.user_minmax[property] = copy.copy(av.data_minmax[property]) 781 | self.plotIm.updateColorMinMax('temperature') 782 | self.plotIm.updateColorMinMax('density') 783 | self.colorDialog.updateColorMinMax() 784 | 785 | def toggleDataIndicatorCheckBox(self, state, property, apply=False): 786 | av = self.model.activeView 787 | av.data_indicator_enabled[property] = bool(state) 788 | 789 | cv = self.model.currentView 790 | cv.data_indicator_enabled[property] = bool(state) 791 | 792 | self.plotIm.updateDataIndicatorVisibility() 793 | if apply: 794 | self.applyChanges() 795 | 796 | def toggleMasking(self, state, apply=False): 797 | self.model.activeView.masking = bool(state) 798 | self.colorDialog.updateMasking() 799 | if apply: 800 | self.applyChanges() 801 | 802 | def toggleHighlighting(self, state, apply=False): 803 | self.model.activeView.highlighting = bool(state) 804 | self.colorDialog.updateHighlighting() 805 | if apply: 806 | self.applyChanges() 807 | 808 | def toggleDockView(self): 809 | if self.dock.isVisible(): 810 | self.dock.hide() 811 | if not self.isMaximized() and not self.dock.isFloating(): 812 | self.resize(self.width() - self.dock.width(), self.height()) 813 | else: 814 | self.dock.setVisible(True) 815 | if not self.isMaximized() and not self.dock.isFloating(): 816 | self.resize(self.width() + self.dock.width(), self.height()) 817 | self.resizePixmap() 818 | self.showMainWindow() 819 | 820 | def toggleTallyDockView(self): 821 | if self.tallyDock.isVisible(): 822 | self.tallyDock.hide() 823 | if not self.isMaximized() and not self.tallyDock.isFloating(): 824 | self.resize(self.width() - self.tallyDock.width(), self.height()) 825 | else: 826 | self.tallyDock.setVisible(True) 827 | if not self.isMaximized() and not self.tallyDock.isFloating(): 828 | self.resize(self.width() + self.tallyDock.width(), self.height()) 829 | self.resizePixmap() 830 | self.showMainWindow() 831 | 832 | def toggleMeshAnnotationDockView(self): 833 | if self.meshAnnotationDock.isVisible(): 834 | self.meshAnnotationDock.hide() 835 | if not self.isMaximized() and not self.meshAnnotationDock.isFloating(): 836 | self.resize(self.width() - self.meshAnnotationDock.width(), self.height()) 837 | else: 838 | self.meshAnnotationDock.setVisible(True) 839 | if not self.isMaximized() and not self.meshAnnotationDock.isFloating(): 840 | self.resize(self.width() + self.meshAnnotationDock.width(), self.height()) 841 | self.resizePixmap() 842 | self.showMainWindow() 843 | 844 | def editZoomAct(self): 845 | percent, ok = QInputDialog.getInt(self, "Edit Zoom", "Zoom Percent:", 846 | self.dock.zoomBox.value(), 25, 2000) 847 | if ok: 848 | self.dock.zoomBox.setValue(percent) 849 | 850 | def editZoom(self, value): 851 | self.zoom = value 852 | self.resizePixmap() 853 | self.dock.zoomBox.setValue(value) 854 | 855 | def showMainWindow(self): 856 | self.raise_() 857 | self.activateWindow() 858 | 859 | def showColorDialog(self): 860 | self.colorDialog.show() 861 | self.colorDialog.raise_() 862 | self.colorDialog.activateWindow() 863 | 864 | def showExportDialog(self): 865 | self.exportDataDialog.show() 866 | self.exportDataDialog.raise_() 867 | self.exportDataDialog.activateWindow() 868 | 869 | # Dock methods: 870 | 871 | def editSingleOrigin(self, value, dimension): 872 | self.model.activeView.origin[dimension] = value 873 | 874 | def editPlotAlpha(self, value): 875 | self.model.activeView.domainAlpha = value 876 | 877 | def editPlotVisibility(self, value): 878 | self.model.activeView.domainVisible = bool(value) 879 | 880 | def toggleOutlines(self, value, apply=False): 881 | self.model.activeView.outlines = bool(value) 882 | self.dock.updateOutlines() 883 | 884 | if apply: 885 | self.applyChanges() 886 | 887 | def editWidth(self, value): 888 | self.model.activeView.width = value 889 | self.onRatioChange() 890 | self.dock.updateWidth() 891 | 892 | def editHeight(self, value): 893 | self.model.activeView.height = value 894 | self.onRatioChange() 895 | self.dock.updateHeight() 896 | 897 | def toggleAspectLock(self, state): 898 | self.model.activeView.aspectLock = bool(state) 899 | self.onRatioChange() 900 | self.dock.updateAspectLock() 901 | 902 | def editVRes(self, value): 903 | self.model.activeView.v_res = value 904 | self.dock.updateVRes() 905 | 906 | def editHRes(self, value): 907 | self.model.activeView.h_res = value 908 | self.onRatioChange() 909 | self.dock.updateHRes() 910 | 911 | # Color dialog methods: 912 | 913 | def editMaskingColor(self): 914 | current_color = self.model.activeView.maskBackground 915 | dlg = QColorDialog(self) 916 | 917 | dlg.setCurrentColor(QtGui.QColor.fromRgb(*current_color)) 918 | if dlg.exec(): 919 | new_color = dlg.currentColor().getRgb()[:3] 920 | self.model.activeView.maskBackground = new_color 921 | self.colorDialog.updateMaskingColor() 922 | 923 | def editHighlightColor(self): 924 | current_color = self.model.activeView.highlightBackground 925 | dlg = QColorDialog(self) 926 | 927 | dlg.setCurrentColor(QtGui.QColor.fromRgb(*current_color)) 928 | if dlg.exec(): 929 | new_color = dlg.currentColor().getRgb()[:3] 930 | self.model.activeView.highlightBackground = new_color 931 | self.colorDialog.updateHighlightColor() 932 | 933 | def editAlpha(self, value): 934 | self.model.activeView.highlightAlpha = value 935 | 936 | def editSeed(self, value): 937 | self.model.activeView.highlightSeed = value 938 | 939 | def editOverlapColor(self, apply=False): 940 | current_color = self.model.activeView.overlap_color 941 | dlg = QColorDialog(self) 942 | dlg.setCurrentColor(QtGui.QColor.fromRgb(*current_color)) 943 | if dlg.exec(): 944 | new_color = dlg.currentColor().getRgb()[:3] 945 | self.model.activeView.overlap_color = new_color 946 | self.colorDialog.updateOverlapColor() 947 | 948 | if apply: 949 | self.applyChanges() 950 | 951 | def editBackgroundColor(self, apply=False): 952 | current_color = self.model.activeView.domainBackground 953 | dlg = QColorDialog(self) 954 | 955 | dlg.setCurrentColor(QtGui.QColor.fromRgb(*current_color)) 956 | if dlg.exec(): 957 | new_color = dlg.currentColor().getRgb()[:3] 958 | self.model.activeView.domainBackground = new_color 959 | self.colorDialog.updateBackgroundColor() 960 | 961 | if apply: 962 | self.applyChanges() 963 | 964 | def resetColors(self): 965 | self.model.resetColors() 966 | self.colorDialog.updateDialogValues() 967 | self.applyChanges() 968 | 969 | # Tally dock methods 970 | 971 | def editSelectedTally(self, event): 972 | av = self.model.activeView 973 | 974 | if event is None or event == "None" or event == "": 975 | av.selectedTally = None 976 | else: 977 | av.selectedTally = int(event.split()[1]) 978 | self.tallyDock.selectTally(event) 979 | 980 | def editTallyValue(self, event): 981 | av = self.model.activeView 982 | av.tallyValue = event 983 | 984 | def toggleTallyVisibility(self, state, apply=False): 985 | av = self.model.activeView 986 | av.tallyDataVisible = bool(state) 987 | if apply: 988 | self.applyChanges() 989 | 990 | def toggleTallyLogScale(self, state, apply=False): 991 | av = self.model.activeView 992 | av.tallyDataLogScale = bool(state) 993 | if apply: 994 | self.applyChanges() 995 | 996 | def toggleTallyMaskZero(self, state): 997 | av = self.model.activeView 998 | av.tallyMaskZeroValues = bool(state) 999 | 1000 | def toggleTallyVolumeNorm(self, state): 1001 | av = self.model.activeView 1002 | av.tallyVolumeNorm = bool(state) 1003 | 1004 | def editTallyAlpha(self, value, apply=False): 1005 | av = self.model.activeView 1006 | av.tallyDataAlpha = value 1007 | if apply: 1008 | self.applyChanges() 1009 | 1010 | def toggleTallyContours(self, state): 1011 | av = self.model.activeView 1012 | av.tallyContours = bool(state) 1013 | 1014 | def editTallyContourLevels(self, value): 1015 | av = self.model.activeView 1016 | av.tallyContourLevels = value 1017 | 1018 | def toggleTallyDataIndicator(self, state, apply=False): 1019 | av = self.model.activeView 1020 | av.tallyDataIndicator = bool(state) 1021 | if apply: 1022 | self.applyChanges() 1023 | 1024 | def toggleTallyDataClip(self, state): 1025 | av = self.model.activeView 1026 | av.clipTallyData = bool(state) 1027 | 1028 | def toggleTallyDataUserMinMax(self, state, apply=False): 1029 | av = self.model.activeView 1030 | av.tallyDataUserMinMax = bool(state) 1031 | self.tallyDock.tallyColorForm.setMinMaxEnabled(bool(state)) 1032 | if apply: 1033 | self.applyChanges() 1034 | 1035 | def editTallyDataMin(self, value, apply=False): 1036 | av = self.model.activeView 1037 | av.tallyDataMin = value 1038 | if apply: 1039 | self.applyChanges() 1040 | 1041 | def editTallyDataMax(self, value, apply=False): 1042 | av = self.model.activeView 1043 | av.tallyDataMax = value 1044 | if apply: 1045 | self.applyChanges() 1046 | 1047 | def editTallyDataColormap(self, cmap, apply=False): 1048 | av = self.model.activeView 1049 | av.tallyDataColormap = cmap 1050 | if apply: 1051 | self.applyChanges() 1052 | 1053 | def toggleReverseCmap(self, state): 1054 | av = self.model.activeView 1055 | av.tallyDataReverseCmap = bool(state) 1056 | 1057 | def updateTallyMinMax(self): 1058 | self.tallyDock.updateMinMax() 1059 | 1060 | # Plot image methods 1061 | def editPlotOrigin(self, xOr, yOr, zOr=None, apply=False): 1062 | if zOr is not None: 1063 | self.model.activeView.origin = [xOr, yOr, zOr] 1064 | else: 1065 | origin = [None, None, None] 1066 | origin[self.xBasis] = xOr 1067 | origin[self.yBasis] = yOr 1068 | origin[self.zBasis] = self.model.activeView.origin[self.zBasis] 1069 | self.model.activeView.origin = origin 1070 | 1071 | self.dock.updateOrigin() 1072 | 1073 | if apply: 1074 | self.applyChanges() 1075 | 1076 | def revertDockControls(self): 1077 | self.dock.revertToCurrent() 1078 | 1079 | def editDomainColor(self, kind, id): 1080 | if kind == 'Cell': 1081 | domain = self.model.activeView.cells 1082 | else: 1083 | domain = self.model.activeView.materials 1084 | 1085 | current_color = domain[id].color 1086 | dlg = QColorDialog(self) 1087 | 1088 | if isinstance(current_color, tuple): 1089 | dlg.setCurrentColor(QtGui.QColor.fromRgb(*current_color)) 1090 | elif isinstance(current_color, str): 1091 | current_color = openmc.plots._SVG_COLORS[current_color] 1092 | dlg.setCurrentColor(QtGui.QColor.fromRgb(*current_color)) 1093 | if dlg.exec(): 1094 | new_color = dlg.currentColor().getRgb()[:3] 1095 | domain.set_color(id, new_color) 1096 | 1097 | self.applyChanges() 1098 | 1099 | def toggleDomainMask(self, state, kind, id): 1100 | if kind == 'Cell': 1101 | domain = self.model.activeView.cells 1102 | else: 1103 | domain = self.model.activeView.materials 1104 | 1105 | domain.set_masked(id, bool(state)) 1106 | self.applyChanges() 1107 | 1108 | def toggleDomainHighlight(self, state, kind, id): 1109 | if kind == 'Cell': 1110 | domain = self.model.activeView.cells 1111 | else: 1112 | domain = self.model.activeView.materials 1113 | 1114 | domain.set_highlight(id, bool(state)) 1115 | self.applyChanges() 1116 | 1117 | # Helper methods: 1118 | 1119 | def restoreWindowSettings(self): 1120 | settings = QtCore.QSettings() 1121 | 1122 | self.resize(settings.value("mainWindow/Size", 1123 | QtCore.QSize(1200, 800))) 1124 | self.move(settings.value("mainWindow/Position", 1125 | QtCore.QPoint(100, 100))) 1126 | self.restoreState(settings.value("mainWindow/State")) 1127 | 1128 | self.colorDialog.resize(settings.value("colorDialog/Size", 1129 | QtCore.QSize(400, 500))) 1130 | self.colorDialog.move(settings.value("colorDialog/Position", 1131 | QtCore.QPoint(600, 200))) 1132 | is_visible = settings.value("colorDialog/Visible", 0) 1133 | # some versions of PySide will return None rather than the default value 1134 | if is_visible is None: 1135 | is_visible = False 1136 | else: 1137 | is_visible = bool(int(is_visible)) 1138 | 1139 | self.colorDialog.setVisible(is_visible) 1140 | 1141 | def resetModels(self): 1142 | self.cellsModel = DomainTableModel(self.model.activeView.cells) 1143 | self.materialsModel = DomainTableModel(self.model.activeView.materials) 1144 | self.cellsModel.beginResetModel() 1145 | self.cellsModel.endResetModel() 1146 | self.materialsModel.beginResetModel() 1147 | self.materialsModel.endResetModel() 1148 | self.colorDialog.updateDomainTabs() 1149 | 1150 | def showCurrentView(self): 1151 | self.updateScale() 1152 | self.updateRelativeBases() 1153 | self.plotIm.updatePixmap() 1154 | 1155 | if self.model.previousViews: 1156 | self.undoAction.setDisabled(False) 1157 | if self.model.subsequentViews: 1158 | self.redoAction.setDisabled(False) 1159 | else: 1160 | self.redoAction.setDisabled(True) 1161 | 1162 | self.adjustWindow() 1163 | 1164 | def updateScale(self): 1165 | cv = self.model.currentView 1166 | self.scale = (cv.h_res / cv.width, 1167 | cv.v_res / cv.height) 1168 | 1169 | def updateRelativeBases(self): 1170 | cv = self.model.currentView 1171 | self.xBasis = 0 if cv.basis[0] == 'x' else 1 1172 | self.yBasis = 1 if cv.basis[1] == 'y' else 2 1173 | self.zBasis = 3 - (self.xBasis + self.yBasis) 1174 | 1175 | def adjustWindow(self): 1176 | self.setMaximumSize(self.screen.width(), self.screen.height()) 1177 | 1178 | def onRatioChange(self): 1179 | av = self.model.activeView 1180 | if av.aspectLock: 1181 | ratio = av.width / max(av.height, .001) 1182 | av.v_res = int(av.h_res / ratio) 1183 | self.dock.updateVRes() 1184 | 1185 | def showCoords(self, xPlotPos, yPlotPos): 1186 | cv = self.model.currentView 1187 | if cv.basis == 'xy': 1188 | coords = ("({}, {}, {})".format(round(xPlotPos, 2), 1189 | round(yPlotPos, 2), 1190 | round(cv.origin[2], 2))) 1191 | elif cv.basis == 'xz': 1192 | coords = ("({}, {}, {})".format(round(xPlotPos, 2), 1193 | round(cv.origin[1], 2), 1194 | round(yPlotPos, 2))) 1195 | else: 1196 | coords = ("({}, {}, {})".format(round(cv.origin[0], 2), 1197 | round(xPlotPos, 2), 1198 | round(yPlotPos, 2))) 1199 | self.coord_label.setText('{}'.format(coords)) 1200 | 1201 | def resizePixmap(self): 1202 | self.plotIm._resize() 1203 | self.plotIm.adjustSize() 1204 | 1205 | def moveEvent(self, event): 1206 | self.adjustWindow() 1207 | 1208 | def resizeEvent(self, event): 1209 | self.plotIm._resize() 1210 | self.adjustWindow() 1211 | self.updateScale() 1212 | if self.shortcutOverlay.isVisible(): 1213 | self.shortcutOverlay.resize(self.width(), self.height()) 1214 | 1215 | def closeEvent(self, event): 1216 | settings = QtCore.QSettings() 1217 | settings.setValue("mainWindow/Size", self.size()) 1218 | settings.setValue("mainWindow/Position", self.pos()) 1219 | settings.setValue("mainWindow/State", self.saveState()) 1220 | 1221 | settings.setValue("colorDialog/Size", self.colorDialog.size()) 1222 | settings.setValue("colorDialog/Position", self.colorDialog.pos()) 1223 | visible = int(self.colorDialog.isVisible()) 1224 | settings.setValue("colorDialog/Visible", visible) 1225 | 1226 | openmc.lib.finalize() 1227 | 1228 | self.saveSettings() 1229 | 1230 | def saveSettings(self): 1231 | if self.model.statepoint: 1232 | self.model.statepoint.close() 1233 | 1234 | # get hashes for material.xml and geometry.xml at close 1235 | mat_xml_hash, geom_xml_hash = hash_model(self.model_path) 1236 | 1237 | pickle_data = { 1238 | 'version': self.model.version, 1239 | 'currentView': self.model.currentView, 1240 | 'statepoint': self.model.statepoint, 1241 | 'mat_xml_hash': mat_xml_hash, 1242 | 'geom_xml_hash': geom_xml_hash 1243 | } 1244 | if self.model_path.is_file(): 1245 | settings_pkl = self.model_path.with_name('plot_settings.pkl') 1246 | else: 1247 | settings_pkl = self.model_path / 'plot_settings.pkl' 1248 | with settings_pkl.open('wb') as file: 1249 | pickle.dump(pickle_data, file) 1250 | 1251 | def exportTallyData(self): 1252 | # show export tool dialog 1253 | self.showExportDialog() 1254 | 1255 | def viewMaterialProps(self, id): 1256 | """display material properties in message box""" 1257 | mat = openmc.lib.materials[id] 1258 | if mat.name: 1259 | msg_str = f"Material {id} ({mat.name}) Properties\n\n" 1260 | else: 1261 | msg_str = f"Material {id} Properties\n\n" 1262 | 1263 | # get density and temperature 1264 | dens_g = mat.get_density(units='g/cm3') 1265 | dens_a = mat.get_density(units='atom/b-cm') 1266 | msg_str += f"Density: {dens_g:.3f} g/cm3 ({dens_a:.3e} atom/b-cm)\n" 1267 | msg_str += f"Temperature: {mat.temperature} K\n\n" 1268 | 1269 | # get nuclides and their densities 1270 | msg_str += "Nuclide densities [atom/b-cm]:\n" 1271 | for nuc, dens in zip(mat.nuclides, mat.densities): 1272 | msg_str += f'{nuc}: {dens:5.3e}\n' 1273 | 1274 | msg_box = QMessageBox(self) 1275 | msg_box.setText(msg_str) 1276 | msg_box.setModal(False) 1277 | msg_box.show() 1278 | -------------------------------------------------------------------------------- /openmc_plotter/overlays.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | 3 | from PySide6 import QtGui, QtCore 4 | from PySide6.QtWidgets import (QWidget, QTableWidget, QSizePolicy, 5 | QPushButton, QTableWidgetItem, QVBoxLayout) 6 | 7 | c_key = "⌘" if platform == 'darwin' else "Ctrl" 8 | 9 | 10 | class ShortcutTableItem(QTableWidgetItem): 11 | 12 | def __init__(self): 13 | super().__init__() 14 | # disable selection, editing 15 | self.setFlags(QtCore.Qt.NoItemFlags) 16 | 17 | 18 | class ShortcutsOverlay(QWidget): 19 | shortcuts = {"Color By": [("Cell", "Alt+C"), 20 | ("Material", "Alt+M"), 21 | ("Temperature", "Alt+T"), 22 | ("Density", "Alt+D")], 23 | "Views": [("Apply Changes", c_key + "+Enter"), 24 | ("Undo", c_key + "+Z"), 25 | ("Redo", "Shift+" + c_key+ "+Z"), 26 | ("Restore Default Plot", c_key + "+R"), 27 | ("Zoom", "Alt+Shift+Z"), 28 | ("Zoom", "Shift+Scroll"), 29 | ("Toggle Masking", c_key + "+M"), 30 | ("Toggle Highlighting", c_key + "+L"), 31 | ("Toggle Overlap Coloring", c_key + "+P"), 32 | ("Toggle Domain Outlines", c_key + "+U"), 33 | ("Set XY Basis", "Alt+X"), 34 | ("Set YZ Basis", "Alt+Y"), 35 | ("Set XZ Basis", "Alt+Z"), 36 | ("Update Plot Origin", "Double-click"), 37 | ("Open Context Menu", "Right-click"), 38 | ("When zoomed:", ""), 39 | ("Vertical Scroll", "Scroll"), 40 | ("Horizontal Scroll", "Alt+Scroll")], 41 | "Menus": [("Hide/Show Geometry Dock", c_key + "+D"), 42 | ("Hide/Show Tally Dock", c_key + "+T"), 43 | ("Hide/Show Mesh Annotation Dock", c_key + "+E"), 44 | ("Reload Model", "Shift+" + c_key + "+R"), 45 | ("Quit", c_key + "+Q"), 46 | ("Display Shortcuts", "?")], 47 | "Input/Output" : [("Save View", c_key + "+S"), 48 | ("Open View", c_key + "+O"), 49 | ("Save Plot Image", "Shift+" + c_key + "+S")]} 50 | 51 | # colors 52 | header_color = QtGui.QColor(150, 150, 150, 255) 53 | fillColor = QtGui.QColor(30, 30, 30, 200) 54 | framePenColor = QtGui.QColor(255, 255, 255, 120) 55 | textPenColor = QtGui.QColor(152, 196, 5, 255) 56 | 57 | def __init__(self, parent): 58 | super().__init__(parent) 59 | 60 | # transparent window fill 61 | self.layout = QVBoxLayout() 62 | self.setLayout(self.layout) 63 | 64 | n_rows = max((len(scts) for scts in self.shortcuts.values())) 65 | n_rows += 1 # plus one for header 66 | n_cols = len(self.shortcuts.keys()) * 3 67 | self.tableWidget = QTableWidget(n_rows, n_cols, self) 68 | self.layout.addWidget(self.tableWidget) 69 | 70 | # set all items to a non-editable cell item 71 | for i in range(n_rows): 72 | for j in range(n_cols): 73 | self.tableWidget.setItem(i, j, ShortcutTableItem()) 74 | 75 | self.tableWidget.setShowGrid(False) 76 | self.tableWidget.setSizePolicy(QSizePolicy.Expanding, 77 | QSizePolicy.Expanding) 78 | self.tableWidget.verticalHeader().setVisible(False) 79 | self.tableWidget.horizontalHeader().setVisible(False) 80 | self.tableWidget.setStyleSheet("background-color: rgba(30, 30, 30, 230);" 81 | "border: 0px;" 82 | "padding: 20px") 83 | 84 | # populate table cells 85 | self.set_cells() 86 | 87 | self.close_btn = QPushButton(self) 88 | self.close_btn.setAttribute(QtCore.Qt.WA_TranslucentBackground) 89 | self.close_btn.setStyleSheet("background-color: rgba(0, 0, 0, 0);" 90 | "border: 0px;" 91 | "color: rgba(150, 150, 150, 255)") 92 | self.close_btn.setText("X") 93 | font = QtGui.QFont() 94 | self.close_btn.setFixedSize(30, 30) 95 | self.close_btn.clicked.connect(self.hide) 96 | 97 | def set_cells(self): 98 | # row, col indices 99 | row_idx = 0 100 | col_idx = 0 101 | 102 | for menu in self.shortcuts: 103 | # set menu header 104 | header_item = self.tableWidget.item(row_idx, col_idx) 105 | header_item.setForeground(QtGui.QColor(150, 150, 150, 255)) 106 | header_item.setText(menu) 107 | header_item.setFlags(QtCore.Qt.NoItemFlags) 108 | row_idx += 1 109 | 110 | for shortcut in self.shortcuts[menu]: 111 | desc_item = self.tableWidget.item(row_idx, col_idx) 112 | desc_item.setForeground(self.textPenColor) 113 | desc_item.setText(shortcut[0]) 114 | 115 | key_item = self.tableWidget.item(row_idx, col_idx + 1) 116 | key_item.setForeground(self.textPenColor) 117 | key_item.setText(shortcut[1]) 118 | row_idx += 1 119 | # update for next menu 120 | row_idx = 0 121 | col_idx += 3 122 | 123 | self.tableWidget.resizeColumnsToContents() 124 | 125 | def keyPressEvent(self, event): 126 | # close if visible and esc is pressed 127 | if self.isVisible() and event.key() == QtCore.Qt.Key_Escape: 128 | self.close() 129 | 130 | def resizeEvent(self, event): 131 | overlay_size = self.size() 132 | btn_size = self.close_btn.size() 133 | x_pos = int(overlay_size.width() - btn_size.width()) - 5 134 | self.close_btn.move(x_pos, 5) 135 | -------------------------------------------------------------------------------- /openmc_plotter/plot_colors.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def random_rgb(): 5 | return tuple(np.random.choice(range(256), size=3)) 6 | 7 | 8 | def rgb_normalize(rgb): 9 | return tuple([c/255. for c in rgb]) 10 | 11 | 12 | def invert_rgb(rgb, normalized=False): 13 | rgb_max = 1.0 if normalized else 255. 14 | inv = [rgb_max - c for c in rgb[0:3]] 15 | return (*inv, *rgb[3:]) 16 | -------------------------------------------------------------------------------- /openmc_plotter/plotgui.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from PySide6 import QtCore, QtGui 4 | from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QVBoxLayout, 5 | QFormLayout, QComboBox, QSpinBox, 6 | QDoubleSpinBox, QSizePolicy, QMessageBox, 7 | QCheckBox, QRubberBand, QMenu, QDialog, 8 | QTabWidget, QTableView, QHeaderView) 9 | from matplotlib.figure import Figure 10 | from matplotlib import lines as mlines 11 | from matplotlib.colors import SymLogNorm 12 | from matplotlib.backends.backend_qt5agg import FigureCanvas 13 | import matplotlib.pyplot as plt 14 | import numpy as np 15 | 16 | from .plot_colors import rgb_normalize, invert_rgb 17 | from .plotmodel import DomainDelegate, PlotModel 18 | from .plotmodel import _NOT_FOUND, _VOID_REGION, _OVERLAP, _MODEL_PROPERTIES 19 | from .scientific_spin_box import ScientificDoubleSpinBox 20 | from .custom_widgets import HorizontalLine 21 | 22 | 23 | 24 | class PlotImage(FigureCanvas): 25 | 26 | def __init__(self, model: PlotModel, parent, main_window): 27 | 28 | self.figure = Figure(dpi=main_window.logicalDpiX()) 29 | super().__init__(self.figure) 30 | 31 | FigureCanvas.setSizePolicy(self, 32 | QSizePolicy.Expanding, 33 | QSizePolicy.Expanding) 34 | 35 | FigureCanvas.updateGeometry(self) 36 | self.model = model 37 | self.main_window = main_window 38 | self.parent = parent 39 | 40 | self.frozen = False 41 | 42 | self.rubber_band = QRubberBand(QRubberBand.Rectangle, self) 43 | self.band_origin = QtCore.QPoint() 44 | self.x_plot_origin = None 45 | self.y_plot_origin = None 46 | 47 | self.colorbar = None 48 | self.data_indicator = None 49 | self.tally_data_indicator = None 50 | self.image = None 51 | 52 | self.menu = QMenu(self) 53 | 54 | def enterEvent(self, event): 55 | self.setCursor(QtCore.Qt.CrossCursor) 56 | self.main_window.coord_label.show() 57 | 58 | def leaveEvent(self, event): 59 | self.main_window.coord_label.hide() 60 | self.main_window.statusBar().showMessage("") 61 | 62 | def mousePressEvent(self, event): 63 | self.main_window.coord_label.hide() 64 | position = event.pos() 65 | # Set rubber band absolute and relative position 66 | self.band_origin = position 67 | self.x_plot_origin, self.y_plot_origin = self.getPlotCoords(position) 68 | 69 | # Create rubber band 70 | self.rubber_band.setGeometry(QtCore.QRect(self.band_origin, 71 | QtCore.QSize())) 72 | 73 | def getPlotCoords(self, pos): 74 | x, y = self.mouseEventCoords(pos) 75 | 76 | # get the normalized axis coordinates from the event display units 77 | transform = self.ax.transAxes.inverted() 78 | xPlotCoord, yPlotCoord = transform.transform((x, y)) 79 | # flip the y-axis (its zero is in the upper left) 80 | 81 | # scale axes using the plot extents 82 | xPlotCoord = self.ax.dataLim.x0 + xPlotCoord * self.ax.dataLim.width 83 | yPlotCoord = self.ax.dataLim.y0 + yPlotCoord * self.ax.dataLim.height 84 | 85 | # set coordinate label if pointer is in the axes 86 | if self.parent.underMouse(): 87 | self.main_window.coord_label.show() 88 | self.main_window.showCoords(xPlotCoord, yPlotCoord) 89 | else: 90 | self.main_window.coord_label.hide() 91 | 92 | return (xPlotCoord, yPlotCoord) 93 | 94 | def _resize(self): 95 | z = self.main_window.zoom / 100.0 96 | # manage scroll bars 97 | if z <= 1.0: 98 | self.parent.verticalScrollBar().hide() 99 | self.parent.horizontalScrollBar().hide() 100 | self.parent.cornerWidget().hide() 101 | self.parent.verticalScrollBar().setEnabled(False) 102 | self.parent.horizontalScrollBar().setEnabled(False) 103 | else: 104 | self.parent.verticalScrollBar().show() 105 | self.parent.horizontalScrollBar().show() 106 | self.parent.cornerWidget().show() 107 | self.parent.verticalScrollBar().setEnabled(True) 108 | self.parent.horizontalScrollBar().setEnabled(True) 109 | 110 | # resize plot 111 | self.resize(self.parent.width() * z, 112 | self.parent.height() * z) 113 | 114 | def saveImage(self, filename): 115 | """Save an image of the current view 116 | 117 | Parameters 118 | ---------- 119 | filename : str or pathlib.Path 120 | Name of the image to save 121 | """ 122 | if "." not in str(filename): 123 | filename += ".png" 124 | self.figure.savefig(filename, transparent=True) 125 | 126 | def getDataIndices(self, event): 127 | cv = self.model.currentView 128 | 129 | x, y = self.mouseEventCoords(event.pos()) 130 | 131 | # get origin in axes coordinates 132 | x0, y0 = self.ax.transAxes.transform((0.0, 0.0)) 133 | 134 | # get the extents of the axes box in axes coordinates 135 | bbox = self.ax.get_window_extent().transformed( 136 | self.figure.dpi_scale_trans.inverted()) 137 | # get dimensions and scale using dpi 138 | width, height = bbox.width, bbox.height 139 | width *= self.figure.dpi 140 | height *= self.figure.dpi 141 | 142 | # use factor to get proper x,y position in pixels 143 | factor = (width/cv.h_res, height/cv.v_res) 144 | xPos = int((x - x0 + 0.01) / factor[0]) 145 | # flip y-axis 146 | yPos = cv.v_res - int((y - y0 + 0.01) / factor[1]) 147 | 148 | return xPos, yPos 149 | 150 | def getTallyIndices(self, event): 151 | 152 | xPos, yPos = self.getPlotCoords(event.pos()) 153 | 154 | ext = self.model.tally_extents 155 | 156 | x0 = ext[0] 157 | y0 = ext[2] 158 | 159 | v_res, h_res = self.model.tally_data.shape 160 | 161 | dx = (ext[1] - ext[0]) / h_res 162 | dy = (ext[3] - ext[2]) / v_res 163 | 164 | i = int((xPos - x0) // dx) 165 | j = v_res - int((yPos - y0) // dy) - 1 166 | 167 | return i, j 168 | 169 | def getTallyInfo(self, event): 170 | cv = self.model. currentView 171 | 172 | xPos, yPos = self.getTallyIndices(event) 173 | 174 | if self.model.tally_data is None: 175 | return -1, None 176 | 177 | if not cv.selectedTally or not cv.tallyDataVisible: 178 | return -1, None 179 | 180 | # don't look up mesh filter data (for now) 181 | tally = self.model.statepoint.tallies[cv.selectedTally] 182 | 183 | # check that the position is in the axes view 184 | v_res, h_res = self.model.tally_data.shape 185 | if 0 <= yPos < v_res and 0 <= xPos < h_res: 186 | value = self.model.tally_data[yPos][xPos] 187 | else: 188 | value = None 189 | 190 | return cv.selectedTally, value 191 | 192 | def getIDinfo(self, event): 193 | 194 | xPos, yPos = self.getDataIndices(event) 195 | 196 | # check that the position is in the axes view 197 | if 0 <= yPos < self.model.currentView.v_res \ 198 | and 0 <= xPos and xPos < self.model.currentView.h_res: 199 | id = self.model.ids[yPos, xPos] 200 | instance = self.model.instances[yPos, xPos] 201 | temp = "{:g}".format(self.model.properties[yPos, xPos, 0]) 202 | density = "{:g}".format(self.model.properties[yPos, xPos, 1]) 203 | else: 204 | id = _NOT_FOUND 205 | instance = _NOT_FOUND 206 | density = str(_NOT_FOUND) 207 | temp = str(_NOT_FOUND) 208 | 209 | if self.model.currentView.colorby == 'cell': 210 | domain = self.model.activeView.cells 211 | domain_kind = 'Cell' 212 | elif self.model.currentView.colorby == 'temperature': 213 | domain = self.model.activeView.materials 214 | domain_kind = 'Temperature' 215 | elif self.model.currentView.colorby == 'density': 216 | domain = self.model.activeView.materials 217 | domain_kind = 'Density' 218 | else: 219 | domain = self.model.activeView.materials 220 | domain_kind = 'Material' 221 | 222 | properties = {'density': density, 223 | 'temperature': temp} 224 | 225 | return id, instance, properties, domain, domain_kind 226 | 227 | def mouseDoubleClickEvent(self, event): 228 | xCenter, yCenter = self.getPlotCoords(event.pos()) 229 | self.main_window.editPlotOrigin(xCenter, yCenter, apply=True) 230 | 231 | def mouseMoveEvent(self, event): 232 | cv = self.model.currentView 233 | # Show Cursor position relative to plot in status bar 234 | xPlotPos, yPlotPos = self.getPlotCoords(event.pos()) 235 | 236 | # Show Cell/Material ID, Name in status bar 237 | id, instance, properties, domain, domain_kind = self.getIDinfo(event) 238 | 239 | domainInfo = "" 240 | tallyInfo = "" 241 | 242 | if self.parent.underMouse(): 243 | 244 | if domain_kind.lower() in _MODEL_PROPERTIES: 245 | line_val = float(properties[domain_kind.lower()]) 246 | line_val = max(line_val, 0.0) 247 | self.updateDataIndicatorValue(line_val) 248 | domain_kind = 'Material' 249 | 250 | temperature = properties['temperature'] 251 | density = properties['density'] 252 | 253 | if instance != _NOT_FOUND and domain_kind == 'Cell': 254 | instanceInfo = f" ({instance})" 255 | else: 256 | instanceInfo = "" 257 | if id == _VOID_REGION: 258 | domainInfo = ("VOID") 259 | elif id == _OVERLAP: 260 | domainInfo = ("OVERLAP") 261 | elif id != _NOT_FOUND and domain[id].name: 262 | domainInfo = ("{} {}{}: \"{}\"\t Density: {} g/cc\t" 263 | "Temperature: {} K".format( 264 | domain_kind, 265 | id, 266 | instanceInfo, 267 | domain[id].name, 268 | density, 269 | temperature 270 | )) 271 | elif id != _NOT_FOUND: 272 | domainInfo = ("{} {}{}\t Density: {} g/cc\t" 273 | "Temperature: {} K".format(domain_kind, 274 | id, 275 | instanceInfo, 276 | density, 277 | temperature)) 278 | else: 279 | domainInfo = "" 280 | 281 | if self.model.tally_data is not None: 282 | tid, value = self.getTallyInfo(event) 283 | if value is not None and value != np.nan: 284 | self.updateTallyDataIndicatorValue(value) 285 | tallyInfo = "Tally {} {}: {:.5E}".format( 286 | tid, cv.tallyValue, value) 287 | else: 288 | self.updateTallyDataIndicatorValue(0.0) 289 | else: 290 | self.updateTallyDataIndicatorValue(0.0) 291 | self.updateDataIndicatorValue(0.0) 292 | 293 | if domainInfo: 294 | self.main_window.statusBar().showMessage( 295 | " " + domainInfo + " " + tallyInfo) 296 | else: 297 | self.main_window.statusBar().showMessage(" " + tallyInfo) 298 | 299 | # Update rubber band and values if mouse button held down 300 | if event.buttons() == QtCore.Qt.LeftButton: 301 | self.rubber_band.setGeometry( 302 | QtCore.QRect(self.band_origin, event.pos()).normalized()) 303 | 304 | # Show rubber band if both dimensions > 10 pixels 305 | if self.rubber_band.width() > 10 and self.rubber_band.height() > 10: 306 | self.rubber_band.show() 307 | else: 308 | self.rubber_band.hide() 309 | 310 | # Update plot X Origin 311 | xCenter = (self.x_plot_origin + xPlotPos) / 2 312 | yCenter = (self.y_plot_origin + yPlotPos) / 2 313 | self.main_window.editPlotOrigin(xCenter, yCenter) 314 | 315 | modifiers = event.modifiers() 316 | 317 | # Zoom out if Shift held 318 | if modifiers == QtCore.Qt.ShiftModifier: 319 | cv = self.model.currentView 320 | bandwidth = abs(self.band_origin.x() - event.pos().x()) 321 | width = cv.width * (cv.h_res / max(bandwidth, .001)) 322 | bandheight = abs(self.band_origin.y() - event.pos().y()) 323 | height = cv.height * (cv.v_res / max(bandheight, .001)) 324 | # Zoom in 325 | else: 326 | width = max(abs(self.x_plot_origin - xPlotPos), 0.1) 327 | height = max(abs(self.y_plot_origin - yPlotPos), 0.1) 328 | 329 | self.main_window.editWidth(width) 330 | self.main_window.editHeight(height) 331 | 332 | def mouseReleaseEvent(self, event): 333 | 334 | if self.rubber_band.isVisible(): 335 | self.rubber_band.hide() 336 | self.main_window.applyChanges() 337 | else: 338 | self.main_window.revertDockControls() 339 | 340 | def wheelEvent(self, event): 341 | 342 | if event.angleDelta() and event.modifiers() == QtCore.Qt.ShiftModifier: 343 | numDegrees = event.angleDelta() / 8 344 | 345 | if 24 < self.main_window.zoom + numDegrees < 5001: 346 | self.main_window.editZoom(self.main_window.zoom + numDegrees) 347 | 348 | def contextMenuEvent(self, event): 349 | 350 | self.menu.clear() 351 | 352 | self.main_window.undoAction.setText( 353 | '&Undo ({})'.format(len(self.model.previousViews))) 354 | self.main_window.redoAction.setText( 355 | '&Redo ({})'.format(len(self.model.subsequentViews))) 356 | 357 | id, instance, properties, domain, domain_kind = self.getIDinfo(event) 358 | 359 | cv = self.model.currentView 360 | 361 | # always provide undo option 362 | self.menu.addSeparator() 363 | self.menu.addAction(self.main_window.undoAction) 364 | self.menu.addAction(self.main_window.redoAction) 365 | self.menu.addSeparator() 366 | 367 | if int(id) not in (_NOT_FOUND, _OVERLAP) and \ 368 | cv.colorby not in _MODEL_PROPERTIES: 369 | 370 | # Domain ID 371 | if domain[id].name: 372 | domainID = self.menu.addAction( 373 | "{} {} Info: \"{}\"".format(domain_kind, id, domain[id].name)) 374 | else: 375 | domainID = self.menu.addAction( 376 | "{} {} Info".format(domain_kind, id)) 377 | 378 | # add connector to a new window of info here for material props 379 | if domain_kind == 'Material': 380 | mat_prop_connector = partial( 381 | self.main_window.viewMaterialProps, id) 382 | domainID.triggered.connect(mat_prop_connector) 383 | 384 | self.menu.addSeparator() 385 | 386 | colorAction = self.menu.addAction( 387 | 'Edit {} Color...'.format(domain_kind)) 388 | colorAction.setDisabled(cv.highlighting) 389 | colorAction.setToolTip('Edit {} color'.format(domain_kind)) 390 | colorAction.setStatusTip('Edit {} color'.format(domain_kind)) 391 | domain_color_connector = partial(self.main_window.editDomainColor, 392 | domain_kind, 393 | id) 394 | colorAction.triggered.connect(domain_color_connector) 395 | 396 | maskAction = self.menu.addAction('Mask {}'.format(domain_kind)) 397 | maskAction.setCheckable(True) 398 | maskAction.setChecked(domain[id].masked) 399 | maskAction.setDisabled(not cv.masking) 400 | maskAction.setToolTip('Toggle {} mask'.format(domain_kind)) 401 | maskAction.setStatusTip('Toggle {} mask'.format(domain_kind)) 402 | mask_connector = partial(self.main_window.toggleDomainMask, 403 | kind=domain_kind, 404 | id=id) 405 | maskAction.toggled.connect(mask_connector) 406 | 407 | highlightAction = self.menu.addAction( 408 | 'Highlight {}'.format(domain_kind)) 409 | highlightAction.setCheckable(True) 410 | highlightAction.setChecked(domain[id].highlight) 411 | highlightAction.setDisabled(not cv.highlighting) 412 | highlightAction.setToolTip( 413 | 'Toggle {} highlight'.format(domain_kind)) 414 | highlightAction.setStatusTip( 415 | 'Toggle {} highlight'.format(domain_kind)) 416 | highlight_connector = partial(self.main_window.toggleDomainHighlight, 417 | kind=domain_kind, 418 | id=id) 419 | highlightAction.toggled.connect(highlight_connector) 420 | 421 | else: 422 | self.menu.addAction(self.main_window.undoAction) 423 | self.menu.addAction(self.main_window.redoAction) 424 | 425 | if cv.colorby not in _MODEL_PROPERTIES: 426 | self.menu.addSeparator() 427 | if int(id) == _NOT_FOUND: 428 | bgColorAction = self.menu.addAction( 429 | 'Edit Background Color...') 430 | bgColorAction.setToolTip('Edit background color') 431 | bgColorAction.setStatusTip('Edit plot background color') 432 | connector = partial(self.main_window.editBackgroundColor, 433 | apply=True) 434 | bgColorAction.triggered.connect(connector) 435 | elif int(id) == _OVERLAP: 436 | olapColorAction = self.menu.addAction( 437 | 'Edit Overlap Color...') 438 | olapColorAction.setToolTip('Edit overlap color') 439 | olapColorAction.setStatusTip('Edit plot overlap color') 440 | connector = partial(self.main_window.editOverlapColor, 441 | apply=True) 442 | olapColorAction.triggered.connect(connector) 443 | 444 | self.menu.addSeparator() 445 | self.menu.addAction(self.main_window.saveImageAction) 446 | self.menu.addAction(self.main_window.saveViewAction) 447 | self.menu.addAction(self.main_window.openAction) 448 | self.menu.addSeparator() 449 | self.menu.addMenu(self.main_window.basisMenu) 450 | self.menu.addMenu(self.main_window.colorbyMenu) 451 | self.menu.addSeparator() 452 | if domain_kind.lower() not in ('density', 'temperature'): 453 | self.menu.addAction(self.main_window.maskingAction) 454 | self.menu.addAction(self.main_window.highlightingAct) 455 | self.menu.addAction(self.main_window.overlapAct) 456 | self.menu.addSeparator() 457 | self.menu.addAction(self.main_window.dockAction) 458 | 459 | self.main_window.maskingAction.setChecked(cv.masking) 460 | self.main_window.highlightingAct.setChecked(cv.highlighting) 461 | self.main_window.overlapAct.setChecked(cv.color_overlaps) 462 | 463 | if self.main_window.dock.isVisible(): 464 | self.main_window.dockAction.setText('Hide &Dock') 465 | else: 466 | self.main_window.dockAction.setText('Show &Dock') 467 | 468 | self.menu.exec(event.globalPos()) 469 | 470 | def generatePixmap(self, update=False): 471 | if self.frozen: 472 | return 473 | 474 | self.model.generatePlot() 475 | if update: 476 | self.updatePixmap() 477 | 478 | def updatePixmap(self): 479 | 480 | # clear out figure 481 | self.figure.clear() 482 | 483 | cv = self.model.currentView 484 | # set figure bg color to match window 485 | window_bg = self.parent.palette().color(QtGui.QPalette.Window) 486 | self.figure.patch.set_facecolor(rgb_normalize(window_bg.getRgb())) 487 | 488 | # set data extents for automatic reporting of pointer location 489 | # in model units 490 | data_bounds = [cv.origin[self.main_window.xBasis] - cv.width/2., 491 | cv.origin[self.main_window.xBasis] + cv.width/2., 492 | cv.origin[self.main_window.yBasis] - cv.height/2., 493 | cv.origin[self.main_window.yBasis] + cv.height/2.] 494 | 495 | # make sure we have a domain image to load 496 | if not hasattr(self.model, 'image'): 497 | self.model.generatePlot() 498 | 499 | ### DRAW DOMAIN IMAGE ### 500 | 501 | # still generate the domain image if the geometric 502 | # plot isn't visible so mouse-over info can still 503 | # be shown 504 | alpha = cv.domainAlpha if cv.domainVisible else 0.0 505 | if cv.colorby in ('material', 'cell'): 506 | self.image = self.figure.subplots().imshow(self.model.image, 507 | extent=data_bounds, 508 | alpha=alpha) 509 | else: 510 | cmap = cv.colormaps[cv.colorby] 511 | if cv.colorby == 'temperature': 512 | idx = 0 513 | cmap_label = "Temperature (K)" 514 | else: 515 | idx = 1 516 | cmap_label = "Density (g/cc)" 517 | 518 | norm = SymLogNorm( 519 | 1E-10) if cv.color_scale_log[cv.colorby] else None 520 | 521 | data = self.model.properties[:, :, idx] 522 | self.image = self.figure.subplots().imshow(data, 523 | cmap=cmap, 524 | norm=norm, 525 | extent=data_bounds, 526 | alpha=cv.domainAlpha) 527 | 528 | # add colorbar 529 | self.colorbar = self.figure.colorbar(self.image, 530 | anchor=(1.0, 0.0)) 531 | self.colorbar.set_label(cmap_label, 532 | rotation=-90, 533 | labelpad=15) 534 | # draw line on colorbar 535 | dl = self.colorbar.ax.dataLim.get_points() 536 | self.data_indicator = mlines.Line2D(dl[:][0], 537 | [0.0, 0.0], 538 | linewidth=3., 539 | color='blue', 540 | clip_on=True) 541 | self.colorbar.ax.add_line(self.data_indicator) 542 | self.colorbar.ax.margins(0.0, 0.0) 543 | self.updateDataIndicatorVisibility() 544 | self.updateColorMinMax(cv.colorby) 545 | 546 | self.ax = self.figure.axes[0] 547 | self.ax.margins(0.0, 0.0) 548 | 549 | # set axis labels 550 | axis_label_str = "{} (cm)" 551 | self.ax.set_xlabel(axis_label_str.format(cv.basis[0])) 552 | self.ax.set_ylabel(axis_label_str.format(cv.basis[1])) 553 | 554 | # generate tally image 555 | image_data, extents, data_min, data_max, units = self.model.create_tally_image() 556 | 557 | ### DRAW TALLY IMAGE ### 558 | 559 | # draw tally image 560 | if image_data is not None: 561 | 562 | if not cv.tallyDataUserMinMax: 563 | cv.tallyDataMin = data_min 564 | cv.tallyDataMax = data_max 565 | else: 566 | data_min = cv.tallyDataMin 567 | data_max = cv.tallyDataMax 568 | 569 | # always mask out negative values 570 | image_mask = image_data < 0.0 571 | 572 | if cv.clipTallyData: 573 | image_mask |= image_data < data_min 574 | image_mask |= image_data > data_max 575 | 576 | if cv.tallyMaskZeroValues: 577 | image_mask |= image_data == 0.0 578 | 579 | # mask out invalid values 580 | image_data = np.ma.masked_where(image_mask, image_data) 581 | 582 | if extents is None: 583 | extents = data_bounds 584 | 585 | self.model.tally_data = image_data 586 | self.model.tally_extents = extents if extents is not None else data_bounds 587 | 588 | norm = SymLogNorm(1E-30) if cv.tallyDataLogScale else None 589 | 590 | cmap = cv.tallyDataColormap 591 | if cv.tallyDataReverseCmap: 592 | cmap += '_r' 593 | 594 | if cv.tallyContours: 595 | # parse the levels line 596 | levels = self.parseContoursLine(cv.tallyContourLevels) 597 | self.tally_image = self.ax.contour(image_data, 598 | origin='image', 599 | levels=levels, 600 | alpha=cv.tallyDataAlpha, 601 | cmap=cmap, 602 | norm=norm, 603 | extent=extents, 604 | algorithm='serial') 605 | 606 | else: 607 | self.tally_image = self.ax.imshow(image_data, 608 | alpha=cv.tallyDataAlpha, 609 | cmap=cmap, 610 | norm=norm, 611 | extent=extents) 612 | # add colorbar 613 | self.tally_colorbar = self.figure.colorbar(self.tally_image, 614 | anchor=(1.0, 0.0)) 615 | 616 | if cv.tallyContours: 617 | fmt = "%.2E" 618 | self.ax.clabel(self.tally_image, 619 | self.tally_image.levels, 620 | inline=True, 621 | fmt=fmt) 622 | 623 | # draw line on colorbar 624 | self.tally_data_indicator = mlines.Line2D([0.0, 1.0], 625 | [0.0, 0.0], 626 | linewidth=3., 627 | color='blue', 628 | clip_on=True) 629 | self.tally_colorbar.ax.add_line(self.tally_data_indicator) 630 | self.tally_colorbar.ax.margins(0.0, 0.0) 631 | 632 | self.tally_data_indicator.set_visible(cv.tallyDataIndicator) 633 | 634 | self.main_window.updateTallyMinMax() 635 | 636 | self.tally_colorbar.mappable.set_clim(data_min, data_max) 637 | self.tally_colorbar.set_label(units, 638 | rotation=-90, 639 | labelpad=15) 640 | 641 | # annotate outlines 642 | self.add_outlines() 643 | self.plotSourceSites() 644 | 645 | # annotate mesh boundaries 646 | for mesh_id in cv.mesh_annotations: 647 | self.annotate_mesh(mesh_id) 648 | 649 | # always make sure the data bounds are set correctly 650 | self.ax.set_xbound(data_bounds[0], data_bounds[1]) 651 | self.ax.set_ybound(data_bounds[2], data_bounds[3]) 652 | self.ax.dataLim.x0 = data_bounds[0] 653 | self.ax.dataLim.x1 = data_bounds[1] 654 | self.ax.dataLim.y0 = data_bounds[2] 655 | self.ax.dataLim.y1 = data_bounds[3] 656 | 657 | self.draw() 658 | return "Done" 659 | 660 | def current_view_data_bounds(self): 661 | cv = self.model.currentView 662 | return [cv.origin[self.main_window.xBasis] - cv.width/2., 663 | cv.origin[self.main_window.xBasis] + cv.width/2., 664 | cv.origin[self.main_window.yBasis] - cv.height/2., 665 | cv.origin[self.main_window.yBasis] + cv.height/2.] 666 | 667 | def annotate_mesh(self, mesh_id): 668 | mesh_bins = self.model.mesh_plot_bins(mesh_id) 669 | 670 | data_bounds = self.current_view_data_bounds() 671 | self.mesh_contours = self.ax.contour( 672 | mesh_bins, 673 | origin='upper', 674 | colors='k', 675 | linestyles='solid', 676 | levels=np.unique(mesh_bins), 677 | extent=data_bounds, 678 | algorithm='serial' 679 | ) 680 | 681 | def plotSourceSites(self): 682 | if not self.model.sourceSitesVisible or self.model.sourceSites is None: 683 | return 684 | 685 | cv = self.model.currentView 686 | basis = cv.view_params.basis 687 | 688 | h_idx = 'xyz'.index(basis[0]) 689 | v_idx = 'xyz'.index(basis[1]) 690 | 691 | sites = self.model.sourceSites 692 | 693 | slice_ax = cv.view_params.slice_axis 694 | 695 | if self.model.sourceSitesApplyTolerance: 696 | sites_to_plot = sites[np.abs(sites[:, slice_ax] - cv.origin[slice_ax]) <= self.model.sourceSitesTolerance] 697 | else: 698 | sites_to_plot = sites 699 | 700 | self.ax.scatter([s[h_idx] for s in sites_to_plot], 701 | [s[v_idx] for s in sites_to_plot], 702 | marker='o', 703 | color=rgb_normalize(self.model.sourceSitesColor)) 704 | 705 | def add_outlines(self): 706 | cv = self.model.currentView 707 | # draw outlines as isocontours 708 | if cv.outlines: 709 | # set data extents for automatic reporting of pointer location 710 | data_bounds = self.current_view_data_bounds() 711 | levels = np.unique(self.model.ids) 712 | self.contours = self.ax.contour(self.model.ids, 713 | origin='upper', 714 | colors='k', 715 | linestyles='solid', 716 | levels=levels, 717 | extent=data_bounds, 718 | algorithm='serial') 719 | 720 | @staticmethod 721 | def parseContoursLine(line): 722 | # if there are any commas in the line, treat as level values 723 | line = line.strip() 724 | if ',' in line: 725 | return [float(val) for val in line.split(",") if val != ''] 726 | else: 727 | return int(line) 728 | 729 | def updateColorbarScale(self): 730 | self.updatePixmap() 731 | 732 | def updateTallyDataIndicatorValue(self, y_val): 733 | cv = self.model.currentView 734 | 735 | if not cv.tallyDataVisible or not cv.tallyDataIndicator: 736 | return 737 | 738 | if self.tally_data_indicator is not None: 739 | data = self.tally_data_indicator.get_data() 740 | # use norm to get axis value if log scale 741 | if cv.tallyDataLogScale: 742 | y_val = self.tally_image.norm(y_val) 743 | self.tally_data_indicator.set_data([data[0], [y_val, y_val]]) 744 | dl_color = invert_rgb(self.tally_image.get_cmap()(y_val), True) 745 | self.tally_data_indicator.set_c(dl_color) 746 | self.draw() 747 | 748 | def updateDataIndicatorValue(self, y_val): 749 | cv = self.model.currentView 750 | 751 | if cv.colorby not in _MODEL_PROPERTIES or \ 752 | not cv.data_indicator_enabled[cv.colorby]: 753 | return 754 | 755 | if self.data_indicator: 756 | data = self.data_indicator.get_data() 757 | # use norm to get axis value if log scale 758 | if cv.color_scale_log[cv.colorby]: 759 | y_val = self.image.norm(y_val) 760 | self.data_indicator.set_data([data[0], [y_val, y_val]]) 761 | dl_color = invert_rgb(self.image.get_cmap()(y_val), True) 762 | self.data_indicator.set_c(dl_color) 763 | self.draw() 764 | 765 | def updateDataIndicatorVisibility(self): 766 | cv = self.model.currentView 767 | if self.data_indicator and cv.colorby in _MODEL_PROPERTIES: 768 | val = cv.data_indicator_enabled[cv.colorby] 769 | self.data_indicator.set_visible(val) 770 | self.draw() 771 | 772 | def updateColorMap(self, colormap_name, property_type): 773 | if self.colorbar and property_type == self.model.activeView.colorby: 774 | self.image.set_cmap(colormap_name) 775 | self.figure.draw_without_rendering() 776 | self.draw() 777 | 778 | def updateColorMinMax(self, property_type): 779 | av = self.model.activeView 780 | if self.colorbar and property_type == av.colorby: 781 | clim = av.getColorLimits(property_type) 782 | self.colorbar.mappable.set_clim(*clim) 783 | self.data_indicator.set_data(clim[:2], 784 | (0.0, 0.0)) 785 | self.figure.draw_without_rendering() 786 | self.draw() 787 | 788 | 789 | class ColorDialog(QDialog): 790 | 791 | def __init__(self, model, font_metric, parent=None): 792 | super().__init__(parent) 793 | 794 | self.setWindowTitle('Color Options') 795 | 796 | self.model = model 797 | self.font_metric = font_metric 798 | self.main_window = parent 799 | 800 | self.createDialogLayout() 801 | 802 | def createDialogLayout(self): 803 | 804 | self.createGeneralTab() 805 | 806 | self.cellTable = self.createDomainTable(self.main_window.cellsModel) 807 | self.matTable = self.createDomainTable(self.main_window.materialsModel) 808 | self.tabs = {'cell': self.createDomainTab(self.cellTable), 809 | 'material': self.createDomainTab(self.matTable), 810 | 'temperature': self.createPropertyTab('temperature'), 811 | 'density': self.createPropertyTab('density')} 812 | 813 | self.tab_bar = QTabWidget() 814 | self.tab_bar.setMaximumHeight(800) 815 | self.tab_bar.setSizePolicy( 816 | QSizePolicy.Expanding, QSizePolicy.Expanding) 817 | self.tab_bar.addTab(self.generalTab, 'General') 818 | self.tab_bar.addTab(self.tabs['cell'], 'Cells') 819 | self.tab_bar.addTab(self.tabs['material'], 'Materials') 820 | self.tab_bar.addTab(self.tabs['temperature'], 'Temperature') 821 | self.tab_bar.addTab(self.tabs['density'], 'Density') 822 | 823 | self.createButtonBox() 824 | 825 | self.colorDialogLayout = QVBoxLayout() 826 | self.colorDialogLayout.addWidget(self.tab_bar) 827 | self.colorDialogLayout.addWidget(self.buttonBox) 828 | self.setLayout(self.colorDialogLayout) 829 | 830 | def createGeneralTab(self): 831 | 832 | main_window = self.main_window 833 | 834 | # Masking options 835 | self.maskingCheck = QCheckBox('') 836 | self.maskingCheck.stateChanged.connect(main_window.toggleMasking) 837 | 838 | button_width = self.font_metric.boundingRect("XXXXXXXXXX").width() 839 | self.maskColorButton = QPushButton() 840 | self.maskColorButton.setCursor(QtCore.Qt.PointingHandCursor) 841 | self.maskColorButton.setFixedWidth(button_width) 842 | self.maskColorButton.setFixedHeight(self.font_metric.height() * 1.5) 843 | self.maskColorButton.clicked.connect(main_window.editMaskingColor) 844 | 845 | # Highlighting options 846 | self.hlCheck = QCheckBox('') 847 | self.hlCheck.stateChanged.connect(main_window.toggleHighlighting) 848 | 849 | self.hlColorButton = QPushButton() 850 | self.hlColorButton.setCursor(QtCore.Qt.PointingHandCursor) 851 | self.hlColorButton.setFixedWidth(button_width) 852 | self.hlColorButton.setFixedHeight(self.font_metric.height() * 1.5) 853 | self.hlColorButton.clicked.connect(main_window.editHighlightColor) 854 | 855 | self.alphaBox = QDoubleSpinBox() 856 | self.alphaBox.setRange(0, 1) 857 | self.alphaBox.setSingleStep(.05) 858 | self.alphaBox.valueChanged.connect(main_window.editAlpha) 859 | 860 | self.seedBox = QSpinBox() 861 | self.seedBox.setRange(1, 999) 862 | self.seedBox.valueChanged.connect(main_window.editSeed) 863 | 864 | # General options 865 | self.bgButton = QPushButton() 866 | self.bgButton.setCursor(QtCore.Qt.PointingHandCursor) 867 | self.bgButton.setFixedWidth(button_width) 868 | self.bgButton.setFixedHeight(self.font_metric.height() * 1.5) 869 | self.bgButton.clicked.connect(main_window.editBackgroundColor) 870 | 871 | self.colorbyBox = QComboBox(self) 872 | self.colorbyBox.addItem("material") 873 | self.colorbyBox.addItem("cell") 874 | self.colorbyBox.addItem("temperature") 875 | self.colorbyBox.addItem("density") 876 | self.colorbyBox.currentTextChanged[str].connect( 877 | main_window.editColorBy) 878 | 879 | self.universeLevelBox = QComboBox(self) 880 | self.universeLevelBox.addItem('all') 881 | for i in range(self.model.max_universe_levels): 882 | self.universeLevelBox.addItem(str(i)) 883 | self.universeLevelBox.currentTextChanged[str].connect( 884 | main_window.editUniverseLevel) 885 | 886 | # Overlap plotting 887 | self.overlapCheck = QCheckBox('', self) 888 | overlap_connector = partial(main_window.toggleOverlaps) 889 | self.overlapCheck.stateChanged.connect(overlap_connector) 890 | 891 | self.overlapColorButton = QPushButton() 892 | self.overlapColorButton.setCursor(QtCore.Qt.PointingHandCursor) 893 | self.overlapColorButton.setFixedWidth(button_width) 894 | self.overlapColorButton.setFixedHeight(self.font_metric.height() * 1.5) 895 | self.overlapColorButton.clicked.connect(main_window.editOverlapColor) 896 | 897 | self.colorResetButton = QPushButton("&Reset Colors") 898 | self.colorResetButton.setCursor(QtCore.Qt.PointingHandCursor) 899 | self.colorResetButton.clicked.connect(main_window.resetColors) 900 | 901 | formLayout = QFormLayout() 902 | formLayout.setAlignment(QtCore.Qt.AlignHCenter) 903 | formLayout.setFormAlignment(QtCore.Qt.AlignHCenter) 904 | formLayout.setLabelAlignment(QtCore.Qt.AlignLeft) 905 | 906 | formLayout.addRow('Masking:', self.maskingCheck) 907 | formLayout.addRow('Mask Color:', self.maskColorButton) 908 | formLayout.addRow(HorizontalLine()) 909 | formLayout.addRow('Highlighting:', self.hlCheck) 910 | formLayout.addRow('Highlight Color:', self.hlColorButton) 911 | formLayout.addRow('Highlight Alpha:', self.alphaBox) 912 | formLayout.addRow('Highlight Seed:', self.seedBox) 913 | formLayout.addRow(HorizontalLine()) 914 | formLayout.addRow('Background Color: ', self.bgButton) 915 | formLayout.addRow(HorizontalLine()) 916 | formLayout.addRow('Show Overlaps:', self.overlapCheck) 917 | formLayout.addRow('Overlap Color:', self.overlapColorButton) 918 | formLayout.addRow(HorizontalLine()) 919 | formLayout.addRow('Color Plot By:', self.colorbyBox) 920 | formLayout.addRow('Universe Level:', self.universeLevelBox) 921 | formLayout.addRow(self.colorResetButton, None) 922 | 923 | generalLayout = QHBoxLayout() 924 | innerWidget = QWidget() 925 | generalLayout.setAlignment(QtCore.Qt.AlignVCenter) 926 | innerWidget.setLayout(formLayout) 927 | generalLayout.addStretch(1) 928 | generalLayout.addWidget(innerWidget) 929 | generalLayout.addStretch(1) 930 | 931 | self.generalTab = QWidget() 932 | self.generalTab.setLayout(generalLayout) 933 | 934 | def createDomainTable(self, domainmodel): 935 | 936 | domainTable = QTableView() 937 | domainTable.setModel(domainmodel) 938 | domainTable.setItemDelegate(DomainDelegate(domainTable)) 939 | domainTable.verticalHeader().setVisible(False) 940 | domainTable.resizeColumnsToContents() 941 | domainTable.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 942 | domainTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) 943 | 944 | return domainTable 945 | 946 | def createDomainTab(self, domaintable): 947 | 948 | domainTab = QWidget() 949 | domainTab.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 950 | domainLayout = QVBoxLayout() 951 | domainLayout.addWidget(domaintable) 952 | domainTab.setLayout(domainLayout) 953 | 954 | return domainTab 955 | 956 | def createPropertyTab(self, property_kind): 957 | propertyTab = QWidget() 958 | propertyTab.property_kind = property_kind 959 | propertyTab.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 960 | propertyLayout = QVBoxLayout() 961 | 962 | propertyTab.minMaxCheckBox = QCheckBox() 963 | propertyTab.minMaxCheckBox.setCheckable(True) 964 | connector1 = partial(self.main_window.toggleUserMinMax, 965 | property=property_kind) 966 | propertyTab.minMaxCheckBox.stateChanged.connect(connector1) 967 | 968 | propertyTab.minBox = ScientificDoubleSpinBox(self) 969 | propertyTab.minBox.setMaximum(1E9) 970 | propertyTab.minBox.setMinimum(0) 971 | propertyTab.maxBox = ScientificDoubleSpinBox(self) 972 | propertyTab.maxBox.setMaximum(1E9) 973 | propertyTab.maxBox.setMinimum(0) 974 | 975 | connector2 = partial(self.main_window.editColorbarMin, 976 | property_type=property_kind) 977 | propertyTab.minBox.valueChanged.connect(connector2) 978 | connector3 = partial(self.main_window.editColorbarMax, 979 | property_type=property_kind) 980 | propertyTab.maxBox.valueChanged.connect(connector3) 981 | 982 | propertyTab.colormapBox = QComboBox(self) 983 | cmaps = sorted(m for m in plt.colormaps() 984 | if not m.endswith("_r")) 985 | for cmap in cmaps: 986 | propertyTab.colormapBox.addItem(cmap) 987 | 988 | connector = partial(self.main_window.editColorMap, 989 | property_type=property_kind) 990 | 991 | propertyTab.colormapBox.currentTextChanged[str].connect(connector) 992 | 993 | propertyTab.dataIndicatorCheckBox = QCheckBox() 994 | propertyTab.dataIndicatorCheckBox.setCheckable(True) 995 | connector4 = partial(self.main_window.toggleDataIndicatorCheckBox, 996 | property=property_kind) 997 | propertyTab.dataIndicatorCheckBox.stateChanged.connect(connector4) 998 | 999 | propertyTab.colorBarScaleCheckBox = QCheckBox() 1000 | propertyTab.colorBarScaleCheckBox.setCheckable(True) 1001 | connector5 = partial(self.main_window.toggleColorbarScale, 1002 | property=property_kind) 1003 | propertyTab.colorBarScaleCheckBox.stateChanged.connect(connector5) 1004 | 1005 | formLayout = QFormLayout() 1006 | formLayout.setAlignment(QtCore.Qt.AlignHCenter) 1007 | formLayout.setFormAlignment(QtCore.Qt.AlignHCenter) 1008 | formLayout.setLabelAlignment(QtCore.Qt.AlignLeft) 1009 | 1010 | formLayout.addRow('Colormap:', propertyTab.colormapBox) 1011 | 1012 | formLayout.addRow('Custom Min/Max', propertyTab.minMaxCheckBox) 1013 | formLayout.addRow('Data Indicator', propertyTab.dataIndicatorCheckBox) 1014 | formLayout.addRow('Log Scale', propertyTab.colorBarScaleCheckBox) 1015 | formLayout.addRow(HorizontalLine()) 1016 | formLayout.addRow('Max: ', propertyTab.maxBox) 1017 | formLayout.addRow('Min: ', propertyTab.minBox) 1018 | 1019 | propertyTab.setLayout(formLayout) 1020 | 1021 | return propertyTab 1022 | 1023 | def updateDataIndicatorVisibility(self): 1024 | av = self.model.activeView 1025 | for key, val in av.data_indicator_enabled.items(): 1026 | self.tabs[key].dataIndicatorCheckBox.setChecked(val) 1027 | 1028 | def updateColorMaps(self): 1029 | cmaps = self.model.activeView.colormaps 1030 | for key, val in cmaps.items(): 1031 | idx = self.tabs[key].colormapBox.findText( 1032 | val, 1033 | QtCore.Qt.MatchFixedString) 1034 | if idx >= 0: 1035 | self.tabs[key].colormapBox.setCurrentIndex(idx) 1036 | 1037 | def updateColorMinMax(self): 1038 | minmax = self.model.activeView.user_minmax 1039 | for key, val in minmax.items(): 1040 | self.tabs[key].minBox.setValue(val[0]) 1041 | self.tabs[key].maxBox.setValue(val[1]) 1042 | custom_minmax = self.model.activeView.use_custom_minmax 1043 | for key, val, in custom_minmax.items(): 1044 | self.tabs[key].minMaxCheckBox.setChecked(val) 1045 | self.tabs[key].minBox.setEnabled(val) 1046 | self.tabs[key].maxBox.setEnabled(val) 1047 | 1048 | def updateColorbarScale(self): 1049 | av = self.model.activeView 1050 | for key, val in av.color_scale_log.items(): 1051 | self.tabs[key].colorBarScaleCheckBox.setChecked(val) 1052 | 1053 | def createButtonBox(self): 1054 | 1055 | applyButton = QPushButton("Apply Changes") 1056 | applyButton.clicked.connect(self.main_window.applyChanges) 1057 | closeButton = QPushButton("Close") 1058 | closeButton.clicked.connect(self.hide) 1059 | 1060 | buttonLayout = QHBoxLayout() 1061 | buttonLayout.addStretch(1) 1062 | buttonLayout.addWidget(applyButton) 1063 | buttonLayout.addWidget(closeButton) 1064 | 1065 | self.buttonBox = QWidget() 1066 | self.buttonBox.setLayout(buttonLayout) 1067 | 1068 | def updateDialogValues(self): 1069 | 1070 | self.updateMasking() 1071 | self.updateMaskingColor() 1072 | self.updateColorMaps() 1073 | self.updateColorMinMax() 1074 | self.updateColorbarScale() 1075 | self.updateDataIndicatorVisibility() 1076 | self.updateHighlighting() 1077 | self.updateHighlightColor() 1078 | self.updateAlpha() 1079 | self.updateSeed() 1080 | self.updateBackgroundColor() 1081 | self.updateColorBy() 1082 | self.updateUniverseLevel() 1083 | self.updateDomainTabs() 1084 | self.updateOverlap() 1085 | self.updateOverlapColor() 1086 | 1087 | def updateMasking(self): 1088 | masking = self.model.activeView.masking 1089 | 1090 | self.maskingCheck.setChecked(masking) 1091 | self.maskColorButton.setDisabled(not masking) 1092 | 1093 | if masking: 1094 | self.cellTable.showColumn(4) 1095 | self.matTable.showColumn(4) 1096 | else: 1097 | self.cellTable.hideColumn(4) 1098 | self.matTable.hideColumn(4) 1099 | 1100 | def updateMaskingColor(self): 1101 | color = self.model.activeView.maskBackground 1102 | style_values = "border-radius: 8px; background-color: rgb{}" 1103 | self.maskColorButton.setStyleSheet(style_values.format(str(color))) 1104 | 1105 | def updateHighlighting(self): 1106 | highlighting = self.model.activeView.highlighting 1107 | 1108 | self.hlCheck.setChecked(highlighting) 1109 | self.hlColorButton.setDisabled(not highlighting) 1110 | self.alphaBox.setDisabled(not highlighting) 1111 | self.seedBox.setDisabled(not highlighting) 1112 | 1113 | if highlighting: 1114 | self.cellTable.showColumn(5) 1115 | self.cellTable.hideColumn(2) 1116 | self.cellTable.hideColumn(3) 1117 | self.matTable.showColumn(5) 1118 | self.matTable.hideColumn(2) 1119 | self.matTable.hideColumn(3) 1120 | else: 1121 | self.cellTable.hideColumn(5) 1122 | self.cellTable.showColumn(2) 1123 | self.cellTable.showColumn(3) 1124 | self.matTable.hideColumn(5) 1125 | self.matTable.showColumn(2) 1126 | self.matTable.showColumn(3) 1127 | 1128 | def updateHighlightColor(self): 1129 | color = self.model.activeView.highlightBackground 1130 | style_values = "border-radius: 8px; background-color: rgb{}" 1131 | self.hlColorButton.setStyleSheet(style_values.format(str(color))) 1132 | 1133 | def updateAlpha(self): 1134 | self.alphaBox.setValue(self.model.activeView.highlightAlpha) 1135 | 1136 | def updateSeed(self): 1137 | self.seedBox.setValue(self.model.activeView.highlightSeed) 1138 | 1139 | def updateBackgroundColor(self): 1140 | color = self.model.activeView.domainBackground 1141 | self.bgButton.setStyleSheet("border-radius: 8px;" 1142 | "background-color: rgb%s" % (str(color))) 1143 | 1144 | def updateOverlapColor(self): 1145 | color = self.model.activeView.overlap_color 1146 | self.overlapColorButton.setStyleSheet("border-radius: 8px;" 1147 | "background-color: rgb%s" % (str(color))) 1148 | 1149 | def updateOverlap(self): 1150 | colorby = self.model.activeView.colorby 1151 | overlap_val = self.model.activeView.color_overlaps 1152 | if colorby in ('cell', 'material'): 1153 | self.overlapCheck.setChecked(overlap_val) 1154 | 1155 | def updateColorBy(self): 1156 | colorby = self.model.activeView.colorby 1157 | self.colorbyBox.setCurrentText(colorby) 1158 | self.overlapCheck.setEnabled(colorby in ("cell", "material")) 1159 | self.universeLevelBox.setEnabled(colorby == 'cell') 1160 | 1161 | def updateUniverseLevel(self): 1162 | level = self.model.activeView.level 1163 | if level == -1: 1164 | self.universeLevelBox.setCurrentText('all') 1165 | else: 1166 | self.universeLevelBox.setCurrentText(str(level)) 1167 | 1168 | def updateDomainTabs(self): 1169 | self.cellTable.setModel(self.main_window.cellsModel) 1170 | self.matTable.setModel(self.main_window.materialsModel) 1171 | -------------------------------------------------------------------------------- /openmc_plotter/scientific_spin_box.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from PySide6 import QtGui 4 | from PySide6.QtWidgets import QDoubleSpinBox 5 | import numpy as np 6 | 7 | # Regular expression to find floats. Match groups are the whole string, the 8 | # whole coefficient, the decimal part of the coefficient, and the exponent 9 | # part. 10 | _float_re = re.compile(r'(([+-]?\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)') 11 | 12 | 13 | class FloatValidator(QtGui.QValidator): 14 | """ 15 | Validator class for floats in scientific notation 16 | """ 17 | def validate(self, string, position): 18 | if self.valid_float_string(string): 19 | return self.State.Acceptable 20 | if string == "" or string[position-1] in 'e.-+': 21 | return self.State.Intermediate 22 | return self.State.Invalid 23 | 24 | @staticmethod 25 | def valid_float_string(string): 26 | match = _float_re.search(string) 27 | return match.groups()[0] == string if match else False 28 | 29 | def fixup(self, text): 30 | match = _float_re.search(text) 31 | return match.groups()[0] if match else "" 32 | 33 | 34 | class ScientificDoubleSpinBox(QDoubleSpinBox): 35 | """ 36 | Double spin box which allows use of scientific notation 37 | """ 38 | def __init__(self, *args, **kwargs): 39 | super().__init__(*args, **kwargs) 40 | self.setMinimum(-np.inf) 41 | self.setMaximum(np.inf) 42 | self.validator = FloatValidator() 43 | self.setDecimals(1000) 44 | 45 | def validate(self, text, position): 46 | return self.validator.validate(text, position) 47 | 48 | def fixup(self, text): 49 | return self.validator.fixup(text) 50 | 51 | def valueFromText(self, text): 52 | return float(text) 53 | 54 | def textFromValue(self, value): 55 | """Modified form of the 'g' format specifier.""" 56 | flt_str = "{:g}".format(value).replace("e+", "e") 57 | flt_str = re.sub(r"e(-?)0*(\d+)", r"e\1\2", flt_str) 58 | return flt_str 59 | 60 | def stepBy(self, steps): 61 | text = self.cleanText() 62 | groups = _float_re.search(text).groups() 63 | decimal = float(groups[1]) + steps 64 | new_string = "{:g}".format(decimal) + (groups[3] if groups[3] else "") 65 | self.lineEdit().setText(new_string) 66 | -------------------------------------------------------------------------------- /openmc_plotter/statepointmodel.py: -------------------------------------------------------------------------------- 1 | 2 | import openmc 3 | 4 | 5 | class StatePointModel(): 6 | """ 7 | Class for management of an openmc.StatePoint instance 8 | in the plotting application 9 | """ 10 | def __init__(self, filename, open_file=False): 11 | self.filename = filename 12 | self._sp = None 13 | self.is_open = False 14 | 15 | if open_file: 16 | self.open() 17 | 18 | @property 19 | def tallies(self): 20 | return self._sp.tallies if self.is_open else {} 21 | 22 | @property 23 | def filters(self): 24 | return self._sp.filters if self.is_open else {} 25 | 26 | @property 27 | def universes(self): 28 | if self.is_open and self._sp.summary is not None: 29 | return self._sp.summary.geometry.get_all_universes() 30 | else: 31 | return {} 32 | 33 | def open(self): 34 | if self.is_open: 35 | return 36 | self._sp = openmc.StatePoint(self.filename) 37 | self.is_open = True 38 | 39 | def close(self): 40 | self._sp = None 41 | self.is_open = False 42 | -------------------------------------------------------------------------------- /openmc_plotter/tools.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import numpy as np 4 | import openmc 5 | from PySide6 import QtCore, QtWidgets 6 | 7 | from .custom_widgets import HorizontalLine 8 | from .scientific_spin_box import ScientificDoubleSpinBox 9 | 10 | 11 | class SourceSitesDialog(QtWidgets.QDialog): 12 | def __init__(self, model, font_metric, parent=None): 13 | super().__init__(parent) 14 | 15 | self.setWindowTitle('Sample Source Sites') 16 | self.model = model 17 | self.font_metric = font_metric 18 | self.parent = parent 19 | 20 | self.layout = QtWidgets.QFormLayout() 21 | self.setLayout(self.layout) 22 | 23 | self.populate() 24 | 25 | def populate(self): 26 | self.nSitesBox = QtWidgets.QSpinBox(self) 27 | self.nSitesBox.setMaximum(1_000_000) 28 | self.nSitesBox.setMinimum(0) 29 | self.nSitesBox.setValue(1000) 30 | self.nSitesBox.setToolTip('Number of source sites to sample from the OpenMC source') 31 | 32 | self.sites_visible = QtWidgets.QCheckBox(self) 33 | self.sites_visible.setChecked(self.model.sourceSitesVisible) 34 | self.sites_visible.setToolTip('Toggle visibility of source sites on the slice plane') 35 | self.sites_visible.stateChanged.connect(self._toggle_source_sites) 36 | 37 | self.colorButton = QtWidgets.QPushButton(self) 38 | self.colorButton.setToolTip('Select color for displaying source sites on the slice plane') 39 | self.colorButton.setCursor(QtCore.Qt.PointingHandCursor) 40 | self.colorButton.setFixedHeight(self.font_metric.height() * 1.5) 41 | self.colorButton.clicked.connect(self._select_source_site_color) 42 | rgb = self.model.sourceSitesColor 43 | self.colorButton.setStyleSheet( 44 | f"border-radius: 8px; background-color: rgb{rgb}") 45 | 46 | self.toleranceBox = ScientificDoubleSpinBox() 47 | self.toleranceBox.setToolTip('Slice axis tolerance for displaying source sites on the slice plane') 48 | self.toleranceBox.setValue(self.model.sourceSitesTolerance) 49 | self.toleranceBox.valueChanged.connect(self._set_source_site_tolerance) 50 | self.toleranceBox.setEnabled(self.model.sourceSitesApplyTolerance) 51 | 52 | self.toleranceToggle = QtWidgets.QCheckBox(self) 53 | self.toleranceToggle.setChecked(self.model.sourceSitesApplyTolerance) 54 | self.toleranceToggle.stateChanged.connect(self._toggle_tolerance) 55 | 56 | self.sampleButton = QtWidgets.QPushButton("Sample New Sites") 57 | self.sampleButton.setToolTip('Sample new source sites from the OpenMC source') 58 | self.sampleButton.clicked.connect(self._sample_sites) 59 | 60 | self.closeButton = QtWidgets.QPushButton("Close") 61 | self.closeButton.clicked.connect(self.close) 62 | 63 | self.layout.addRow("Source Sites:", self.nSitesBox) 64 | self.layout.addRow("Visible:", self.sites_visible) 65 | self.layout.addRow("Color:", self.colorButton) 66 | self.layout.addRow('Tolerance:', self.toleranceBox) 67 | self.layout.addRow('Apply tolerance:', self.toleranceToggle) 68 | self.layout.addRow(HorizontalLine()) 69 | self.layout.addRow(self.sampleButton) 70 | self.layout.addRow(self.closeButton) 71 | 72 | def _sample_sites(self): 73 | self.model.getExternalSourceSites(self.nSitesBox.value()) 74 | self.parent.applyChanges() 75 | 76 | def _toggle_source_sites(self): 77 | self.model.sourceSitesVisible = self.sites_visible.isChecked() 78 | self.parent.applyChanges() 79 | 80 | def _select_source_site_color(self): 81 | color = QtWidgets.QColorDialog.getColor() 82 | if color.isValid(): 83 | rgb = self.model.sourceSitesColor = color.getRgb()[:3] 84 | self.colorButton.setStyleSheet( 85 | f"border-radius: 8px; background-color: rgb{rgb}") 86 | self.parent.applyChanges() 87 | 88 | def _toggle_tolerance(self): 89 | self.model.sourceSitesApplyTolerance = self.toleranceToggle.isChecked() 90 | self.toleranceBox.setEnabled(self.toleranceToggle.isChecked()) 91 | self.parent.applyChanges() 92 | 93 | def _set_source_site_tolerance(self): 94 | self.model.sourceSitesTolerance = self.toleranceBox.value() 95 | self.parent.applyChanges() 96 | 97 | 98 | class ExportDataDialog(QtWidgets.QDialog): 99 | """ 100 | A dialog to facilitate generation of VTK files for 101 | the current model and tally data. 102 | """ 103 | def __init__(self, model, font_metric, parent=None): 104 | super().__init__(parent) 105 | 106 | self.model = model 107 | self.font_metric = font_metric 108 | self.parent = parent 109 | 110 | self.layout = QtWidgets.QGridLayout() 111 | self.setLayout(self.layout) 112 | 113 | # disable interaction with main window while this is open 114 | self.setModal(True) 115 | 116 | def show(self): 117 | self.populate() 118 | super().show() 119 | 120 | @staticmethod 121 | def _warn(msg): 122 | msg_box = QtWidgets.QMessageBox() 123 | msg_box.setText(msg) 124 | msg_box.setIcon(QtWidgets.QMessageBox.Information) 125 | msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) 126 | msg_box.exec() 127 | 128 | def populate(self): 129 | cv = self.model.currentView 130 | 131 | self.xminBox = ScientificDoubleSpinBox() 132 | self.xmaxBox = ScientificDoubleSpinBox() 133 | self.yminBox = ScientificDoubleSpinBox() 134 | self.ymaxBox = ScientificDoubleSpinBox() 135 | self.zminBox = ScientificDoubleSpinBox() 136 | self.zmaxBox = ScientificDoubleSpinBox() 137 | 138 | self.bounds_spin_boxes = (self.xminBox, self.xmaxBox, 139 | self.yminBox, self.ymaxBox, 140 | self.zminBox, self.zmaxBox) 141 | 142 | row = 0 143 | 144 | self.layout.addWidget(QtWidgets.QLabel("X-min:"), row, 0) 145 | self.layout.addWidget(self.xminBox, row, 1) 146 | self.layout.addWidget(QtWidgets.QLabel("X-max:"), row, 2) 147 | self.layout.addWidget(self.xmaxBox, row, 3) 148 | 149 | row += 1 150 | 151 | self.layout.addWidget(QtWidgets.QLabel("Y-min:"), row, 0) 152 | self.layout.addWidget(self.yminBox, row, 1) 153 | self.layout.addWidget(QtWidgets.QLabel("Y-max:"), row, 2) 154 | self.layout.addWidget(self.ymaxBox, row, 3) 155 | 156 | row += 1 157 | 158 | self.layout.addWidget(QtWidgets.QLabel("Z-min:"), row, 0) 159 | self.layout.addWidget(self.zminBox, row, 1) 160 | self.layout.addWidget(QtWidgets.QLabel("Z-max:"), row, 2) 161 | self.layout.addWidget(self.zmaxBox, row, 3) 162 | 163 | row +=1 164 | self.layout.addWidget(HorizontalLine(), 3, 0, 1, 6) 165 | 166 | self.xResBox = QtWidgets.QSpinBox() 167 | self.xResBox.setMaximum(1E6) 168 | self.xResBox.setMinimum(0) 169 | self.yResBox = QtWidgets.QSpinBox() 170 | self.yResBox.setMaximum(1E6) 171 | self.yResBox.setMinimum(0) 172 | self.zResBox = QtWidgets.QSpinBox() 173 | self.zResBox.setMaximum(1E6) 174 | self.zResBox.setMinimum(0) 175 | 176 | row += 1 177 | 178 | self.layout.addWidget(QtWidgets.QLabel("X steps:"), row, 0) 179 | self.layout.addWidget(self.xResBox, row, 1) 180 | self.layout.addWidget(QtWidgets.QLabel("Y steps:"), row, 2) 181 | self.layout.addWidget(self.yResBox, row, 3) 182 | self.layout.addWidget(QtWidgets.QLabel("Z steps:"), row, 4) 183 | self.layout.addWidget(self.zResBox, row, 5) 184 | 185 | row += 1 186 | 187 | self.layout.addWidget(HorizontalLine(), row, 0, 1, 6) 188 | 189 | row += 1 190 | 191 | self.tallyCheckBox = QtWidgets.QCheckBox() 192 | self.layout.addWidget(QtWidgets.QLabel("Include tally data:"), 193 | row, 0, 1, 2) 194 | self.layout.addWidget(self.tallyCheckBox, row, 2) 195 | 196 | row += 1 197 | 198 | self.dataLabelField = QtWidgets.QLineEdit() 199 | self.layout.addWidget(QtWidgets.QLabel("VTK Data Label:"), row, 0) 200 | self.layout.addWidget(self.dataLabelField, row, 1, 1, 2) 201 | if cv.selectedTally: 202 | self.dataLabelField.setText("Tally {}".format(cv.selectedTally)) 203 | else: 204 | self.dataLabelField.setText("No tally selected") 205 | self.dataLabelField.setEnabled(False) 206 | self.tallyCheckBox.setEnabled(False) 207 | 208 | row += 1 209 | 210 | self.geomCheckBox = QtWidgets.QCheckBox() 211 | self.layout.addWidget(QtWidgets.QLabel("Include Cells:"), 212 | row, 0, 1, 2) 213 | self.layout.addWidget(self.geomCheckBox, row, 2) 214 | 215 | row += 1 216 | 217 | self.matsCheckBox = QtWidgets.QCheckBox() 218 | self.layout.addWidget(QtWidgets.QLabel("Include Materials:"), 219 | row, 0, 1, 2) 220 | self.layout.addWidget(self.matsCheckBox, row, 2) 221 | 222 | row += 1 223 | 224 | self.tempCheckBox = QtWidgets.QCheckBox() 225 | self.layout.addWidget(QtWidgets.QLabel("Include Temperature:"), 226 | row, 0, 1, 2) 227 | self.layout.addWidget(self.tempCheckBox, row, 2) 228 | 229 | row += 1 230 | 231 | self.densityCheckBox = QtWidgets.QCheckBox() 232 | self.layout.addWidget(QtWidgets.QLabel("Include Density:"), 233 | row, 0, 1, 2) 234 | self.layout.addWidget(self.densityCheckBox, row, 2) 235 | 236 | row += 1 237 | 238 | self.layout.addWidget(HorizontalLine(), row, 0, 1, 6) 239 | 240 | row += 1 241 | 242 | self.exportButton = QtWidgets.QPushButton("Export to VTK") 243 | self.exportButton.clicked.connect(self.export_data) 244 | 245 | self.layout.addWidget(self.exportButton, row, 5, 1, 2) 246 | 247 | if cv.selectedTally: 248 | tally = self.model.statepoint.tallies[cv.selectedTally] 249 | else: 250 | tally = None 251 | 252 | if tally and tally.contains_filter(openmc.MeshFilter): 253 | 254 | mesh_filter = tally.find_filter(openmc.MeshFilter) 255 | mesh = mesh_filter.mesh 256 | assert(mesh.n_dimension == 3) 257 | 258 | bbox = mesh.bounding_box 259 | 260 | llc = bbox.lower_left 261 | self.xminBox.setValue(llc[0]) 262 | self.yminBox.setValue(llc[1]) 263 | self.zminBox.setValue(llc[2]) 264 | 265 | urc = bbox.upper_right 266 | self.xmaxBox.setValue(urc[0]) 267 | self.ymaxBox.setValue(urc[1]) 268 | self.zmaxBox.setValue(urc[2]) 269 | 270 | bounds_msg = "Using MeshFilter to set bounds automatically." 271 | for box in self.bounds_spin_boxes: 272 | box.setEnabled(False) 273 | box.setToolTip(bounds_msg) 274 | 275 | dims = mesh.dimension 276 | if len(dims) == 3: 277 | self.xResBox.setValue(dims[0]) 278 | self.yResBox.setValue(dims[1]) 279 | self.zResBox.setValue(dims[2]) 280 | 281 | resolution_msg = "Using MeshFilter to set resolution automatically." 282 | self.xResBox.setEnabled(False) 283 | self.xResBox.setToolTip(resolution_msg) 284 | self.yResBox.setEnabled(False) 285 | self.yResBox.setToolTip(resolution_msg) 286 | self.zResBox.setEnabled(False) 287 | self.zResBox.setToolTip(resolution_msg) 288 | 289 | else: 290 | # initialize using the bounds of the current view 291 | llc = cv.llc 292 | self.xminBox.setValue(llc[0]) 293 | self.yminBox.setValue(llc[1]) 294 | self.zminBox.setValue(llc[2]) 295 | 296 | urc = cv.urc 297 | self.xmaxBox.setValue(urc[0]) 298 | self.ymaxBox.setValue(urc[1]) 299 | self.zmaxBox.setValue(urc[2]) 300 | 301 | self.xResBox.setValue(10) 302 | self.yResBox.setValue(10) 303 | self.zResBox.setValue(10) 304 | 305 | def export_data(self): 306 | # cache current and active views 307 | av = self.model.activeView 308 | try: 309 | # export the tally data 310 | self._export_data() 311 | finally: 312 | # always reset to the original view 313 | self.model.activeView = av 314 | self.model.makePlot() 315 | 316 | def _export_data(self): 317 | 318 | import vtk 319 | 320 | # collect necessary information from the export box 321 | llc = np.array((self.xminBox.value(), 322 | self.yminBox.value(), 323 | self.zminBox.value())) 324 | urc = np.array((self.xmaxBox.value(), 325 | self.ymaxBox.value(), 326 | self.zmaxBox.value())) 327 | res = np.array((self.xResBox.value(), 328 | self.yResBox.value(), 329 | self.zResBox.value())) 330 | dx, dy, dz = (urc - llc) / res 331 | 332 | if any(llc >= urc): 333 | self._warn("Bounds of export data are invalid.") 334 | return 335 | 336 | filename, ext = QtWidgets.QFileDialog.getSaveFileName( 337 | self, 338 | "Set VTK Filename", 339 | "tally_data.vti", 340 | "VTK Image (.vti)") 341 | 342 | # check for cancellation 343 | if filename == "": 344 | return 345 | 346 | if filename[-4:] != ".vti": 347 | filename += ".vti" 348 | 349 | ### Generate VTK Data ### 350 | 351 | # create empty array to store our values 352 | export_tally_data = self.tallyCheckBox.checkState() == QtCore.Qt.Checked 353 | if export_tally_data: 354 | tally_data = np.zeros(res[::-1], dtype=float) 355 | 356 | # create empty arrays for other model properties if requested 357 | export_cells = self.geomCheckBox.checkState() == QtCore.Qt.Checked 358 | if export_cells: 359 | cells = np.zeros(res[::-1], dtype='int32') 360 | 361 | export_materials = self.matsCheckBox.checkState() == QtCore.Qt.Checked 362 | if export_materials: 363 | mats = np.zeros(res[::-1], dtype='int32') 364 | 365 | export_temperatures = self.tempCheckBox.checkState() == QtCore.Qt.Checked 366 | if export_temperatures: 367 | temps = np.zeros(res[::-1], dtype='float') 368 | 369 | export_densities = self.densityCheckBox.checkState() == QtCore.Qt.Checked 370 | if export_densities: 371 | rhos = np.zeros(res[::-1], dtype='float') 372 | 373 | # get a copy of the current view 374 | view = copy.deepcopy(self.model.currentView) 375 | 376 | # adjust view settings to match those set in the export dialog 377 | x0, y0, z0 = (llc + urc) / 2.0 378 | view.width = urc[0] - llc[0] 379 | view.height = urc[1] - llc[1] 380 | view.h_res = res[0] 381 | view.v_res = res[1] 382 | view.tallyDataVisible = True 383 | 384 | z0 = llc[2] + dz / 2.0 385 | 386 | # progress bar to make sure the user knows something is happening 387 | # large mesh tallies could take a long time to export 388 | progressBar = QtWidgets.QProgressDialog("Accumulating data...", 389 | "Cancel", 390 | 0, 391 | res[2]) 392 | progressBar.setWindowModality(QtCore.Qt.WindowModal) 393 | 394 | # get a view of the tally data for each x, y slice: 395 | for k in range(res[2]): 396 | z = z0 + k*dz 397 | view.origin = (x0, y0, z) 398 | view.basis = 'xy' 399 | self.model.activeView = view 400 | self.model.makePlot() 401 | 402 | if export_tally_data: 403 | image_data = self.model.create_tally_image(view) 404 | tally_data[k] = image_data[0][::-1] 405 | if export_cells: 406 | cells[k] = self.model.cell_ids[::-1] 407 | if export_materials: 408 | mats[k] = self.model.mat_ids[::-1] 409 | if export_temperatures: 410 | temps[k] = self.model.temperatures[::-1] 411 | if export_densities: 412 | rhos[k] = self.model.densities[::-1] 413 | 414 | progressBar.setValue(k) 415 | if progressBar.wasCanceled(): 416 | return 417 | 418 | vtk_image = vtk.vtkImageData() 419 | vtk_image.SetDimensions(res + 1) 420 | vtk_image.SetSpacing(dx, dy, dz) 421 | vtk_image.SetOrigin(llc) 422 | 423 | if export_tally_data: 424 | # assign tally data to double array 425 | vtk_data = vtk.vtkDoubleArray() 426 | vtk_data.SetName(self.dataLabelField.text()) 427 | vtk_data.SetArray(tally_data, tally_data.size, True) 428 | vtk_image.GetCellData().AddArray(vtk_data) 429 | 430 | if export_cells: 431 | cell_data = vtk.vtkIntArray() 432 | cell_data.SetName("cells") 433 | cell_data.SetArray(cells, cells.size, True) 434 | vtk_image.GetCellData().AddArray(cell_data) 435 | 436 | if export_materials: 437 | mat_data = vtk.vtkIntArray() 438 | mat_data.SetName("mats") 439 | mat_data.SetArray(mats, mats.size, True) 440 | vtk_image.GetCellData().AddArray(mat_data) 441 | 442 | if export_temperatures: 443 | temp_data = vtk.vtkDoubleArray() 444 | temp_data.SetName("temperature") 445 | temp_data.SetArray(temps, temps.size, True) 446 | vtk_image.GetCellData().AddArray(temp_data) 447 | 448 | if export_densities: 449 | rho_data = vtk.vtkDoubleArray() 450 | rho_data.SetName("density") 451 | rho_data.SetArray(rhos, rhos.size, True) 452 | vtk_image.GetCellData().AddArray(rho_data) 453 | 454 | progressBar.setLabel( 455 | QtWidgets.QLabel("Writing VTK Image file: {}...".format(filename))) 456 | 457 | writer = vtk.vtkXMLImageDataWriter() 458 | writer.SetInputData(vtk_image) 459 | writer.SetFileName(filename) 460 | writer.Write() 461 | 462 | progressBar.setLabel(QtWidgets.QLabel("Export complete")) 463 | progressBar.setValue(res[2]) 464 | 465 | msg = QtWidgets.QMessageBox() 466 | msg.setText("Export complete!") 467 | msg.setIcon(QtWidgets.QMessageBox.Information) 468 | msg.setStandardButtons(QtWidgets.QMessageBox.Ok) 469 | msg.exec() 470 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | qt_api=pyside6 3 | python_files = test*.py 4 | python_classes = NoThanks 5 | filterwarnings = ignore::UserWarning 6 | addopts = -rs 7 | -------------------------------------------------------------------------------- /screenshots/atr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/atr.png -------------------------------------------------------------------------------- /screenshots/beavrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/beavrs.png -------------------------------------------------------------------------------- /screenshots/beavrs_zoomed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/beavrs_zoomed.png -------------------------------------------------------------------------------- /screenshots/color_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/color_dialog.png -------------------------------------------------------------------------------- /screenshots/dagmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/dagmc.png -------------------------------------------------------------------------------- /screenshots/pincell_tally.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/pincell_tally.png -------------------------------------------------------------------------------- /screenshots/shortcuts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/shortcuts.png -------------------------------------------------------------------------------- /screenshots/source-sites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/screenshots/source-sites.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import setup 4 | 5 | # Get version information from __init__.py. This is ugly, but more reliable than 6 | # using an import. 7 | with open('openmc_plotter/__init__.py', 'r') as f: 8 | version = f.readlines()[-1].split()[-1].strip("'") 9 | 10 | # read the contents of your README file 11 | long_description = Path(__file__).with_name("README.md").read_text() 12 | 13 | kwargs = { 14 | 'name': 'openmc-plotter', 15 | 'version': version, 16 | 'packages': ['openmc_plotter'], 17 | 'package_data': {'openmc_plotter' : ['assets/*.png']}, 18 | 'entry_points': { 19 | 'console_scripts': [ 20 | 'openmc-plotter=openmc_plotter.__main__:main' 21 | ] 22 | }, 23 | 24 | # Metadata 25 | 'author': 'OpenMC Development Team', 26 | 'author_email': 'openmc@anl.gov', 27 | 'description': 'Plotting tool for OpenMC models and tally data', 28 | 'long_description': long_description, 29 | 'long_description_content_type': 'text/markdown', 30 | 'url': 'https://github.com/openmc-dev/plotter', 31 | 'download_url': 'https://github.com/openmc-dev/plotter', 32 | 'project_urls': { 33 | 'Issue Tracker': 'https://github.com/openmc-dev/plotter/issues', 34 | 'Source Code': 'https://github.com/openmc-dev/plotter', 35 | }, 36 | 'classifiers': [ 37 | 'Development Status :: 4 - Beta', 38 | 'Intended Audience :: Developers', 39 | 'Intended Audience :: End Users/Desktop', 40 | 'Intended Audience :: Science/Research', 41 | 'Natural Language :: English', 42 | 'Topic :: Scientific/Engineering', 43 | 'Programming Language :: Python :: 3', 44 | 'Programming Language :: Python :: 3.6', 45 | 'Programming Language :: Python :: 3.7', 46 | 'Programming Language :: Python :: 3.8', 47 | 'Programming Language :: Python :: 3.9', 48 | 'Programming Language :: Python :: 3.10', 49 | 'Programming Language :: Python :: 3.11', 50 | ], 51 | 52 | # Dependencies 53 | 'python_requires': '>=3.8', 54 | 'install_requires': [ 55 | 'openmc>0.14.0', 'numpy', 'matplotlib', 'PySide6' 56 | ], 57 | 'extras_require': { 58 | 'test' : ['pytest', 'pytest-qt'], 59 | 'vtk' : ['vtk'] 60 | }, 61 | } 62 | 63 | setup(**kwargs) 64 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | @pytest.fixture(scope='module', autouse=True) 7 | def setup_regression_test(request): 8 | # Change to test directory 9 | olddir = request.fspath.dirpath().chdir() 10 | try: 11 | yield 12 | finally: 13 | # some cleanup 14 | plot_settings = Path('plot_settings.pkl') 15 | if plot_settings.exists(): 16 | plot_settings.unlink() 17 | 18 | olddir.chdir() 19 | -------------------------------------------------------------------------------- /tests/setup_test/geometry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/setup_test/materials.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/setup_test/ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/tests/setup_test/ref.png -------------------------------------------------------------------------------- /tests/setup_test/ref1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/tests/setup_test/ref1.png -------------------------------------------------------------------------------- /tests/setup_test/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | plot 5 | 6 | 7 | 5 4 3 8 | -10 -10 -10 9 | 10 10 10 10 | 11 | 1 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/setup_test/test.pltvw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/tests/setup_test/test.pltvw -------------------------------------------------------------------------------- /tests/setup_test/test.py: -------------------------------------------------------------------------------- 1 | import filecmp 2 | import shutil 3 | 4 | import pytest 5 | 6 | from openmc_plotter.main_window import MainWindow, _openmcReload 7 | 8 | @pytest.fixture 9 | def run_in_tmpdir(tmpdir): 10 | orig = tmpdir.chdir() 11 | try: 12 | yield 13 | finally: 14 | orig.chdir() 15 | 16 | def test_window(tmpdir, qtbot): 17 | orig = tmpdir.chdir() 18 | mw = MainWindow(model_path=orig) 19 | _openmcReload(model_path=orig) 20 | mw.loadGui() 21 | 22 | try: 23 | mw.saveImage(tmpdir / 'test.png') 24 | 25 | qtbot.addWidget(mw) 26 | finally: 27 | orig.chdir() 28 | 29 | filecmp.cmp(orig / 'ref.png', tmpdir / 'test.png') 30 | 31 | mw.close() 32 | 33 | def test_batch_image(tmpdir, qtbot): 34 | orig = tmpdir.chdir() 35 | 36 | # move view file into tmpdir 37 | shutil.copy2(orig / 'test.pltvw', tmpdir) 38 | shutil.copy2(orig / 'test1.pltvw', tmpdir) 39 | 40 | _openmcReload(model_path=orig) 41 | 42 | mw = MainWindow(model_path=orig) 43 | mw.loadGui() 44 | 45 | try: 46 | mw.saveBatchImage('test.pltvw') 47 | qtbot.addWidget(mw) 48 | 49 | mw.saveBatchImage('test1.pltvw') 50 | qtbot.addWidget(mw) 51 | finally: 52 | orig.chdir() 53 | 54 | filecmp.cmp(orig / 'ref.png', tmpdir / 'test.png') 55 | filecmp.cmp(orig / 'ref1.png', tmpdir / 'test1.png') 56 | 57 | mw.close() -------------------------------------------------------------------------------- /tests/setup_test/test1.pltvw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openmc-dev/plotter/68307771be6d4dd246566613bd31434545c07856/tests/setup_test/test1.pltvw --------------------------------------------------------------------------------