import pyvista as pv
import functools
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import vtk
from upxo._sup import dataTypeHandlers as dth
from shapely.geometry import MultiPolygon
"""
Visualization utilities for grain structure data.
Usage
-----
from upxo.viz import gsviz
"""
# Visualization presets for different use cases
PLOT_PRESETS = {
'publication': {
'figsize': (6, 4.5), 'dpi': 300, 'cmap': 'viridis',
'colorbar': True, 'interpolation': None
},
'presentation': {
'figsize': (10, 8), 'dpi': 150, 'cmap': 'plasma',
'colorbar': True, 'interpolation': None
},
'quick': {
'figsize': (6, 6), 'dpi': 100, 'cmap': 'viridis',
'colorbar': True, 'interpolation': None
},
'minimal': {
'figsize': (5, 5), 'dpi': 80, 'cmap': 'gray',
'colorbar': False, 'interpolation': None
}
}
[docs]
def see_map(lfi, fids=None, cmap='viridis',
figsize=(8, 6), dpi=100, vmin=None, vmax=None,
title='Map View', xlabel='X-axis', ylabel='Y-axis',
cmlabel='Value', preset='minimal', ax=None, show=True,
cbar_ticks=None, mbar=False, mbar_length=10, mbar_loc='bot_left',
mbar_units='μm',
**imshow_kwargs):
"""
Visualize 2D map data with flexible styling options.
Parameters
----------
mapdata : array_like
2D data array to visualize
cmap : str, optional
Colormap name. Default is 'viridis'.
figsize : tuple, optional
Figure size (width, height). Default is (8, 6).
dpi : int, optional
Figure resolution. Default is 100.
vmin, vmax : float, optional
Color scale limits
title : str, optional
Plot title. Default is 'Map View'.
xlabel, ylabel : str, optional
Axis labels. Default is 'X-axis', 'Y-axis'.
cmlabel : str, optional
Colorbar label. Default is 'Value'.
preset : str, optional
Use predefined style preset ('publication', 'presentation', 'quick', 'minimal').
Overrides figsize, dpi, cmap if specified. Individual parameters override preset.
ax : matplotlib.axes.Axes, optional
Existing axis to plot on. If None, creates new figure.
show : bool, optional
Whether to call plt.show(). Default is True. Set False for further customization.
**kwargs : dict
Additional arguments passed to imshow (e.g., interpolation, alpha, extent)
Returns
-------
ax : matplotlib.axes.Axes
The axis object for further customization
Usage
-----
from upxo.viz.gsviz import see_map
Examples
--------
>>> # Quick visualization
>>> see_map(data)
>>> # Publication-ready figure
>>> see_map(data, preset='publication', title='Grain Structure')
>>> # Custom styling with preset base
>>> ax = see_map(data, preset='presentation', cmap='coolwarm', show=False)
>>> ax.set_aspect('equal')
>>> plt.show()
"""
# Apply preset if specified
params = {}
if preset is not None:
if preset not in PLOT_PRESETS:
raise ValueError(f"Unknown preset '{preset}'. Available: {list(PLOT_PRESETS.keys())}")
params = PLOT_PRESETS[preset].copy()
# --------------------------------------------------------
# Override preset with explicit parameters
if cmap != 'viridis' or not preset:
params['cmap'] = cmap
if figsize != (8, 6) or not preset:
params['figsize'] = figsize
if dpi != 100 or not preset:
params['dpi'] = dpi
# --------------------------------------------------------
# Extract colorbar setting from preset or default to True
show_colorbar = params.pop('colorbar', True)
# --------------------------------------------------------
# Merge remaining kwargs
imshow_params = {k: v for k, v in params.items() if k not in ['figsize', 'dpi', 'cmap']}
imshow_params.update(imshow_kwargs)
# --------------------------------------------------------
# Create figure/axis if needed
if ax is None:
fig, ax = plt.subplots(figsize=params.get('figsize', figsize),
dpi=params.get('dpi', dpi))
created_fig = True
else:
created_fig = False
# --------------------------------------------------------
# Process lfi
if fids is not None:
lfi = np.where(np.isin(lfi, fids), lfi, np.nan)
# --------------------------------------------------------
im = ax.imshow(lfi, cmap=params.get('cmap', cmap),
vmin=vmin, vmax=vmax, **imshow_params)
# --------------------------------------------------------
# Add scale bar if requested
if mbar:
xstart, ystart = 0, 0
xsize, ysize = lfi.shape[1], lfi.shape[0]
if mbar_loc == 'bot_left':
mbar_xstart = xstart+0.05*min(xsize, ysize)
mbar_ystart = ystart+0.95*min(xsize, ysize)
elif mbar_loc == 'top_left':
mbar_xstart = xstart+0.05*min(xsize, ysize)
mbar_ystart = ystart+0.05*min(xsize, ysize)
elif mbar_loc == 'bot_right':
mbar_xstart = xstart+0.95*min(xsize, ysize)
mbar_ystart = ystart+0.95*min(xsize, ysize)
elif mbar_loc == 'top_right':
mbar_xstart = xstart+0.95*min(xsize, ysize)
mbar_ystart = ystart+0.05*min(xsize, ysize)
mbar_xend = mbar_xstart+mbar_length
mbar_ends = [[mbar_xstart, mbar_xend], [mbar_ystart, mbar_ystart]]
xtext = 0.2*sum(mbar_ends[0])
ytext = 0.5*sum(mbar_ends[1])-0.2*mbar_length
ax.plot(mbar_ends[0], mbar_ends[1], c='k', linewidth=4)
ax.text(xtext, ytext, f'{mbar_length} {mbar_units}',
fontsize=10, bbox={'color': 'white', 'alpha': 0.75})
# --------------------------------------------------------------------
if show_colorbar:
plt.colorbar(im, ax=ax, label=cmlabel, ticks=cbar_ticks)
# --------------------------------------------------------
ax.set_title(title)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
# Only show if we created the figure and show=True
if created_fig and show:
plt.show()
return ax
[docs]
def see_bsegs_gid(lfi, pcid, dim=2, bsegGeomType='pix',
figsize=(5, 5), dpi=75):
"""
Orchastrator function to visualize boundary segments.
Parameters
----------
lfi : ndarray
Local feature ID array. here, this should be the lfi of segments and not
the usual lfi/lgi of MCGS2D/MCGS3D.
dim: int
Dimensionality
pcid : int
Parent cell ID. This could be the grain ID you are interested in.
bsegGeomType : str
Boundary segment geometry type. Options include: 'pix' (for 2D),
'vox' (for 3D) and 'geom' (for 2D and 3D: MCGS or VTGS).
Currently only 'pix' (pixel) is supported.
figsize : tuple, optional
Figure size. Default is (5, 5).
dpi : int, optional
Figure resolution. Default is 75.
Returns
-------
None
Functions orchastrated
----------------------
see_bsegs_gid_pix(lfi, pcid)
Examples
--------
see_bsegs_gid(localSegIDMasked_lfi, pcid, dim=2, bsegGeomType='pix')
"""
if dim == 2 and bsegGeomType=='pix':
see_bsegs_gid_pix(lfi, pcid, figsize=figsize, dpi=dpi)
if dim == 2 and bsegGeomType=='geom':
raise NotImplementedError("2D geometric boundary segment visualization not yet implemented.")
if dim == 3 and bsegGeomType=='vox':
raise NotImplementedError("3D voxel boundary segment visualization not yet implemented.")
if dim == 3 and bsegGeomType=='geom':
raise NotImplementedError("3D geometric boundary segment visualization not yet implemented.")
[docs]
def see_bsegs_gid_pix(lfi, pcid, figsize=(8, 6), dpi=100, vmin=None, vmax=None,
title='Map View', xlabel='X-axis', ylabel='Y-axis',
cmlabel='Value', preset='minimal', ax=None, show=True,
cbar_ticks=None, **imshow_kwargs):
"""
Visualize boundary segments for a given parent cell ID in 2D pixel data.
Parameters
----------
lfi : ndarray
Local feature ID array. here, this should be the lfi of segments and not
the usual lfi/lgi of MCGS2D/MCGS3D.
pcid : int
Parent cell ID. This could be the grain ID you are interested in.
figsize : tuple, optional
Figure size. Default is (5, 5).
dpi : int, optional
Figure resolution. Default is 75.
Returns
-------
None
Examples
--------
>>> # Visualize boundary segments for parent cell ID of largest grain
>>> pcid = np.argmax(pxt.gs[3].prop.npixels.to_numpy())+1
>>> see_bsegs_gid_pix(localSegIDMasked_lfi, pcid, figsize=(5,5), dpi=75)
"""
scaled_mask = np.asarray(lfi >= pcid, dtype=float)
scaled_mask = scaled_mask*np.asarray(lfi < pcid+1, dtype=float)
scaled_mask = lfi*scaled_mask
'''scaled_mask = lfi*np.asarray(lfi >= pcid, dtype=float)*np.asarray(lfi < pcid+1, dtype=float)'''
scaled_mask[scaled_mask < pcid] = np.nan
# ----------------------------------------------
see_map(scaled_mask, cmap='viridis',
figsize=(figsize), dpi=dpi, vmin=vmin, vmax=vmax,
title=title, xlabel=xlabel, ylabel=ylabel,
cmlabel=cmlabel, preset=preset, ax=ax, show=show,
cbar_ticks=cbar_ticks, **imshow_kwargs)
[docs]
def plot_multipolygon_geometric(gs_geometric, fig=None, ax=None, cmap='tab20', edgecolor='black',
alpha=0.7, lw=1, figsize=(10, 10), dpi=100,
points=None, point_color='red', point_size=20,
point_marker='o', point_alpha=0.8, point_label='Points'):
"""
Plot a Shapely MultiPolygon object using matplotlib with unique colors per polygon.
Parameters
----------
gs_geometric : shapely.geometry.MultiPolygon
The MultiPolygon object to plot
cmap : str or matplotlib.colors.Colormap, optional
Colormap for polygon colors. Default 'tab20'
edgecolor : str, optional
Edge color for polygons. Default 'black'
alpha : float, optional
Transparency level (0-1). Default 0.7
lw : float, optional
Line width for edges. Default 1
figsize : tuple, optional
Figure size if creating new figure. Default (10, 10)
dpi : int, optional
DPI for figure. Default 100
points : ndarray or None, optional
N×2 array of coordinate points to plot on top of polygons.
Format: [[x1, y1], [x2, y2], ...]. Default None (no points plotted)
point_color : str, optional
Color for plotted points. Default 'red'
point_size : float, optional
Size of plotted points. Default 20
point_marker : str, optional
Marker style for points. Default 'o' (circle)
point_alpha : float, optional
Transparency for points (0-1). Default 0.8
point_label : str, optional
Label for points in legend. Default 'Points'
Returns
-------
fig : matplotlib.figure.Figure
Figure object
ax : matplotlib.axes.Axes
Axes object
Usage
-----
from upxo.viz.gsviz import plot_multipolygon_geometric
Function signature
------------------
plot_multipolygon_geometric(gs_geometric, fig=None, ax=None, cmap='tab20', edgecolor='black',
alpha=0.7, lw=1, figsize=(10, 10), dpi=100,
points=None, point_color='red', point_size=20,
point_marker='o', point_alpha=0.8, point_label='Points')
"""
from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection
import matplotlib.cm as cm
if ax is None:
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
# Get colormap
if isinstance(cmap, str):
cmap = cm.get_cmap(cmap)
num_polygons = len(gs_geometric.geoms)
colors = [cmap(i / num_polygons) for i in range(num_polygons)]
# Plot each polygon individually with unique color
for idx, poly in enumerate(gs_geometric.geoms):
# Get exterior coordinates
exterior_coords = np.array(poly.exterior.coords)
polygon_patch = Polygon(exterior_coords, closed=True,
facecolor=colors[idx], edgecolor=edgecolor,
linewidth=lw, alpha=alpha)
ax.add_patch(polygon_patch)
# Plot holes (interiors) if any - use white or transparent
for interior in poly.interiors:
interior_coords = np.array(interior.coords)
hole_patch = Polygon(interior_coords, closed=True,
facecolor='white', edgecolor=edgecolor,
linewidth=lw, alpha=1.0)
ax.add_patch(hole_patch)
# Plot coordinate points if provided
if points is not None:
# Validate that points is a numpy array with shape (N, 2)
if isinstance(points, np.ndarray) and points.ndim == 2 and points.shape[1] == 2:
ax.scatter(points[:, 0], points[:, 1],
c=point_color, s=point_size, marker=point_marker,
alpha=point_alpha, label=point_label, zorder=10)
# Add legend if points are plotted
if points.shape[0] > 0:
ax.legend(loc='best')
ax.autoscale_view()
ax.set_aspect('equal')
ax.set_xlabel('X-axis')
ax.set_ylabel('Y-axis')
ax.set_title('')
return fig, ax
[docs]
def make_pvgrid(lfi, scalar_name='lfi', origin=(0, 0, 0), spacing=(1, 1, 1)):
"""Build and return pvgrid."""
pvgrid = pv.ImageData()
pvgrid.dimensions = np.array(lfi.shape) + 1
pvgrid.origin = origin
pvgrid.spacing = spacing
pvgrid.cell_data[str(scalar_name)] = lfi.flatten(order="F")
return pvgrid
[docs]
def plot_pvgrid(pvgrid, scalar_name='lfi', show_edges=False, alpha=1.0, title='',
cmap='nipy_spectral', _xname_='', _yname_='', _zname_=''):
"""
gsviz.plot_pvgrid(gsviz.make_pvgrid(lfi, scalar_name='lfi'),
scalar_name, show_edges=False, alpha=1.0, title='',
cmap='nipy_spectral', _xname_='', _yname_='', _zname_='')
Usage
-----
from upxo.viz import gsviz
"""
pvp = pv.Plotter()
pvp.add_mesh(pvgrid, scalars=scalar_name,
show_edges=show_edges, opacity=alpha, cmap=cmap)
pvp.add_text(f"{title}", font_size=10)
_ = pvp.add_axes(line_width=5, cone_radius=0.6,
shaft_length=0.7, tip_length=0.3,
ambient=0.5, label_size=(0.4, 0.16),
xlabel=_xname_, ylabel=_yname_, zlabel=_zname_,
viewport=(0, 0, 0.25, 0.25))
pvp.show()
[docs]
def grain_viewer(lfi):
"""
Create an interactive 3D visualization with a slider to explore individual grains.
Parameters
----------
lfi : np.ndarray
3D labeled image array where each voxel contains a grain ID.
Returns
-------
None
Usage
-----
from upxo.viz import gsviz
Use as: gsviz.grain_viewer(lfi)
"""
max_gid = int(np.max(lfi))
grid = pv.ImageData(dimensions=np.array(lfi.shape)+1,
spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0),)
grid.cell_data["gid"] = lfi.ravel(order="F")
plotter = pv.Plotter()
actor = {"mesh": None}
def show_grain(gid):
"""Visualise grain using Matplotlib or PyVista."""
if actor["mesh"] is not None:
plotter.remove_actor(actor["mesh"])
actor["mesh"] = None
grain = grid.threshold([gid - 0.5, gid + 0.5], scalars="gid")
if grain.n_cells == 0:
plotter.render()
return
actor["mesh"] = plotter.add_mesh(grain, show_edges=True, color="tan", opacity=1.0)
plotter.set_focus(grain.center)
plotter.reset_camera()
plotter.render()
plotter.add_slider_widget(callback=lambda v: show_grain(int(v)),
rng=[1, max_gid], value=1, title="Grain ID", fmt="%0.f",)
show_grain(1)
plotter.show()
[docs]
def view_selected_grain_boundary_voxels(lfi, grain_ids, viewInternalOnly=True, spacing=(1.0, 1.0, 1.0),
origin=(0.0, 0.0, 0.0), cmap="tab20", opacity=1.0, point_size=6.0,
show_as_cubes=True, show=True):
"""
Visualize boundary voxels of selected grain IDs in 3D.
Parameters
----------
lfi : ndarray[int]
3D grain ID array.
grain_ids : iterable of int
Grain IDs to visualize. Should be a list, set, or array of integers corresponding to
grain IDs in the LFI.
viewInternalOnly : bool, optional
If True, only visualize internal grain boundaries. If False, include outer RVE
boundaries. Default is True.
spacing : tuple of float, optional
Voxel spacing in (x, y, z) directions. Default is (1.0, 1.0, 1.0).
origin : tuple of float, optional
Origin coordinates for the grid. Default is (0.0, 0.0, 0.0).
cmap : str, optional
Colormap for visualizing different grain IDs. Default is "tab20".
opacity : float, optional
Opacity for the visualized voxels (0.0 to 1.0). Default is 1.0 (fully opaque).
point_size : float, optional
Size of the points representing boundary voxels. Default is 6.0.
show_as_cubes : bool, optional
If True, visualize boundary voxels as cubes. If False, visualize as points.
Default is True.
show : bool, optional
Whether to display the plot immediately. Default is True. Set to False for
further customization before showing.
Returns
-------
pl : pyvista.Plotter
The PyVista plotter object containing the visualization. Can be used for further
customization or saving the plot.
pdata : pyvista.PolyData
The PolyData object containing the boundary voxel points and their associated grain IDs.
Can be used for further analysis or custom visualization.
Usage
-----
from upxo.viz import gsviz
Example usage:
>>> # Visualize boundary voxels for grain IDs 1, 2, and 3
>>> gsviz.view_selected_grain_boundary_voxels(lfi, grain_ids=[1, 2, 3], viewInternalOnly=True, spacing=(1.0, 1.0, 1.0),
... origin=(0.0, 0.0, 0.0), cmap="tab20", opacity=0.9, point_size=6.0, show_as_cubes=True, show=True)
"""
import upxo.gbops.grainBoundOps3d as gbOps
lfi = np.asarray(lfi)
if lfi.ndim != 3:
raise ValueError("lfi must be a 3D array.")
if grain_ids is None:
raise ValueError("grain_ids must be a non-empty iterable of grain IDs.")
gids = np.asarray(list(grain_ids), dtype=lfi.dtype)
if gids.size == 0:
raise ValueError("grain_ids must be non-empty.")
if viewInternalOnly:
boundary = gbOps.compute_gb_boundary_mask_interiorVoxels(lfi)
else:
boundary = gbOps.compute_gb_boundary_mask(lfi)
mask = boundary & np.isin(lfi, gids)
ijk = np.argwhere(mask)
if ijk.size == 0:
raise ValueError("No boundary voxels found for the selected grain IDs.")
pts = ijk.astype(np.float64)
pts[:, 0] = origin[0] + (pts[:, 0]+0.5) * spacing[0]
pts[:, 1] = origin[1] + (pts[:, 1]+0.5) * spacing[1]
pts[:, 2] = origin[2] + (pts[:, 2]+0.5) * spacing[2]
pdata = pv.PolyData(pts)
pdata["gid"] = lfi[mask].astype(np.int32)
pl = pv.Plotter()
if show_as_cubes:
cube = pv.Cube(center=(0.0, 0.0, 0.0), x_length=spacing[0],
y_length=spacing[1], z_length=spacing[2],)
voxels = pdata.glyph(geom=cube, scale=False, orient=False)
pl.add_mesh(voxels, scalars="gid", cmap=cmap, opacity=opacity,
show_scalar_bar=True, show_edges=True, edge_color='black', lighting=True)
else:
pl.add_mesh(pdata, scalars="gid", cmap=cmap, opacity=opacity,
point_size=point_size, render_points_as_spheres=True,
show_scalar_bar=True,)
pl.add_axes()
pl.show_grid()
if show:
pl.show()
return pl, pdata
[docs]
def viz_clip_plane(lfi, normal='x', origin=[5.0, 5.0, 5.0], scalarName='lfi',
cmap='viridis', invert=True, crinkle=True,
normal_rotation=True, add_outline=False, throw=False,
pvp=None):
"""
Visualize grain structure along a clip plane.
Parameters
----------
normal : str or dth.dt.ITERABLE(float), optional
Normal specification of clipping plane. Default value is 'x'.
origin : dth.dt.ITERABLE(float), optional
Specification of origin, that is clip plane centre coordinate.
scalarName : str, optional
self.pvgrid cell_data scalar specification. Default value is 'lfi'.
cmap : str, optional
Colour map specification. Default value is 'viridis'.
Recommended values:
* viridis
* nipy_spectral
invert : bool, optional
Invert clip sense if True, dont if False. Default value is True.
crinkle : bool, optional
Crinkle view voxels if True, section view if False. Default value
is True.
normal_rotation : bool, optional
Rotation specification of normal. Default value is True.
NOTE: To be implemented completely.
add_outline : bool, optional
Add an outline around the grain structure. Default value is False.
throw : bool, optional
Throw the pvp if True, dont if False. Default value is False.
pvp : pv.Plotter, optional
PyVista plotter object to plot over. If no pvp has been provided,
new pvp shall be created. Default value is None.
Usage
-----
from upxo.viz import gsviz
Use as: gsviz.viz_clip_plane(lfi, normal='x', origin=[5.0, 5.0, 5.0], scalarName='lfi',
cmap='viridis', invert=True, crinkle=True,
normal_rotation=True, add_outline=False, throw=False, pvp=None)
"""
pvgrid = make_pvgrid(lfi, scalar_name=scalarName, origin=(0, 0, 0), spacing=(1, 1, 1))
if pvp is None or not isinstance(pvp, pv.Plotter):
pvp = pv.Plotter()
# -------------------------------------
if add_outline:
pvp.add_mesh(pvgrid.outline())
# -------------------------------------
pvp.add_mesh_clip_plane(pvgrid, normal=normal, origin=origin,
scalars=scalarName, cmap=cmap, invert=invert,
crinkle=crinkle,
normal_rotation=normal_rotation, tubing=False,
interaction_event=vtk.vtkCommand.InteractionEvent)
# -------------------------------------
if throw:
return pvp
else:
pvp.show()
[docs]
def viz_mesh_slice(lfi, normal='x', origin=[5.0, 5.0, 5.0], scalarName='lgi',
cmap='viridis', normal_rotation=True, add_outline=False,
throw=False, pvp=None):
"""
Visualize grain structure along a slice plane.
Parameters
----------
lfi : object
The grain structure object to visualize.
normal : str or dth.dt.ITERABLE(float), optional
Normal specification of clipping plane. Default value is 'x'.
origin : dth.dt.ITERABLE(float), optional
Specification of origin, that is clip plane centre coordinate.
scalarName : str, optional
self.pvgrid cell_data scalar specification. Default value is 'lgi'.
cmap : str, optional
Colour map specification. Default value is 'viridis'.
Recommended values:
* viridis
* nipy_spectral
add_outline : bool, optional
Add an outline around the grain structure. Default value is False.
throw : bool, optional
Throw the pvp if True, dont if False. Default value is False.
pvp : bool, optional
PyVista plotter object to plot over. If no pvp has been provided,
new pvp shall be created. Default value is None.
Usage
-----
from upxo.viz import gsviz
gsviz.viz_mesh_slice(lfi, normal='x', origin=[5.0, 5.0, 5.0], scalar='lgi',
cmap='viridis', normal_rotation=True, add_outline=False,
throw=False, pvp=None)
"""
pvgrid = make_pvgrid(lfi, scalar_name=scalarName, origin=(0, 0, 0), spacing=(1, 1, 1))
if pvp is None or not isinstance(pvp, pv.Plotter):
pvp = pv.Plotter()
# -------------------------------------
if add_outline:
pvp.add_mesh(pvgrid.outline())
# -------------------------------------
pvp.add_mesh_slice(pvgrid, scalars=scalarName,
normal=normal, origin=origin, cmap=cmap,
normal_rotation=False,
interaction_event=vtk.vtkCommand.InteractionEvent)
# -------------------------------------
if throw:
return pvp
else:
pvp.show()
[docs]
def viz_mesh_slice_ortho(lfi, scalarName='lfi', cmap='viridis',
style='surface', add_outline=False,
throw=False, pvp=None):
"""
Viz. grain str. along three fundamental mutually orthogonal planes.
Parameters
----------
lfi : object
The grain structure object to visualize.
scalarName : str, optional
self.pvgrid cell_data scalar specification. Default value is 'lgi'.
cmap : str, optional
Colour map specification. Default value is 'viridis'.
Recommended values:
* viridis
* nipy_spectral
add_outline : bool, optional
Add an outline around the grain structure. Default value is False.
throw : bool, optional
Throw the pvp if True, dont if False. Default value is False.
pvp : bool, optional
PyVista plotter object to plot over. If no pvp has been provided,
new pvp shall be created. Default value is None.
Usage
-----
from upxo.viz import gsviz
gsviz.viz_mesh_slice_ortho(lfi, scalarName='lfi', cmap='viridis',
style='surface', add_outline=False,
throw=False, pvp=None)
"""
pvgrid = make_pvgrid(lfi, scalar_name=scalarName, origin=(0, 0, 0), spacing=(1, 1, 1))
if pvp is None or not isinstance(pvp, pv.Plotter):
pvp = pv.Plotter()
# -------------------------------------
if add_outline:
pvp.add_mesh(pvgrid.outline())
# -------------------------------------
pvp.add_mesh_slice_orthogonal(pvgrid, scalars=scalarName,
style=style, cmap=cmap,
interaction_event=vtk.vtkCommand.InteractionEvent)
# -------------------------------------
if throw:
return pvp
else:
pvp.show()
# ===============================================================================================
[docs]
def vox2geom_plots(plotType, **kwargs):
"""Vox2geom plots."""
if plotType == 1:
view_grains(**kwargs)
elif plotType == 2:
view_boundary_voxels(**kwargs)
elif plotType == 3:
see_clip_plane(**kwargs)
elif plotType == 4:
see_mesh_slice(**kwargs)
elif plotType == 5:
see_mesh_slice_ortho(**kwargs)
else:
raise ValueError(f"Unknown plotType '{plotType}'. Available options: 'grain_viewer'\n",
" 'view_boundary_voxels', 'see_clip_plane', 'see_mesh_slice', 'see_mesh_slice_ortho'.")
[docs]
def view_grains(lfi=None, **kwargs):
"""View grains."""
grain_viewer(lfi, **kwargs)
[docs]
def view_boundary_voxels(lfi=None, **kwargs):
"""View boundary voxels."""
view_selected_grain_boundary_voxels(lfi[::1, ::1, ::1], np.unique(lfi),
viewInternalOnly=True, spacing=(1.0, 1.0, 1.0),
origin=(0.0, 0.0, 0.0), cmap="gist_ncar", opacity=1.0, point_size=6.0,
show_as_cubes=True, show=True)
[docs]
def see_clip_plane(lfi=None, **kwargs):
"""See clip plane."""
viz_clip_plane(lfi, normal='x', origin=[5.0, 5.0, 5.0], scalarName='lfi',
cmap='gist_ncar', invert=True, crinkle=True,
normal_rotation=True, add_outline=False, throw=False, pvp=None)
[docs]
def see_mesh_slice(lfi=None, **kwargs):
"""See mesh slice."""
viz_mesh_slice(lfi, normal='x', origin=[5.0, 5.0, 5.0], scalarName='lfi',
cmap='gist_ncar', normal_rotation=True, add_outline=False,
throw=False, pvp=None)
[docs]
def see_mesh_slice_ortho(lfi=None, **kwargs):
"""See mesh slice ortho."""
viz_mesh_slice_ortho(lfi, scalarName='lfi', cmap='gist_ncar',
style='surface', add_outline=False,
throw=False, pvp=None)
# ===============================================================================================
[docs]
def plot_manifold_geom(cells_dict_list, figsize=(12, 6), dpi=100, inlude_legend=True):
"""
Helper to handle MultiPolygon and GeometryCollection iteration for plotting.
Parameters
----------
cells_dict_list : list of dict
List of dictionaries, each mapping grain IDs to Shapely geometries
(which may be Polygons, MultiPolygons, or GeometryCollections).
figsize : tuple, optional
Figure size (width, height). Default is (12, 6).
Returns
-------
fig : matplotlib.figure.Figure
Figure object
axes : ndarray of matplotlib.axes.Axes
Array of axes objects
Usage
-----
from upxo.viz import gsviz
Use as: gsviz.plot_manifold_geom([cells_dict_1, cells_dict_2], figsize=(12, 6))
"""
from shapely.geometry import Polygon, MultiPolygon, GeometryCollection
def get_polygons(g):
"""Recursively extract only Polygon parts from any geometry type."""
if isinstance(g, Polygon):
return [g]
elif isinstance(g, (MultiPolygon, GeometryCollection)):
res = []
for part in g.geoms:
res.extend(get_polygons(part))
return res
return [] # Filter out Points and LineStrings
num_plots = len(cells_dict_list)
fig, axes = plt.subplots(1, num_plots, figsize=figsize, dpi=dpi)
# Handle single subplot case (axes is not an array)
if num_plots == 1:
axes = [axes]
for idx, (ax, cells_dict) in enumerate(zip(axes, cells_dict_list)):
for gid, geom in cells_dict.items():
# Clean the geometry to ensure we only have polygons
polys = get_polygons(geom)
# Plot each valid polygonal part
first_part = True
for part in polys:
x, y = part.exterior.xy
# Maintain consistent coloring for all parts of the same Grain ID
if first_part:
line, = ax.plot(x, y, linewidth=1.5, label=f'Grain {gid}')
color = line.get_color()
first_part = False
else:
ax.plot(x, y, linewidth=1.5, color=color)
ax.fill(x, y, alpha=0.2, color=color)
ax.set_aspect('equal')
if inlude_legend:
ax.legend(loc='best')
ax.set_title(f'Subplot {idx + 1}')
plt.tight_layout()
return fig, axes
[docs]
def see_2dPoints(points, figsize=(6, 6), dpi=100, title='2D Points', xlabel='X-axis', ylabel='Y-axis',
point_color='black', point_size=20, point_marker='.', point_alpha=0.8, label='Points',
plot_legend=True):
"""
Visualize a set of 2D points using matplotlib.
Parameters
----------
points : ndarray
N×2 array of coordinate points to visualize. Format: [[x1, y1], [x2, y2], ...].
figsize : tuple, optional
Figure size (width, height). Default is (6, 6).
dpi : int, optional
Figure resolution. Default is 100.
title : str, optional
Plot title. Default is '2D Points'.
xlabel : str, optional
X-axis label. Default is 'X-axis'.
ylabel : str, optional
Y-axis label. Default is 'Y-axis'.
point_color : str or list of str, optional
Color for the points. Can be a single color or a list of colors for each point. Default is 'black'.
point_size : float or list of float, optional
Size of the points. Can be a single size or a list of sizes for each point. Default is 20.
point_marker : str, optional
Marker style for the points (e.g., '.' for dots). Default is '.'.
point_alpha : float or list of float, optional
Transparency level for the points (0-1). Can be a single value or a list for each point. Default is 0.8.
Returns
-------
fig : matplotlib.figure.Figure
The figure object containing the plot.
ax : matplotlib.axes.Axes
The axes object containing the plot.
Usage
-----
from upxo.viz import gsviz
Use as: gsviz.see_2dPoints(points_array)
"""
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
# Validate that points is a numpy array with shape (N, 2)
if isinstance(points, np.ndarray) and points.ndim == 2 and points.shape[1] == 2:
ax.scatter(points[:, 0], points[:, 1],
c=point_color, s=point_size, marker=point_marker,
alpha=point_alpha, label=label, zorder=10)
if plot_legend:
ax.legend(loc='best')
else:
raise ValueError("points must be a 2D array with shape (N, 2).")
ax.set_title(title)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_aspect('equal')
plt.tight_layout()
return fig, ax