#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
A Graphical User Interface for create_rve.py, cuboid_grains.py and cpnvert_ang2rve.py
Created on May 2024
last Update Oct 2024
@author: Ronak Shoghi, Alexander Hartmaier
"""
import sys
import time
import itertools
import numpy as np
import tkinter as tk
from tkinter import ttk, Toplevel, filedialog
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from .api import Microstructure
from .initializations import RVE_creator, mesh_creator
from .input_output import import_stats, write_stats
from .entities import Simulation_Box
if 'kanapy_mtex' in sys.modules:
from kanapy_mtex.texture import EBSDmap
else:
from kanapy.texture import EBSDmap
[docs]
def self_closing_message(message, duration=4000):
"""
Display a temporary popup message box that closes automatically after a given duration
Parameters
----------
message : str
The message text to display in the popup window
duration : int, optional
Time in milliseconds before the message box closes automatically (default is 4000)
Returns
-------
None
The function creates and destroys a popup window; no value is returned
"""
popup = Toplevel()
popup.title("Information")
popup.geometry("300x100")
screen_width = popup.winfo_screenwidth()
screen_height = popup.winfo_screenheight()
window_width = 300
window_height = 100
x_coordinate = int((screen_width / 2) - (window_width / 2))
y_coordinate = int((screen_height / 2) - (window_height / 2))
popup.geometry(f"{window_width}x{window_height}+{x_coordinate}+{y_coordinate}")
label = ttk.Label(popup, text=message, font=("Helvetica", 12), wraplength=250)
label.pack(expand=True)
popup.after(duration, popup.destroy)
popup.update_idletasks()
return
[docs]
def add_label_and_entry(frame, row, label_text, entry_var, entry_type="entry", bold=False,
options=None, col=0):
"""
Add a label and an input widget (entry, checkbox, or combobox) to a given frame
Parameters
----------
frame : tkinter.Frame
Parent frame where the widgets will be added
row : int
Row index in the frame grid layout
label_text : str or None
Text for the label; if None, no label is created
entry_var : tkinter variable
Variable bound to the input widget (e.g., StringVar, BooleanVar)
entry_type : str, optional
Type of input widget: "entry", "checkbox", or "combobox" (default is "entry")
bold : bool, optional
Whether to display the label text in bold (default is False)
options : list, optional
List of selectable options when entry_type is "combobox"
col : int, optional
Column index for placing the label and entry in the grid (default is 0)
Returns
-------
None
The function adds widgets directly to the frame
"""
if label_text is not None:
label_font = ("Helvetica", 12, "bold") if bold else ("Helvetica", 12)
ttk.Label(frame, text=label_text, font=label_font).grid(row=row, column=col, sticky='w')
col_entry = col + 1
else:
col_entry = col
if entry_type == "entry":
ttk.Entry(frame, textvariable=entry_var, width=20, font=("Helvetica", 12)) \
.grid(row=row, column=col_entry, sticky='e')
elif entry_type == "checkbox":
ttk.Checkbutton(frame, variable=entry_var).grid(row=row, column=col_entry, sticky='e')
elif entry_type == "combobox" and options is not None:
combobox = ttk.Combobox(frame, textvariable=entry_var, values=options, state='readonly',
width=19)
combobox.grid(row=row, column=col_entry, sticky='e')
combobox.configure(font=("Helvetica", 12))
combobox.current(0)
return
[docs]
def parse_entry(entry):
"""
Convert a comma-separated string into a list of integers
Parameters
----------
entry : str
String of comma-separated integer values
Returns
-------
list of int
List of integers parsed from the input string
"""
return list(map(int, entry.strip().split(',')))
[docs]
class particle_rve(object):
"""
Class for generating Representative Volume Elements (RVEs) based on particle simulations
first tab
This class defines the GUI logic and default parameters for building RVEs using
particle-based microstructure simulations. It manages various parameters such as
particle size distribution, aspect ratio, texture settings, and periodic boundary conditions.
Parameters
----------
app : tkinter.Tk or tkinter.Frame
Reference to the main Tkinter application instance.
notebook : ttk.Notebook
The notebook widget in which this tab (RVE setup) is placed.
Attributes
----------
app : tkinter.Tk or tkinter.Frame
Main application reference.
ms : object or None
Placeholder for microstructure data.
ms_stats : object or None
Placeholder for microstructure statistical data.
ebsd : object or None
Placeholder for EBSD data (if used).
stats_canvas1 : tkinter.Canvas or None
Canvas for displaying statistical plots.
rve_canvas1 : tkinter.Canvas or None
Canvas for displaying the RVE visualization.
texture_var1 : tk.StringVar
Texture type, default "random".
matname_var1 : tk.StringVar
Material name, default "Simulanium".
nphases : tk.IntVar
Number of material phases, default 1.
ialloy : tk.IntVar
Alloy type indicator, default 2.
nvox_var1 : tk.IntVar
Number of voxels per dimension in the RVE, default 30.
size_var1 : tk.IntVar
Physical size of the RVE, default 30.
periodic_var1 : tk.BooleanVar
Whether to apply periodic boundary conditions, default True.
volume_fraction : tk.DoubleVar
Target total volume fraction, default 1.0.
vf_act : tk.DoubleVar
Actual volume fraction, initialized to 0.0.
eq_diameter_sig : tk.DoubleVar
Standard deviation of equivalent diameter distribution.
eq_diameter_scale : tk.DoubleVar
Scale parameter for equivalent diameter distribution.
eq_diameter_loc : tk.DoubleVar
Location parameter for equivalent diameter distribution.
eq_diameter_min : tk.DoubleVar
Minimum equivalent diameter.
eq_diameter_max : tk.DoubleVar
Maximum equivalent diameter.
eqd_act_sig : tk.DoubleVar
Active (fitted or adjusted) standard deviation for equivalent diameter.
eqd_act_scale : tk.DoubleVar
Active scale for equivalent diameter.
eqd_act_loc : tk.DoubleVar
Active location for equivalent diameter.
aspect_ratio_sig : tk.DoubleVar
Standard deviation for aspect ratio distribution.
aspect_ratio_scale : tk.DoubleVar
Scale parameter for aspect ratio distribution.
aspect_ratio_loc : tk.DoubleVar
Location parameter for aspect ratio distribution.
aspect_ratio_min : tk.DoubleVar
Minimum aspect ratio.
aspect_ratio_max : tk.DoubleVar
Maximum aspect ratio.
ar_act_sig : tk.DoubleVar
Active standard deviation for aspect ratio.
ar_act_scale : tk.DoubleVar
Active scale for aspect ratio.
ar_act_loc : tk.DoubleVar
Active location for aspect ratio.
tilt_angle_kappa : tk.DoubleVar
Concentration parameter for tilt angle distribution.
tilt_angle_loc : tk.DoubleVar
Mean (location) of tilt angle distribution in radians.
tilt_angle_min : tk.DoubleVar
Minimum tilt angle (0.0).
tilt_angle_max : tk.DoubleVar
Maximum tilt angle (π).
kernel_var1 : tk.StringVar
Orientation kernel width; depends on texture setting.
euler_var1 : tk.StringVar
Euler angles defining the texture orientation; depends on texture setting.
"""
def __init__(self, app, notebook):
# define standard parameters
self.app = app
self.ms = None
self.ms_stats = None
self.ebsd = None
self.stats_canvas1 = None
self.rve_canvas1 = None
self.texture_var1 = tk.StringVar(value="random")
self.matname_var1 = tk.StringVar(value="Simulanium")
self.nphases = tk.IntVar(value=1)
self.ialloy = tk.IntVar(value=2)
self.nvox_var1 = tk.IntVar(value=30)
self.size_var1 = tk.IntVar(value=30)
self.periodic_var1 = tk.BooleanVar(value=True)
self.volume_fraction = tk.DoubleVar(value=1.0)
self.vf_act = tk.DoubleVar(value=0.0)
self.eq_diameter_sig = tk.DoubleVar(value=0.7)
self.eq_diameter_scale = tk.DoubleVar(value=20.0)
self.eq_diameter_loc = tk.DoubleVar(value=0.0)
self.eq_diameter_min = tk.DoubleVar(value=9.0)
self.eq_diameter_max = tk.DoubleVar(value=18.0)
self.eqd_act_sig = tk.DoubleVar(value=0.0)
self.eqd_act_scale = tk.DoubleVar(value=0.0)
self.eqd_act_loc = tk.DoubleVar(value=0.0)
self.aspect_ratio_sig = tk.DoubleVar(value=0.9)
self.aspect_ratio_scale = tk.DoubleVar(value=2.5)
self.aspect_ratio_loc = tk.DoubleVar(value=0.0)
self.aspect_ratio_min = tk.DoubleVar(value=1.0)
self.aspect_ratio_max = tk.DoubleVar(value=4.0)
self.ar_act_sig = tk.DoubleVar(value=0.0)
self.ar_act_scale = tk.DoubleVar(value=0.0)
self.ar_act_loc = tk.DoubleVar(value=0.0)
self.tilt_angle_kappa = tk.DoubleVar(value=1.0)
self.tilt_angle_loc = tk.DoubleVar(value=round(0.5 * np.pi, 4))
self.tilt_angle_min = tk.DoubleVar(value=0.0)
self.tilt_angle_max = tk.DoubleVar(value=round(np.pi, 4))
if self.texture_var1.get() == 'random':
self.kernel_var1 = tk.StringVar(value="-")
self.euler_var1 = tk.StringVar(value="-")
else:
self.kernel_var1 = tk.StringVar(value="7.5")
self.euler_var1 = tk.StringVar(value="0.0, 45.0, 0.0")
self.texture_var1.trace('w', self.update_kernel_var)
self.texture_var1.trace('w', self.update_euler_var)
# plot frames
tab1 = ttk.Frame(notebook)
notebook.add(tab1, text="Standard RVE")
main_frame1 = ttk.Frame(tab1)
main_frame1.grid(row=0, column=0, sticky='nsew', padx=20, pady=20)
plot_frame1 = ttk.Frame(tab1)
plot_frame1.grid(row=0, column=1, sticky='nsew', padx=20, pady=20)
plot_frame1.rowconfigure(0, weight=1)
plot_frame1.columnconfigure(0, weight=1)
self.stats_plot_frame = ttk.Frame(plot_frame1)
self.stats_plot_frame.grid(row=0, column=0, sticky='nsew')
self.rve_plot_frame1 = ttk.Frame(plot_frame1)
self.rve_plot_frame1.grid(row=1, column=0, sticky='nsew')
# define labels and entries
line_seq = np.linspace(0, 50, dtype=int)
line = iter(line_seq)
ttk.Label(main_frame1, text="Simulation box", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
add_label_and_entry(main_frame1, next(line), "Number of Voxels", self.nvox_var1)
add_label_and_entry(main_frame1, next(line), "Size of RVE (in micron)", self.size_var1)
add_label_and_entry(main_frame1, next(line), "Periodic", self.periodic_var1, entry_type="checkbox",
bold=False)
ttk.Label(main_frame1, text="Phases", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
#add_label_and_entry(main_frame1, next(line), "Number of phases", self.nphases)
add_label_and_entry(main_frame1, next(line), "Phase Name", self.matname_var1)
add_label_and_entry(main_frame1, next(line), "Phase Number", self.ialloy)
hl = next(line)
ttk.Label(main_frame1, text=" target", font=("Helvetica", 12, "italic")) \
.grid(row=hl, column=1, pady=(10, 0), sticky='w')
ttk.Label(main_frame1, text=" actual", font=("Helvetica", 12, "italic")) \
.grid(row=hl, column=2, pady=(10, 0), sticky='w')
hl = next(line)
add_label_and_entry(main_frame1, hl, "Volume fraction", self.volume_fraction)
add_label_and_entry(main_frame1, hl, label_text=None, entry_var=self.vf_act, col=2)
ttk.Label(main_frame1, text="Equivalent Diameter Parameters", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
hl = next(line)
add_label_and_entry(main_frame1, hl, "Sigma", self.eq_diameter_sig)
add_label_and_entry(main_frame1, hl, None, self.eqd_act_sig, col=2)
hl = next(line)
add_label_and_entry(main_frame1, hl, "Scale", self.eq_diameter_scale)
add_label_and_entry(main_frame1, hl, None, self.eqd_act_scale, col=2)
hl = next(line)
add_label_and_entry(main_frame1, hl, "Location", self.eq_diameter_loc)
add_label_and_entry(main_frame1, hl, None, self.eqd_act_loc, col=2)
add_label_and_entry(main_frame1, next(line), "Min", self.eq_diameter_min)
add_label_and_entry(main_frame1, next(line), "Max", self.eq_diameter_max)
ttk.Label(main_frame1, text="Aspect Ratio Parameters", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
hl = next(line)
add_label_and_entry(main_frame1, hl, "Sigma", self.aspect_ratio_sig)
add_label_and_entry(main_frame1, hl, None, self.ar_act_sig, col=2)
hl = next(line)
add_label_and_entry(main_frame1, hl, "Scale", self.aspect_ratio_scale)
add_label_and_entry(main_frame1, hl, None, self.ar_act_scale, col=2)
hl = next(line)
add_label_and_entry(main_frame1, hl, "Location", self.aspect_ratio_loc)
add_label_and_entry(main_frame1, hl, None, self.ar_act_loc, col=2)
add_label_and_entry(main_frame1, next(line), "Min", self.aspect_ratio_min)
add_label_and_entry(main_frame1, next(line), "Max", self.aspect_ratio_max)
ttk.Label(main_frame1, text="Tilt Angle Parameters", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
hl = next(line)
add_label_and_entry(main_frame1, hl, "Kappa", self.tilt_angle_kappa)
#add_label_and_entry(main_frame1, hl, None, self.ta_act_kappa, col=2)
hl = next(line)
add_label_and_entry(main_frame1, hl, "Location", self.tilt_angle_loc)
add_label_and_entry(main_frame1, next(line), "Min", self.tilt_angle_min)
add_label_and_entry(main_frame1, next(line), "Max", self.tilt_angle_max)
ttk.Label(main_frame1, text="Orientation Parameters", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
add_label_and_entry(main_frame1, next(line), "Texture", self.texture_var1, entry_type="combobox",
options=["random", "unimodal", "EBSD-ODF"])
add_label_and_entry(main_frame1, next(line), "Kernel Half Width (degree)", self.kernel_var1,
bold=False)
add_label_and_entry(main_frame1, next(line), "Euler Angles (degree)", self.euler_var1)
# create buttons
button_frame1 = ttk.Frame(main_frame1)
button_frame1.grid(row=next(line), column=0, columnspan=3, pady=10, sticky='ew')
button_import_ebsd = ttk.Button(button_frame1, text="Import EBSD", style='TButton',
command=self.import_ebsd)
button_import_ebsd.grid(row=0, column=0, padx=(10, 5), pady=5, sticky='ew')
button_import_stats = ttk.Button(button_frame1, text="Import Statistics", style='TButton',
command=self.import_stats)
button_import_stats.grid(row=0, column=1, padx=(10, 5), pady=5, sticky='ew')
write_stats_button = ttk.Button(button_frame1, text="Export Statistics", style='TButton',
command=self.write_stat_param)
write_stats_button.grid(row=0, column=2, padx=(10, 5), pady=5, sticky='ew')
button_statistics = ttk.Button(button_frame1, text="Plot Statistics", style='TButton',
command=self.create_and_plot_stats)
button_statistics.grid(row=1, column=0, padx=(10, 5), pady=5, sticky='ew')
button_create_rve = ttk.Button(button_frame1, text="Create RVE", style='TButton',
command=self.create_and_plot_rve)
button_create_rve.grid(row=1, column=1, padx=(10, 5), pady=5, sticky='ew')
button_create_ori = ttk.Button(button_frame1, text="Create Orientations", style='TButton',
command=self.create_orientation)
button_create_ori.grid(row=1, column=2, padx=(10, 5), pady=5, sticky='ew')
write_files_button = ttk.Button(button_frame1, text="Write Abaqus Input", style='TButton',
command=self.export_abq)
write_files_button.grid(row=2, column=0, padx=(10, 5), pady=5, sticky='ew')
button_exit1 = ttk.Button(button_frame1, text="Exit", style='TButton', command=self.close)
button_exit1.grid(row=2, column=2, padx=(10, 5), pady=5, sticky='ew')
[docs]
def close(self):
"""
Quit and destroy the particle_rve GUI main window
Notes
-----
This method quits the Tkinter application and destroys the main window
"""
self.app.quit()
self.app.destroy()
[docs]
def update_kernel_var(self, *args):
"""
Update the kernel variable based on the current texture selection
Parameters
----------
*args : tuple
Optional arguments passed by the tkinter trace callback, not used directly
Notes
-----
If `self.texture_var1` is 'unimodal', `self.kernel_var1` is set to "7.5"
Otherwise, `self.kernel_var1` is set to "-"
"""
self.kernel_var1.set("7.5" if self.texture_var1.get() == 'unimodal' else "-")
[docs]
def update_euler_var(self, *args):
"""
Update the Euler angles variable based on the current texture selection
Parameters
----------
*args : tuple
Optional arguments passed by the tkinter trace callback, not used directly
Notes
-----
If `self.texture_var1` is 'unimodal', `self.euler_var1` is set to "0.0, 45.0, 0.0"
Otherwise, `self.euler_var1` is set to "-"
"""
self.euler_var1.set("0.0, 45.0, 0.0" if self.texture_var1.get() == 'unimodal' else "-")
[docs]
def display_plot(self, fig, plot_type):
"""
Show a statistics graph or RVE plot on the GUI canvas
Parameters
----------
fig : matplotlib.figure.Figure
The figure object to be displayed
plot_type : str
Type of plot to display, either 'stats' for statistics graph or 'rve' for RVE plot
Notes
-----
Existing canvas for the specified plot type will be destroyed before displaying the new figure
The GUI window size is updated to fit the new plot
"""
self.app.update_idletasks()
width, height = self.app.winfo_reqwidth(), self.app.winfo_reqheight()
self.app.geometry(f"{width}x{height}")
if plot_type == "stats":
if self.stats_canvas1 is not None:
self.stats_canvas1.get_tk_widget().destroy()
self.stats_canvas1 = FigureCanvasTkAgg(fig, master=self.stats_plot_frame)
self.stats_canvas1.draw()
self.stats_canvas1.get_tk_widget().pack(fill=tk.BOTH, expand=True)
elif plot_type == "rve":
if self.rve_canvas1 is not None:
self.rve_canvas1.get_tk_widget().destroy()
self.rve_canvas1 = FigureCanvasTkAgg(fig, master=self.rve_plot_frame1)
self.rve_canvas1.draw()
self.rve_canvas1.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.app.update_idletasks()
width, height = self.app.winfo_reqwidth(), self.app.winfo_reqheight()
self.app.geometry(f"{width}x{height}")
[docs]
def import_ebsd(self):
"""
Import an EBSD map and extract microstructure parameters
Notes
-----
Opens a file dialog to select an EBSD map with extensions '.ctf' or '.ang'
Displays messages indicating progress and success or failure
Calls `extract_microstructure_params` and `reset_act` after successful import
"""
file_path = filedialog.askopenfilename(title="Select EBSD map", filetypes=[("EBSD maps", ".ctf .ang")])
if file_path:
self_closing_message("Reading EBSD map, please wait..")
try:
self.ebsd = EBSDmap(file_path, show_plot=True)
self.extract_microstructure_params()
self.reset_act()
self_closing_message("Data from EBSD map imported successfully.")
except:
self_closing_message("ERROR: Could not read EBSD map!")
[docs]
def write_stat_param(self):
"""
Save the current microstructure statistics to a file
Notes
-----
If `self.ms_stats` is None, a message is shown indicating no stats are available
Opens a file dialog to select the save location
Calls `write_stats` to write the statistics to the selected file
"""
if self.ms_stats is None:
self_closing_message("No stats created yet.")
else:
file_path = filedialog.asksaveasfilename()
if file_path:
self_closing_message(f"Saving stats file as {file_path}.")
write_stats(self.ms_stats, file_path)
[docs]
def reset_act(self):
"""
Reset all activity-related variables to zero
Notes
-----
Sets `vf_act`, `eqd_act_sig`, `eqd_act_scale`, `ar_act_sig`, and `ar_act_scale` to 0.0
"""
self.vf_act.set(0.0)
self.eqd_act_sig.set(0.0)
self.eqd_act_scale.set(0.0)
self.ar_act_sig.set(0.0)
self.ar_act_scale.set(0.0)
[docs]
def readout_stats(self):
"""
Collect and return the current microstructure and RVE statistics
Returns
-------
dict
A dictionary containing:
- 'Grain type': Type of grain
- 'Equivalent diameter': Distribution parameters (sig, scale, loc, cutoff_min, cutoff_max)
- 'Aspect ratio': Distribution parameters (sig, scale, loc, cutoff_min, cutoff_max)
- 'Tilt angle': Orientation parameters (kappa, loc, cutoff_min, cutoff_max)
- 'RVE': RVE size and voxel counts
- 'Simulation': Simulation settings including periodicity and output units
- 'Phase': Material phase information including name, number, and volume fraction
"""
matname = self.matname_var1.get()
nvox = int(self.nvox_var1.get())
size = int(self.size_var1.get())
periodic = self.periodic_var1.get()
sd = {
'Grain type': 'Elongated',
'Equivalent diameter': {
'sig': self.eq_diameter_sig.get(), 'scale': self.eq_diameter_scale.get(),
'loc': self.eq_diameter_loc.get(), 'cutoff_min': self.eq_diameter_min.get(),
'cutoff_max': self.eq_diameter_max.get()
},
'Aspect ratio': {
'sig': self.aspect_ratio_sig.get(), 'scale': self.aspect_ratio_scale.get(),
'loc': self.aspect_ratio_loc.get(), 'cutoff_min': self.aspect_ratio_min.get(),
'cutoff_max': self.aspect_ratio_max.get()
},
"Tilt angle": {
"kappa": self.tilt_angle_kappa.get(), "loc": self.tilt_angle_loc.get(),
"cutoff_min": self.tilt_angle_min.get(), "cutoff_max": self.tilt_angle_max.get()
},
'RVE': {'sideX': size, 'sideY': size, 'sideZ': size, 'Nx': nvox, 'Ny': nvox, 'Nz': nvox,
'ialloy': int(self.ialloy.get())},
'Simulation': {'periodicity': periodic, 'output_units': 'mm'},
'Phase': {'Name': matname, 'Number': 0, 'Volume fraction': self.volume_fraction.get()}
}
return sd
[docs]
def import_stats(self):
"""
Import statistical data from a JSON file and update GUI variables
Notes
-----
Opens a file dialog to select a statistics file with '.json' extension
Updates GUI variables for material name, RVE size, voxel counts, periodicity, volume fraction,
equivalent diameter, aspect ratio, and tilt angle based on imported data
Calls `reset_act` and clears `ebsd` after successful import
Displays a message indicating success or failure
"""
file_path = filedialog.askopenfilename(title="Select statistics file", filetypes=[("Stats files", ".json")])
if file_path:
try:
ms_stats = import_stats(file_path)
self.matname_var1.set(ms_stats['Phase']['Name'])
self.nvox_var1.set(ms_stats['RVE']['Nx'])
self.size_var1.set(ms_stats['RVE']['sideX'])
self.periodic_var1.set(ms_stats['Simulation']['periodicity'])
self.volume_fraction.set(ms_stats['Phase']['Volume fraction'])
self.eq_diameter_sig.set(ms_stats['Equivalent diameter']['sig'])
self.eq_diameter_scale.set(ms_stats['Equivalent diameter']['scale'])
if 'loc' in ms_stats['Equivalent diameter'].keys():
self.eq_diameter_loc.set(ms_stats['Equivalent diameter']['loc'])
else:
self.eq_diameter_loc.set(0.0)
self.eq_diameter_min.set(ms_stats['Equivalent diameter']['cutoff_min'])
self.eq_diameter_max.set(ms_stats['Equivalent diameter']['cutoff_max'])
self.aspect_ratio_sig.set(ms_stats['Aspect ratio']['sig'])
self.aspect_ratio_scale.set(ms_stats['Aspect ratio']['scale'])
if 'loc' in ms_stats['Equivalent diameter'].keys():
self.aspect_ratio_loc.set(ms_stats['Aspect ratio']['loc'])
else:
self.aspect_ratio_loc.set(0.0)
self.aspect_ratio_min.set(ms_stats['Aspect ratio']['cutoff_min'])
self.aspect_ratio_max.set(ms_stats['Aspect ratio']['cutoff_max'])
self.tilt_angle_kappa.set(ms_stats['Tilt angle']['kappa'])
self.tilt_angle_loc.set(ms_stats['Tilt angle']['loc'])
self.tilt_angle_min.set(ms_stats['Tilt angle']['cutoff_min'])
self.tilt_angle_max.set(ms_stats['Tilt angle']['cutoff_max'])
self.ms_stats = ms_stats
self.reset_act()
self.ebsd = None
self_closing_message("Statistical data imported successfully.")
except Exception as e:
self_closing_message(f"ERROR: Statistical parameters could not be imported! {e}")
[docs]
def extract_microstructure_params(self):
"""
Extract microstructure parameters from the EBSD data and update GUI variables
Notes
-----
If `self.ebsd` is None, the method does nothing
Extracts grain size, aspect ratio, and orientation parameters from the first EBSD dataset
Updates GUI variables for material name, volume fraction, equivalent diameter, aspect ratio,
tilt angle, and texture type
Computed min and max cutoff values are based on statistical scaling of the extracted parameters
"""
if self.ebsd is None:
return
ms_data = self.ebsd.ms_data[0]
gs_param = ms_data['gs_param']
ar_param = ms_data['ar_param']
om_param = ms_data['om_param']
self.matname_var1.set(ms_data['name'])
self.volume_fraction.set(1.0)
self.eq_diameter_sig.set(gs_param[0].round(3))
self.eq_diameter_scale.set(gs_param[2].round(3))
self.eq_diameter_loc.set(gs_param[1].round(3))
co_min = gs_param[2] * 0.5 + gs_param[1]
co_max = gs_param[2] * 1.1 + gs_param[1]
self.eq_diameter_min.set(co_min.round(3))
self.eq_diameter_max.set(co_max.round(3))
self.aspect_ratio_sig.set(ar_param[0].round(3))
self.aspect_ratio_scale.set(ar_param[2].round(3))
self.aspect_ratio_loc.set(ar_param[1].round(3))
ar_max = ar_param[2] * 1.4 + ar_param[1]
self.aspect_ratio_min.set(0.95)
self.aspect_ratio_max.set(ar_max.round(3))
self.tilt_angle_kappa.set(om_param[0].round(3))
self.tilt_angle_loc.set(om_param[1].round(3))
self.tilt_angle_min.set(0.0)
self.tilt_angle_max.set(3.1416)
self.texture_var1.set('EBSD-ODF')
[docs]
def create_and_plot_stats(self):
"""
Plot statistics of the current microstructure descriptors and initialize a new Microstructure object
Notes
-----
Will erase the global microstructure object `self.ms` if it exists
Reads current statistics using `readout_stats`
Initializes `self.ms` with the selected material and texture
Plots statistics using `plot_stats_init` and displays each figure on the GUI
Resets activity-related variables after plotting
"""
self.ms_stats = self.readout_stats()
texture = self.texture_var1.get()
matname = self.matname_var1.get()
self.ms = Microstructure(descriptor=self.ms_stats, name=f"{matname}_{texture}_texture")
self.ms.init_RVE()
if self.ebsd is None:
gs_data = None
ar_data = None
else:
gs_data = self.ebsd.ms_data[0]['gs_data']
ar_data = self.ebsd.ms_data[0]['ar_data']
flist, descs = self.ms.plot_stats_init(gs_data=gs_data, ar_data=ar_data, silent=True)
for fig in flist:
self.display_plot(fig, plot_type="stats")
self.reset_act()
[docs]
def create_and_plot_rve(self):
"""
Create the RVE from current statistics and plot it along with associated statistics
Notes
-----
Will overwrite existing `ms_stats` and `ms` objects
Displays a progress message and measures processing time
Initializes `self.ms` with the selected material and voxelizes the RVE
Plots the voxelized RVE using `plot_voxels` and displays it on the GUI
Updates activity-related variables based on voxelized statistics
Also plots statistics figures returned from `plot_stats_init`
"""
self_closing_message("The process has been started, please wait...")
start_time = time.time()
matname = self.matname_var1.get()
self.ms_stats = self.readout_stats()
self.ms = Microstructure(descriptor=self.ms_stats, name=f"{matname}")
self.ms.init_RVE()
self.ms.pack()
self.ms.voxelize()
fig = self.ms.plot_voxels(silent=True, sliced=False)
self.display_plot(fig, plot_type="rve")
flist, descs = self.ms.plot_stats_init(silent=True, get_res=True, return_descriptors=True)
self.vf_act.set(self.ms.vf_vox[0])
self.eqd_act_sig.set(descs[0]['eqd']['std'])
self.eqd_act_scale.set(descs[0]['eqd']['mean'])
self.ar_act_sig.set(descs[0]['ar']['std'])
self.ar_act_scale.set(descs[0]['ar']['mean'])
end_time = time.time()
duration = end_time - start_time
self_closing_message(f"Process completed in {duration:.2f} seconds")
for fig in flist:
self.display_plot(fig, plot_type="stats")
[docs]
def create_orientation(self):
"""
Generate grain orientations based on the selected texture and assign them to the RVE
Notes
-----
Supports 'unimodal' textures with user-specified kernel and Euler angles, or 'EBSD-ODF' textures
Automatically generates the RVE if `ms_stats` or `ms` objects are not initialized
Calls `generate_orientations` to assign orientations to the microstructure
Writes voxel data to a JSON file and plots the voxelized RVE with orientations
Displays messages indicating progress and processing time
"""
self_closing_message("The process has been started, please wait...")
texture = self.texture_var1.get()
matname = self.matname_var1.get()
if texture == 'unimodal':
omega = float(self.kernel_var1.get())
ang_string = self.euler_var1.get()
ang = [float(angle.strip()) for angle in ang_string.split(',')]
else:
omega = None
ang = None
if texture == 'EBSD-ODF':
texture = self.ebsd
if self.ms_stats is None or self.ms is None or self.ms.mesh is None:
self_closing_message("Generating RVE and assigning orientations to grains.")
self.create_and_plot_rve()
start_time = time.time()
self.ms.generate_orientations(texture, ang=ang, omega=omega)
self.ms.write_voxels(file=f'{matname}_voxels.json', script_name=__file__, mesh=False,
system=False)
fig = self.ms.plot_voxels(silent=True, sliced=False, ori=True)
self.display_plot(fig, plot_type="rve")
end_time = time.time()
duration = end_time - start_time
self_closing_message(f"Process completed in {duration:.2f} seconds, the Voxel file has been saved. ")
[docs]
def export_abq(self):
"""
Export the RVE mesh to an Abaqus input file
Notes
-----
If `ms_stats`, `ms`, or `ms.mesh` is not initialized, the RVE is first generated without orientations
Checks if the material is dual-phase based on volume fraction
Calls `write_abq` to export the mesh in millimeter units
Displays a progress message if RVE generation is required
"""
if self.ms_stats is None or self.ms is None or self.ms.mesh is None:
self_closing_message("Generating and exporting RVE without orientations.")
self.create_and_plot_rve()
dp = self.volume_fraction.get() < 1.0
self.ms.write_abq('v', dual_phase=dp, units='mm')
[docs]
class cuboid_rve(object):
"""
Class for managing RVEs with cuboid grains and associated GUI controls
Parameters
----------
app: tk.Tk or tk.Frame
Reference to the main Tkinter application instance
notebook: ttk.Notebook
Reference to the parent notebook widget for GUI tab placement
Attributes
----------
app: tk.Tk or tk.Frame
Reference to the main Tkinter application instance
canvas: tk.Canvas
Canvas for displaying the cuboid RVE
ms: object
Placeholder for the microstructure object
texture_var2: tk.StringVar
Selected texture type (default "random")
matname_var2: tk.StringVar
Material name (default "Simulanium")
ialloy: tk.IntVar
Number of alloys (default 2)
ngr_var: tk.StringVar
Number of grains in each direction (default "5, 5, 5")
nv_gr_var: tk.StringVar
Number of voxels per grain in each direction (default "3, 3, 3")
size_var2: tk.StringVar
RVE size in each direction (default "45, 45, 45")
kernel_var2: tk.StringVar
Kernel parameter for unimodal textures (default "-" or "7.5")
euler_var2: tk.StringVar
Euler angles for unimodal textures (default "-" or "0.0, 45.0, 0.0")
Notes
-----
The class handles creation, visualization, and parameter management for cuboid RVEs.
GUI variable traces are used to automatically update dependent parameters such as
kernel and Euler angle values based on the selected texture type.
"""
def __init__(self, app, notebook):
# define standard parameters
self.app = app
self.ms = None
self.canvas = None
self.texture_var2 = tk.StringVar(value="random")
self.matname_var2 = tk.StringVar(value="Simulanium")
self.ialloy = tk.IntVar(value=2)
self.ngr_var = tk.StringVar(value="5, 5, 5")
self.nv_gr_var = tk.StringVar(value="3, 3, 3")
self.size_var2 = tk.StringVar(value="45, 45, 45")
if self.texture_var2.get() == 'random':
self.kernel_var2 = tk.StringVar(value="-")
self.euler_var2 = tk.StringVar(value="-")
else:
self.kernel_var2 = tk.StringVar(value="7.5")
self.euler_var2 = tk.StringVar(value="0.0, 45.0, 0.0")
self.texture_var2.trace('w', self.update_kernel_var)
self.texture_var2.trace('w', self.update_euler_var)
# plot frames
tab2 = ttk.Frame(notebook)
notebook.add(tab2, text="Cuboid Grains")
main_frame2 = ttk.Frame(tab2)
main_frame2.grid(row=0, column=0, sticky='nsew', padx=20, pady=20)
plot_frame2 = ttk.Frame(tab2)
plot_frame2.grid(row=0, column=1, sticky='nsew', padx=20, pady=20)
plot_frame2.rowconfigure(0, weight=1)
plot_frame2.columnconfigure(0, weight=1)
self.rve_plot_frame2 = ttk.Frame(plot_frame2)
self.rve_plot_frame2.grid(row=0, column=0, sticky='nsew')
# define labels and entries
line_seq = np.linspace(0, 50, dtype=int)
line = iter(line_seq)
ttk.Label(main_frame2, text="General Parameters", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
add_label_and_entry(main_frame2, next(line), "Material Name", self.matname_var2)
add_label_and_entry(main_frame2, next(line), "Material Number", self.ialloy)
add_label_and_entry(main_frame2, next(line), "Number of Voxels", self.nv_gr_var)
add_label_and_entry(main_frame2, next(line), "Number of Grains", self.ngr_var)
add_label_and_entry(main_frame2, next(line), "Size of RVE (in micron)", self.size_var2)
ttk.Label(main_frame2, text="Orientation Parameters", font=("Helvetica", 12, "bold")) \
.grid(row=next(line), column=0, columnspan=2, pady=(10, 0), sticky='w')
add_label_and_entry(main_frame2, next(line), "Texture", self.texture_var2, entry_type="combobox",
options=["random", "unimodal"])
add_label_and_entry(main_frame2, next(line), "Kernel Half Width (degree)", self.kernel_var2,
bold=False)
add_label_and_entry(main_frame2, next(line), "Euler Angles (degree)", self.euler_var2)
# add buttons
button_frame2 = ttk.Frame(main_frame2)
button_frame2.grid(row=next(line), column=0, columnspan=2, pady=10, sticky='ew')
run_simulation_button = ttk.Button(button_frame2, text="Create RVE", style='TButton',
command=self.create_cubes_and_plot)
run_simulation_button.grid(row=0, column=0, padx=10, pady=5, sticky='ew')
create_orientation_button = ttk.Button(button_frame2, text="Create Orientations", style='TButton',
command=self.create_orientation)
create_orientation_button.grid(row=0, column=1, padx=10, pady=5, sticky='ew')
write_files_button = ttk.Button(button_frame2, text="Write Abaqus Input", style='TButton',
command=self.export_abq)
write_files_button.grid(row=1, column=0, padx=10, pady=5, sticky='ew')
button_exit2 = ttk.Button(button_frame2, text="Exit", style='TButton', command=self.close)
button_exit2.grid(row=1, column=1, padx=10, pady=5, sticky='ew')
[docs]
def close(self):
"""
Quit and destroy the cuboid_rve GUI main window
Notes
-----
This method terminates the Tkinter application and closes the main window
"""
self.app.quit()
self.app.destroy()
[docs]
def update_kernel_var(self, *args):
"""
Update the kernel parameter variable based on the current texture selection
Parameters
----------
*args : tuple
Optional arguments passed by the tkinter trace callback, not used directly
Notes
-----
Sets `kernel_var2` to "7.5" if `texture_var2` is 'unimodal', otherwise sets it to "-"
"""
self.kernel_var2.set("7.5" if self.texture_var2.get() == 'unimodal' else "-")
[docs]
def update_euler_var(self, *args):
"""
Update the Euler angles variable based on the current texture selection
Parameters
----------
*args : tuple
Optional arguments passed by the tkinter trace callback, not used directly
Notes
-----
Sets `euler_var2` to "0.0, 45.0, 0.0" if `texture_var2` is 'unimodal', otherwise sets it to "-"
"""
self.euler_var2.set("0.0, 45.0, 0.0" if self.texture_var2.get() == 'unimodal' else "-")
[docs]
def display_cuboid(self, fig):
"""
Display the cuboid RVE figure on the GUI canvas
Parameters
----------
fig : matplotlib.figure.Figure
The figure object representing the cuboid RVE to display
Notes
-----
Destroys any existing canvas before displaying the new figure
Updates the Tkinter window geometry to fit the figure
"""
self.app.update_idletasks()
width, height = self.app.winfo_reqwidth(), self.app.winfo_reqheight()
self.app.geometry(f"{width}x{height}")
if self.canvas is not None:
self.canvas.get_tk_widget().destroy()
self.canvas = FigureCanvasTkAgg(fig, master=self.rve_plot_frame2)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.app.update_idletasks()
width, height = self.app.winfo_reqwidth(), self.app.winfo_reqheight()
self.app.geometry(f"{width}x{height}")
[docs]
def create_cubes_and_plot(self):
"""
Create a microstructure object with cuboid grains and plot the RVE
Notes
-----
Parses GUI parameters for number of grains, voxels per grain, and RVE size
Initializes a Microstructure object and sets up the voxel mesh
Assigns each cuboid grain an index and populates grain dictionaries
Calls `plot_voxels` to generate the RVE figure and displays it on the GUI
Returns nothing; updates the GUI canvas with the plotted RVE
"""
matname = self.matname_var2.get()
ngr = parse_entry(self.ngr_var.get())
nv_gr = parse_entry(self.nv_gr_var.get())
size = parse_entry(self.size_var2.get())
dim = (ngr[0] * nv_gr[0], ngr[1] * nv_gr[1], ngr[2] * nv_gr[2])
stats_dict = {
'RVE': {'sideX': size[0], 'sideY': size[1], 'sideZ': size[2],
'Nx': dim[0], 'Ny': dim[1], 'Nz': dim[2],
'ialloy': int(self.ialloy.get())},
'Simulation': {'periodicity': False, 'output_units': 'um'},
'Phase': {'Name': matname, 'Volume fraction': 1.0}
}
self.ms = Microstructure('from_voxels')
self.ms.name = matname
self.ms.Ngr = np.prod(ngr)
self.ms.nphases = 1
self.ms.descriptor = [stats_dict]
self.ms.ngrains = [self.ms.Ngr]
self.ms.rve = RVE_creator(self.ms.descriptor, from_voxels=True)
self.ms.simbox = Simulation_Box(size)
self.ms.mesh = mesh_creator(dim)
self.ms.mesh.create_voxels(self.ms.simbox)
grains = np.zeros(dim, dtype=int)
grain_dict = {}
grain_phase_dict = {}
for ih in range(ngr[0]):
for ik in range(ngr[1]):
for il in range(ngr[2]):
igr = il + ik * ngr[1] + ih * ngr[0] * ngr[1] + 1
grain_dict[igr] = []
grain_phase_dict[igr] = 0
ind0 = np.arange(nv_gr[0], dtype=int) + ih * nv_gr[0]
ind1 = np.arange(nv_gr[1], dtype=int) + ik * nv_gr[1]
ind2 = np.arange(nv_gr[2], dtype=int) + il * nv_gr[2]
ind_list = itertools.product(*[ind0, ind1, ind2])
for ind in ind_list:
nv = np.ravel_multi_index(ind, dim, order='F')
grain_dict[igr].append(nv + 1)
grains[ind0[0]:ind0[-1] + 1, ind1[0]:ind1[-1] + 1, ind2[0]:ind2[-1] + 1] = igr
self.ms.mesh.grains = grains
self.ms.mesh.grain_dict = grain_dict
self.ms.mesh.phases = np.zeros(dim, dtype=int)
self.ms.mesh.grain_phase_dict = grain_phase_dict
self.ms.mesh.ngrains_phase = self.ms.ngrains
fig = self.ms.plot_voxels(sliced=False, silent=True)
self.display_cuboid(fig)
return
[docs]
def create_orientation(self):
"""
Create grain orientations for the cuboid RVE based on texture descriptors
Notes
-----
Supports 'unimodal' textures with user-specified kernel and Euler angles
Automatically generates the cuboid RVE if `ms` or `ms.mesh` is not initialized
Calls `generate_orientations` to assign orientations to the microstructure
Writes voxel data to a JSON file and plots the voxelized RVE with orientations
Displays messages indicating progress and processing time
"""
self_closing_message("The process has been started, please wait...")
texture = self.texture_var2.get()
matname = self.matname_var2.get()
if texture == 'unimodal':
omega = float(self.kernel_var2.get())
ang_string = self.euler_var2.get()
ang = [float(angle.strip()) for angle in ang_string.split(',')]
else:
omega = None
ang = None
start_time = time.time()
if self.ms is None or self.ms.mesh is None:
self_closing_message("Generating RVE with cuboid grains to assign orientation to.")
self.create_cubes_and_plot()
self.ms.generate_orientations(texture, ang=ang, omega=omega)
self.ms.write_voxels(file=f'{matname}_voxels.json', script_name=__file__, mesh=False, system=False)
fig = self.ms.plot_voxels(silent=True, sliced=False, ori=True)
self.display_cuboid(fig)
end_time = time.time()
duration = end_time - start_time
self_closing_message(f"Process completed in {duration:.2f} seconds, the Voxel file has been saved.")
[docs]
def export_abq(self):
"""
Export the cuboid RVE mesh to an Abaqus input file
Notes
-----
Automatically generates the cuboid RVE without orientations if `ms` is not initialized
Calls `write_abq` to export the mesh in millimeter units
Displays a progress message if RVE generation is required
"""
if self.ms is None:
self_closing_message("Generating and exporting RVE with cuboid grains w/o orientations.")
self.create_cubes_and_plot()
self.ms.write_abq('v', units='mm')