1from collections.abc import Iterable, Mapping
2from numbers import Real, Integral
3from pathlib import Path
4import subprocess
5from xml.etree import ElementTree as ET
6
7import numpy as np
8
9import openmc
10import openmc.checkvalue as cv
11from ._xml import clean_indentation, reorder_attributes
12from .mixin import IDManagerMixin
13
14
15_BASES = ['xy', 'xz', 'yz']
16
17_SVG_COLORS = {
18    'aliceblue': (240, 248, 255),
19    'antiquewhite': (250, 235, 215),
20    'aqua': (0, 255, 255),
21    'aquamarine': (127, 255, 212),
22    'azure': (240, 255, 255),
23    'beige': (245, 245, 220),
24    'bisque': (255, 228, 196),
25    'black': (0, 0, 0),
26    'blanchedalmond': (255, 235, 205),
27    'blue': (0, 0, 255),
28    'blueviolet': (138, 43, 226),
29    'brown': (165, 42, 42),
30    'burlywood': (222, 184, 135),
31    'cadetblue': (95, 158, 160),
32    'chartreuse': (127, 255, 0),
33    'chocolate': (210, 105, 30),
34    'coral': (255, 127, 80),
35    'cornflowerblue': (100, 149, 237),
36    'cornsilk': (255, 248, 220),
37    'crimson': (220, 20, 60),
38    'cyan': (0, 255, 255),
39    'darkblue': (0, 0, 139),
40    'darkcyan': (0, 139, 139),
41    'darkgoldenrod': (184, 134, 11),
42    'darkgray': (169, 169, 169),
43    'darkgreen': (0, 100, 0),
44    'darkgrey': (169, 169, 169),
45    'darkkhaki': (189, 183, 107),
46    'darkmagenta': (139, 0, 139),
47    'darkolivegreen': (85, 107, 47),
48    'darkorange': (255, 140, 0),
49    'darkorchid': (153, 50, 204),
50    'darkred': (139, 0, 0),
51    'darksalmon': (233, 150, 122),
52    'darkseagreen': (143, 188, 143),
53    'darkslateblue': (72, 61, 139),
54    'darkslategray': (47, 79, 79),
55    'darkslategrey': (47, 79, 79),
56    'darkturquoise': (0, 206, 209),
57    'darkviolet': (148, 0, 211),
58    'deeppink': (255, 20, 147),
59    'deepskyblue': (0, 191, 255),
60    'dimgray': (105, 105, 105),
61    'dimgrey': (105, 105, 105),
62    'dodgerblue': (30, 144, 255),
63    'firebrick': (178, 34, 34),
64    'floralwhite': (255, 250, 240),
65    'forestgreen': (34, 139, 34),
66    'fuchsia': (255, 0, 255),
67    'gainsboro': (220, 220, 220),
68    'ghostwhite': (248, 248, 255),
69    'gold': (255, 215, 0),
70    'goldenrod': (218, 165, 32),
71    'gray': (128, 128, 128),
72    'green': (0, 128, 0),
73    'greenyellow': (173, 255, 47),
74    'grey': (128, 128, 128),
75    'honeydew': (240, 255, 240),
76    'hotpink': (255, 105, 180),
77    'indianred': (205, 92, 92),
78    'indigo': (75, 0, 130),
79    'ivory': (255, 255, 240),
80    'khaki': (240, 230, 140),
81    'lavender': (230, 230, 250),
82    'lavenderblush': (255, 240, 245),
83    'lawngreen': (124, 252, 0),
84    'lemonchiffon': (255, 250, 205),
85    'lightblue': (173, 216, 230),
86    'lightcoral': (240, 128, 128),
87    'lightcyan': (224, 255, 255),
88    'lightgoldenrodyellow': (250, 250, 210),
89    'lightgray': (211, 211, 211),
90    'lightgreen': (144, 238, 144),
91    'lightgrey': (211, 211, 211),
92    'lightpink': (255, 182, 193),
93    'lightsalmon': (255, 160, 122),
94    'lightseagreen': (32, 178, 170),
95    'lightskyblue': (135, 206, 250),
96    'lightslategray': (119, 136, 153),
97    'lightslategrey': (119, 136, 153),
98    'lightsteelblue': (176, 196, 222),
99    'lightyellow': (255, 255, 224),
100    'lime': (0, 255, 0),
101    'limegreen': (50, 205, 50),
102    'linen': (250, 240, 230),
103    'magenta': (255, 0, 255),
104    'maroon': (128, 0, 0),
105    'mediumaquamarine': (102, 205, 170),
106    'mediumblue': (0, 0, 205),
107    'mediumorchid': (186, 85, 211),
108    'mediumpurple': (147, 112, 219),
109    'mediumseagreen': (60, 179, 113),
110    'mediumslateblue': (123, 104, 238),
111    'mediumspringgreen': (0, 250, 154),
112    'mediumturquoise': (72, 209, 204),
113    'mediumvioletred': (199, 21, 133),
114    'midnightblue': (25, 25, 112),
115    'mintcream': (245, 255, 250),
116    'mistyrose': (255, 228, 225),
117    'moccasin': (255, 228, 181),
118    'navajowhite': (255, 222, 173),
119    'navy': (0, 0, 128),
120    'oldlace': (253, 245, 230),
121    'olive': (128, 128, 0),
122    'olivedrab': (107, 142, 35),
123    'orange': (255, 165, 0),
124    'orangered': (255, 69, 0),
125    'orchid': (218, 112, 214),
126    'palegoldenrod': (238, 232, 170),
127    'palegreen': (152, 251, 152),
128    'paleturquoise': (175, 238, 238),
129    'palevioletred': (219, 112, 147),
130    'papayawhip': (255, 239, 213),
131    'peachpuff': (255, 218, 185),
132    'peru': (205, 133, 63),
133    'pink': (255, 192, 203),
134    'plum': (221, 160, 221),
135    'powderblue': (176, 224, 230),
136    'purple': (128, 0, 128),
137    'red': (255, 0, 0),
138    'rosybrown': (188, 143, 143),
139    'royalblue': (65, 105, 225),
140    'saddlebrown': (139, 69, 19),
141    'salmon': (250, 128, 114),
142    'sandybrown': (244, 164, 96),
143    'seagreen': (46, 139, 87),
144    'seashell': (255, 245, 238),
145    'sienna': (160, 82, 45),
146    'silver': (192, 192, 192),
147    'skyblue': (135, 206, 235),
148    'slateblue': (106, 90, 205),
149    'slategray': (112, 128, 144),
150    'slategrey': (112, 128, 144),
151    'snow': (255, 250, 250),
152    'springgreen': (0, 255, 127),
153    'steelblue': (70, 130, 180),
154    'tan': (210, 180, 140),
155    'teal': (0, 128, 128),
156    'thistle': (216, 191, 216),
157    'tomato': (255, 99, 71),
158    'turquoise': (64, 224, 208),
159    'violet': (238, 130, 238),
160    'wheat': (245, 222, 179),
161    'white': (255, 255, 255),
162    'whitesmoke': (245, 245, 245),
163    'yellow': (255, 255, 0),
164    'yellowgreen': (154, 205, 50)
165}
166
167
168class Plot(IDManagerMixin):
169    """Definition of a finite region of space to be plotted.
170
171    OpenMC is capable of generating two-dimensional slice plots and
172    three-dimensional voxel plots. Colors that are used in plots can be given as
173    RGB tuples, e.g. (255, 255, 255) would be white, or by a string indicating a
174    valid `SVG color <https://www.w3.org/TR/SVG11/types.html#ColorKeywords>`_.
175
176    Parameters
177    ----------
178    plot_id : int
179        Unique identifier for the plot
180    name : str
181        Name of the plot
182
183    Attributes
184    ----------
185    id : int
186        Unique identifier
187    name : str
188        Name of the plot
189    width : Iterable of float
190        Width of the plot in each basis direction
191    pixels : Iterable of int
192        Number of pixels to use in each basis direction
193    origin : tuple or list of ndarray
194        Origin (center) of the plot
195    filename :
196        Path to write the plot to
197    color_by : {'cell', 'material'}
198        Indicate whether the plot should be colored by cell or by material
199    type : {'slice', 'voxel'}
200        The type of the plot
201    basis : {'xy', 'xz', 'yz'}
202        The basis directions for the plot
203    background : Iterable of int or str
204        Color of the background
205    mask_components : Iterable of openmc.Cell or openmc.Material
206        The cells or materials to plot
207    mask_background : Iterable of int or str
208        Color to apply to all cells/materials not listed in mask_components
209    show_overlaps : bool
210        Inidicate whether or not overlapping regions are shown
211    overlap_color : Iterable of int or str
212        Color to apply to overlapping regions
213    colors : dict
214        Dictionary indicating that certain cells/materials (keys) should be
215        displayed with a particular color.
216    level : int
217        Universe depth to plot at
218    meshlines : dict
219        Dictionary defining type, id, linewidth and color of a mesh to be
220        plotted on top of a plot
221
222    """
223
224    next_id = 1
225    used_ids = set()
226
227    def __init__(self, plot_id=None, name=''):
228        # Initialize Plot class attributes
229        self.id = plot_id
230        self.name = name
231        self._width = [4.0, 4.0]
232        self._pixels = [400, 400]
233        self._origin = [0., 0., 0.]
234        self._filename = None
235        self._color_by = 'cell'
236        self._type = 'slice'
237        self._basis = 'xy'
238        self._background = None
239        self._mask_components = None
240        self._mask_background = None
241        self._show_overlaps = False
242        self._overlap_color = None
243        self._colors = {}
244        self._level = None
245        self._meshlines = None
246
247    @property
248    def name(self):
249        return self._name
250
251    @property
252    def width(self):
253        return self._width
254
255    @property
256    def pixels(self):
257        return self._pixels
258
259    @property
260    def origin(self):
261        return self._origin
262
263    @property
264    def filename(self):
265        return self._filename
266
267    @property
268    def color_by(self):
269        return self._color_by
270
271    @property
272    def type(self):
273        return self._type
274
275    @property
276    def basis(self):
277        return self._basis
278
279    @property
280    def background(self):
281        return self._background
282
283    @property
284    def mask_components(self):
285        return self._mask_components
286
287    @property
288    def mask_background(self):
289        return self._mask_background
290
291    @property
292    def show_overlaps(self):
293        return self._show_overlaps
294
295    @property
296    def overlap_color(self):
297        return self._overlap_color
298
299    @property
300    def colors(self):
301        return self._colors
302
303    @property
304    def level(self):
305        return self._level
306
307    @property
308    def meshlines(self):
309        return self._meshlines
310
311    @name.setter
312    def name(self, name):
313        cv.check_type('plot name', name, str)
314        self._name = name
315
316    @width.setter
317    def width(self, width):
318        cv.check_type('plot width', width, Iterable, Real)
319        cv.check_length('plot width', width, 2, 3)
320        self._width = width
321
322    @origin.setter
323    def origin(self, origin):
324        cv.check_type('plot origin', origin, Iterable, Real)
325        cv.check_length('plot origin', origin, 3)
326        self._origin = origin
327
328    @pixels.setter
329    def pixels(self, pixels):
330        cv.check_type('plot pixels', pixels, Iterable, Integral)
331        cv.check_length('plot pixels', pixels, 2, 3)
332        for dim in pixels:
333            cv.check_greater_than('plot pixels', dim, 0)
334        self._pixels = pixels
335
336    @filename.setter
337    def filename(self, filename):
338        cv.check_type('filename', filename, str)
339        self._filename = filename
340
341    @color_by.setter
342    def color_by(self, color_by):
343        cv.check_value('plot color_by', color_by, ['cell', 'material'])
344        self._color_by = color_by
345
346    @type.setter
347    def type(self, plottype):
348        cv.check_value('plot type', plottype, ['slice', 'voxel'])
349        self._type = plottype
350
351    @basis.setter
352    def basis(self, basis):
353        cv.check_value('plot basis', basis, _BASES)
354        self._basis = basis
355
356    @background.setter
357    def background(self, background):
358        self._check_color('plot background', background)
359        self._background = background
360
361    @colors.setter
362    def colors(self, colors):
363        cv.check_type('plot colors', colors, Mapping)
364        for key, value in colors.items():
365            cv.check_type('plot color key', key, (openmc.Cell, openmc.Material))
366            self._check_color('plot color value', value)
367        self._colors = colors
368
369    @mask_components.setter
370    def mask_components(self, mask_components):
371        cv.check_type('plot mask components', mask_components, Iterable,
372                      (openmc.Cell, openmc.Material))
373        self._mask_components = mask_components
374
375    @mask_background.setter
376    def mask_background(self, mask_background):
377        self._check_color('plot mask background', mask_background)
378        self._mask_background = mask_background
379
380    @show_overlaps.setter
381    def show_overlaps(self, show_overlaps):
382        cv.check_type('Show overlaps flag for Plot ID="{}"'.format(self.id),
383                      show_overlaps, bool)
384        self._show_overlaps = show_overlaps
385
386    @overlap_color.setter
387    def overlap_color(self, overlap_color):
388        self._check_color('plot overlap color', overlap_color)
389        self._overlap_color = overlap_color
390
391    @level.setter
392    def level(self, plot_level):
393        cv.check_type('plot level', plot_level, Integral)
394        cv.check_greater_than('plot level', plot_level, 0, equality=True)
395        self._level = plot_level
396
397    @meshlines.setter
398    def meshlines(self, meshlines):
399        cv.check_type('plot meshlines', meshlines, dict)
400        if 'type' not in meshlines:
401            msg = 'Unable to set the meshlines to "{}" which ' \
402                  'does not have a "type" key'.format(meshlines)
403            raise ValueError(msg)
404
405        elif meshlines['type'] not in ['tally', 'entropy', 'ufs', 'cmfd']:
406            msg = 'Unable to set the meshlines with ' \
407                  'type "{}"'.format(meshlines['type'])
408            raise ValueError(msg)
409
410        if 'id' in meshlines:
411            cv.check_type('plot meshlines id', meshlines['id'], Integral)
412            cv.check_greater_than('plot meshlines id', meshlines['id'], 0,
413                                  equality=True)
414
415        if 'linewidth' in meshlines:
416            cv.check_type('plot mesh linewidth', meshlines['linewidth'], Integral)
417            cv.check_greater_than('plot mesh linewidth', meshlines['linewidth'],
418                                  0, equality=True)
419
420        if 'color' in meshlines:
421            self._check_color('plot meshlines color', meshlines['color'])
422
423        self._meshlines = meshlines
424
425    @staticmethod
426    def _check_color(err_string, color):
427        cv.check_type(err_string, color, Iterable)
428        if isinstance(color, str):
429            if color.lower() not in _SVG_COLORS:
430                raise ValueError("'{}' is not a valid color.".format(color))
431        else:
432            cv.check_length(err_string, color, 3)
433            for rgb in color:
434                cv.check_type(err_string, rgb, Real)
435                cv.check_greater_than('RGB component', rgb, 0, True)
436                cv.check_less_than('RGB component', rgb, 256)
437
438    def __repr__(self):
439        string = 'Plot\n'
440        string += '{: <16}=\t{}\n'.format('\tID', self._id)
441        string += '{: <16}=\t{}\n'.format('\tName', self._name)
442        string += '{: <16}=\t{}\n'.format('\tFilename', self._filename)
443        string += '{: <16}=\t{}\n'.format('\tType', self._type)
444        string += '{: <16}=\t{}\n'.format('\tBasis', self._basis)
445        string += '{: <16}=\t{}\n'.format('\tWidth', self._width)
446        string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin)
447        string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels)
448        string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by)
449        string += '{: <16}=\t{}\n'.format('\tBackground', self._background)
450        string += '{: <16}=\t{}\n'.format('\tMask components',
451                                          self._mask_components)
452        string += '{: <16}=\t{}\n'.format('\tMask background',
453                                          self._mask_background)
454        string += '{: <16}=\t{}\n'.format('\tOverlap Color',
455                                          self._overlap_color)
456        string += '{: <16}=\t{}\n'.format('\tColors', self._colors)
457        string += '{: <16}=\t{}\n'.format('\tLevel', self._level)
458        string += '{: <16}=\t{}\n'.format('\tMeshlines', self._meshlines)
459        return string
460
461    @classmethod
462    def from_geometry(cls, geometry, basis='xy', slice_coord=0.):
463        """Return plot that encompasses a geometry.
464
465        Parameters
466        ----------
467        geometry : openmc.Geometry
468            The geometry to base the plot off of
469        basis : {'xy', 'xz', 'yz'}
470            The basis directions for the plot
471        slice_coord : float
472            The level at which the slice plot should be plotted. For example, if
473            the basis is 'xy', this would indicate the z value used in the
474            origin.
475
476        """
477        cv.check_type('geometry', geometry, openmc.Geometry)
478        cv.check_value('basis', basis, _BASES)
479
480        # Decide which axes to keep
481        if basis == 'xy':
482            pick_index = (0, 1)
483            slice_index = 2
484        elif basis == 'yz':
485            pick_index = (1, 2)
486            slice_index = 0
487        elif basis == 'xz':
488            pick_index = (0, 2)
489            slice_index = 1
490
491        # Get lower-left and upper-right coordinates for desired axes
492        lower_left, upper_right = geometry.bounding_box
493        lower_left = lower_left[np.array(pick_index)]
494        upper_right = upper_right[np.array(pick_index)]
495
496        if np.any(np.isinf((lower_left, upper_right))):
497            raise ValueError('The geometry does not appear to be bounded '
498                             'in the {} plane.'.format(basis))
499
500        plot = cls()
501        plot.origin = np.insert((lower_left + upper_right)/2,
502                                slice_index, slice_coord)
503        plot.width = upper_right - lower_left
504        return plot
505
506    def colorize(self, geometry, seed=1):
507        """Generate a color scheme for each domain in the plot.
508
509        This routine may be used to generate random, reproducible color schemes.
510        The colors generated are based upon cell/material IDs in the geometry.
511
512        Parameters
513        ----------
514        geometry : openmc.Geometry
515            The geometry for which the plot is defined
516        seed : Integral
517            The random number seed used to generate the color scheme
518
519        """
520
521        cv.check_type('geometry', geometry, openmc.Geometry)
522        cv.check_type('seed', seed, Integral)
523        cv.check_greater_than('seed', seed, 1, equality=True)
524
525        # Get collections of the domains which will be plotted
526        if self.color_by == 'material':
527            domains = geometry.get_all_materials().values()
528        else:
529            domains = geometry.get_all_cells().values()
530
531        # Set the seed for the random number generator
532        np.random.seed(seed)
533
534        # Generate random colors for each feature
535        for domain in domains:
536            self.colors[domain] = np.random.randint(0, 256, (3,))
537
538    def highlight_domains(self, geometry, domains, seed=1,
539                          alpha=0.5, background='gray'):
540        """Use alpha compositing to highlight one or more domains in the plot.
541
542        This routine generates a color scheme and applies alpha compositing to
543        make all domains except the highlighted ones appear partially
544        transparent.
545
546        Parameters
547        ----------
548        geometry : openmc.Geometry
549            The geometry for which the plot is defined
550        domains : Iterable of openmc.Cell or openmc.Material
551            A collection of the domain IDs to highlight in the plot
552        seed : int
553            The random number seed used to generate the color scheme
554        alpha : float
555            The value between 0 and 1 to apply in alpha compisiting
556        background : 3-tuple of int or str
557            The background color to apply in alpha compisiting
558
559        """
560
561        cv.check_type('domains', domains, Iterable,
562                      (openmc.Cell, openmc.Material))
563        cv.check_type('alpha', alpha, Real)
564        cv.check_greater_than('alpha', alpha, 0., equality=True)
565        cv.check_less_than('alpha', alpha, 1., equality=True)
566        cv.check_type('background', background, Iterable)
567
568        # Get a background (R,G,B) tuple to apply in alpha compositing
569        if isinstance(background, str):
570            if background.lower() not in _SVG_COLORS:
571                raise ValueError("'{}' is not a valid color.".format(background))
572            background = _SVG_COLORS[background.lower()]
573
574        # Generate a color scheme
575        self.colorize(geometry, seed)
576
577        # Apply alpha compositing to the colors for all domains
578        # other than those the user wishes to highlight
579        for domain, color in self.colors.items():
580            if domain not in domains:
581                if isinstance(color, str):
582                    color = _SVG_COLORS[color.lower()]
583                r, g, b = color
584                r = int(((1-alpha) * background[0]) + (alpha * r))
585                g = int(((1-alpha) * background[1]) + (alpha * g))
586                b = int(((1-alpha) * background[2]) + (alpha * b))
587                self._colors[domain] = (r, g, b)
588
589    def to_xml_element(self):
590        """Return XML representation of the plot
591
592        Returns
593        -------
594        element : xml.etree.ElementTree.Element
595            XML element containing plot data
596
597        """
598
599        element = ET.Element("plot")
600        element.set("id", str(self._id))
601        if self._filename is not None:
602            element.set("filename", self._filename)
603        element.set("color_by", self._color_by)
604        element.set("type", self._type)
605
606        if self._type == 'slice':
607            element.set("basis", self._basis)
608
609        subelement = ET.SubElement(element, "origin")
610        subelement.text = ' '.join(map(str, self._origin))
611
612        subelement = ET.SubElement(element, "width")
613        subelement.text = ' '.join(map(str, self._width))
614
615        subelement = ET.SubElement(element, "pixels")
616        subelement.text = ' '.join(map(str, self._pixels))
617
618        if self._background is not None:
619            subelement = ET.SubElement(element, "background")
620            color = self._background
621            if isinstance(color, str):
622                color = _SVG_COLORS[color.lower()]
623            subelement.text = ' '.join(str(x) for x in color)
624
625        if self._colors:
626            for domain, color in sorted(self._colors.items(),
627                                        key=lambda x: x[0].id):
628                subelement = ET.SubElement(element, "color")
629                subelement.set("id", str(domain.id))
630                if isinstance(color, str):
631                    color = _SVG_COLORS[color.lower()]
632                subelement.set("rgb", ' '.join(str(x) for x in color))
633
634        if self._mask_components is not None:
635            subelement = ET.SubElement(element, "mask")
636            subelement.set("components", ' '.join(
637                str(d.id) for d in self._mask_components))
638            color = self._mask_background
639            if color is not None:
640                if isinstance(color, str):
641                    color = _SVG_COLORS[color.lower()]
642                subelement.set("background", ' '.join(
643                    str(x) for x in color))
644
645        if self._show_overlaps:
646            subelement = ET.SubElement(element, "show_overlaps")
647            subelement.text = "true"
648
649            if self._overlap_color is not None:
650                color = self._overlap_color
651                if isinstance(color, str):
652                    color = _SVG_COLORS[color.lower()]
653                subelement = ET.SubElement(element, "overlap_color")
654                subelement.text = ' '.join(str(x) for x in color)
655
656
657        if self._level is not None:
658            subelement = ET.SubElement(element, "level")
659            subelement.text = str(self._level)
660
661        if self._meshlines is not None:
662            subelement = ET.SubElement(element, "meshlines")
663            subelement.set("meshtype", self._meshlines['type'])
664            if self._meshlines['id'] is not None:
665                subelement.set("id", str(self._meshlines['id']))
666            if self._meshlines['linewidth'] is not None:
667                subelement.set("linewidth", str(self._meshlines['linewidth']))
668            if self._meshlines['color'] is not None:
669                subelement.set("color", ' '.join(map(
670                    str, self._meshlines['color'])))
671
672        return element
673
674    def to_ipython_image(self, openmc_exec='openmc', cwd='.',
675                         convert_exec='convert'):
676        """Render plot as an image
677
678        This method runs OpenMC in plotting mode to produce a bitmap image which
679        is then converted to a .png file and loaded in as an
680        :class:`IPython.display.Image` object. As such, it requires that your
681        model geometry, materials, and settings have already been exported to
682        XML.
683
684        Parameters
685        ----------
686        openmc_exec : str
687            Path to OpenMC executable
688        cwd : str, optional
689            Path to working directory to run in
690        convert_exec : str, optional
691            Command that can convert PPM files into PNG files
692
693        Returns
694        -------
695        IPython.display.Image
696            Image generated
697
698        """
699        from IPython.display import Image
700
701        # Create plots.xml
702        Plots([self]).export_to_xml()
703
704        # Run OpenMC in geometry plotting mode
705        openmc.plot_geometry(False, openmc_exec, cwd)
706
707        # Convert to .png
708        if self.filename is not None:
709            ppm_file = '{}.ppm'.format(self.filename)
710        else:
711            ppm_file = 'plot_{}.ppm'.format(self.id)
712        png_file = ppm_file.replace('.ppm', '.png')
713        subprocess.check_call([convert_exec, ppm_file, png_file])
714
715        return Image(png_file)
716
717
718class Plots(cv.CheckedList):
719    """Collection of Plots used for an OpenMC simulation.
720
721    This class corresponds directly to the plots.xml input file. It can be
722    thought of as a normal Python list where each member is a :class:`Plot`. It
723    behaves like a list as the following example demonstrates:
724
725    >>> xz_plot = openmc.Plot()
726    >>> big_plot = openmc.Plot()
727    >>> small_plot = openmc.Plot()
728    >>> p = openmc.Plots((xz_plot, big_plot))
729    >>> p.append(small_plot)
730    >>> small_plot = p.pop()
731
732    Parameters
733    ----------
734    plots : Iterable of openmc.Plot
735        Plots to add to the collection
736
737    """
738
739    def __init__(self, plots=None):
740        super().__init__(Plot, 'plots collection')
741        self._plots_file = ET.Element("plots")
742        if plots is not None:
743            self += plots
744
745    def append(self, plot):
746        """Append plot to collection
747
748        Parameters
749        ----------
750        plot : openmc.Plot
751            Plot to append
752
753        """
754        super().append(plot)
755
756    def insert(self, index, plot):
757        """Insert plot before index
758
759        Parameters
760        ----------
761        index : int
762            Index in list
763        plot : openmc.Plot
764            Plot to insert
765
766        """
767        super().insert(index, plot)
768
769    def colorize(self, geometry, seed=1):
770        """Generate a consistent color scheme for each domain in each plot.
771
772        This routine may be used to generate random, reproducible color schemes.
773        The colors generated are based upon cell/material IDs in the geometry.
774        The color schemes will be consistent for all plots in "plots.xml".
775
776        Parameters
777        ----------
778        geometry : openmc.Geometry
779            The geometry for which the plots are defined
780        seed : Integral
781            The random number seed used to generate the color scheme
782
783        """
784
785        for plot in self:
786            plot.colorize(geometry, seed)
787
788
789    def highlight_domains(self, geometry, domains, seed=1,
790                          alpha=0.5, background='gray'):
791        """Use alpha compositing to highlight one or more domains in the plot.
792
793        This routine generates a color scheme and applies alpha compositing to
794        make all domains except the highlighted ones appear partially
795        transparent.
796
797        Parameters
798        ----------
799        geometry : openmc.Geometry
800            The geometry for which the plot is defined
801        domains : Iterable of openmc.Cell or openmc.Material
802            A collection of the domain IDs to highlight in the plot
803        seed : int
804            The random number seed used to generate the color scheme
805        alpha : float
806            The value between 0 and 1 to apply in alpha compisiting
807        background : 3-tuple of int or str
808            The background color to apply in alpha compisiting
809
810        """
811
812        for plot in self:
813            plot.highlight_domains(geometry, domains, seed, alpha, background)
814
815    def _create_plot_subelements(self):
816        for plot in self:
817            xml_element = plot.to_xml_element()
818
819            if len(plot.name) > 0:
820                self._plots_file.append(ET.Comment(plot.name))
821
822            self._plots_file.append(xml_element)
823
824    def export_to_xml(self, path='plots.xml'):
825        """Export plot specifications to an XML file.
826
827        Parameters
828        ----------
829        path : str
830            Path to file to write. Defaults to 'plots.xml'.
831
832        """
833        # Reset xml element tree
834        self._plots_file.clear()
835
836        self._create_plot_subelements()
837
838        # Clean the indentation in the file to be user-readable
839        clean_indentation(self._plots_file)
840
841        # Check if path is a directory
842        p = Path(path)
843        if p.is_dir():
844            p /= 'plots.xml'
845
846        # Write the XML Tree to the plots.xml file
847        reorder_attributes(self._plots_file)  # TODO: Remove when support is Python 3.8+
848        tree = ET.ElementTree(self._plots_file)
849        tree.write(str(p), xml_declaration=True, encoding='utf-8')
850