Source code for armi.utils.plotting

# Copyright 2019 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This module makes heavy use of matplotlib. Beware that plots generated with matplotlib
may not free their memory, even after the plot is closed, and excessive use of
plotting functions may gobble up all of your machine's memory.

Therefore, you should use these plotting tools judiciously. It is not advisable to,
for instance, plot some sequence of objects in a loop at every time node. If you start
to see your memory usage grow inexplicably, you should question any plots that you are
generating.
"""

import collections
import itertools
import math
import os
import re

from matplotlib.collections import PatchCollection
from matplotlib.widgets import Slider
from mpl_toolkits import axes_grid1
from ordered_set import OrderedSet
import matplotlib.colors as mcolors
import matplotlib.patches
import matplotlib.pyplot as plt
import matplotlib.text as mpl_text
import numpy

from armi import runLog
from armi.bookkeeping import report
from armi.materials import custom
from armi.nuclearDataIO.cccc.rtflux import RtfluxData
from armi.reactor import grids
from armi.reactor.components import Helix, Circle, DerivedShape
from armi.reactor.components.basicShapes import Hexagon, Rectangle, Square
from armi.reactor.flags import Flags
from armi.utils import hexagon


LUMINANCE_WEIGHTS = numpy.array([0.3, 0.59, 0.11, 0.0])


[docs]def colorGenerator(skippedColors=10): """ Selects a color from the matplotlib css color database. Parameters ---------- skippedColors: int Number of colors to skip in the matplotlib CSS color database when generating the next color. Without skipping colors the next color may be similar to the previous color. Notes ----- Will cycle indefinitely to accommodate large cores. Colors will repeat. """ colors = list(mcolors.CSS4_COLORS) for start in itertools.cycle(range(20, 20 + skippedColors)): for i in range(start, len(colors), skippedColors): yield colors[i]
[docs]def plotBlockDepthMap( core, param="pdens", fName=None, bare=False, cmapName="jet", labels=(), labelFmt="{0:.3f}", legendMap=None, fontSize=None, minScale=None, maxScale=None, axisEqual=False, makeColorBar=False, cBarLabel="", title="", shuffleArrows=False, titleSize=25, depthIndex=0, ): """ Plot a param distribution in xy space with the ability to page through depth. Notes ----- This is useful for visualizing the spatial distribution of a param through the core. Blocks could possibly not be in alignment between assemblies, but the depths viewable are based on the first fuel assembly. Parameters ---------- The kwarg definitions are the same as those of ``plotFaceMap``. depthIndex: int The the index of the elevation to show block params. The index is determined by the index of the blocks in the first fuel assembly. """ fuelAssem = core.getFirstAssembly(typeSpec=Flags.FUEL) if not fuelAssem: raise ValueError( "Could not find fuel assembly. " "This method uses the first fuel blocks mesh for the axial mesh of the plot. " "Cannot proceed without fuel block." ) # block mid point elevation elevations = [elev for _b, elev in fuelAssem.getBlocksAndZ()] data = [] for elevation in elevations: paramValsAtElevation = [] for a in core: paramValsAtElevation.append(a.getBlockAtElevation(elevation).p[param]) data.append(paramValsAtElevation) data = numpy.array(data) fig = plt.figure(figsize=(12, 12), dpi=100) # Make these now, so they are still referenceable after plotFaceMap. patches = _makeAssemPatches(core) collection = PatchCollection(patches, cmap=cmapName, alpha=1.0) texts = [] plotFaceMap( core, param=param, vals="peak", data=None, # max values so legend is set correctly bare=bare, cmapName=cmapName, labels=labels, labelFmt=labelFmt, legendMap=legendMap, fontSize=fontSize, minScale=minScale, maxScale=maxScale, axisEqual=axisEqual, makeColorBar=makeColorBar, cBarLabel=cBarLabel, title=title, shuffleArrows=shuffleArrows, titleSize=titleSize, referencesToKeep=[patches, collection, texts], ) # make space for the slider fig.subplots_adjust(bottom=0.15) ax_slider = fig.add_axes([0.1, 0.05, 0.8, 0.04]) # This controls what the slider does. def update(i): # int, since we are indexing an array. i = int(i) collection.set_array(data[i, :]) for valToPrint, text in zip(data[i, :], texts): text.set_text(labelFmt.format(valToPrint)) # Slider doesn't seem to work unless assigned to variable _slider = DepthSlider( ax_slider, "Depth(cm)", elevations, update, "green", valInit=depthIndex ) if fName: plt.savefig(fName, dpi=150) else: plt.show() plt.close() return fName
[docs]def plotFaceMap( core, param="pdens", vals="peak", data=None, fName=None, bare=False, cmapName="jet", labels=(), labelFmt="{0:.3f}", legendMap=None, fontSize=None, minScale=None, maxScale=None, axisEqual=False, makeColorBar=False, cBarLabel="", title="", shuffleArrows=False, titleSize=25, referencesToKeep=None, ): """ Plot a face map of the core. Parameters ---------- core: Core The core to plot. param : str, optional The block-parameter to plot. Default: pdens vals : str, optional Can be 'peak', 'average', or 'sum'. The type of vals to produce. Will find peak, average, or sum of block values in an assembly. Default: peak data : list, optional rather than using param and vals, use the data supplied as is. It must be in the same order as iter(r). fName : str, optional File name to create. If none, will show on screen. bare : bool, optional If True, will skip axis labels, etc. cmapName : str The name of the matplotlib colormap to use. Default: jet Other possibilities: http://matplotlib.org/examples/pylab_examples/show_colormaps.html labels : list of str, optional Data labels corresponding to data values. labelFmt : str, optional A format string that determines how the data is printed if ``labels`` is not provided. E.g. ``"{:.1e}"`` legendMap : list, optional A tuple list of (value, lable, decription), to define the data in the legend. fontSize : int, optional Font size in points minScale : float, optional The minimum value for the low color on your colormap (to set scale yourself) Default: autoscale maxScale : float, optional The maximum value for the high color on your colormap (to set scale yourself) Default: autoscale axisEqual : Boolean, optional If True, horizontal and vertical axes are scaled equally such that a circle appears as a circle rather than an ellipse. If False, this scaling constraint is not imposed. makeColorBar : Boolean, optional If True, a vertical color bar is added on the right-hand side of the plot. If False, no color bar is added. cBarLabel : String, optional If True, this string is the color bar quantity label. If False, the color bar will have no label. When makeColorBar=False, cBarLabel affects nothing. title : String, optional If True, the string is added as the plot title. If False, no plot title is added. shuffleArrows : list, optional Adds arrows indicating fuel shuffling maneuvers titleSize : int, optional Size of title on plot referencesToKeep : list, optional References to previous plots you might want to plot on: patches, collection, texts. Examples -------- Plotting a BOL assembly type facemap with a legend:: >>> plotFaceMap(core, param='typeNumAssem', cmapName='RdYlBu') """ if referencesToKeep: patches, collection, texts = referencesToKeep fig, ax = plt.gcf(), plt.gca() else: fig, ax = plt.subplots(figsize=(12, 12), dpi=100) # set patch (shapes such as hexagon) heat map values patches = _makeAssemPatches(core) collection = PatchCollection(patches, cmap=cmapName, alpha=1.0) texts = [] ax.set_title(title, size=titleSize) # get param vals if data is None: data = [] for a in core: if vals == "peak": data.append(a.getMaxParam(param)) elif vals == "average": data.append(a.calcAvgParam(param)) elif vals == "sum": data.append(a.calcTotalParam(param)) else: raise ValueError( "{0} is an invalid entry for `vals` in plotFaceMap. Use peak, average, or sum.".format( vals ) ) if not labels: labels = [None] * len(data) if len(data) != len(labels): raise ValueError( "Data had length {}, but lables had length {}. " "They should be equal length.".format(len(data), len(labels)) ) collection.set_array(numpy.array(data)) if minScale or maxScale: collection.set_clim([minScale, maxScale]) else: collection.norm.autoscale(numpy.array(data)) ax.add_collection(collection) # Makes text in the center of each shape displaying the values. # (The text is either black or white depending on the background color it is written on) _setPlotValText(ax, texts, core, data, labels, labelFmt, fontSize, collection) # allow a color bar option if makeColorBar: collection2 = PatchCollection(patches, cmap=cmapName, alpha=1.0) if minScale and maxScale: collection2.set_array(numpy.array([minScale, maxScale])) else: collection2.set_array(numpy.array(data)) if "radial" in cBarLabel: colbar = fig.colorbar( collection2, ticks=[x + 1 for x in range(max(data))], shrink=0.43 ) else: colbar = fig.colorbar(collection2, ax=ax, shrink=0.43) colbar.set_label(cBarLabel, size=20) colbar.ax.tick_params(labelsize=16) if legendMap is not None: legend = _createLegend(legendMap, collection) else: legend = None if axisEqual: # don't "squish" patches vertically or horizontally ax.set_aspect("equal", "datalim") ax.autoscale_view(tight=True) # make it 2-D, for now... shuffleArrows = shuffleArrows or [] for (sourceCoords, destinationCoords) in shuffleArrows: ax.annotate( "", xy=destinationCoords[:2], xytext=sourceCoords[:2], arrowprops={"arrowstyle": "->", "color": "white"}, ) if bare: ax.set_xticks([]) ax.set_yticks([]) ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) ax.spines["left"].set_visible(False) ax.spines["bottom"].set_visible(False) else: ax.set_xlabel("x (cm)") ax.set_ylabel("y (cm)") if fName: if legend: # expand so the legend fits if necessary pltKwargs = {"bbox_extra_artists": (legend,), "bbox_inches": "tight"} else: pltKwargs = {} try: plt.savefig(fName, dpi=150, **pltKwargs) except IOError: runLog.warning( "Cannot update facemap at {0}: IOError. Is the file open?" "".format(fName) ) elif referencesToKeep: # Don't show yet, since it will be updated. return fName else: plt.show() # don't close figure here. Have caller call plotting.close or plt.close when # they are done with it. return fName
[docs]def close(fig=None): """ Wrapper for matplotlib close. This is useful to avoid needing to import plotting and matplotlib. The plot functions cannot always close their figure if it is going to be used somewhere else after becoming active (e.g. in reports or gallery examples). """ plt.close(fig)
def _makeAssemPatches(core): """Return a list of assembly shaped patch for each assembly.""" patches = [] if isinstance(core.spatialGrid, grids.HexGrid): nSides = 6 elif isinstance(core.spatialGrid, grids.ThetaRZGrid): raise TypeError( "This plot function is not currently supported for ThetaRZGrid grids." ) else: nSides = 4 pitch = core.getAssemblyPitch() for a in core: x, y, _ = a.spatialLocator.getLocalCoordinates() if nSides == 6: assemPatch = matplotlib.patches.RegularPolygon( (x, y), nSides, pitch / math.sqrt(3), orientation=math.pi / 2.0 ) elif nSides == 4: # for rectangle x, y is defined as sides instead of center assemPatch = matplotlib.patches.Rectangle( (x - pitch[0] / 2, y - pitch[1] / 2), *pitch ) else: raise ValueError(f"Unexpected number of sides: {nSides}.") patches.append(assemPatch) return patches def _setPlotValText(ax, texts, core, data, labels, labelFmt, fontSize, collection): """Write param values down, and return text so it can be edited later.""" _ = core.getAssemblyPitch() for a, val, label in zip(core, data, labels): x, y, _ = a.spatialLocator.getLocalCoordinates() cmap = collection.get_cmap() patchColor = numpy.asarray(cmap(collection.norm(val))) luminance = patchColor.dot(LUMINANCE_WEIGHTS) dark = luminance < 0.5 if dark: color = "white" else: color = "black" # Write text on top of patch locations. if label is None and labelFmt is not None: # Write the value labelText = labelFmt.format(val) text = ax.text( x, y, labelText, zorder=1, ha="center", va="center", fontsize=fontSize, color=color, ) elif label is not None: text = ax.text( x, y, label, zorder=1, ha="center", va="center", fontsize=fontSize, color=color, ) else: # labelFmt was none, so they don't want any text plotted continue texts.append(text) def _createLegend(legendMap, collection, size=9, shape=Hexagon): """Make special legend for the assembly face map plot with assembly counts, and Block Diagrams.""" class AssemblyLegend: """ Custom Legend artist handler. Matplotlib allows you to define a class that implements ``legend_artist`` to give you full control over how the legend keys and labels are drawn. This is done here to get Hexagons with Letters in them on the legend, which is not a built-in legend option. See: http://matplotlib.org/users/legend_guide.html#implementing-a-custom-legend-handler """ def legend_artist(self, _legend, orig_handle, _fontsize, handlebox): letter, index = orig_handle x0, y0 = handlebox.xdescent, handlebox.ydescent width, height = handlebox.width, handlebox.height x = x0 + width / 2.0 y = y0 + height / 2.0 normVal = collection.norm(index) cmap = collection.get_cmap() colorRgb = cmap(normVal) if shape == Hexagon: patch = matplotlib.patches.RegularPolygon( (x, y), 6, height, orientation=math.pi / 2.0, facecolor=colorRgb, transform=handlebox.get_transform(), ) elif shape == Rectangle: patch = matplotlib.patches.Rectangle( (x - height / 2, y - height / 2), height * 2, height, facecolor=colorRgb, transform=handlebox.get_transform(), ) else: patch = matplotlib.patches.Circle( (x, y), height, facecolor=colorRgb, transform=handlebox.get_transform(), ) luminance = numpy.array(colorRgb).dot(LUMINANCE_WEIGHTS) dark = luminance < 0.5 if dark: color = "white" else: color = "black" handlebox.add_artist(patch) txt = mpl_text.Text( x=x, y=y, text=letter, ha="center", va="center", size=7, color=color ) handlebox.add_artist(txt) return (patch, txt) ax = plt.gca() keys = [] labels = [] for value, label, description in legendMap: keys.append((label, value)) labels.append(description) legend = ax.legend( keys, labels, handler_map={tuple: AssemblyLegend()}, loc="center left", bbox_to_anchor=(1.0, 0.5), frameon=False, prop={"size": size}, ) return legend
[docs]class DepthSlider(Slider): """Page slider used to view params at different depths.""" def __init__( self, ax, sliderLabel, depths, updateFunc, selectedDepthColor, fontsize=8, valInit=0, **kwargs, ): # The color of the currently displayed depth page. self.selectedDepthColor = selectedDepthColor self.nonSelectedDepthColor = "w" self.depths = depths # Make the selection depth buttons self.depthSelections = [] numDepths = float(len(depths)) rectangleBot = 0 textYCoord = 0.5 # startBoundaries go from zero to just below 1. leftBoundary = [i / numDepths for i, _depths in enumerate(depths)] for leftBoundary, depth in zip(leftBoundary, depths): # First depth (leftBoundary==0) is on, rest are off. if leftBoundary == 0: color = self.selectedDepthColor else: color = self.nonSelectedDepthColor depthSelectBox = matplotlib.patches.Rectangle( (leftBoundary, rectangleBot), 1.0 / numDepths, 1, transform=ax.transAxes, facecolor=color, ) ax.add_artist(depthSelectBox) self.depthSelections.append(depthSelectBox) # Make text halfway into box textXCoord = leftBoundary + 0.5 / numDepths ax.text( textXCoord, textYCoord, "{:.1f}".format(depth), ha="center", va="center", transform=ax.transAxes, fontsize=fontsize, ) # Make forward and backward button backwardArrow, forwardArrow = "$\u25C0$", "$\u25B6$" divider = axes_grid1.make_axes_locatable(ax) buttonWidthPercent = "5%" backwardAxes = divider.append_axes("right", size=buttonWidthPercent, pad=0.03) forwardAxes = divider.append_axes("right", size=buttonWidthPercent, pad=0.03) self.backButton = matplotlib.widgets.Button( backwardAxes, label=backwardArrow, color=self.nonSelectedDepthColor, hovercolor=self.selectedDepthColor, ) self.backButton.label.set_fontsize(fontsize) self.backButton.on_clicked(self.previous) self.forwardButton = matplotlib.widgets.Button( forwardAxes, label=forwardArrow, color=self.nonSelectedDepthColor, hovercolor=self.selectedDepthColor, ) self.forwardButton.label.set_fontsize(fontsize) self.forwardButton.on_clicked(self.next) # init at end since slider will set val to 0, and it needs to have state # setup before doing that Slider.__init__(self, ax, sliderLabel, 0, len(depths), valinit=0, **kwargs) self.on_changed(updateFunc) self.set_val(valInit) # need to set after updateFunc is added. # Turn off value visibility since the buttons text shows the value self.valtext.set_visible(False)
[docs] def set_val(self, val): """ Set the value and update the color. Notes ----- valmin/valmax are set on the parent to 0 and len(depths). """ val = int(val) # valmax is not allowed, since it is out of the array. # valmin is allowed since 0 index is in depth array. if val < self.valmin or val >= self.valmax: # invalid, so ignore return # activate color is first since we still have access to self.val self.updatePageDepthColor(val) Slider.set_val(self, val)
[docs] def next(self, _event): """Move forward to the next depth (page).""" self.set_val(self.val + 1)
[docs] def previous(self, _event): """Move backward to the previous depth (page).""" self.set_val(self.val - 1)
[docs] def updatePageDepthColor(self, newVal): """Update the page colors.""" self.depthSelections[self.val].set_facecolor(self.nonSelectedDepthColor) self.depthSelections[newVal].set_facecolor(self.selectedDepthColor)
[docs]def plotAssemblyTypes( blueprints=None, fileName=None, assems=None, maxAssems=None, showBlockAxMesh=True, yAxisLabel=None, title=None, ) -> plt.Figure: """ Generate a plot showing the axial block and enrichment distributions of each assembly type in the core. Parameters ---------- blueprints: Blueprints The blueprints to plot assembly types of. (Either this or ``assems`` must be non-None.) fileName : str or None Base for filename to write, or None for just returning the fig assems: list list of assembly objects to be plotted. (Either this or ``blueprints`` must be non-None.) maxAssems: integer maximum number of assemblies to plot in the assems list. showBlockAxMesh: bool if true, the axial mesh information will be displayed on the right side of the assembly plot. yAxisLabel: str Optionally, provide a label for the Y-axis. title: str Optionally, provide a title for the plot. Returns ------- fig : plt.Figure The figure object created """ # input validation if assems is None and blueprints is None: raise ValueError( "At least one of these inputs must be non-None: blueprints, assems" ) # handle defaults if assems is None: assems = list(blueprints.assemblies.values()) if not isinstance(assems, (list, set, tuple)): assems = [assems] if maxAssems is not None and not isinstance(maxAssems, int): raise TypeError("Maximum assemblies should be an integer") numAssems = len(assems) if maxAssems is None: maxAssems = numAssems if yAxisLabel is None: yAxisLabel = "THERMALLY EXPANDED AXIAL HEIGHTS (CM)" if title is None: title = "Assembly Designs" # Set assembly/block size constants yBlockHeights = [] yBlockAxMesh = OrderedSet() assemWidth = 5.0 assemSeparation = 0.3 xAssemLoc = 0.5 xAssemEndLoc = numAssems * (assemWidth + assemSeparation) + assemSeparation # Setup figure fig, ax = plt.subplots(figsize=(15, 15), dpi=300) for index, assem in enumerate(assems): isLastAssem = True if index == (numAssems - 1) else False (xBlockLoc, yBlockHeights, yBlockAxMesh) = _plotBlocksInAssembly( ax, assem, isLastAssem, yBlockHeights, yBlockAxMesh, xAssemLoc, xAssemEndLoc, showBlockAxMesh, ) xAxisLabel = re.sub(" ", "\n", assem.getType().upper()) ax.text( xBlockLoc + assemWidth / 2.0, -5, xAxisLabel, fontsize=13, ha="center", va="top", ) xAssemLoc += assemWidth + assemSeparation # Set up plot layout ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) ax.spines["bottom"].set_visible(False) ax.yaxis.set_ticks_position("left") yBlockHeights.insert(0, 0.0) yBlockHeights.sort() yBlockHeightDiffs = numpy.diff( yBlockHeights ) # Compute differential heights between each block ax.set_yticks([0.0] + list(set(numpy.cumsum(yBlockHeightDiffs)))) ax.xaxis.set_visible(False) ax.set_title(title, y=1.03) ax.set_ylabel(yAxisLabel, labelpad=20) ax.set_xlim([0.0, 0.5 + maxAssems * (assemWidth + assemSeparation)]) # Plot and save figure ax.plot() if fileName: fig.savefig(fileName) runLog.debug("Writing assem layout {} in {}".format(fileName, os.getcwd())) plt.close(fig) return fig
def _plotBlocksInAssembly( axis, assem, isLastAssem, yBlockHeights, yBlockAxMesh, xAssemLoc, xAssemEndLoc, showBlockAxMesh, ): # Set dictionary of pre-defined block types and colors for the plot lightsage = "xkcd:light sage" blockTypeColorMap = collections.OrderedDict( { "fuel": "tomato", "shield": "cadetblue", "reflector": "darkcyan", "aclp": "lightslategrey", "plenum": "white", "duct": "plum", "control": lightsage, "handling socket": "lightgrey", "grid plate": "lightgrey", "inlet nozzle": "lightgrey", } ) # Initialize block positions blockWidth = 5.0 yBlockLoc = 0 xBlockLoc = xAssemLoc xTextLoc = xBlockLoc + blockWidth / 20.0 for b in assem: blockHeight = b.getHeight() blockXsId = b.p.xsType yBlockCenterLoc = yBlockLoc + blockHeight / 2.5 # Get the basic text label for the block try: blockType = [ bType for bType in blockTypeColorMap.keys() if b.hasFlags(Flags.fromString(bType)) ][0] color = blockTypeColorMap[blockType] except IndexError: blockType = b.getType() color = "grey" # Get the detailed text label for the block dLabel = "" if b.hasFlags(Flags.FUEL): dLabel = " {:0.2f}%".format(b.getFissileMassEnrich() * 100) elif b.hasFlags(Flags.CONTROL): blockType = "ctrl" dLabel = " {:0.2f}%".format(b.getBoronMassEnrich() * 100) dLabel += " ({})".format(blockXsId) # Set up block rectangle blockPatch = matplotlib.patches.Rectangle( (xBlockLoc, yBlockLoc), blockWidth, blockHeight, facecolor=color, alpha=0.7, edgecolor="k", lw=1.0, ls="solid", ) axis.add_patch(blockPatch) axis.text( xTextLoc, yBlockCenterLoc, blockType.upper() + dLabel, ha="left", fontsize=10, ) yBlockLoc += blockHeight yBlockHeights.append(yBlockLoc) # Add location, block heights, and axial mesh points to ordered set yBlockAxMesh.add((yBlockCenterLoc, blockHeight, b.p.axMesh)) # Add the block heights, block number of axial mesh points on the far right of the plot. if isLastAssem and showBlockAxMesh: xEndLoc = 0.5 + xAssemEndLoc for bCenter, bHeight, axMeshPoints in yBlockAxMesh: axis.text( xEndLoc, bCenter, "{} cm ({})".format(bHeight, axMeshPoints), fontsize=10, ha="left", ) return xBlockLoc, yBlockHeights, yBlockAxMesh
[docs]def plotBlockFlux(core, fName=None, bList=None, peak=False, adjoint=False, bList2=[]): """ Produce energy spectrum plot of real and/or adjoint flux in one or more blocks. Parameters ---------- core : Core Core object fName : str, optional the name of the plot file to produce. If none, plot will be shown. A text file with the flux values will also be generated if this is non-empty. bList : iterable, optional is a single block or a list of blocks to average over. If no bList, full core is assumed. peak : bool, optional a flag that will produce the peak as well as the average on the plot. adjoint : bool, optional plot the adjoint as well. bList2 : a separate list of blocks that will also be plotted on a separate axis on the same plot. This is useful for comparing flux in some blocks with flux in some other blocks. Notes ----- This is not a great method. It should be cleand up and migrated into ``utils.plotting``. """ class BlockListFlux: def __init__( self, nGroup, blockList=[], adjoint=False, peak=False, primary=False ): self.nGroup = nGroup self.blockList = blockList self.adjoint = adjoint self.peak = peak self.avgHistogram = None self.eHistogram = None self.peakHistogram = None self.E = None if not blockList: self.avgFlux = numpy.zeros(self.nGroup) self.peakFlux = numpy.zeros(self.nGroup) self.lineAvg = "-" self.linePeak = "-" else: self.avgFlux = numpy.zeros(self.nGroup) self.peakFlux = numpy.zeros(self.nGroup) if self.adjoint: self.labelAvg = "Average Adjoint Flux" self.labelPeak = "Peak Adjoint Flux" else: self.labelAvg = "Average Flux" self.labelPeak = "Peak Flux" if primary: self.lineAvg = "-" self.linePeak = "-" else: self.lineAvg = "r--" self.linePeak = "k--" def calcAverage(self): for b in self.blockList: thisFlux = numpy.array(b.getMgFlux(adjoint=self.adjoint)) self.avgFlux += numpy.array(thisFlux) if sum(thisFlux) > sum(self.peakFlux): self.peakFlux = thisFlux self.avgFlux = self.avgFlux / len(bList) def setEnergyStructure(self, upperEnergyBounds): self.E = [eMax / 1e6 for eMax in upperEnergyBounds] def makePlotHistograms(self): self.eHistogram, self.avgHistogram = makeHistogram(self.E, self.avgFlux) if self.peak: _, self.peakHistogram = makeHistogram(self.E, self.peakFlux) def checkSize(self): if not len(self.E) == len(self.avgFlux): runLog.error(self.avgFlux) raise def getTable(self): return enumerate(zip(self.E, self.avgFlux, self.peakFlux)) if bList is None: bList = core.getBlocks() bList = list(bList) if adjoint and bList2: runLog.warning("Cannot plot adjoint flux with bList2 argument") return elif adjoint: bList2 = bList try: G = len(core.lib.neutronEnergyUpperBounds) except: runLog.warning("No ISOTXS library attached so no flux plots.") return BlockListFluxes = set() bf1 = BlockListFlux(G, blockList=bList, peak=peak, primary=True) BlockListFluxes.add(bf1) if bList2: bf2 = BlockListFlux(G, blockList=bList2, adjoint=adjoint, peak=peak) BlockListFluxes.add(bf2) for bf in BlockListFluxes: bf.calcAverage() bf.setEnergyStructure(core.lib.neutronEnergyUpperBounds) bf.checkSize() bf.makePlotHistograms() if fName: # write a little flux text file. txtFileName = os.path.splitext(fName)[0] + ".txt" with open(txtFileName, "w") as f: f.write( "{0:16s} {1:16s} {2:16s}\n".format( "Energy_Group", "Average_Flux", "Peak_Flux" ) ) for _, (eMax, avgFlux, peakFlux) in bf1.getTable(): f.write("{0:12E} {1:12E} {2:12E}\n".format(eMax, avgFlux, peakFlux)) if max(bf1.avgFlux) <= 0.0: runLog.warning( "Cannot plot flux with maxval=={0} in {1}".format(bf1.avgFlux, bList[0]) ) return plt.figure() plt.plot(bf1.eHistogram, bf1.avgHistogram, bf1.lineAvg, label=bf1.labelAvg) if peak: plt.plot(bf1.eHistogram, bf1.peakHistogram, bf1.linePeak, label=bf1.labelPeak) ax = plt.gca() ax.set_xscale("log") ax.set_yscale("log") plt.xlabel("Energy (MeV)") plt.ylabel("Flux (n/cm$^2$/s)") if peak or bList2: plt.legend(loc="lower right") plt.grid(color="0.70") if bList2: if adjoint: plt.twinx() plt.ylabel("Adjoint Flux (n/cm$^2$/s)", rotation=270) ax2 = plt.gca() ax2.set_yscale("log") plt.plot(bf2.eHistogram, bf2.avgHistogram, bf2.lineAvg, label=bf2.labelAvg) if peak and not adjoint: plt.plot( bf2.eHistogram, bf2.peakHistogram, bf2.linePeak, label=bf2.labelPeak ) plt.legend(loc="lower left") plt.title("Group flux") if fName: plt.savefig(fName) report.setData( "Flux Plot {}".format(os.path.split(fName)[1]), os.path.abspath(fName), report.FLUX_PLOT, ) else: plt.show()
[docs]def makeHistogram(x, y): """ Take a list of x and y values, and return a histogram-ified version Good for plotting multigroup flux spectrum or cross sections """ if not len(x) == len(y): raise ValueError( "Cannot make a histogram unless the x and y lists are the same size." + "len(x) == {} and len(y) == {}".format(len(x), len(y)) ) n = len(x) xHistogram = numpy.zeros(2 * n) yHistogram = numpy.zeros(2 * n) for i in range(n): lower = 2 * i upper = 2 * i + 1 xHistogram[lower] = x[i - 1] xHistogram[upper] = x[i] yHistogram[lower] = y[i] yHistogram[upper] = y[i] xHistogram[0] = x[0] / 2.0 return xHistogram, yHistogram
def _makeBlockPinPatches(block, cold): """Return lists of block component patches and corresponding data and names (which relates to material of the component for later plot-coloring/legend) for a single block. Takes in a block that must have a spatialGrid attached as well as a variable which signifies whether the dimensions of the components are at hot or cold temps. When cold is set to true, you would get the BOL cold temp dimensions. Parameters ---------- block : Block cold : boolean true for cold temps, hot = false Return ------ patches : list list of patches for block components data : list list of the materials these components are made of name : list list of the names of these components """ patches = [] data = [] names = [] if isinstance(block.spatialGrid, grids.HexGrid): largestPitch, comp = block.getPitch(returnComp=True) elif isinstance(block.spatialGrid, grids.ThetaRZGrid): raise TypeError( "This plot function is not currently supported for ThetaRZGrid grids." ) else: largestPitch, comp = block.getPitch(returnComp=True) if block.getPitch()[0] != block.getPitch()[1]: raise ValueError("Only works for blocks with equal length and width.") sortedComps = sorted(block, reverse=True) derivedComponents = block.getComponentsOfShape(DerivedShape) if len(derivedComponents) == 1: derivedComponent = derivedComponents[0] sortedComps.remove(derivedComponent) cName = derivedComponent.name if isinstance(derivedComponent.material, custom.Custom): material = derivedComponent.p.customIsotopicsName else: material = derivedComponent.material.name location = comp.spatialLocator if isinstance(location, grids.MultiIndexLocation): location = location[0] x, y, _ = location.getLocalCoordinates() if isinstance(comp, Hexagon): derivedPatch = matplotlib.patches.RegularPolygon( (x, y), 6, largestPitch / math.sqrt(3) ) elif isinstance(comp, Square): derivedPatch = matplotlib.patches.Rectangle( (x - largestPitch[0] / 2, y - largestPitch[0] / 2), largestPitch[0], largestPitch[0], ) else: raise TypeError( "Shape of the pitch-defining element is not a Square or Hex it is {}, cannot plot for this type of block".format( comp.shape ) ) patches.append(derivedPatch) data.append(material) names.append(cName) for component in sortedComps: locs = component.spatialLocator if not isinstance(locs, grids.MultiIndexLocation): # make a single location a list to iterate. locs = [locs] for loc in locs: x, y, _ = loc.getLocalCoordinates() # goes through each location # want to place a patch at that location blockPatches = _makeComponentPatch(component, (x, y), cold) for element in blockPatches: patches.append(element) if isinstance(component.material, custom.Custom): material = component.p.customIsotopicsName else: material = component.material.name data.append(material) names.append(component.name) return patches, data, names def _makeComponentPatch(component, position, cold): """Makes a component shaped patch to later be used for making block diagrams. Parameters ---------- component: a component of a block position: tuple (x, y) position cold: boolean True if looking for dimension at cold temps Return ------ blockPatch: List A list of Patch objects that together represent a component in the diagram. Notes ----- Currently accepts components of shape DerivedShape, Helix, Circle, or Square """ x = position[0] y = position[1] if isinstance(component, Helix): blockPatch = matplotlib.patches.Wedge( ( x + component.getDimension("helixDiameter", cold=cold) / 2 * math.cos(math.pi / 6), y + component.getDimension("helixDiameter", cold=cold) / 2 * math.sin(math.pi / 6), ), component.getDimension("od", cold=cold) / 2, 0, 360, width=(component.getDimension("od", cold=cold) / 2) - (component.getDimension("id", cold=cold) / 2), ) elif isinstance(component, Circle): blockPatch = matplotlib.patches.Wedge( (x, y), component.getDimension("od", cold=cold) / 2, 0, 360, width=(component.getDimension("od", cold=cold) / 2) - (component.getDimension("id", cold=cold) / 2), ) elif isinstance(component, Hexagon): if component.getDimension("ip", cold=cold) != 0: innerPoints = numpy.array( hexagon.corners(30) * component.getDimension("ip", cold=cold) ) outerPoints = numpy.array( hexagon.corners(30) * component.getDimension("op", cold=cold) ) blockPatch = [] for n in range(6): corners = [ innerPoints[n], innerPoints[(n + 1) % 6], outerPoints[(n + 1) % 6], outerPoints[n], ] patch = matplotlib.patches.Polygon(corners, fill=True) blockPatch.append(patch) else: # Just make it a hexagon... blockPatch = matplotlib.patches.RegularPolygon( (x, y), 6, component.getDimension("op", cold=cold) / math.sqrt(3) ) elif isinstance(component, Rectangle): if component.getDimension("widthInner", cold=cold) != 0: innerPoints = numpy.array( [ [ x + component.getDimension("widthInner", cold=cold) / 2, y + component.getDimension("lengthInner", cold=cold) / 2, ], [ x + component.getDimension("widthInner", cold=cold) / 2, y - component.getDimension("lengthInner", cold=cold) / 2, ], [ x - component.getDimension("widthInner", cold=cold) / 2, y - component.getDimension("lengthInner", cold=cold) / 2, ], [ x - component.getDimension("widthInner", cold=cold) / 2, y + component.getDimension("lengthInner", cold=cold) / 2, ], ] ) outerPoints = numpy.array( [ [ x + component.getDimension("widthOuter", cold=cold) / 2, y + component.getDimension("lengthOuter", cold=cold) / 2, ], [ x + component.getDimension("widthOuter", cold=cold) / 2, y - component.getDimension("lengthOuter", cold=cold) / 2, ], [ x - component.getDimension("widthOuter", cold=cold) / 2, y - component.getDimension("lengthOuter", cold=cold) / 2, ], [ x - component.getDimension("widthOuter", cold=cold) / 2, y + component.getDimension("lengthOuter", cold=cold) / 2, ], ] ) blockPatch = [] for n in range(4): corners = [ innerPoints[n], innerPoints[(n + 1) % 4], outerPoints[(n + 1) % 4], outerPoints[n], ] patch = matplotlib.patches.Polygon(corners, fill=True) blockPatch.append(patch) else: # Just make it a rectangle... blockPatch = matplotlib.patches.Rectangle( ( x - component.getDimension("widthOuter", cold=cold) / 2, y - component.getDimension("lengthOuter", cold=cold) / 2, ), component.getDimension("widthOuter", cold=cold), component.getDimension("lengthOuter", cold=cold), ) if isinstance(blockPatch, list): return blockPatch return [blockPatch]
[docs]def plotBlockDiagram(block, fName, cold, cmapName="RdYlBu", materialList=None): """Given a Block with a spatial Grid, plot the diagram of it with all of its components. (wire, duct, coolant, etc...) Parameters ---------- block : block object fName : String name of the file to save to cold : boolean true is for cold temps, hot is false. cmapName : String name of a colorMap to use for block colors materialList: List a list of material names across all blocks to be plotted so that same material on all diagrams will have the same color. """ _, ax = plt.subplots(figsize=(20, 20), dpi=200) if block.spatialGrid is None: return None if materialList is None: materialList = [] for component in block: if isinstance(component.material, custom.Custom): materialName = component.p.customIsotopicsName else: materialName = component.material.name if materialName not in materialList: materialList.append(materialName) materialMap = { material: ai for ai, material in enumerate(numpy.unique(materialList)) } patches, data, _ = _makeBlockPinPatches(block, cold) collection = PatchCollection(patches, cmap=cmapName, alpha=1.0) allColors = numpy.array(list(materialMap.values())) ourColors = numpy.array([materialMap[materialName] for materialName in data]) collection.set_array(ourColors) ax.add_collection(collection) collection.norm.autoscale(allColors) legendMap = [ ( materialMap[materialName], "", "{}".format(materialName), ) for materialName in numpy.unique(data) ] legend = _createLegend(legendMap, collection, size=50, shape=Rectangle) pltKwargs = { "bbox_extra_artists": (legend,), "bbox_inches": "tight", } ax.set_xticks([]) ax.set_yticks([]) ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) ax.spines["left"].set_visible(False) ax.spines["bottom"].set_visible(False) ax.margins(0) plt.savefig(fName, format="svg", **pltKwargs) return os.path.abspath(fName)
[docs]def plotTriangleFlux( rtfluxData: RtfluxData, axialZ, energyGroup, hexPitch=math.sqrt(3.0), hexSideSubdivisions=1, imgFileExt=".png", ): """ Plot region total flux for one core-wide axial slice on triangular/hexagonal geometry. .. warning:: This will run on non-triangular meshes but will look wrong. Parameters ---------- rtfluxData : RtfluxData object The RTFLUX/ATFLUX data object containing all read file data. Alternatively, this could be a FIXSRC file object, but only if FIXSRC.fixSrc is first renamed FIXSRC.triangleFluxes. axialZ : int The DIF3D axial node index of the core-wide slice to plot. energyGroup : int The energy group index to plot. hexPitch: float, optional The flat-to-flat hexagonal assembly pitch in this core. By default, it is sqrt(3) so that the triangle edge length is 1 if hexSideSubdivisions=1. hexSideSubdivisions : int, optional By default, it is 1 so that the triangle edge length is 1 if hexPitch=sqrt(3). imgFileExt : str, optional The image file extension. Examples -------- >>> rtflux = rtflux.RtfluxStream.readBinary("RTFLUX") >>> plotTriangleFlux(rtflux, axialZ=10, energyGroup=4) """ triHeightInCm = hexPitch / 2.0 / hexSideSubdivisions sideLengthInCm = triHeightInCm / (math.sqrt(3.0) / 2.0) s2InCm = sideLengthInCm / 2.0 vals = rtfluxData.groupFluxes[:, :, axialZ, energyGroup] patches = [] colorVals = [] for i in range(vals.shape[0]): for j in range(vals.shape[1]): # use (i+j)%2 for rectangular meshing flipped = i % 2 xInCm = s2InCm * (i - j) yInCm = triHeightInCm * j + sideLengthInCm / 2.0 / math.sqrt(3) * ( 1 + flipped ) flux = vals[i][j] if flux: triangle = patches.mpatches.RegularPolygon( (xInCm, yInCm), 3, sideLengthInCm / math.sqrt(3), orientation=math.pi * flipped, linewidth=0.0, ) patches.append(triangle) colorVals.append(flux) collection = PatchCollection(patches, alpha=1.0, linewidths=(0,), edgecolors="none") # add color map to this collection ONLY (pins, not ducts) collection.set_array(numpy.array(colorVals)) plt.figure() ax = plt.gca() ax.add_collection(collection) colbar = plt.colorbar(collection) colbar.set_label("n/s/cm$^3$") plt.ylabel("cm") plt.xlabel("cm") ax.autoscale_view() plt.savefig("RTFLUX-z" + str(axialZ + 1) + "-g" + str(energyGroup + 1) + imgFileExt) plt.close()
[docs]def plotNucXs( isotxs, nucNames, xsNames, fName=None, label=None, noShow=False, title=None ): """ generates a XS plot for a nuclide on the ISOTXS library Parameters ---------- isotxs : IsotxsLibrary A collection of cross sections (XS) for both neutron and gamma reactions. nucNames : str or list The nuclides to plot xsNames : str or list the XS to plot e.g. n,g, n,f, nalph, etc. see xsCollections for actual names. fName : str, optional if fName is given, the file will be written rather than plotting to screen label : str, optional is an optional label for image legends, useful in ipython sessions. noShow : bool, optional Won't finalize plot. Useful for using this to make custom plots. Examples -------- >>> l = ISOTXS() >>> plotNucXs(l, 'U238NA','fission') >>> # Plot n,g for all xenon and krypton isotopes >>> f = lambda name: 'XE' in name or 'KR' in name >>> plotNucXs(l, sorted(filter(f,l.nuclides.keys())),itertools.repeat('nGamma')) See Also -------- armi.nucDirectory.nuclide.plotScatterMatrix """ # convert all input to lists if isinstance(nucNames, str): nucNames = [nucNames] if isinstance(xsNames, str): xsNames = [xsNames] for nucName, xsName in zip(nucNames, xsNames): nuc = isotxs[nucName] thisLabel = label or "{0} {1}".format(nucName, xsName) x = isotxs.neutronEnergyUpperBounds / 1e6 y = nuc.micros[xsName] plt.plot(x, y, "-", label=thisLabel, drawstyle="steps-post") ax = plt.gca() ax.set_xscale("log") ax.set_yscale("log") plt.grid(color="0.70") plt.title(title or " microscopic XS from {0}".format(isotxs)) plt.xlabel("Energy (MeV)") plt.ylabel("microscopic XS (barns)") plt.legend() if fName: plt.savefig(fName) elif not noShow: plt.show()