Skip to content

Commit e0eb91b

Browse files
authored
using id_map in model.plot for more efficient plotting (#3678)
1 parent d118356 commit e0eb91b

File tree

3 files changed

+219
-116
lines changed

3 files changed

+219
-116
lines changed

openmc/model/model.py

Lines changed: 96 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from openmc.executor import _process_CLI_arguments
2424
from openmc.checkvalue import check_type, check_value, PathLike
2525
from openmc.exceptions import InvalidIDError
26-
from openmc.plots import add_plot_params, _BASIS_INDICES
26+
from openmc.plots import add_plot_params, _BASIS_INDICES, id_map_to_rgb
2727
from openmc.utility_funcs import change_directory
2828

2929

@@ -1114,13 +1114,12 @@ def plot(
11141114
color_by: str = 'cell',
11151115
colors: dict | None = None,
11161116
seed: int | None = None,
1117-
openmc_exec: PathLike = 'openmc',
11181117
axes=None,
11191118
legend: bool = False,
11201119
axis_units: str = 'cm',
11211120
outline: bool | str = False,
11221121
show_overlaps: bool = False,
1123-
overlap_color: Sequence[int] | str | None = None,
1122+
overlap_color: Sequence[int] | str = (255, 0, 0),
11241123
n_samples: int | None = None,
11251124
plane_tolerance: float = 1.,
11261125
legend_kwargs: dict | None = None,
@@ -1132,7 +1131,6 @@ def plot(
11321131
11331132
.. versionadded:: 0.15.1
11341133
"""
1135-
import matplotlib.image as mpimg
11361134
import matplotlib.patches as mpatches
11371135
import matplotlib.pyplot as plt
11381136

@@ -1162,125 +1160,108 @@ def plot(
11621160
y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units]
11631161
y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units]
11641162

1165-
# Determine whether any materials contains macroscopic data and if so,
1166-
# set energy mode accordingly
1167-
_energy_mode = self.settings._energy_mode
1168-
for mat in self.geometry.get_all_materials().values():
1169-
if mat._macroscopic is not None:
1170-
self.settings.energy_mode = 'multi-group'
1171-
break
1172-
1173-
with TemporaryDirectory() as tmpdir:
1174-
_plot_seed = self.settings.plot_seed
1175-
if seed is not None:
1176-
self.settings.plot_seed = seed
1163+
# Get ID map from the C API
1164+
id_map = self.id_map(
1165+
origin=origin,
1166+
width=width,
1167+
pixels=pixels,
1168+
basis=basis,
1169+
color_overlaps=show_overlaps
1170+
)
11771171

1178-
# Create plot object matching passed arguments
1172+
# Generate colors if not provided
1173+
if colors is None and seed is not None:
1174+
# Use the colorize method to generate random colors
11791175
plot = openmc.SlicePlot()
1180-
plot.origin = origin
1181-
plot.width = width
1182-
plot.pixels = pixels
1183-
plot.basis = basis
11841176
plot.color_by = color_by
1185-
plot.show_overlaps = show_overlaps
1186-
if overlap_color is not None:
1187-
plot.overlap_color = overlap_color
1188-
if colors is not None:
1189-
plot.colors = colors
1190-
self.plots.append(plot)
1191-
1192-
# Run OpenMC in geometry plotting mode
1193-
self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec)
1194-
1195-
# Undo changes to model
1196-
self.plots.pop()
1197-
self.settings._plot_seed = _plot_seed
1198-
self.settings._energy_mode = _energy_mode
1199-
1200-
# Read image from file
1201-
img_path = Path(tmpdir) / f'plot_{plot.id}.png'
1202-
if not img_path.is_file():
1203-
img_path = img_path.with_suffix('.ppm')
1204-
img = mpimg.imread(str(img_path))
1205-
1206-
# Create a figure sized such that the size of the axes within
1207-
# exactly matches the number of pixels specified
1208-
if axes is None:
1209-
px = 1/plt.rcParams['figure.dpi']
1210-
fig, axes = plt.subplots()
1211-
axes.set_xlabel(xlabel)
1212-
axes.set_ylabel(ylabel)
1213-
params = fig.subplotpars
1214-
width = pixels[0]*px/(params.right - params.left)
1215-
height = pixels[1]*px/(params.top - params.bottom)
1216-
fig.set_size_inches(width, height)
1217-
1218-
if outline:
1219-
# Combine R, G, B values into a single int
1220-
rgb = (img * 256).astype(int)
1221-
image_value = (rgb[..., 0] << 16) + \
1222-
(rgb[..., 1] << 8) + (rgb[..., 2])
1223-
1224-
# Set default arguments for contour()
1225-
if contour_kwargs is None:
1226-
contour_kwargs = {}
1227-
contour_kwargs.setdefault('colors', 'k')
1228-
contour_kwargs.setdefault('linestyles', 'solid')
1229-
contour_kwargs.setdefault('algorithm', 'serial')
1230-
1231-
axes.contour(
1232-
image_value,
1233-
origin="upper",
1234-
levels=np.unique(image_value),
1235-
extent=(x_min, x_max, y_min, y_max),
1236-
**contour_kwargs
1237-
)
1238-
1239-
# add legend showing which colors represent which material
1240-
# or cell if that was requested
1241-
if legend:
1242-
if plot.colors == {}:
1243-
raise ValueError("Must pass 'colors' dictionary if you "
1244-
"are adding a legend via legend=True.")
1177+
plot.colorize(self.geometry, seed=seed)
1178+
colors = plot.colors
1179+
1180+
# Convert ID map to RGB image
1181+
img = id_map_to_rgb(
1182+
id_map=id_map,
1183+
color_by=color_by,
1184+
colors=colors,
1185+
overlap_color=overlap_color
1186+
)
12451187

1246-
if color_by == "cell":
1247-
expected_key_type = openmc.Cell
1188+
# Create a figure sized such that the size of the axes within
1189+
# exactly matches the number of pixels specified
1190+
if axes is None:
1191+
px = 1/plt.rcParams['figure.dpi']
1192+
fig, axes = plt.subplots()
1193+
axes.set_xlabel(xlabel)
1194+
axes.set_ylabel(ylabel)
1195+
params = fig.subplotpars
1196+
width_px = pixels[0]*px/(params.right - params.left)
1197+
height_px = pixels[1]*px/(params.top - params.bottom)
1198+
fig.set_size_inches(width_px, height_px)
1199+
1200+
if outline:
1201+
# Combine R, G, B values into a single int for contour detection
1202+
rgb = (img * 256).astype(int)
1203+
image_value = (rgb[..., 0] << 16) + \
1204+
(rgb[..., 1] << 8) + (rgb[..., 2])
1205+
1206+
# Set default arguments for contour()
1207+
if contour_kwargs is None:
1208+
contour_kwargs = {}
1209+
contour_kwargs.setdefault('colors', 'k')
1210+
contour_kwargs.setdefault('linestyles', 'solid')
1211+
contour_kwargs.setdefault('algorithm', 'serial')
1212+
1213+
axes.contour(
1214+
image_value,
1215+
origin="upper",
1216+
levels=np.unique(image_value),
1217+
extent=(x_min, x_max, y_min, y_max),
1218+
**contour_kwargs
1219+
)
1220+
1221+
# If only showing outline, set the axis limits and aspect explicitly
1222+
if outline == 'only':
1223+
axes.set_xlim(x_min, x_max)
1224+
axes.set_ylim(y_min, y_max)
1225+
axes.set_aspect('equal')
1226+
1227+
# Add legend showing which colors represent which material or cell
1228+
if legend:
1229+
if colors is None or len(colors) == 0:
1230+
raise ValueError("Must pass 'colors' dictionary if you "
1231+
"are adding a legend via legend=True.")
1232+
1233+
if color_by == "cell":
1234+
expected_key_type = openmc.Cell
1235+
else:
1236+
expected_key_type = openmc.Material
1237+
1238+
patches = []
1239+
for key, color in colors.items():
1240+
if isinstance(key, int):
1241+
raise TypeError(
1242+
"Cannot use IDs in colors dict for auto legend.")
1243+
elif not isinstance(key, expected_key_type):
1244+
raise TypeError(
1245+
"Color dict key type does not match color_by")
1246+
1247+
# this works whether we're doing cells or materials
1248+
label = key.name if key.name != '' else key.id
1249+
1250+
# matplotlib takes RGB on 0-1 scale rather than 0-255
1251+
if len(color) == 3 and not isinstance(color, str):
1252+
scaled_color = (
1253+
color[0]/255, color[1]/255, color[2]/255)
12481254
else:
1249-
expected_key_type = openmc.Material
1250-
1251-
patches = []
1252-
for key, color in plot.colors.items():
1253-
1254-
if isinstance(key, int):
1255-
raise TypeError(
1256-
"Cannot use IDs in colors dict for auto legend.")
1257-
elif not isinstance(key, expected_key_type):
1258-
raise TypeError(
1259-
"Color dict key type does not match color_by")
1260-
1261-
# this works whether we're doing cells or materials
1262-
label = key.name if key.name != '' else key.id
1263-
1264-
# matplotlib takes RGB on 0-1 scale rather than 0-255. at
1265-
# this point PlotBase has already checked that 3-tuple
1266-
# based colors are already valid, so if the length is three
1267-
# then we know it just needs to be converted to the 0-1
1268-
# format.
1269-
if len(color) == 3 and not isinstance(color, str):
1270-
scaled_color = (
1271-
color[0]/255, color[1]/255, color[2]/255)
1272-
else:
1273-
scaled_color = color
1274-
1275-
key_patch = mpatches.Patch(color=scaled_color, label=label)
1276-
patches.append(key_patch)
1255+
scaled_color = color
12771256

1278-
axes.legend(handles=patches, **legend_kwargs)
1257+
key_patch = mpatches.Patch(color=scaled_color, label=label)
1258+
patches.append(key_patch)
12791259

1280-
# Plot image and return the axes
1281-
if outline != 'only':
1282-
axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs)
1260+
axes.legend(handles=patches, **legend_kwargs)
12831261

1262+
# Plot image and return the axes
1263+
if outline != 'only':
1264+
axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs)
12841265

12851266
if n_samples:
12861267
# Sample external source particles

openmc/plots.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Iterable, Mapping
1+
from collections.abc import Iterable, Mapping, Sequence
22
from numbers import Integral, Real
33
from pathlib import Path
44
from textwrap import dedent
@@ -355,6 +355,86 @@ def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'):
355355
return output
356356

357357

358+
def id_map_to_rgb(
359+
id_map: np.ndarray,
360+
color_by: str = 'cell',
361+
colors: dict | None = None,
362+
overlap_color: Sequence[int] | str = (255, 0, 0)
363+
) -> np.ndarray:
364+
"""Convert ID map array to RGB image array.
365+
366+
Parameters
367+
----------
368+
id_map : numpy.ndarray
369+
Array with shape (v_pixels, h_pixels, 3) containing cell IDs,
370+
cell instances, and material IDs
371+
color_by : {'cell', 'material'}
372+
Whether to color by cell or material
373+
colors : dict, optional
374+
Dictionary mapping cells/materials to colors
375+
overlap_color : sequence of int or str, optional
376+
Color to use for overlaps. Defaults to red (255, 0, 0).
377+
378+
Returns
379+
-------
380+
numpy.ndarray
381+
RGB image array with shape (v_pixels, h_pixels, 3) with values
382+
in range [0, 1] for matplotlib
383+
"""
384+
# Initialize RGB array with white background (values between 0 and 1 for matplotlib)
385+
img = np.ones(id_map.shape, dtype=float)
386+
387+
# Get the appropriate index based on color_by
388+
if color_by == 'cell':
389+
id_index = 0 # Cell IDs are in the first channel
390+
elif color_by == 'material':
391+
id_index = 2 # Material IDs are in the third channel
392+
else:
393+
raise ValueError("color_by must be either 'cell' or 'material'")
394+
395+
# Get all unique IDs in the plot
396+
unique_ids = np.unique(id_map[:, :, id_index])
397+
398+
# Generate default colors if not provided
399+
if colors is None:
400+
colors = {}
401+
402+
# Convert colors dict to use IDs as keys
403+
color_map = {}
404+
for key, color in colors.items():
405+
if isinstance(key, (openmc.Cell, openmc.Material)):
406+
color_map[key.id] = color
407+
else:
408+
color_map[key] = color
409+
410+
# Generate random colors for IDs not in color_map
411+
rng = np.random.RandomState(1)
412+
for uid in unique_ids:
413+
if uid > 0 and uid not in color_map:
414+
color_map[uid] = rng.randint(0, 256, (3,))
415+
416+
# Apply colors to each pixel
417+
for uid in unique_ids:
418+
if uid == -1: # Background/void
419+
continue
420+
elif uid == -3: # Overlap (only present if color_overlaps was True)
421+
if isinstance(overlap_color, str):
422+
rgb = _SVG_COLORS[overlap_color.lower()]
423+
else:
424+
rgb = overlap_color
425+
mask = id_map[:, :, id_index] == uid
426+
img[mask] = np.array(rgb) / 255.0
427+
elif uid in color_map:
428+
color = color_map[uid]
429+
if isinstance(color, str):
430+
rgb = _SVG_COLORS[color.lower()]
431+
else:
432+
rgb = color
433+
mask = id_map[:, :, id_index] == uid
434+
img[mask] = np.array(rgb) / 255.0
435+
436+
return img
437+
358438
class PlotBase(IDManagerMixin):
359439
"""
360440
Parameters

tests/unit_tests/test_model.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import openmc
99
import openmc.lib
10+
from openmc.plots import id_map_to_rgb
1011

1112

1213
@pytest.fixture(scope='function')
@@ -996,3 +997,44 @@ def modify_radius(radius):
996997
# Check that total_batches property works
997998
assert result.total_batches == sum(result.batches)
998999
assert result.total_batches > 0
1000+
1001+
1002+
def test_id_map_to_rgb():
1003+
"""Test conversion of ID map to RGB image array."""
1004+
# Create a simple model
1005+
mat = openmc.Material()
1006+
mat.set_density('g/cm3', 1.0)
1007+
mat.add_nuclide('Li7', 1.0)
1008+
1009+
sphere = openmc.Sphere(r=5.0, boundary_type='vacuum')
1010+
cell = openmc.Cell(fill=mat, region=-sphere)
1011+
geometry = openmc.Geometry([cell])
1012+
settings = openmc.Settings(
1013+
batches=10, particles=100, run_mode='fixed source'
1014+
)
1015+
model = openmc.Model(geometry, settings=settings)
1016+
1017+
id_data = np.zeros((10, 10, 3), dtype=np.int32)
1018+
id_data[:, :, 0] = cell.id # Cell IDs
1019+
id_data[:, :, 2] = mat.id # Material IDs
1020+
1021+
# Test color_by with default colors
1022+
for color_by in ['cell', 'material']:
1023+
rgb = id_map_to_rgb(id_data, color_by=color_by)
1024+
assert rgb.shape == (10, 10, 3)
1025+
assert rgb.dtype == float
1026+
assert np.all((rgb >= 0) & (rgb <= 1)) # RGB values in [0, 1]
1027+
1028+
# Test with custom colors
1029+
colors = {cell.id: (255, 0, 0)} # Red
1030+
rgb_custom = id_map_to_rgb(id_data, color_by='cell', colors=colors)
1031+
assert np.allclose(rgb_custom, [1.0, 0.0, 0.0]) # All pixels should be red
1032+
1033+
# Test with overlaps
1034+
id_data_overlap = id_data.copy()
1035+
id_data_overlap[5:, 5:, 0] = -3 # Mark some pixels as overlaps
1036+
rgb_overlap = id_map_to_rgb(
1037+
id_data_overlap, overlap_color=(0, 255, 0)
1038+
)
1039+
# Check that overlap region is green
1040+
assert np.allclose(rgb_overlap[5:, 5:], [0.0, 1.0, 0.0])

0 commit comments

Comments
 (0)