""" Module defining class Microstructure that contains the necessary
methods and attributes to analyze experimental microstructures in form
of EBSD maps to generate statistical descriptors for 3D microstructures, and
to create synthetic RVE that fulfill the required statistical microstructure
descriptors.
The methods of the class Microstructure for an API that can be used to generate
Python workflows.
Authors: Alexander Hartmaier, Golsa Tolooei Eshlghi, Abhishek Biswas
Institution: ICAMS, Ruhr University Bochum
"""
import os
import sys
import json
import logging
import platform
import hashlib
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import Delaunay
from typing import Dict, Any, List, Optional, Union
from importlib.metadata import version as pkg_version
from datetime import datetime
from .grains import calc_polygons
from .entities import Simulation_Box
from .input_output import export2abaqus, writeAbaqusMat, read_dump
from .initializations import RVE_creator, mesh_creator
from .packing import packingRoutine
from .voxelization import voxelizationRoutine
from .smoothingGB import smoothingRoutine
from .rve_stats import get_stats_vox, get_stats_part, get_stats_poly
from .plotting import plot_init_stats, plot_voxels_3D, plot_ellipsoids_3D, \
plot_polygons_3D, plot_output_stats, plot_particles_3D
[docs]
class Microstructure(object):
"""
Define a class for creating and managing synthetic microstructures
This class provides tools to define, generate, and analyze synthetic
microstructures composed of one or multiple phases. It integrates particle
packing, voxelization, and grain geometry generation for RVE (Representative
Volume Element) modeling.
Parameters
----------
descriptor : list of dict, optional
List of dictionaries describing the microstructure of each phase.
Dictionary keys typically include "Grains type", "Equivalent diameter",
"Aspect ratio", "Tilt Angle", "RVE", and "Simulation".
file : str, optional
Path to a JSON or other input file to initialize the microstructure.
name : str, default='Microstructure'
Name of the microstructure
Attributes
----------
name : str
Name of the microstructure
nphases : int or None
Number of phases in the microstructure
ngrains : ndarray or None
Array of grain counts in each phase
nparticles : list or None
List of the number of particles in each phase
descriptor : list of dict or None
Descriptor used to define microstructure if provided
precipit : float or None
Indicates the presence of precipitates, pores, or secondary particles
in a continuous matrix. If float, specifies their volume fraction
from_voxels : bool
True if the microstructure is imported from a voxel file
particles : list or None
List of particle objects containing geometric information
rve : RVE_creator or None
Object containing RVE information including mesh dimensions,
particle count, periodicity, and phase volume fractions
simbox : Simulation_Box or None
Object defining the geometric boundaries of the RVE simulation domain
mesh : mesh_creator or None
Object storing voxelized mesh data including grain assignments,
voxel connectivity, and smoothed node coordinates
geometry : dict or None
Dictionary of grain geometries. Keys may include "Vertices", "Points",
"Simplices", "Facets", "Grains", "GBnodes", "GBarea", "GBfaces"
rve_stats : list of dict or None
Statistical information for different RVE representations,
including particles, voxels, and polyhedral grains
rve_stats_labels : list of str or None
Labels corresponding to the types of RVEs analyzed, e.g., "Particles",
"Voxels", "Grains"
vf_vox : ndarray or None
Phase volume fractions obtained from voxelized structures
ialloy : int or None
Alloy identifier used for material library mapping
Notes
-----
- The class can initialize from either a statistical descriptor or a JSON data file
- When only one phase is specified with a volume fraction < 1.0,
a matrix phase is automatically added with a complementary fraction (1 - vf)
- Kanapy is tested for up to two phases; using more may yield unpredictable results
"""
def __init__(self, descriptor=None, file=None, name='Microstructure'):
self.name = name
self.nphases = None
self.ngrains = None
self.nparticles = None
self.precipit = None
self.rve = None
self.particles = None
self.geometry = None
self.simbox = None
self.mesh = None
self.rve_stats = None
self.rve_stats_labels = None
self.from_voxels = False
self.ialloy = None
self.vf_vox = None
if descriptor is None:
if file is None:
raise ValueError('Please provide either a dictionary with statistics or an data file name')
# Open the user data statistics file and read the data
try:
with open(os.path.normpath(file)) as json_file:
self.descriptor = json.load(json_file)
except Exception as e:
logging.error(f'An unexpected exception occurred: {e}')
raise FileNotFoundError("File: '{}' does not exist in the current working directory!\n".format(file))
elif descriptor == 'from_voxels':
self.from_voxels = True
else:
if type(descriptor) is not list:
self.descriptor = [descriptor]
self.nphases = 1
else:
self.descriptor = descriptor
self.nphases = len(self.descriptor)
if self.nphases > 2:
logging.warning(f'Kanapy is only tested for 2 phases, use at own risk for {self.nphases} phases')
if file is not None:
logging.warning(
'WARNING: Input parameter (descriptor) and file are given. Only descriptor will be used.')
if self.nphases == 1 and 'Phase' in self.descriptor[0].keys():
vf = self.descriptor[0]['Phase']['Volume fraction']
if vf < 1.0:
# consider precipitates/pores/particles with volume fraction fill_factor in a matrix
# precipitates will be phase 0, matrix phase will get number 1 and be assigned to grain with ID 0
self.precipit = vf
self.nphases = 2
logging.info(f'Only one phase with volume fraction {vf} is given.')
logging.info('Will consider a sparse distribution in a matrix phase with phase number 1, ' +
'which will be assigned to grain with ID 0.')
return
"""
-------- Routines for user interface --------
"""
[docs]
def init_RVE(self, descriptor=None, nsteps=1000):
"""
Initialize the Representative Volume Element (RVE) of the microstructure
This method creates the voxel- or particle-based RVE using the provided
phase descriptor(s). It sets up the mesh dimensions, particle distribution,
and simulation box geometry. The RVE object is constructed using the
`RVE_creator` class, and key attributes such as `nparticles` and `simbox`
are updated accordingly.
Parameters
----------
descriptor : list of dict, dict, or None, optional
Description of the microstructure phases. Each dictionary specifies
parameters such as grain type, equivalent diameter, aspect ratio,
and tilt angle. If `None`, the class attribute `self.descriptor` is used.
nsteps : int, optional
Number of optimization or relaxation steps for RVE generation.
Default is 1000.
Notes
-----
- Assumes that `RVE_creator` and `Simulation_Box` are available and correctly configured.
- If `self.precipit` is defined, an additional 'Matrix' phase is automatically
added to preserve total volume fraction normalization.
Attributes Updated
------------------
rve : RVE_creator
The created RVE object containing voxel/particle information, size,
periodicity, and phase volume fractions.
nparticles : list
Number of particles per phase after RVE generation.
simbox : Simulation_Box
Object containing the geometric boundaries of the RVE domain.
precipit : float or None
If not None, a 'Matrix' phase is appended to `rve.phase_names` and its
volume fraction is adjusted as (1.0 - precipit).
Returns
-------
None
Updates class attributes with the initialized RVE and simulation box.
"""
if descriptor is None:
descriptor = self.descriptor
if type(descriptor) is not list:
descriptor = [descriptor]
# initialize RVE, including mesh dimensions and particle distribution
self.rve = RVE_creator(descriptor, nsteps=nsteps)
if self.precipit is not None:
self.rve.phase_names.append('Matrix')
self.rve.phase_vf.append(1.0 - self.precipit)
self.nparticles = self.rve.nparticles
# store geometry in simbox object
self.simbox = Simulation_Box(self.rve.size)
[docs]
def pack(self, particle_data=None,
k_rep=0.0, k_att=0.0, fill_factor=None,
poly=None, save_files=False, verbose=False):
"""
Pack particles into the simulation box according to the RVE settings.
Parameters
----------
particle_data : array-like or None, optional
Particle information. If None, uses self.rve.particle_data.
k_rep : float, optional
Repulsion coefficient between particles. Default is 0.0.
k_att : float, optional
Attraction coefficient between particles. Default is 0.0.
fill_factor : float or None, optional
Fraction of simulation box to fill. Defaults to 1.0 if self.precipit is set.
poly : optional
Additional packing options for polyhedral particles.
save_files : bool, optional
If True, saves packed particle data to files.
verbose : bool, optional
If True, prints progress and warnings during packing.
Returns
-------
particles : list
List of packed particle objects with positions and geometrical information.
simbox : Simulation_Box
Updated simulation box reflecting particle packing.
Raises
------
ValueError
If no particle data is available.
AttributeError
If RVE or simulation box is not initialized before packing.
Notes
-----
If self.precipit > 0.65, particle overlaps may occur.
Requires self.rve and self.simbox to be initialized before calling.
"""
if particle_data is None:
particle_data = self.rve.particle_data
if particle_data is None:
raise ValueError('No particle_data in pack. Run create_RVE first.')
if fill_factor is None and self.precipit is not None:
fill_factor = 1.0 # pack to full volume fraction defined in particles
print(f'Sparse particles (precipitates/pores): '
f'Packing up to particle volume fraction of {(100 * self.precipit):.1f}%.')
if self.precipit > 0.65:
print('Overlap of particles will occur since volume fraction > 65%')
self.particles, self.simbox = \
packingRoutine(particle_data, self.rve.periodic,
self.rve.packing_steps, self.simbox,
k_rep=k_rep, k_att=k_att, fill_factor=fill_factor,
poly=poly, save_files=save_files, verbose=verbose)
[docs]
def voxelize(self, particles=None, dim=None):
"""
Generate the RVE by assigning voxels to grains.
Parameters
----------
particles : list or None, optional
List of particle objects to voxelize. If None, uses `self.particles`.
dim : tuple of int, optional
3-tuple specifying the number of voxels in each spatial direction.
If None, uses `self.rve.dim`.
Returns
-------
None
The voxelized mesh is stored in `self.mesh`. Phase voxel fractions are
stored in `self.vf_vox` and printed to the console.
Raises
------
ValueError
- If `particles` is None and `self.particles` is not available.
- If `dim` is not a 3-tuple when specified.
AttributeError
If `self.rve` or `self.simbox` is not initialized before voxelization.
Notes
-----
- Requires `self.particles` to be initialized by `pack()`.
- Updates `self.mesh`, `self.ngrains`, `self.Ngr`, and `self.vf_vox`.
- Logs warnings if phase volume fractions do not sum to 1.
- Removes polyhedral grain geometries (`self.geometry`) after re-meshing to avoid inconsistencies.
"""
if particles is None:
particles = self.particles
if particles is None:
raise ValueError('No particles in voxelize. Run pack first.')
if dim is None:
dim = self.rve.dim
else:
if len(dim) != 3 or type(dim) is not tuple:
raise ValueError(f'"dim" must be a 3-tuple of the voxel numbers in each direction, not {dim}.')
self.rve.dim = dim
# initialize voxel structure (= mesh)
self.mesh = mesh_creator(dim)
self.mesh.nphases = self.nphases
self.mesh.create_voxels(self.simbox)
self.mesh = \
voxelizationRoutine(particles, self.mesh, self.nphases, prec_vf=self.precipit)
if np.any(self.nparticles != self.mesh.ngrains_phase):
logging.info(f'Number of grains per phase changed from {self.nparticles} to ' +
f'{list(self.mesh.ngrains_phase)} during voxelization.')
self.ngrains = self.mesh.ngrains_phase
self.Ngr = np.sum(self.mesh.ngrains_phase, dtype=int)
# extract volume fractions from voxelized grains
if self.nphases > 1:
self.vf_vox = np.zeros(self.nphases)
vox_count = np.zeros(self.nphases, dtype=int)
for igr, ip in self.mesh.grain_phase_dict.items():
vox_count[ip] += len(self.mesh.grain_dict[igr])
print('Volume fractions of phases in voxel structure:')
vt = 0.
for ip in range(self.nphases):
vf_act = vox_count[ip] / self.mesh.nvox
self.vf_vox[ip] = vf_act
vt += vf_act
print(f'{ip}: {self.rve.phase_names[ip]} ({(vf_act * 100):.3f}%)')
if not np.isclose(vt, 1.0):
logging.warning(f'Volume fractions of phases in voxels do not add up to 1. Value: {vt}')
else:
self.vf_vox = np.ones(1)
# remove grain information if it already exists to avoid inconsistencies
if self.geometry is not None:
logging.info('Removing polyhedral grain geometries and statistical data after re-meshing.')
self.geometry = None
[docs]
def smoothen(self, nodes_v=None, voxel_dict=None, grain_dict=None):
"""
Generate smoothed grain boundaries from a voxelated mesh.
Parameters
----------
nodes_v : array-like or None, optional
Mesh node coordinates. If None, uses `self.mesh.nodes`.
voxel_dict : dict or None, optional
Dictionary of voxels in the mesh. If None, uses `self.mesh.voxel_dict`.
grain_dict : dict or None, optional
Dictionary mapping grains to their voxels. If None, uses `self.mesh.grain_dict`.
Returns
-------
None
The smoothed nodes are stored in `self.mesh.nodes_smooth`. Grain boundary
faces are optionally stored in `self.geometry['GBfaces']`.
Raises
------
ValueError
If `nodes_v` is None and `self.mesh.nodes` is not available.
AttributeError
If `self.mesh` is not initialized before calling `smoothen()`.
Notes
-----
- Requires `self.mesh` to be initialized by `voxelize()`.
- Updates `self.mesh.nodes_smooth` with smoothed node coordinates.
- Adds 'GBfaces' entry to `self.geometry` if it exists.
"""
if nodes_v is None:
nodes_v = self.mesh.nodes
if nodes_v is None:
raise ValueError('No nodes_v in smoothen. Run voxelize first.')
if voxel_dict is None:
voxel_dict = self.mesh.voxel_dict
if grain_dict is None:
grain_dict = self.mesh.grain_dict
self.mesh.nodes_smooth, grain_facesDict = \
smoothingRoutine(nodes_v, voxel_dict, grain_dict)
if isinstance(self.geometry, dict):
self.geometry['GBfaces'] = grain_facesDict
[docs]
def generate_grains(self):
"""
Calculate and store polyhedral grain geometry, including particle- and grain-diameter attributes,
for statistical comparison
Notes
-----
- Requires `self.mesh` to be initialized by `voxelize`.
- Updates `self.geometry` with polyhedral grain volumes and shared grain boundary (GB) areas.
- If `self.precipit` is True, irregular grain 0 is temporarily removed from analysis.
- Logs warnings if grains are not represented in the polyhedral geometry.
- Prints volume fractions of each phase in the polyhedral geometry.
Parameters
----------
None
Returns
-------
None
The calculated grain geometry is stored in `self.geometry`. Phase volume fractions
are printed to the console.
Raises
------
ValueError
- If `self.mesh` or `self.mesh.grains` is None (i.e., voxelized microstructure not available).
"""
if self.mesh is None or self.mesh.grains is None:
raise ValueError('No information about voxelized microstructure. Run voxelize first.')
if self.precipit and 0 in self.mesh.grain_dict.keys():
# in case of precipit, remove irregular grain 0 from analysis
empty_vox = self.mesh.grain_dict.pop(0)
grain_store = self.mesh.grain_phase_dict.pop(0)
else:
empty_vox = None
grain_store = None
self.geometry: dict = \
calc_polygons(self.rve, self.mesh) # updates RVE_data
# verify that geometry['Grains'] and mesh.grain_dict are consistent
"""if np.any(self.geometry['Ngrains'] != self.ngrains):
logging.warning(f'Only facets for {self.geometry["Ngrains"]} created, but {self.Ngr} grains in voxels.')
for igr in self.mesh.grain_dict.keys():
if igr not in self.geometry['Grains'].keys():
logging.warning(f'Grain: {igr} not in geometry. Be aware when creating GB textures.')"""
# verify that geometry['GBarea'] is consistent with geometry['Grains']
gba = self.geometry['GBarea']
ind = []
igr = []
for i, gblist in enumerate(gba):
if not gblist[0] in self.geometry['Grains'].keys():
ind.append(i)
igr.append(gblist[0])
continue
if not gblist[1] in self.geometry['Grains'].keys():
ind.append(i)
igr.append(gblist[1])
if len(ind) > 0:
logging.warning(f'{len(ind)} grains are not represented in polyhedral geometry.')
# logging.warning('Consider increasing the number of voxels, as grains appear to be very irregular.')
"""ind.reverse()
igr.reverse()
for j, i in enumerate(ind):
logging.warning(f'Removing {gba[i]} from GBarea as grain {igr[j]} does not exist.')
gba.pop(i)
self.geometry['GBarea'] = gba"""
# extract volume fractions from polyhedral grains
if self.nphases > 1:
ph_vol = np.zeros(self.nphases)
for igr, grd in self.geometry['Grains'].items():
ip = grd['Phase']
ph_vol[ip] += grd['Volume']
print('Volume fractions of phases in polyhedral geometry:')
for ip in range(self.nphases):
vf = 100.0 * ph_vol[ip] / np.prod(self.rve.size)
print(f'{ip}: {self.rve.phase_names[ip]} ({vf.round(1)}%)')
if empty_vox is not None:
# add removed grain again
self.mesh.grain_dict[0] = empty_vox
self.mesh.grain_phase_dict[0] = grain_store
[docs]
def generate_orientations(self, data, ang=None, omega=None, Nbase=5000,
hist=None, shared_area=None, iphase=None, verbose=False):
"""
Generate orientations for grains in a representative volume element (RVE)
to achieve a desired crystallographic texture
This method assigns grain orientations to achieve a specified texture. The
input can be an EBSDmap object or a string defining the type of orientation set
(random or unimodal). For unimodal textures, `ang` and `omega` must be specified.
Generated orientations are stored in `self.mesh.grain_ori_dict`, and
`self.mesh.texture` is updated accordingly.
Parameters
----------
data : EBSDmap or str
Input source for orientations. Can be an `EBSDmap` object or a string
specifying the type of orientation set:
- 'random' or 'rnd' : generate a random orientation set
- 'unimodal', 'uni_mod', or 'uni_modal' : generate a unimodal orientation set
ang : float, optional
Orientation angle for unimodal texture (required if `data` is unimodal).
omega : float, optional
Kernel halfwidth for unimodal texture (required if `data` is unimodal).
Nbase : int, default=5000
Number of base orientations used in random or unimodal generation.
hist : array_like, optional
Histogram for the grain orientations, used to weight orientations.
shared_area : array_like or float, optional
Shared grain boundary area for weighted orientation generation.
iphase : int, optional
Phase index for which orientations are generated. If None, all phases are processed.
verbose : bool, default=False
If True, prints additional information during orientation generation.
Returns
-------
None
The generated orientations are stored in `self.mesh.grain_ori_dict` and
`self.mesh.texture` is updated to reflect the type of generated texture.
Raises
------
ValueError
- If grain geometry is not defined (i.e., `self.mesh.grains` is None).
- If `data` is unimodal but `ang` or `omega` are not provided.
- If `data` is neither `EBSDmap` nor a recognized string option.
- If histogram is provided but GB areas are not defined (and `shared_area` is None).
Examples
--------
>>> # Generate random orientations for all grains
>>> rve.generate_orientations('random')
>>> # Generate unimodal orientations with specified angle and halfwidth
>>> rve.generate_orientations('unimodal', ang=30.0, omega=10.0)
>>> # Generate orientations for a specific phase with histogram weighting
>>> rve.generate_orientations('random', hist=hist_array, iphase=1)
>>> # Generate orientations using an EBSDmap object
>>> rve.generate_orientations(ebsd_map_obj)
"""
from kanapy import __backend__
if __backend__ == 'mtex':
from kanapy_mtex.texture import EBSDmap, createOrisetRandom, createOriset
logging.info('Using MTEX library to read EBSD maps and generate orientations.')
MTEX = True
else:
from kanapy.texture import EBSDmap, createOrisetRandom, createOriset
logging.info('Using ORIX library to read EBSD maps and generate orientations.')
MTEX = False
if self.mesh.grains is None:
raise ValueError('Grain geometry is not defined. Run voxelize first.')
if shared_area is None:
if hist is None:
gba = None
else:
if self.geometry is None:
raise ValueError('If histogram for GB texture is provided, GB areas must be defined.\n' +
'Run generate_grains() first, to calculate GB areas.')
gba = self.geometry['GBarea']
else:
if shared_area == 0:
gba = None
else:
gba = shared_area
ori_dict = dict()
for ip, ngr in enumerate(self.ngrains):
if isinstance(data, EBSDmap):
if iphase is None or iphase == ip:
if gba is not None and not MTEX:
logging.warning('Shared are is currently only available in kanapy-mtex.\n'
'This option will be ignored.')
gba = None
ori_rve = data.calcORI(ngr, iphase=ip, shared_area=gba)
self.mesh.texture = "ODF"
elif isinstance(data, str):
if data.lower() in ['random', 'rnd']:
ori_rve = createOrisetRandom(ngr, Nbase=Nbase, hist=hist, shared_area=gba)
self.mesh.texture = "Random"
elif data.lower() in ['unimodal', 'uni_mod', 'uni_modal']:
if ang is None or omega is None:
raise ValueError('To generate orientation sets of type "unimodal" angle "ang" and kernel' +
'halfwidth "omega" are required.')
ori_rve = createOriset(ngr, ang, omega, hist=hist, shared_area=gba, verbose=verbose)
self.mesh.texture = "Unimodal"
else:
self.mesh.texture = None
raise ValueError('Argument to generate grain orientation must be either of type EBSDmap or ' +
'"random" or "unimodal"')
for i, igr in enumerate(self.mesh.grain_dict.keys()):
if self.mesh.grain_phase_dict[igr] == ip:
if iphase is None or iphase == ip:
ind = i - ip * self.ngrains[0]
ori_dict[igr] = ori_rve[ind, :]
self.mesh.grain_ori_dict = ori_dict
return
"""
-------- Plotting methods --------
"""
[docs]
def plot_ellipsoids(self, cmap='prism', dual_phase=None, phases=False):
"""
Generate a 3D plot of particles in the RVE
This function visualizes ellipsoidal particles in the Representative Volume Element (RVE).
Particles can be colored according to phase, and a custom colormap can be used.
Note that `dual_phase` is deprecated; use `phases` instead.
Parameters
----------
cmap : str, optional
Colormap used for plotting particles. Default is 'prism'.
dual_phase : bool or None, optional
Deprecated parameter for indicating dual-phase visualization.
Use `phases` instead. Default is None.
phases : bool, optional
If True, color particles according to their phase. Default is False.
Notes
-----
- Requires `self.particles` to be initialized by `pack`.
- Automatically calculates aspect ratios of the RVE for proper 3D plotting.
- Prints a warning if `dual_phase` is used and maps it to `phases`.
Returns
-------
None
Displays a 3D plot of ellipsoidal particles using `plot_ellipsoids_3D`.
Raises
------
ValueError
- If `self.particles` is None.
Examples
--------
>>> # Simple 3D plot with default colormap
>>> rve.plot_ellipsoids()
>>> # Color particles according to phase
>>> rve.plot_ellipsoids(phases=True)
>>> # Use a custom colormap
>>> rve.plot_ellipsoids(cmap='viridis')
"""
if dual_phase is not None:
print('Use of "dual_phase" is depracted. Use parameter "phases" instead.')
phases = dual_phase
if self.particles is None:
raise ValueError('No particle to plot. Run pack first.')
hmin = min(self.rve.size)
asp_arr = [int(self.rve.size[0] / hmin),
int(self.rve.size[1] / hmin),
int(self.rve.size[2] / hmin)]
plot_ellipsoids_3D(self.particles, cmap=cmap, phases=phases, asp_arr=asp_arr)
[docs]
def plot_particles(self, cmap='prism', dual_phase=None, phases=False, plot_hull=True):
"""
Generate a 3D plot of particles in the RVE.
This function visualizes particles in the Representative Volume Element (RVE) in 3D.
Particles can be colored according to phase, plotted with their convex hulls, and
displayed using a custom colormap.
Parameters
----------
cmap : str, optional
Colormap used for plotting particles. Default is 'prism'.
dual_phase : bool or None, optional
Deprecated parameter for indicating dual-phase visualization.
Use `phases` instead. Default is None.
phases : bool, optional
If True, color particles according to their phase. Default is False.
plot_hull : bool, optional
If True, plot the convex hull (inner polygon) of each particle. Default is True.
Notes
-----
- Requires `self.particles` to be initialized by `pack`.
- Automatically calculates aspect ratios of the RVE for proper 3D plotting.
- Prints a warning if `dual_phase` is used and maps it to `phases`.
Returns
-------
None
Displays a 3D plot of particles using `plot_particles_3D`.
Raises
------
ValueError
- If `self.particles` is None.
- If particles do not have inner polygons.
Examples
--------
>>> # Simple 3D plot with default colormap and convex hulls
>>> rve.plot_particles()
>>> # Color particles according to phase
>>> rve.plot_particles(phases=True)
>>> # Plot particles without convex hulls and use a custom colormap
>>> rve.plot_particles(cmap='viridis', plot_hull=False)
"""
if dual_phase is not None:
print('Use of "dual_phase" is depracted. Use parameter "phases" instead.')
phases = dual_phase
if self.particles is None:
raise ValueError('No particle to plot. Run pack first.')
if self.particles[0].inner is None:
raise ValueError('Ellipsoids without inner polygon cannot be plotted.')
hmin = min(self.rve.size)
asp_arr = [int(self.rve.size[0] / hmin),
int(self.rve.size[1] / hmin),
int(self.rve.size[2] / hmin)]
plot_particles_3D(self.particles, cmap=cmap,
phases=phases, plot_hull=plot_hull, asp_arr=asp_arr)
[docs]
def plot_voxels(self, sliced=False, dual_phase=None, phases=False, cmap='prism', ori=None,
color_key=0, silent=False):
"""
Generate a 3D visualization of the voxelized RVE structure
This function visualizes the voxel-based microstructure of the RVE. It supports
coloring by phase, grain ID, or crystallographic orientation, and can optionally
render a sliced view of the 3D voxel mesh.
Parameters
----------
sliced : bool, optional
If True, generates a sliced view of the voxel mesh to visualize the internal structure.
Default is False.
dual_phase : bool or None, optional
Deprecated parameter for dual-phase visualization. Use `phases` instead.
Default is None.
phases : bool, optional
If True, color voxels by phase instead of grain ID. Default is False.
cmap : str, optional
Name of the matplotlib colormap used for rendering. Default is 'prism'.
ori : array-like, bool, or None, optional
Array of grain orientations, or True to use `self.mesh.grain_ori_dict` for coloring
via inverse pole figure (IPF) mapping. Default is None.
color_key : int, optional
Selects the color mapping for orientations:
- 0: iphHSVKey
- 1: BungeColorKey
- 2: ipfHKLKey
Default is 0.
silent : bool, optional
If True, suppresses figure display and returns the matplotlib figure object instead.
Default is False.
Returns
-------
fig : matplotlib.figure.Figure or None
Returns the figure object if `silent=True`. Otherwise, displays the 3D plot and returns None.
Raises
------
ValueError
If `self.mesh.grains` is None, indicating that voxelization has not been performed.
Notes
-----
- Requires that the voxel mesh (`self.mesh`) has been generated by `voxelize()`.
- If `ori` is provided, orientation-based coloring is applied using `get_ipf_colors()`.
- Prints a warning if `dual_phase` is used, since it is deprecated and replaced by `phases`.
- Automatically computes aspect ratios of the RVE to ensure accurate 3D visualization.
Examples
--------
>>> # Simple 3D plot of voxelized RVE
>>> rve.plot_voxels()
>>> # Plot a sliced view of the voxel mesh
>>> rve.plot_voxels(sliced=True)
>>> # Color voxels by phase and use a custom colormap
>>> rve.plot_voxels(phases=True, cmap='viridis')
>>> # Use orientation-based coloring via IPF mapping
>>> rve.plot_voxels(ori=True, color_key=2)
>>> # Suppress figure display and get the matplotlib Figure object
>>> fig = rve.plot_voxels(silent=True)
"""
if dual_phase is not None:
print('Use of "dual_phase" is depracted. Use parameter "phases" instead.')
phases = dual_phase
if self.mesh.grains is None:
raise ValueError('No voxels or elements to plot. Run voxelize first.')
elif phases:
data = self.mesh.phases
else:
data = self.mesh.grains
if ori is not None:
from kanapy import __backend__
if __backend__ == "mtex":
from kanapy_mtex.texture import get_ipf_colors
else:
from kanapy.texture import get_ipf_colors
if isinstance(ori, bool) and ori:
ori = np.array([val for val in self.mesh.grain_ori_dict.values()])
clist = get_ipf_colors(ori, color_key)
else:
clist = None
hmin = min(self.rve.size)
asp_arr = [int(self.rve.size[0] / hmin),
int(self.rve.size[1] / hmin),
int(self.rve.size[2] / hmin)]
fig = plot_voxels_3D(data, sliced=sliced,
phases=phases, cmap=cmap, clist=clist,
silent=silent, asp_arr=asp_arr)
if silent:
return fig
[docs]
def plot_grains(self, geometry=None, cmap='prism', alpha=0.4,
ec=None, dual_phase=None, phases=False):
"""
Plot the polygonalized microstructure of the RVE in 3D.
This function visualizes the polygonal (polyhedral) grain geometry of the
microstructure, typically after `generate_grains()` has been executed.
Users can customize the colormap, transparency, and edge color. It also
supports coloring grains by phase.
Parameters
----------
geometry : dict or None, optional
Dictionary containing the polygonal grain geometries. If None, uses
`self.geometry`.
cmap : str, optional
Matplotlib colormap name for rendering grain colors. Default is 'prism'.
alpha : float, optional
Transparency level of the grain surfaces (0 = fully transparent,
1 = fully opaque). Default is 0.4.
ec : list or None, optional
Edge color specified as an RGBA list, e.g. `[0.5, 0.5, 0.5, 0.1]`.
Default is `[0.5, 0.5, 0.5, 0.1]`.
dual_phase : bool or None, optional
Deprecated parameter for dual-phase coloring. Use `phases` instead.
Default is None.
phases : bool, optional
If True, color grains by phase rather than by grain ID. Default is False.
Notes
-----
- Requires polygonal grain data generated by `generate_grains()`.
- Automatically computes the aspect ratio of the RVE for correct 3D scaling.
- Visualization is handled by `plot_polygons_3D()`.
- The parameter `dual_phase` is deprecated; prefer using `phases` instead.
Returns
-------
None
Displays a 3D plot of polygonal grains.
Raises
------
ValueError
If no polygonal geometry is available (i.e., `self.geometry` is None).
Examples
--------
>>> # Simple 3D plot of polygonal grains with default settings
>>> rve.plot_grains()
>>> # Color grains by phase
>>> rve.plot_grains(phases=True)
>>> # Use a custom colormap and set transparency
>>> rve.plot_grains(cmap='viridis', alpha=0.6)
>>> # Specify edge color
>>> rve.plot_grains(ec=[0.2, 0.2, 0.2, 0.3])
"""
if ec is None:
ec = [0.5, 0.5, 0.5, 0.1]
if dual_phase is not None:
print('Use of "dual_phase" is depracted. Use parameter "phases" instead.')
phases = dual_phase
if geometry is None:
geometry = self.geometry
if geometry is None:
raise ValueError('No polygons for grains defined. Run generate_grains() first')
hmin = min(self.rve.size)
asp_arr = [int(self.rve.size[0] / hmin),
int(self.rve.size[1] / hmin),
int(self.rve.size[2] / hmin)]
plot_polygons_3D(geometry, cmap=cmap, alpha=alpha, ec=ec,
phases=phases, asp_arr=asp_arr)
[docs]
def plot_stats(self, data=None,
gs_data=None, gs_param=None,
ar_data=None, ar_param=None,
dual_phase=None, phases=False,
save_files=False,
show_all=False, verbose=False,
silent=False, enhanced_plot=False):
"""
Plot particle, voxel, and grain diameter statistics for comparison
This method analyzes the microstructure at different representation levels
(particles, voxels, and polyhedral grains) and plots corresponding statistical
distributions. It can optionally handle multiphase materials, save plots,
and display detailed information about geometric parameters.
Parameters
----------
data : str or None, optional
Specifies which type of data to analyze and plot:
- 'p' : particles
- 'v' : voxels
- 'g' : grains
If None, all available data types are analyzed.
gs_data : list or array-like, optional
Grain size data for comparison with simulation results.
gs_param : list or array-like, optional
Parameters for fitting grain size distributions.
ar_data : list or array-like, optional
Aspect ratio data for comparison with simulation results.
ar_param : list or array-like, optional
Parameters for fitting aspect ratio distributions.
dual_phase : bool or None, optional
Deprecated. Use `phases` instead.
phases : bool, optional
If True, perform separate statistical analysis for each phase.
save_files : bool, optional
If True, save generated plots and statistical results to files.
show_all : bool, optional
If True, display all generated plots interactively.
verbose : bool, optional
If True, print detailed numerical results during analysis.
silent : bool, optional
If True, suppresses console output and returns figures directly.
enhanced_plot : bool, optional
If True, use enhanced plot styling (automatically enabled when `silent=True`).
Returns
-------
flist : list of matplotlib.figure.Figure or None
List of generated figure objects if `silent=True`, otherwise None.
Raises
------
None
This function does not raise exceptions if data is missing. Instead, it logs an
error message and returns immediately.
Notes
-----
- Computes and compares geometric parameters such as principal axis lengths
(a, b, c), aspect ratios, rotation axes, and equivalent diameters.
- Results are stored in `self.rve_stats` and labeled in `self.rve_stats_labels`.
- For multiphase structures, statistics are calculated per phase.
- `dual_phase` is deprecated; prefer using `phases`.
Examples
--------
>>> # Plot all statistics with default settings
>>> rve.plot_stats()
>>> # Plot only particle statistics and show all plots interactively
>>> rve.plot_stats(data='p', show_all=True)
>>> # Perform multiphase statistical analysis and save figures
>>> rve.plot_stats(phases=True, save_files=True)
>>> # Suppress console output and get figure objects
>>> figs = rve.plot_stats(silent=True)
"""
if dual_phase is not None:
print('Use of "dual_phase" is depracted. Use parameter "phases" instead.')
phases = dual_phase
if silent:
verbose = False
show_all = False
enhanced_plot = True
ax_max = np.prod(self.rve.size) ** (1 / 3)
if phases:
nphases = self.nphases
if self.precipit and 0 in self.mesh.grain_dict.keys():
# in case of precipit, remove irregular grain 0 from analysis
nphases -= 1
else:
nphases = 1
if not (isinstance(gs_data, list) and len(gs_data) == nphases):
gs_data = [gs_data] * nphases
if not (isinstance(gs_param, list) and len(gs_param) == nphases):
gs_param = [gs_param] * nphases
if not (isinstance(ar_data, list) and len(ar_data) == nphases):
ar_data = [ar_data] * nphases
if not (isinstance(ar_param, list) and len(ar_param) == len(gs_param)):
ar_param = [ar_param] * nphases
"""
gs_data = [ebsd.ms_data[i]['gs_data'] for i in range(nphases)]
gs_param = [ebsd.ms_data[i]['gs_param'] for i in range(nphases)]
ar_data = [ebsd.ms_data[i]['ar_data'] for i in range(nphases)]
ar_param = [ebsd.ms_data[i]['ar_param'] for i in range(nphases)]
"""
iphase = None
flist = []
for ip in range(nphases):
stats_list = []
labels = []
if phases:
iphase = ip
print(f'Plotting statistical information for phase {ip}')
# Analyze and plot particles statistics
if (data is None and self.particles is not None) or \
(type(data) is str and 'p' in data.lower()):
if self.particles is None:
logging.error('Particle statistics requested, but no particles defined. '
'Run "pack()" first.')
return
part_stats = get_stats_part(self.particles, iphase=iphase, ax_max=ax_max,
show_plot=show_all,
verbose=verbose, save_files=save_files)
stats_list.append(part_stats)
labels.append("Partcls")
# Analyze and plot statistics of voxel structure in RVE
if (data is None and self.mesh is not None) or \
(type(data) is str and 'v' in data.lower()):
if self.mesh is None:
logging.error('Voxel statistics requested, but no voxel mesh defined. '
'Run "voxelize()" first.')
return
vox_stats = get_stats_vox(self.mesh, iphase=iphase, ax_max=ax_max,
show_plot=show_all,
verbose=verbose, save_files=save_files)
stats_list.append(vox_stats)
labels.append('Voxels')
# Analyze and plot statistics of polyhedral grains in RVE
if (data is None and self.geometry is not None) or \
(type(data) is str and 'g' in data.lower()):
if self.geometry is None:
logging.error('Geometry statistics requested, but no polyhedral grains defined. '
'Run "generate_grains()" first.')
return
grain_stats = get_stats_poly(self.geometry['Grains'], iphase=iphase, ax_max=ax_max,
show_plot=show_all, phase_dict=self.mesh.grain_phase_dict,
verbose=verbose, save_files=save_files)
stats_list.append(grain_stats)
labels.append('Grains')
if phases:
print(f'\nStatistical microstructure parameters of phase {iphase} in RVE')
print('-------------------------------------------------------')
else:
print('\nStatistical microstructure parameters of RVE')
print('--------------------------------------------')
print(f'Type\t| a (µm) \t| b (µm) \t| c (µm) \t| std.dev\t| rot.axis\t| asp.ratio\t| std.dev\t|'
f' equ.dia. (µm)\t| std.dev')
for i, sd in enumerate(stats_list):
av_std = np.mean([sd['a_sig'], sd['b_sig'], sd['c_sig']])
print(f'{labels[i]}\t| {sd["a_scale"]:.3f}\t| {sd["b_scale"]:.3f}\t| {sd["c_scale"]:.3f}\t| '
f'{av_std:.4f}\t| {sd["ind_rot"]} \t| {sd["ar_scale"]:.3f}\t| {sd["ar_sig"]:.4f}\t| '
f'{sd["eqd_scale"]:.3f} \t| {sd["eqd_sig"]:.4f}')
self.rve_stats = stats_list
self.rve_stats_labels = labels
fig = plot_output_stats(stats_list, labels, iphase=iphase,
gs_data=gs_data[ip], gs_param=gs_param[ip],
ar_data=ar_data[ip], ar_param=ar_param[ip],
save_files=save_files, silent=silent,
enhanced_plot=enhanced_plot)
flist.append(fig)
if silent:
return flist
[docs]
def plot_stats_init(self, descriptor=None, gs_data=None, ar_data=None,
porous=False,
get_res=False, show_res=False,
save_files=False, silent=False, return_descriptors=False):
"""
Plot initial statistical microstructure descriptors of RVE and optionally return computed descriptors
This method analyzes the initial microstructure defined by input descriptors
(or the class attribute `self.descriptor`) and plots the distributions of
equivalent diameters and aspect ratios. For elongated grains, it prints a
summary of input and output statistics. Optionally, statistical descriptors
can be returned for further processing.
Parameters
----------
descriptor : list of dict, dict, or None, optional
Microstructure phase descriptor(s). If None, uses `self.descriptor`.
gs_data : list or array-like, optional
Grain size data for comparison with initial statistics.
ar_data : list or array-like, optional
Aspect ratio data for comparison with initial statistics.
porous : bool, optional
If True, only the first phase is considered (e.g., for porous structures).
get_res : bool, optional
If True, computes statistical descriptors from the voxelized structure.
show_res : bool, optional
If True, prints detailed statistical results to the console.
save_files : bool, optional
If True, saves generated plots to files.
silent : bool, optional
If True, suppresses console output and returns figures directly.
return_descriptors : bool, optional
If True, returns computed statistical descriptors along with figures.
Returns
-------
flist : list of matplotlib.figure.Figure
List of generated figure objects.
descs : list of dict
Optional list of computed statistical descriptors if `return_descriptors=True`.
Notes
-----
- Requires voxelized microstructure (`self.mesh`) for computing descriptors.
- Computes statistics for equivalent diameter, aspect ratio, principal axes,
and rotation axes when applicable.
- Can handle multiphase or single-phase microstructures.
Examples
--------
>>> # Plot initial statistics with default settings
>>> rve.plot_stats_init()
>>> # Plot only the first phase (porous) and save figures
>>> rve.plot_stats_init(porous=True, save_files=True)
>>> # Compute descriptors and get figures and descriptor objects
>>> figs, descs = rve.plot_stats_init(get_res=True, return_descriptors=True)
>>> # Suppress console output and return figure objects
>>> figs = rve.plot_stats_init(silent=True)
"""
def analyze_voxels(ip, des):
"""
Compute voxel-based statistical descriptors for a given phase of the RVE
This function calculates equivalent diameter, aspect ratio, principal axes, and rotation axis
statistics from the voxelized microstructure. It prints input vs output statistics if the phase
is elongated and returns formatted arrays for grain size, aspect ratio, and a dictionary of descriptors
Parameters
----------
ip : int
Index of the phase to analyze
des : dict
Descriptor dictionary for the phase containing grain type and target statistics
Returns
-------
gsp : list
Summary of equivalent diameter statistics for plotting
arp : list
Summary of aspect ratio statistics for plotting
statistical_descriptors : dict
Detailed statistics including mean, std, min, max of eqd, ar, axes, and rotation axis
"""
if self.mesh is None:
raise ValueError('show_res is True, but no voxels have been defined. Run voxelize first.')
vox_stats = get_stats_vox(self.mesh, iphase=ip, show_plot=show_res)
gsp = [vox_stats['eqd_sig'], 0.0, vox_stats['eqd_scale'],
min(vox_stats['eqd']), max(vox_stats['eqd'])]
arp = [vox_stats['ar_sig'], 0.0, vox_stats['ar_scale'],
min(vox_stats['ar']), max(vox_stats['ar'])]
if 'Grain type' in des.keys() and des['Grain type'] == 'Elongated':
if nel > 1:
print(f'\nStatistical microstructure parameters of phase {ip} in RVE')
print('-------------------------------------------------------')
else:
print('\nStatistical microstructure parameters of RVE')
print('--------------------------------------------')
print(f'Type\t| a (µm) \t| b (µm) \t| c (µm) \t| std.dev\t| rot.axis\t| asp.ratio\t| std.dev\t|'
f' equ.dia. (µm)\t| std.dev')
av_std = np.mean([vox_stats['a_sig'], vox_stats['b_sig'], vox_stats['c_sig']])
print(f'Input\t| - \t| - \t| - \t| - \t| - \t| '
f'{des["Aspect ratio"]["scale"]:.3f}\t| {des["Aspect ratio"]["sig"]:.4f}\t| '
f'{des["Equivalent diameter"]["scale"]:.3f} \t| {des["Equivalent diameter"]["sig"]:.4f}')
print(f'Output\t| {vox_stats["a_scale"]:.3f}\t| {vox_stats["b_scale"]:.3f}\t| '
f'{vox_stats["c_scale"]:.3f}\t| {av_std:.4f}\t| {vox_stats["ind_rot"]} \t| '
f'{vox_stats["ar_scale"]:.3f}\t| {vox_stats["ar_sig"]:.4f}\t| '
f'{vox_stats["eqd_scale"]:.3f} \t| {vox_stats["eqd_sig"]:.4f}')
statistical_descriptors = {
'eqd': {
'mean': float(vox_stats['eqd_scale']),
'std': float(vox_stats['eqd_sig']),
'min': float(min(vox_stats['eqd'])),
'max': float(max(vox_stats['eqd'])),
},
'ar': {
'mean': float(vox_stats['ar_scale']),
'std': float(vox_stats['ar_sig']),
'min': float(min(vox_stats['ar'])),
'max': float(max(vox_stats['ar'])),
},
'axes': {
'a': float(vox_stats['a_scale']),
'b': float(vox_stats['b_scale']),
'c': float(vox_stats['c_scale']),
'a_std': float(vox_stats.get('a_sig', np.nan)),
'b_std': float(vox_stats.get('b_sig', np.nan)),
'c_std': float(vox_stats.get('c_sig', np.nan)),
},
'rotation_axis': vox_stats.get('ind_rot', None),
}
return gsp, arp, statistical_descriptors
if show_res: get_res = True
if silent: show_res = False
if descriptor is None: descriptor = self.descriptor
if not isinstance(descriptor, list): descriptor = [descriptor]
if porous: descriptor = descriptor[0:1]
nel = len(descriptor)
if not (isinstance(gs_data, list) and len(gs_data) == nel): gs_data = [gs_data] * nel
if not (isinstance(ar_data, list) and len(ar_data) == nel): ar_data = [ar_data] * nel
flist, descs = [] , []
for ip, des in enumerate(descriptor):
gsp = arp = None
statistical_descriptors = None
if get_res:
gsp, arp, statistical_descriptors = analyze_voxels(ip, des)
fig = plot_init_stats(des, gs_data=gs_data[ip], ar_data=ar_data[ip],
gs_param=gsp, ar_param=arp,
save_files=save_files, silent=silent)
flist.append(fig)
if return_descriptors:
descs.append({'phase': ip, **(statistical_descriptors or {})})
if return_descriptors or silent: return flist, descs
[docs]
def plot_slice(self, cut='xy', data=None, pos=None, fname=None,
dual_phase=False, save_files=False):
"""
Plot a 2D slice through the microstructure.
The function visualizes a cross-section of the microstructure. If a polygonalized
microstructure is available, it will be used as the plotting basis; otherwise,
or if `data='voxels'`, the voxelized microstructure will be plotted. This method
internally calls `output_ang` with plotting enabled and file writing disabled.
Parameters
----------
cut : str, optional
The cutting plane of the slice. Options are 'xy', 'xz', or 'yz'.
Default is 'xy'.
data : str, optional
Data basis for plotting. Options are 'voxels' or 'poly'. Default is None.
pos : str or float, optional
Position of the slice, either as an absolute value or as one of
'top', 'bottom', 'left', 'right'. Default is None.
fname : str, optional
Filename to save the figure as a PDF. Default is None.
dual_phase : bool, optional
If True, enable dual-phase visualization. Default is False.
save_files : bool, optional
If True, the figure will be saved to disk. Default is False.
Returns
-------
None
Examples
--------
>>> micro.plot_slice(cut='xz', data='poly', pos='top', save_files=True)
"""
self.output_ang(cut=cut, data=data, plot=True, save_files=False,
pos=pos, fname=fname, dual_phase=dual_phase,
save_plot=save_files)
"""
-------- Import/Export methods --------
"""
[docs]
def write_abq(self, nodes=None, file=None, path='./', voxel_dict=None, grain_dict=None,
dual_phase=False, thermal=False, units=None, ialloy=None, nsdv=200, crystal_plasticity=False,
phase_props=None,
*,
boundary_conditions: Optional[Dict[str, Any]] = None):
"""
Write the Abaqus input deck (.inp) for the generated RVE
This method generates an Abaqus input file for the current RVE. It supports
voxelized or smoothened meshes, dual-phase materials, crystal plasticity,
thermal analysis, custom material properties, and optional boundary conditions.
Material definitions and mesh data are automatically handled based on provided
arguments or class attributes.
Parameters
----------
nodes : str or array-like, optional
Defines the mesh to write:
- 'voxels', 'v' : use voxelized mesh
- 'smooth', 's' : use smoothened mesh
- array-like : explicit nodal coordinates
Default is None, automatically selecting available mesh.
file : str, optional
Filename for the Abaqus input deck. Default is auto-generated.
path : str, optional
Directory path to save the input deck. Default is './'.
voxel_dict : dict, optional
Dictionary with voxel information. Default is `self.mesh.voxel_dict`.
grain_dict : dict, optional
Dictionary mapping grain IDs to nodes. Default is `self.mesh.grain_dict`.
dual_phase : bool, optional
If True, generate input for dual-phase materials. Default is False.
thermal : bool, optional
If True, include thermal material definitions. Default is False.
units : str, optional
Units for the model, 'mm' or 'um'. Default is `self.rve.units`.
ialloy : list or object, optional
Material definitions for each phase. Default is `self.rve.ialloy`.
nsdv : int, optional
Number of state variables per integration point for crystal plasticity. Default is 200.
crystal_plasticity : bool, optional
If True, enable crystal plasticity material definitions. Default is False.
phase_props : dict, optional
Additional phase-specific material properties.
boundary_conditions : dict, optional
Dictionary specifying boundary conditions to write. Default is None.
Returns
-------
file : str
Full path to the generated Abaqus input file.
Raises
------
ValueError
- If no voxelized or smoothened mesh is available when required.
- If invalid `nodes` argument is provided.
- If units are not 'mm' or 'um'.
- If the list `ialloy` is longer than the number of phases in the RVE.
- If periodic boundary conditions are requested but the RVE is non-periodic.
Notes
-----
- Automatically selects voxel or smoothened mesh if `nodes` is None.
- Handles dual-phase structures and crystal plasticity input.
- Writes additional material files if orientations are available and `ialloy` is provided.
- Visualization or mesh generation must be performed before calling this function.
Examples
--------
>>> # Write voxelized RVE input deck with default settings
>>> abq_file = rve.write_abq(nodes='voxels')
>>> # Write smoothened RVE deck for dual-phase material
>>> abq_file = rve.write_abq(nodes='smooth', dual_phase=True, ialloy=alloy_list)
>>> # Include boundary conditions
>>> bc = {'ux': [0, 0.1], 'uy': [0, 0.1]}
>>> abq_file = rve.write_abq(nodes='voxels', boundary_conditions=bc)
"""
if nodes is None:
if self.mesh.nodes_smooth is not None and 'GBarea' in self.geometry.keys():
logging.warning('\nWarning: No argument "nodes" is given, will write smoothened structure')
nodes = self.mesh.nodes_smooth
faces = self.geometry['GBarea']
ntag = '_smooth'
elif self.mesh.nodes is not None:
logging.warning('\nWarning: No argument "nodes" is given, will write voxelized structure')
nodes = self.mesh.nodes
faces = None
ntag = '_voxels'
else:
raise ValueError('No information about voxelized microstructure. Run voxelize first.')
elif type(nodes) is not str:
faces = None
ntag = '_voxels'
elif nodes.lower() in ['smooth', 's']:
if self.mesh.nodes_smooth is not None and 'GBarea' in self.geometry.keys():
nodes = self.mesh.nodes_smooth
faces = self.geometry['GBarea'] # use tet elements for smoothened structure
ntag = '_smooth'
else:
raise ValueError('No information about smoothed microstructure. Run smoothen first.')
elif nodes.lower() in ['voxels', 'v', 'voxel']:
if self.mesh.nodes is not None:
nodes = self.mesh.nodes
faces = None # use brick elements for voxel structure
ntag = '_voxels'
else:
raise ValueError('No information about voxelized microstructure. Run voxelize first.')
else:
raise ValueError('Wrong value for parameter "nodes". Must be either "smooth" ' +
f'or "voxels", not {nodes}')
if voxel_dict is None:
voxel_dict = self.mesh.voxel_dict
if units is None:
units = self.rve.units
elif (not units == 'mm') and (not units == 'um'):
raise ValueError(f'Units must be either "mm" or "um", not {units}.')
if dual_phase:
nct = 'abq_dual_phase'
if grain_dict is None:
grain_dict = dict()
for i in range(self.nphases):
grain_dict[i] = list()
for igr, ip in self.mesh.grain_phase_dict.items():
grain_dict[ip] = np.concatenate(
[grain_dict[ip], self.mesh.grain_dict[igr]])
else:
if grain_dict is None:
grain_dict = self.mesh.grain_dict
nct = f'abq_px_{len(grain_dict)}'
if ialloy is None:
ialloy = self.rve.ialloy
if type(ialloy) is list and len(ialloy) > self.nphases:
raise ValueError('List of values in ialloy is larger than number of phases in RVE.' +
f'({len(ialloy)} > {self.nphases})')
if self.nphases > 1:
grpd = self.mesh.grain_phase_dict
else:
grpd = None
if file is None:
if self.name == 'Microstructure':
file = nct + ntag + '_geom.inp'
else:
file = self.name + ntag + '_geom.inp'
path = os.path.normpath(path)
file = os.path.join(path, file)
periodic = self.rve.periodic
if not periodic and periodicBC:
raise ValueError("Periodic boundary conditions cannot be applied to a non-periodic RVE.")
export2abaqus(nodes, file, grain_dict, voxel_dict,
units=units, gb_area=faces,
dual_phase=dual_phase,
ialloy=ialloy, grain_phase_dict=grpd,
thermal=thermal,
crystal_plasticity = crystal_plasticity,
phase_props = phase_props, boundary_conditions = boundary_conditions)
# if orientations exist and ialloy is defined also write material file with Euler angles
if not (self.mesh.grain_ori_dict is None or ialloy is None):
writeAbaqusMat(ialloy, self.mesh.grain_ori_dict,
file=file[0:-8] + 'mat.inp',
grain_phase_dict=grpd, nsdv=nsdv)
return file
[docs]
def write_abq_ori(self, ialloy=None, ori=None, file=None, path='./', nsdv=200):
"""
Write Abaqus material input file using grain orientations for crystal plasticity
This function generates an Abaqus `.inp` file containing material definitions
for crystal plasticity, using the provided grain orientations and material IDs.
If `ialloy` or `ori` are not specified, the method uses the RVE's default values.
Parameters
----------
ialloy : list or object, optional
Material identifiers for each phase. Default is `self.rve.ialloy`.
ori : dict or array-like, optional
Dictionary or array of grain orientations. Default is `self.mesh.grain_ori_dict`.
file : str, optional
Name of the output Abaqus material file. Default is auto-generated based on RVE name.
path : str, optional
Directory path where the file will be saved. Default is './'.
nsdv : int, optional
Number of state variables per integration point for crystal plasticity. Default is 200.
Returns
-------
file : str
Full path to the generated Abaqus material input file.
Raises
------
ValueError
- If `ialloy` is not provided and `self.rve.ialloy` is None.
- If `ori` is not provided and `self.mesh.grain_ori_dict` is None.
Notes
-----
- Calls `writeAbaqusMat` internally to generate the material definitions.
- Assumes the RVE and grain orientations have been initialized prior to calling.
Examples
--------
>>> # Write material file using default RVE orientations and material numbers
>>> abq_file = rve.write_abq_ori()
>>> # Specify custom material IDs and orientations
>>> abq_file = rve.write_abq_ori(ialloy=alloy_list, ori=ori_dict, file='custom_mat.inp')
"""
if ialloy is None:
ialloy = self.rve.ialloy
if ialloy is None:
raise ValueError('Value of material number in ICAMS CP-UMAT (ialloy) not defined.')
if ori is None:
ori = self.mesh.grain_ori_dict
if ori is None:
raise ValueError('No orientations present. Run "generate_orientations" first.')
if file is None:
if self.name == 'Microstructure':
file = f'abq_px_{self.Ngr}_mat.inp'
else:
file = self.name + '_mat.inp'
path = os.path.normpath(path)
file = os.path.join(path, file)
writeAbaqusMat(ialloy, ori, file=file, nsdv=nsdv)
[docs]
def output_neper(self):
"""
Write particle position and weight files for Neper tessellation
This function generates two text files required for tessellation in Neper:
- `sphere_positions.txt` containing the x, y, z coordinates of each particle
- `sphere_weights.txt` containing the weight (radius) of each particle
The files are written in the current working directory. Each particle in
`self.particles` is included in the output.
Raises
------
ValueError
If `self.particles` is None, indicating that particles have not been packed yet.
Notes
-----
- Requires that particles are initialized via the `pack()` method.
- The particle dictionary is built internally but not returned.
Examples
--------
>>> # Write Neper input files for current particles
>>> rve.output_neper()
"""
# write_position_weights(timestep)
if self.particles is None:
raise ValueError('No particle to plot. Run pack first.')
print('')
print('Writing position and weights files for NEPER', end="")
par_dict = dict()
for pa in self.particles:
x, y, z = pa.x, pa.y, pa.z
a = pa.a
par_dict[pa] = [x, y, z, a]
with open('sphere_positions.txt', 'w') as fd:
for key, value in par_dict.items():
fd.write('{0} {1} {2}\n'.format(value[0], value[1], value[2]))
with open('sphere_weights.txt', 'w') as fd:
for key, value in par_dict.items():
fd.write('{0}\n'.format(value[3]))
print('---->DONE!\n')
[docs]
def output_ang(self, ori=None, cut='xy', data=None, plot=True, cs=None,
pos=None, fname=None, matname='XXXX', save_files=True,
dual_phase=False, save_plot=False):
"""
Convert microstructure orientations into a .ang file, optionally plotting a slice
This function generates a .ang file mimicking an EBSD map. If polygonalized
microstructure is available, it will be used; otherwise, or if `data='voxels'`,
the voxelized microstructure will be exported. If no orientations are provided,
each grain receives a random Euler angle. The function can also plot a 2D slice
of the orientation map along a specified cutting plane.
Parameters
----------
ori : array-like of shape (self.Ngr, 3), optional
Euler angles of grains. If None, random angles are generated.
cut : {'xy', 'xz', 'yz'}, optional
Plane along which to take a slice. Default is 'xy'.
data : {'voxels', 'poly'}, optional
Basis for generating the ANG file. Default is None, automatically selected.
plot : bool, optional
Whether to display a plot of the slice. Default is True.
cs : str, optional
Crystal symmetry. Default is None.
pos : float or str, optional
Slice position as a numerical value or one of 'top', 'bottom', 'left', 'right'.
Default is None (uses top/right edge).
fname : str, optional
Filename for the output ANG file. Default is generated automatically.
matname : str, optional
Material name to write in the ANG file. Default is 'XXXX'.
save_files : bool, optional
Whether to save the ANG file. Default is True.
dual_phase : bool, optional
Whether to output dual-phase information. Default is False.
save_plot : bool, optional
Whether to save the plot as a PDF. Default is False.
Returns
-------
fname : str
Name of the generated ANG file.
Raises
------
ValueError
- If `pos` is not a float or one of 'top', 'bottom', 'left', 'right'.
- If `cut` is not 'xy', 'xz', or 'yz'.
- If neither polygonal nor voxel microstructure is available.
Notes
-----
- Uses Delaunay triangulation to determine which grain each pixel belongs to
when exporting polygons.
- Slice plotting uses a colormap (default 'prism') and includes optional
dual-phase coloring.
Examples
--------
>>> # Export an XY slice with default settings
>>> fname = rve.output_ang(cut='xy')
>>> # Export a slice using voxel data and a specific Euler angles array
>>> fname = rve.output_ang(ori=euler_array, data='voxels', cut='xz', pos=10)
>>> # Plot and save the slice as PDF
>>> fname = rve.output_ang(save_plot=True)
"""
if type(ori) is dict:
ori = np.array([val for val in ori.values()])
cut = cut.lower()
if type(pos) is str:
pos = pos.lower()
botlist = ['bottom', 'bot', 'left', 'b', 'l']
toplist = ['top', 'right', 't', 'r']
if cut == 'xy':
sizeX = self.rve.size[0]
sizeY = self.rve.size[1]
(sx, sy, sz) = np.divide(self.rve.size, self.rve.dim)
ix = np.arange(self.rve.dim[0])
iy = np.arange(self.rve.dim[1])
if pos is None or pos in toplist:
iz = self.rve.dim[2] - 1
elif pos in botlist:
iz = 0
elif type(pos) is float or type(pos) is int:
iz = int(pos / sz)
else:
raise ValueError('"pos" must be either float or "top", "bottom", "left" or "right"')
if pos is None:
pos = int(iz * sz)
xl = r'x ($\mu$m)'
yl = r'y ($\mu$m)'
title = r'XY slice at z={} $\mu$m'.format(round(iz * sz, 1))
elif cut == 'xz':
sizeX = self.rve.size[0]
sizeY = self.rve.size[2]
vox_res = np.divide(self.rve.size, self.rve.dim)
sx = vox_res[0]
sy = vox_res[1]
sz = vox_res[2]
ix = np.arange(self.rve.dim[0])
iy = np.arange(self.rve.dim[2])
if pos is None or pos in toplist:
iz = self.rve.dim[1] - 1
elif pos in botlist:
iz = 0
elif type(pos) is float or type(pos) is int:
iz = int(pos / sy)
else:
raise ValueError('"pos" must be either float or "top", "bottom", "left" or "right"')
if pos is None:
pos = int(iz * sz)
xl = r'x ($\mu$m)'
yl = r'z ($\mu$m)'
title = r'XZ slice at y={} $\mu$m'.format(round(iz * sz, 1))
elif cut == 'yz':
sizeX = self.rve.size[1]
sizeY = self.rve.size[2]
vox_res = np.divide(self.rve.size, self.rve.dim)
sx = vox_res[0]
sy = vox_res[1]
sz = vox_res[2]
ix = np.arange(self.rve.dim[1])
iy = np.arange(self.rve.dim[2])
if pos is None or pos in toplist:
iz = self.rve.dim[0] - 1
elif pos in botlist:
iz = 0
elif type(pos) is float or type(pos) is int:
iz = int(pos / sx)
else:
raise ValueError('"pos" must be either float or "top", "bottom", "left" or "right"')
if pos is None:
pos = int(iz * sz)
xl = r'y ($\mu$m)'
yl = r'z ($\mu$m)'
title = r'YZ slice at x={} $\mu$m'.format(round(iz * sz, 1))
else:
raise ValueError('"cut" must bei either "xy", "xz" or "yz".')
# ANG file header
head = ['# TEM_PIXperUM 1.000000\n',
'# x-star 0.000000\n',
'# y-star 0.000000\n',
'# z-star 0.000000\n',
'# WorkingDistance 0.000000\n',
'#\n',
'# Phase 0\n',
'# MaterialName {}\n'.format(matname),
'# Formula\n',
'# Info\n',
'# Symmetry m-3m\n',
'# LatticeConstants 4.050 4.050 4.050 90.000 90.000 90.000\n',
'# NumberFamilies 0\n',
'# ElasticConstants 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000\n',
'# ElasticConstants 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000\n',
'# ElasticConstants 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000\n',
'# ElasticConstants 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000\n',
'# ElasticConstants 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000\n',
'# ElasticConstants 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000\n',
'# Categories0 0 0 0 0\n',
'# \n',
'# GRID: SqrGrid\n',
'# XSTEP: {}\n'.format(round(sx, 6)),
'# YSTEP: {}\n'.format(round(sy, 6)),
'# NCOLS_ODD: {}\n'.format(ix),
'# NCOLS_EVEN: {}\n'.format(ix),
'# NROWS: {}\n'.format(iy),
'#\n',
'# OPERATOR: Administrator\n',
'#\n',
'# SAMPLEID:\n',
'#\n',
'# SCANID:\n',
'#\n'
]
# determine whether polygons or voxels shall be exported
if data is None:
if 'Grains' in self.geometry.keys():
data = 'poly'
elif self.mesh.voxels is None:
raise ValueError('Neither polygons nor voxels for grains are present.\
\nRun voxelize and generate_grains first.')
else:
data = 'voxels'
elif data != 'voxels' and data != 'poly':
raise ValueError('"data" must be either "voxels" or "poly".')
if data == 'voxels':
title += ' (Voxels)'
if cut == 'xy':
g_slice = np.array(self.mesh.grains[:, :, iz], dtype=int)
elif cut == 'xz':
g_slice = np.array(self.mesh.grains[:, iz, :], dtype=int)
else:
g_slice = np.array(self.mesh.grains[iz, :, :], dtype=int)
if dual_phase:
if cut == 'xy':
g_slice_phase = np.array(self.mesh.phases[:, :, iz], dtype=int)
elif cut == 'xz':
g_slice_phase = np.array(self.mesh.phases[:, iz, :], dtype=int)
else:
g_slice_phase = np.array(self.mesh.phases[iz, :, :], dtype=int)
else:
title += ' (Polygons)'
xv, yv = np.meshgrid(ix * sx, iy * sy, indexing='ij')
grain_slice = np.ones(len(ix) * len(iy), dtype=int)
if cut == 'xy':
mesh_slice = np.array([xv.flatten(), yv.flatten(), grain_slice * iz * sz]).T
elif cut == 'xz':
mesh_slice = np.array([xv.flatten(), grain_slice * iz * sz, yv.flatten()]).T
else:
mesh_slice = np.array([grain_slice * iz * sz, xv.flatten(), yv.flatten()]).T
grain_slice = np.zeros(len(ix) * len(iy), dtype=int)
for igr in self.geometry['Grains'].keys():
pts = self.geometry['Grains'][igr]['Points']
try:
tri = Delaunay(pts)
i = tri.find_simplex(mesh_slice)
ind = np.nonzero(i >= 0)[0]
grain_slice[ind] = igr
except Exception as e:
logging.error(f'An unexpected exception occurred: {e}')
logging.error('Grain #{} has no convex hull (Nvertices: {})'
.format(igr, len(pts)))
if np.any(grain_slice == 0):
ind = np.nonzero(grain_slice == 0)[0]
logging.error('Incomplete slicing for {} pixels in {} slice at {}.'
.format(len(ind), cut, pos))
g_slice = grain_slice.reshape(xv.shape)
if save_files:
if ori is None:
ori = np.zeros((self.Ngr, 3))
ori[:, 0] = np.random.rand(self.Ngr) * 2 * np.pi
ori[:, 1] = np.random.rand(self.Ngr) * 0.5 * np.pi
ori[:, 2] = np.random.rand(self.Ngr) * 0.5 * np.pi
# write data to ang file
fname = '{0}_slice_{1}_{2}.ang'.format(cut.upper(), pos, data)
with open(fname, 'w') as f:
f.writelines(head)
for j in iy:
for i in ix:
p1 = ori[g_slice[j, i] - 1, 0]
P = ori[g_slice[j, i] - 1, 1]
p2 = ori[g_slice[j, i] - 1, 2]
f.write(' {0} {1} {2} {3} {4} 0.0 0.000 0 1 0.000\n'
.format(round(p1, 5), round(P, 5), round(p2, 5),
round(sizeX - i * sx, 5), round(sizeY - j * sy, 5)))
if plot:
# plot grains on slice
# cmap = plt.cm.get_cmap('gist_rainbow')
cmap = plt.cm.get_cmap('prism')
fig, ax = plt.subplots(1)
ax.grid(False)
ax.imshow(g_slice, cmap=cmap, interpolation='none',
extent=[0, sizeX, 0, sizeY])
ax.set(xlabel=xl, ylabel=yl)
ax.set_title(title)
if save_plot:
plt.savefig(fname[:-4] + '.pdf', format='pdf', dpi=300)
plt.show()
if dual_phase:
fig, ax = plt.subplots(1)
ax.grid(False)
ax.imshow(g_slice_phase, cmap=cmap, interpolation='none',
extent=[0, sizeX, 0, sizeY])
ax.set(xlabel=xl, ylabel=yl)
ax.set_title(title)
if save_plot:
plt.savefig(fname[:-4] + '.pdf', format='pdf', dpi=300)
plt.show()
return fname
[docs]
def write_stl(self, data='grains', file=None, path='./',
phases=False, phase_num=None):
"""
Export grains or particles as STL files representing convex polyhedra
This function writes STL files with triangular facets for grains or particles.
Each facet is written in standard STL ASCII format:
```
solid name
facet normal n1 n2 n3
outer loop
vertex p1x p1y p1z
vertex p2x p2y p2z
vertex p3x p3y p3z
endloop
endfacet
endsolid name
```
Parameters
----------
data : {'grains', 'particles'}, optional
Determines whether to export grains or particles. Default is 'grains'.
file : str, optional
Filename for the STL file. Default is generated from `self.name`.
path : str, optional
Directory to save the STL file. Default is './'.
phases : bool, optional
If True, export only grains of a specific phase. Default is False.
phase_num : int, optional
Phase number to export if `phases=True`. Required in that case.
Returns
-------
None
Raises
------
ValueError
- If `phases=True` but `phase_num` is not provided.
Notes
-----
- Facet normals are automatically computed using cross products of vertices.
- Acute or irregular facets trigger warnings.
- For particle export, each particle must have a valid inner polyhedron.
Examples
--------
>>> # Export all grains to STL
>>> rve.write_stl()
>>> # Export particles to STL
>>> rve.write_stl(data='particles')
>>> # Export only phase 1 grains
>>> rve.write_stl(phases=True, phase_num=1)
"""
def write_facet(nv, pts, ft):
"""
Write a single triangular facet to the STL file with a normalized normal vector
Checks for degenerate or nearly zero-length normals and issues warnings
if the facet is acute or irregular. Writes the facet in ASCII STL format.
Parameters
----------
nv : ndarray
Initial normal vector of the facet.
pts : ndarray
3x3 array of vertex coordinates defining the facet.
ft : int
Facet index, used for logging warnings.
"""
if np.linalg.norm(nv) < 1.e-5:
logging.warning(f'Acute facet detected. Facet: {ft}')
nv = np.cross(pts[1] - pts[0], pts[2] - pts[1])
if np.linalg.norm(nv) < 1.e-5:
logging.warning(f'Irregular facet detected. Facet: {ft}')
nv /= np.linalg.norm(nv)
f.write(" facet normal {} {} {}\n"
.format(nv[0], nv[1], nv[2]))
f.write(" outer loop\n")
f.write(" vertex {} {} {}\n"
.format(pts[0, 0], pts[0, 1], pts[0, 2]))
f.write(" vertex {} {} {}\n"
.format(pts[1, 0], pts[1, 1], pts[1, 2]))
f.write(" vertex {} {} {}\n"
.format(pts[2, 0], pts[2, 1], pts[2, 2]))
f.write(" endloop\n")
f.write(" endfacet\n")
def write_grains():
"""
Write all grain facets of the microstructure to the STL file
Iterates over all facets defined in `self.geometry['Facets']`, computes
the facet normal, and calls `write_facet` to output each triangular facet.
"""
for ft in self.geometry['Facets']:
pts = self.geometry['Points'][ft]
nv = np.cross(pts[1] - pts[0], pts[2] - pts[0]) # facet normal
write_facet(nv, pts, ft)
def write_phases(ip):
"""
Write facets of grains belonging to a specific phase to the STL file
Parameters
----------
ip : int
Phase number for which the grain facets should be exported.
Notes
-----
Iterates over all grains in `self.geometry['Grains']` and writes only
the facets of grains whose 'Phase' matches `ip`. Each facet is output
using the `write_facet` function.
"""
for grain in self.geometry['Grains'].values():
if grain['Phase'] == ip:
for ft in grain['Simplices']:
pts = self.geometry['Points'][ft]
nv = np.cross(pts[1] - pts[0], pts[2] - pts[0]) # facet normal
write_facet(nv, pts, ft)
def write_particles():
"""
Write facets of all particle convex hulls to the STL file
Notes
-----
Iterates over all particles in `self.particles` and exports each facet
from the particle's inner convex hull using the `write_facet` function.
"""
for pa in self.particles:
for ft in pa.inner.convex_hull:
pts = pa.inner.points[ft]
nv = np.cross(pts[1] - pts[0], pts[2] - pts[0]) # facet normal
write_facet(nv, pts, ft)
if file is None:
if self.name == 'Microstructure':
file = 'px_{}grains.stl'.format(self.Ngr)
else:
file = self.name + '.stl'
path = os.path.normpath(path)
file = os.path.join(path, file)
with open(file, 'w') as f:
f.write("solid {}\n".format(self.name))
if data in ['particles', 'pa', 'p']:
if self.particles[0].inner is None:
logging.error("Particles don't contain inner polyhedron, cannot write STL file.")
else:
for pa in self.particles:
pa.sync_poly()
write_particles()
else:
if phases:
if phase_num is None:
raise ValueError('Phase-specific output requested, but no phase number specified.')
write_phases(phase_num)
else:
write_grains()
f.write("endsolid\n")
return
[docs]
def write_centers(self, file=None, path='./', grains=None):
"""
Write the center positions of grains to a CSV file
Parameters
----------
file : str, optional
Filename for the CSV file. Default is auto-generated based on `self.name`.
path : str, optional
Directory path to save the CSV file. Default is './'.
grains : dict, optional
Dictionary of grain data. Default is `self.geometry['Grains']`.
Returns
-------
None
Notes
-----
- Each line in the CSV file corresponds to a grain center in the format: x, y, z.
- Polyhedral grains without simplices are skipped.
"""
if file is None:
if self.name == 'Microstructure':
file = 'px_{}grains_centroid.csv'.format(self.Ngr)
else:
file = self.name + '_centroid.csv'
path = os.path.normpath(path)
file = os.path.join(path, file)
if grains is None:
grains = self.geometry['Grains']
with open(file, 'w') as f:
for gr in grains.values():
# if polyhedral grain has no simplices, center should not be written!!!
ctr = gr['Center']
f.write('{}, {}, {}\n'.format(ctr[0], ctr[1], ctr[2]))
return
[docs]
def write_ori(self, angles=None, file=None, path='./'):
"""
Write grain orientations (Euler angles) to a CSV file
This method exports the orientations of grains in the microstructure. If no angles
are provided, the method uses the stored grain orientations from the mesh. The CSV
file contains one row per grain with three values representing the Euler angles
(phi1, Phi, phi2).
Parameters
----------
angles : array-like of shape (N, 3), optional
Euler angles for each grain. If None, the stored orientations are used.
file : str, optional
Name of the output CSV file. If None, a default name based on the microstructure
name and number of grains is used.
path : str, optional
Directory path where the CSV file will be saved. Default is './'.
Returns
-------
str
Full path to the generated CSV file.
Raises
------
ValueError
If `angles` is None and no grain orientations are stored in `self.mesh.grain_ori_dict`.
Notes
-----
- Each row in the CSV corresponds to one grain.
- Values are written in the order: phi1, Phi, phi2 (in radians).
Examples
--------
>>> # Export stored orientations to default file
>>> rve.write_ori()
>>> # Export provided orientations to a specific file
>>> rve.write_ori(angles=my_angles, file='grain_orientations.csv', path='./data')
"""
if file is None:
if self.name == 'Microstructure':
file = 'px_{}grains_ori.csv'.format(self.Ngr)
else:
file = self.name + '_ori.csv'
path = os.path.normpath(path)
file = os.path.join(path, file)
if angles is None:
if self.mesh.grain_ori_dict is None:
raise ValueError('No grain orientations given or stored.')
angles = [val for val in self.mesh.grain_ori_dict.values()]
with open(file, 'w') as f:
for ori in angles:
f.write('{}, {}, {}\n'.format(ori[0], ori[1], ori[2]))
return
[docs]
def write_voxels(self, angles=None, script_name=None, file=None, path='./',
mesh=True, source=None, system=False):
"""
Write voxel structure of the microstructure to a JSON file
This function exports the voxel-based representation of the microstructure,
including grain numbers, optional orientations, and mesh data. Metadata such
as owner, system information, and script name are included.
Parameters
----------
angles : np.ndarray, optional
Array of Euler angles for grain orientations. If None, stored orientations
are used if available. Default is None.
script_name : str, optional
Name of the script used to generate the microstructure. Default is the
current script file.
file : str, optional
Name of the output JSON file. Default is based on microstructure name and
number of grains.
path : str, optional
Directory path to save the JSON file. Default is current directory './'.
mesh : bool, optional
Whether to include mesh nodes and voxel connectivity in the output. Default
is True.
source : Any, optional
Optional input or source information to include in the JSON metadata.
system : bool, optional
Whether to include system information in the JSON metadata. Default is False.
Returns
-------
None
The function writes a JSON file and does not return any value.
"""
import platform
import getpass
from datetime import date
from kanapy import __version__
if script_name is None:
script_name = __file__
if file is None:
if self.name == 'Microstructure':
file = f'px_{self.Ngr}grains_voxels.json'
else:
file = self.name + '_voxels.json'
path = os.path.normpath(path)
file = os.path.join(path, file)
print(f'Writing voxel information of microstructure to {file}.')
# metadata
today = str(date.today()) # date
owner = getpass.getuser() # username
sys_info = platform.uname() # system information
# output dict
structure = {
"Info": {
"Owner": owner,
"Institution": "ICAMS, Ruhr University Bochum, Germany",
"Date": today,
"Description": "Voxels of microstructure",
"Method": "Synthetic microstructure generator Kanapy",
},
"Model": {
"Creator": "kanapy",
"Version": __version__,
"Repository": "https://github.com/ICAMS/Kanapy.git",
"Input": source,
"Script": script_name,
"Material": self.name,
"Phase_names": self.rve.phase_names,
"Size": [int(val) for val in self.rve.size],
"Periodicity": str(self.rve.periodic),
"Units": {
'Length': self.rve.units,
},
},
"Data": {
"Description": 'Grain numbers per voxel',
"Type": 'int',
"Shape": self.rve.dim,
"Order": 'C',
"Values": [int(val) for val in self.mesh.grains.flatten()],
},
"Grains": {
"Description": "Grain-related data",
"Orientation": "Euler-Bunge angle",
"Phase": "Phase number"
},
}
if system:
structure["Info"]["System"] = {
"sysname": sys_info[0],
"nodename": sys_info[1],
"release": sys_info[2],
"version": sys_info[3],
"machine": sys_info[4],
}
for igr in self.mesh.grain_dict.keys():
structure["Grains"][int(igr)] = {
"Phase": int(self.mesh.grain_phase_dict[igr])
}
if angles is None:
if self.mesh.grain_ori_dict is None:
logging.info('No angles for grains are given. Writing only geometry of RVE.')
else:
for igr in self.mesh.grain_ori_dict.keys():
structure["Grains"][igr]["Orientation"] = list(self.mesh.grain_ori_dict[igr])
else:
for i, igr in enumerate(self.mesh.grain_dict.keys()):
structure["Grains"][igr]["Orientation"] = list(angles[i, :])
if mesh:
structure['Mesh'] = {
"Nodes": {
"Description": 'Nodal coordinates',
"Type": 'float',
"Shape": self.mesh.nodes.shape,
"Values": [list(val) for val in self.mesh.nodes],
},
"Voxels": {
"Description": 'Node list per voxel',
"Type": 'int',
"Shape": (len(self.mesh.voxel_dict.keys()), 8),
"Values": [int(val) for val in self.mesh.voxel_dict.values()],
}
}
# write file
with open(file, 'w') as fp:
json.dump(structure, fp)
return
[docs]
def write_dataSchema(self,
user_metadata: Optional[Dict[str, Any]] = None,
boundary_condition: Optional[Dict[str, Any]] = None,
phases: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = None,
interactive: bool = True,
structured: bool = True,
ialloy: int = 1,
length_unit: str = 'µm') -> dict:
"""
Generate a JSON-compatible data schema containing user, system, and job-specific elements
This function builds a comprehensive data dictionary for a microstructure simulation,
including metadata, boundary conditions, phase information, voxel and grain data.
It supports interactive prompts for missing information, structured/unstructured meshes,
and unit scaling.
Parameters
----------
user_metadata : dict, optional
Prefilled metadata fields; required if interactive=False. Default is None.
boundary_condition : dict, optional
Dictionary specifying mechanical or thermal boundary conditions. Default is None.
phases : dict or list of dicts, optional
Phase-specific information; if None, default material library and ialloy are used.
interactive : bool, optional
If True, prompt user for missing information. Default is True.
structured : bool, optional
Whether the mesh is structured. Default is True.
ialloy : int, optional
Alloy index for selecting material properties from the built-in library. Default is 1.
length_unit : str, optional
Unit for length scaling; either 'µm' or 'mm'. Default is 'µm'.
Returns
-------
dict
A nested dictionary containing the complete data schema for the simulation.
Raises
------
ValueError
If interactive=False and required metadata fields or boundary conditions are missing,
or if length_unit is invalid
TypeError
If `phases` is not a dict or a list of dicts when provided
"""
# interpret length_unit argument
if length_unit == 'µm':
length_scale = 1.0
elif length_unit == 'mm':
length_scale = 1e-3
else:
raise ValueError("length_unit must be 'µm' or 'mm'")
# Material library definitions (pulled from mod_alloys.f)
material_library = {
1: { # Aluminum
'ialloy': 1,
'material_identifier': 'Aluminum',
'elastic_model_name': 'Anisotropic Elasticity',
'elastic_parameters': {
'C11': 247000.0, 'C12': 147000.0, 'C44': 125000.0
},
'plastic_model_name': 'Crystal Plasticity',
'plastic_parameters': {
'reference_shear_rate': 1e-6, # shrt0
'initial_critical_resolved_shear_stress': 20.0, # crss0
'saturated_slip_resistance': 1500.0, # crsss
'strain_rate_sensitivity_exponent': 20.0, # pwfl
'reference_hardening_rate': 60.0, # hdrt0
'hardening_exponent': 2.25 # pwhd
}
},
2: { # Copper
'ialloy': 2,
'material_identifier': 'Copper',
'elastic_model_name': 'Anisotropic Elasticity',
'elastic_parameters': {
'C11': 170000.0, 'C12': 124000.0, 'C44': 75000.0
},
'plastic_model_name': 'Crystal Plasticity',
'plastic_parameters': {
'reference_shear_rate': 0.001, # shrt0
'initial_critical_resolved_shear_stress': 16.0, # crss0
'saturated_slip_resistance': 148.0, # crsss
'strain_rate_sensitivity_exponent': 83.0, # pwfl
'reference_hardening_rate': 250.0, # hdrt0
'hardening_exponent': 2.25 # pwhd
}
},
3: { # Ferrite
'ialloy': 3,
'material_identifier': 'Ferrite',
'elastic_model_name': 'Anisotropic Elasticity',
'elastic_parameters': {
'C11': 230000.0, 'C12': 135000.0, 'C44': 116000.0
},
'plastic_model_name': 'Crystal Plasticity',
'plastic_parameters': {
'reference_shear_rate': 1e-6,
'initial_critical_resolved_shear_stress': 25.0,
'saturated_slip_resistance': 1600.0,
'strain_rate_sensitivity_exponent': 18.0,
'reference_hardening_rate': 70.0,
'hardening_exponent': 2.0
}
},
4: { # Austenite
'ialloy': 4,
'material_identifier': 'Austenite',
'elastic_model_name': 'Anisotropic Elasticity',
'elastic_parameters': {
'C11': 190000.0, 'C12': 130000.0, 'C44': 115000.0
},
'plastic_model_name': 'Crystal Plasticity',
'plastic_parameters': {
'reference_shear_rate': 1e-6,
'initial_critical_resolved_shear_stress': 15.0,
'saturated_slip_resistance': 1400.0,
'strain_rate_sensitivity_exponent': 22.0,
'reference_hardening_rate': 65.0,
'hardening_exponent': 2.2
}
},
5: { # Superalloy
'ialloy': 5,
'material_identifier': 'Superalloy',
'elastic_model_name': 'Anisotropic Elasticity',
'elastic_parameters': {
'C11': 260000.0, 'C12': 150000.0, 'C44': 120000.0
},
'plastic_model_name': 'Crystal Plasticity',
'plastic_parameters': {
'reference_shear_rate': 1e-6,
'initial_critical_resolved_shear_stress': 30.0,
'saturated_slip_resistance': 1700.0,
'strain_rate_sensitivity_exponent': 25.0,
'reference_hardening_rate': 80.0,
'hardening_exponent': 2.8
}
},
6: { # Nickel
'ialloy': 6,
'material_identifier': 'Nickel',
'elastic_model_name': 'Anisotropic Elasticity',
'elastic_parameters': {
'C11': 246000.0, 'C12': 147000.0, 'C44': 124000.0
},
'plastic_model_name': 'Crystal Plasticity',
'plastic_parameters': {
'reference_shear_rate': 1e-6,
'initial_critical_resolved_shear_stress': 18.0,
'saturated_slip_resistance': 1450.0,
'strain_rate_sensitivity_exponent': 20.0,
'reference_hardening_rate': 75.0,
'hardening_exponent': 2.4
}
}
}
# Define required fields
required_fields = [
'identifier', 'title',
'creator', 'creator_ORCID', 'creator_affiliation', 'creator_institute', 'creator_group',
'contributor', 'contributor_ORCID', 'contributor_affiliation', 'contributor_institute', 'contributor_group',
'date', 'shared_with', 'description', 'rights', 'rights_holder','funder_name', 'fund_identifier',
'publisher', 'relation', 'keywords'
]
def prompt_list(field_name: str) -> List[str]:
"""
Prompt the user to enter a comma-separated list of values
This function requests user input for a given field name and splits
the input string into a list of stripped strings. Returns an empty
list if the input is blank.
Parameters
----------
field_name : str
Name of the field to prompt the user for
Returns
-------
list of str
List of values entered by the user, with leading/trailing whitespace removed
"""
vals = input(f"Enter comma-separated {field_name}: ").strip()
return [v.strip() for v in vals.split(',')] if vals else []
# Gather metadata
if interactive and user_metadata is None:
use: Dict[str, Any] = {}
# identifier
ident = input("Identifier (leave blank to auto-generate): ").strip()
if not ident:
now = datetime.utcnow().isoformat()
ident = hashlib.sha256(now.encode()).hexdigest()[:8]
use['identifier'] = ident
use['title'] = input("Title: ").strip()
# creator fields
use['creator'] = prompt_list('creator names (e.g. Last, First)')
use['creator_ORCID'] = prompt_list('creator ORCID(s)')
use['creator_affiliation'] = prompt_list('creator affiliations')
use['creator_institute'] = prompt_list('creator institutes')
use['creator_group'] = prompt_list('creator groups')
# contributor fields
use['contributor'] = prompt_list('contributor names')
use['contributor_ORCID'] = prompt_list('contributor ORCID(s)')
use['contributor_affiliation'] = prompt_list('contributor affiliations')
use['contributor_institute'] = prompt_list('contributor institutes')
use['contributor_group'] = prompt_list('contributor groups')
use['date'] = input("Date (YYYY-MM-DD): ").strip() or datetime.utcnow().strftime('%Y-%m-%d')
# shared_with
shared: List[Dict[str, str]] = []
print("Enter shared_with access entries. Valid types: c, u, g, all. Blank to stop.")
while True:
atype = input(" access_type: ").strip()
if not atype:
break
shared.append({'access_type': atype})
use['shared_with'] = shared
use['description'] = input("Description: ").strip()
use['rights'] = input("Rights (e.g. Creative Commons Attribution 4.0 International): ").strip()
use['rights_holder'] = prompt_list('rights_holder')
# other fields
use['funder_name'] = input("Funder name: ").strip()
use['fund_identifier'] = input("Fund identifier: ").strip()
use['publisher'] = input("Publisher: ").strip()
use['relation'] = prompt_list('relation (DOI or URL)')
use['keywords'] = prompt_list('keywords')
else:
if not user_metadata:
raise ValueError("user_metadata must be provided when interactive is False.")
use = user_metadata.copy()
# Validate presence of required fields
missing = [f for f in required_fields if f not in use]
if missing:
raise ValueError(f"Missing required metadata fields: {', '.join(missing)}")
ig = {
'RVE_size': [float(v) * length_scale for v in self.rve.size],
'RVE_continuity': self.rve.periodic,
'discretization_type': 'Structured' if structured else 'Unstructured',
'discretization_unit_size': [(float(s) / float(d)) * length_scale for s, d in zip(self.rve.size, self.rve.dim)],
'discretization_count': int(self.mesh.nvox),
'global_rotation_convention': str(),
'Origin': {
'software': 'kanapy',
'software_version': pkg_version('kanapy'),
'system': platform.system(),
'system_version': platform.version()
}
}
# ─── Show vertex‐diagram for BC reference ────────────────────────────────
try:
# locate the package root, two levels up from this file
script_dir = os.path.dirname(__file__)
project_root = os.path.abspath(os.path.join(script_dir, os.pardir, os.pardir))
img_path = os.path.join(project_root, 'docs', 'figs', 'RVE', 'Vertices.png')
if os.path.exists(img_path):
# first try with Pillow
try:
from PIL import Image
Image.open(img_path).show()
except ImportError:
# fallback to the system browser/viewer
import webbrowser
webbrowser.open(f'file://{img_path}')
except (FileNotFoundError, OSError) as e:
logging.warning(f"[Warning] Unable to open Vertices.png: {e}")
# Boundary conditions
if boundary_condition:
# Ensure 'type' is present
if 'mechanical_BC' in boundary_condition:
# Determine mechanical vs thermal by key presence
mech_list = boundary_condition['mechanical_BC']
# Normalize to list
if not isinstance(mech_list, list):
mech_list = [mech_list]
job_bc = {'mechanical_BC': mech_list}
elif 'thermal_BC' in boundary_condition:
th_list = boundary_condition['thermal_BC']
if not isinstance(th_list, list):
th_list = [th_list]
job_bc = {'thermal_BC': th_list}
else:
# fallback if wrong keys provided
if interactive:
# ask user which type to populate
bc_choice = input("Boundary condition key not found. Enter 'mechanical' or 'thermal': ").strip()
if bc_choice == 'mechanical':
job_bc = {'mechanical_BC': []}
else:
job_bc = {'thermal_BC': {}}
else:
raise ValueError(
"boundary_condition dict must include 'mechanical_BC' or 'thermal_BC' key when interactive=False.")
elif interactive: # Interactive entry of multiple mechanical BCs
mech_entries: List[Dict[str, Any]] = []
bc_type = input("Boundary condition type ('mechanical' or 'thermal'): ").strip()
if bc_type == 'mechanical':
while True:
print("Define a mechanical BC (leave vertex_list blank to stop):")
vertex_list = prompt_list('vertex_list')
if not vertex_list:
break
constraints = input("Constraints xyz (e.g. 'free,fixed,loaded'): ").split(',')
loading_type = input("Loading type (force/displacement/stress/strain/none): ").strip()
loading_mode = input("Loading mode (static/cyclic/monotonic/intermittent): ").strip()
# count how many of those constraints are “loaded”:
num = sum(1 for c in constraints if c.strip().lower() == 'loaded')
loads = []
for i in range(num):
print(f"Load entry #{i + 1}:")
mag = float(input(" magnitude: "))
freq = float(input(" frequency: "))
dur = float(input(" duration: "))
R = float(input(" R: "))
loads.append({'magnitude': mag, 'frequency': freq, 'duration': dur, 'R': R})
mech_entries.append({
'vertex_list': vertex_list,
'constraints': constraints,
'loading_type': loading_type,
'loading_mode': loading_mode,
'applied_load': loads
})
job_bc = {'mechanical_BC': mech_entries}
else:
job_bc = {'thermal_BC': {}}
else:
job_bc = {}
# Phase data
phase_list = []
if phases: # user provided a dict or list of dicts
if isinstance(phases, dict):
phase_list = [phases]
elif isinstance(phases, list):
phase_list = phases
else:
raise TypeError("`phases` must be a dict or list of dicts.")
else: # fallback: use ialloy + material_library
if not ialloy or ialloy not in material_library:
if interactive:
ialloy = int(input(f"Choose ialloy from {list(material_library.keys())}: "))
else:
raise ValueError(
f"No phases provided and invalid ialloy. "
f"Valid ialloy values: {list(material_library.keys())}"
)
for idx in range(self.nphases):
phase_name = self.rve.phase_names[idx]
vf = self.rve.phase_vf[idx]
mat = material_library[ialloy]
pe = mat['elastic_parameters']
pp = mat['plastic_parameters']
phase_entry = {
"id": idx,
"phase_identifier": phase_name,
"constitutive_model": {
"$schema": "http://json-schema.org/draft-04/schema#",
"elastic_model_name": mat['elastic_model_name'],
"elastic_parameters": pe,
"plastic_model_name": mat['plastic_model_name'],
"plastic_parameters": pp
},
"microstructural_information": {
"phase_volume_fraction": float(vf),
"grain_count": int(self.ngrains[idx]),
"texture_type": getattr(self.mesh, "texture", None),
"lattice_structure": None,
}
}
phase_list.append(phase_entry)
# ─── pull Mesh + RVE into locals ─────────────────────────────────────────
grain_phase_dict = getattr(self.mesh, 'grain_phase_dict', {}) or {} # {gid: phase_id}
grain_ori_dict = getattr(self.mesh, 'grain_ori_dict', None) # can be None ({gid: [euler…]})
vox_center_dict = getattr(self.mesh, 'vox_center_dict', {}) or {} # {vid: (x,y,z)}
grain_to_voxels = getattr(self.mesh, 'grain_dict', {}) or {} # {gid: [vid,…]}
rve_size = getattr(self.rve, 'size', [0, 0, 0]) or [0, 0, 0] # e.g. [20.0,20.0,20.0]
rve_dim = getattr(self.rve, 'dim', [1, 1, 1]) or [1, 1, 1] # # e.g. [10,10,10] (avoid /0)
# Is orientation available?
include_orientation = isinstance(grain_ori_dict, dict) and len(grain_ori_dict) > 0
# ─── compute one‐voxel volume ─────────────────────────────────────────────
unit_sizes = []
for s, d in zip(rve_size, rve_dim):
try:
unit_sizes.append((float(s) / float(d)) * length_scale if float(d) != 0 else 0.0)
except Exception:
unit_sizes.append(0.0)
voxel_volume = (unit_sizes[0] if len(unit_sizes) > 0 else 0.0) \
* (unit_sizes[1] if len(unit_sizes) > 1 else 0.0) \
* (unit_sizes[2] if len(unit_sizes) > 2 else 0.0)
# grid size per axis
Nx, Ny, Nz = int(rve_dim[0]), int(rve_dim[1]), int(rve_dim[2])
# infer origin from centers if available: origin = min(center) - 0.5*unit
if len(vox_center_dict) > 0:
xs = [float(c[0])* length_scale for c in vox_center_dict.values()]
ys = [float(c[1])* length_scale for c in vox_center_dict.values()]
zs = [float(c[2])* length_scale for c in vox_center_dict.values()]
ox = (min(xs) if xs else 0.0) - 0.5 * unit_sizes[0]
oy = (min(ys) if ys else 0.0) - 0.5 * unit_sizes[1]
oz = (min(zs) if zs else 0.0) - 0.5 * unit_sizes[2]
else:
ox = oy = oz = 0.0
origin = [ox, oy, oz]
# ─── precompute voxel→grain lookup ───────────────────────────────────────
voxel_to_grain = {
vid: gid
for gid, vids in grain_to_voxels.items()
for vid in vids
}
# ─── build time‐0 grain dict ────────────────────────────────────────────
grains_t0 = []
for gid in grain_phase_dict.keys():
entry = {
"grain_id": gid,
"phase_id": grain_phase_dict.get(gid),
"grain_volume": len(grain_to_voxels.get(gid, [])) * voxel_volume,
}
if include_orientation:
ori = grain_ori_dict.get(gid)
if ori is not None:
entry["orientation"] = list(ori)
grains_t0.append(entry)
grains_t0_sorted = sorted(grains_t0, key=lambda d: int(d["grain_id"]))
# ─── Build time‐0 voxel dictionary ────────────────────────────────────────
def _coord_to_index_1based(c, o, d):
"""
Convert a coordinate to a 1-based voxel index
This function computes the voxel index corresponding to a coordinate `c`,
given the origin `o` and voxel size `d`. The result is rounded and adjusted
to be 1-based, robust to small floating-point errors.
Parameters
----------
c : float
Coordinate value
o : float
Origin coordinate of the voxel grid
d : float
Size of a voxel along the corresponding axis
Returns
-------
int
1-based voxel index corresponding to the coordinate
"""
# i = round((c - o)/d + 0.5), robust to tiny float noise
return int(round((float(c) - float(o)) / float(d) + 0.5))
def _clamp(v, lo, hi):
"""
Clamp a value between a lower and upper bound
This function ensures that the input value `v` lies within the range
[`lo`, `hi`]. Values below `lo` are set to `lo`, and values above `hi`
are set to `hi`.
Parameters
----------
v : float
Value to be clamped
lo : float
Lower bound
hi : float
Upper bound
Returns
-------
float
The clamped value within the specified bounds
"""
return max(lo, min(hi, v))
voxels_t0 = []
for vid, gid in voxel_to_grain.items():
cx, cy, cz = vox_center_dict.get(vid, (0.0, 0.0, 0.0))
centroid = [float(cx) * length_scale,
float(cy) * length_scale,
float(cz) * length_scale]
# Compute 1-based voxel indices from centers
ix = _coord_to_index_1based(centroid[0], origin[0], unit_sizes[0])
iy = _coord_to_index_1based(centroid[1], origin[1], unit_sizes[1])
iz = _coord_to_index_1based(centroid[2], origin[2], unit_sizes[2])
# Clamp to valid range [1..N*]
ix = _clamp(ix, 1, Nx)
iy = _clamp(iy, 1, Ny)
iz = _clamp(iz, 1, Nz)
entry = {
"voxel_id": vid,
"grain_id": gid,
"centroid_coordinates": centroid, # scaled for output
"voxel_index": [ix, iy, iz], # 1-based indices
"voxel_volume": voxel_volume,
}
if include_orientation:
ori = grain_ori_dict.get(gid)
if ori is not None:
entry["orientation"] = list(ori)
voxels_t0.append(entry)
voxels_t0_sorted = sorted(voxels_t0, key=lambda d: int(d["voxel_id"]))
# ─── wrap under the time‐step keys ────────────────────────────────────────
time_steps = [
{ "time" : 0 ,
"grains": grains_t0_sorted,
"voxels": voxels_t0_sorted,
},
# …etc…
]
# Assemble final structure with placeholders
data = {
**use, # expand user-specific dict entries directly
'software': '',
'software_version': '',
'system': '',
'system_version': '',
'processor_specifications': '',
'input_path': '',
'results_path': '',
**ig, # expand initial geometry dict entries directly
'global_temperature': 298,
**job_bc, # expand boundary condition dict entries directly
'phases':phase_list,
'microstructure_evolution': time_steps # Time-level storage: time-frame data of voxels and grains
}
return data
[docs]
def pckl(self, file=None, path='./'):
"""
Write the microstructure into a pickle file
This function serializes the microstructure object using pickle, allowing
complex structures to be stored and later reloaded.
Parameters
----------
file : str, optional
File name for the pickled microstructure. If None, the filename defaults
to the microstructure name plus '.pckl'.
path : str, optional
Directory path to save the pickle file. Default is current directory './'.
Returns
-------
None
"""
import pickle
if file is None:
if self.name == 'Microstructure':
file = 'px_{}grains_microstructure.pckl'.format(self.Ngr)
else:
file = self.name + '_microstructure.pckl'
path = os.path.normpath(path)
file = os.path.join(path, file)
with open(file, 'wb') as output:
pickle.dump(self, output, pickle.HIGHEST_PROTOCOL)
return
[docs]
def import_particles(self, file, path='./'):
"""
Import particle data from a dump file
This function reads particle information from a specified dump file and
stores it in the microstructure object, updating the simulation box and
particle list.
Parameters
----------
file : str
Name of the input dump file containing particle data
path : str, optional
Directory path where the dump file is located. Default is current directory './'
Returns
-------
None
Updates the object's simbox and particles attributes
"""
path = os.path.normpath(path)
file = os.path.join(path, file)
self.simbox, self.particles = read_dump(file)
"""
-------- legacy methods --------
"""
[docs]
def init_stats(self, descriptor=None, gs_data=None, ar_data=None, porous=False, save_files=False):
"""
Legacy function that redirects to plot_stats_init
This function is deprecated and serves as a wrapper for `plot_stats_init`.
A warning is logged advising users to use `plot_stats_init` directly.
Parameters
----------
descriptor : Any, optional
Descriptor data passed to `plot_stats_init`. Default is None.
gs_data : Any, optional
Grain size data passed to `plot_stats_init`. Default is None.
ar_data : Any, optional
Aspect ratio data passed to `plot_stats_init`. Default is None.
porous : bool, optional
Deprecated parameter, ignored. Default is False.
save_files : bool, optional
Whether to save generated statistics files. Default is False.
Returns
-------
None
Calls `plot_stats_init` and does not return any value
"""
logging.warning('"init_stats" is a legacy function and will be depracted, please use "plot_stats_init()".')
self.plot_stats_init(descriptor, gs_data=gs_data, ar_data=ar_data, save_files=save_files)
[docs]
def output_abq(self, nodes=None, name=None,
voxel_dict=None, grain_dict=None, faces=None,
dual_phase=False, thermal=False, units=None):
"""
Legacy function that redirects to write_abq
This function is deprecated and serves as a wrapper for `write_abq`.
Warnings are logged advising users to use `write_abq` directly. The `faces`
parameter is determined automatically if provided.
Parameters
----------
nodes : array-like, optional
Nodal coordinates to include in the output. Default is None.
name : str, optional
File name for the Abaqus input file. Default is None.
voxel_dict : dict, optional
Voxel connectivity information. Default is None.
grain_dict : dict, optional
Grain-to-voxel mapping. Default is None.
faces : Any, optional
Deprecated parameter, automatically determined. Default is None.
dual_phase : bool, optional
Whether to include dual-phase data. Default is False.
thermal : bool, optional
Whether to include thermal data. Default is False.
units : dict, optional
Dictionary of units for the Abaqus model. Default is None.
Returns
-------
None
Calls `write_abq` and does not return any value
"""
logging.warning('"output_abq" is a legacy function and will be depracted, please use "write_abq()".')
if faces is not None:
logging.warning('Parameter "faces" will be determined automatically.')
self.write_abq(nodes=nodes, file=name, voxel_dict=voxel_dict, grain_dict=grain_dict,
dual_phase=dual_phase, thermal=thermal, units=units)