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