diff --git a/docs/source/environments_template_linux.yaml b/docs/source/environments_template_linux.yaml index 3d7e1d30..d072a1b2 100644 --- a/docs/source/environments_template_linux.yaml +++ b/docs/source/environments_template_linux.yaml @@ -105,3 +105,21 @@ - command: python "<>" <> num_cores: 1 parallel_mode: null + +- name: cipher_env + executables: + - label: cipher + instances: + - command: /full/path/to/cipher/bin/cipher.exe --config cipher_input.yaml + num_cores: 1 + parallel_mode: null + +- name: cipher_processing_env + setup: | + source /full/path/to/.venv/bin/activate + executables: + - label: python_script + instances: + - command: python "<>" <> + num_cores: 1 + parallel_mode: null diff --git a/matflow/data/demo_data/clustering_quaternions.zip b/matflow/data/demo_data/clustering_quaternions.zip new file mode 100644 index 00000000..7364957e Binary files /dev/null and b/matflow/data/demo_data/clustering_quaternions.zip differ diff --git a/matflow/data/demo_data_manifest/demo_data_manifest.json b/matflow/data/demo_data_manifest/demo_data_manifest.json index ab8dd93b..a44bfbb8 100644 --- a/matflow/data/demo_data_manifest/demo_data_manifest.json +++ b/matflow/data/demo_data_manifest/demo_data_manifest.json @@ -31,5 +31,8 @@ }, "load_cases.npz": { "in_zip": "load_cases_npz.zip" + }, + "clustering_quaternions": { + "in_zip": "clustering_quaternions.zip" } } diff --git a/matflow/data/scripts/cipher/generate_RVE_from_statistics_dual_phase_pipeline_writer.py b/matflow/data/scripts/cipher/generate_RVE_from_statistics_dual_phase_pipeline_writer.py new file mode 100644 index 00000000..0637030c --- /dev/null +++ b/matflow/data/scripts/cipher/generate_RVE_from_statistics_dual_phase_pipeline_writer.py @@ -0,0 +1,1202 @@ +import numpy as np +import warnings +import copy +import json + +from pathlib import Path +from damask_parse.utils import validate_orientations +from damask_parse.quats import axang2quat, multiply_quaternions + + +def generate_RVE_from_statistics_dual_phase_pipeline_writer( + path, + grid_size, + resolution, + size, + origin, + periodic, + phase_statistics, + precipitates, +): + return generate_RVE_from_statistics_pipeline_writer( + path, + grid_size, + resolution, + size, + origin, + periodic, + phase_statistics, + precipitates, + orientations=None, + ) + + +def generate_RVE_from_statistics_pipeline_writer( + path, + grid_size, + resolution, + size, + origin, + periodic, + phase_statistics, + precipitates, + orientations, +): + + # TODO: fix BoxDimensions in filter 01? + # TODO: unsure how to set precipitate RDF BoxRes? + + if resolution is None: + resolution = [i / j for i, j in zip(size, grid_size)] + + if origin is None: + origin = [0, 0, 0] + + REQUIRED_PHASE_BASE_KEYS = { + "type", + "name", + "crystal_structure", + "volume_fraction", + } + REQUIRED_PHASE_NON_MATRIX_KEYS = REQUIRED_PHASE_BASE_KEYS | { + "size_distribution", + } + REQUIRED_PHASE_KEYS = { + "matrix": REQUIRED_PHASE_BASE_KEYS, + "primary": REQUIRED_PHASE_NON_MATRIX_KEYS, + "precipitate": REQUIRED_PHASE_NON_MATRIX_KEYS + | { + "radial_distribution_function", + "number_fraction_on_boundary", + }, + } + ALLOWED_PHASE_NON_MATRIX_KEYS = REQUIRED_PHASE_NON_MATRIX_KEYS | { + "preset_statistics_model", + "ODF", + "axis_ODF", + } + ALLOWED_PHASE_KEYS = { + "matrix": REQUIRED_PHASE_KEYS["matrix"], + "primary": REQUIRED_PHASE_KEYS["primary"] | ALLOWED_PHASE_NON_MATRIX_KEYS, + "precipitate": REQUIRED_PHASE_KEYS["precipitate"] | ALLOWED_PHASE_NON_MATRIX_KEYS, + } + ALLOWED_PHASE_TYPES = set(REQUIRED_PHASE_KEYS.keys()) + REQUIRED_PHASE_SIZE_DIST_KEYS = { + "ESD_log_stddev", + } + ALLOWED_PHASE_SIZE_DIST_KEYS = REQUIRED_PHASE_SIZE_DIST_KEYS | { + "ESD_log_mean", + "ESD_mean", + "ESD_log_stddev_min_cut_off", + "ESD_log_stddev_max_cut_off", + "bin_step_size", + "num_bins", + "omega3", + "b/a", + "c/a", + "neighbours", + } + ALLOWED_PRECIP_RDF_KEYS = { + "min_distance", + "max_distance", + "num_bins", + "box_size", + } + ALLOWED_CRYSTAL_STRUCTURES = { # values are crystal symmetry index: + "hexagonal": 0, + "cubic": 1, + } + SIGMA_MIN_DEFAULT = 5 + SIGMA_MAX_DEFAULT = 5 + # Distributions defined for each size distribution bin: + DISTRIBUTIONS_MAP = { + "omega3": { + "type": "beta", + "default_keys": { + "alpha": 10.0, + "beta": 1.5, + }, + "label": "FeatureSize Vs Omega3 Distributions", + }, + "b/a": { + "type": "beta", + "default_keys": { + "alpha": 10.0, + "beta": 1.5, + }, + "label": "FeatureSize Vs B Over A Distributions", + }, + "c/a": { + "type": "beta", + "default_keys": { + "alpha": 10.0, + "beta": 1.5, + }, + "label": "FeatureSize Vs C Over A Distributions", + }, + "neighbours": { + "type": "lognormal", + "default_keys": { + "average": 2.0, + "stddev": 0.5, + }, + "label": "FeatureSize Vs Neighbors Distributions", + }, + } + DISTRIBUTIONS_TYPE_LABELS = { + "lognormal": "Log Normal Distribution", + "beta": "Beta Distribution", + } + DISTRIBUTIONS_KEY_LABELS = { + "alpha": "Alpha", + "beta": "Beta", + "average": "Average", + "stddev": "Standard Deviation", + } + PRESETS_TYPE_KEYS = { + "primary_equiaxed": { + "type", + }, + "primary_rolled": { + "type", + "A_axis_length", + "B_axis_length", + "C_axis_length", + }, + "precipitate_equiaxed": { + "type", + }, + "precipitate_rolled": { + "type", + "A_axis_length", + "B_axis_length", + "C_axis_length", + }, + } + REQUIRED_PHASE_AXIS_ODF_KEYS = {"orientations"} + ALLOWED_PHASE_AXIS_ODF_KEYS = REQUIRED_PHASE_AXIS_ODF_KEYS | {"weights", "sigmas"} + REQUIRED_PHASE_ODF_KEYS = set() # presets can be specified instead of orientations + ALLOWED_PHASE_ODF_KEYS = ALLOWED_PHASE_AXIS_ODF_KEYS | {"presets"} + DEFAULT_ODF_WEIGHT = 500_000 + DEFAULT_ODF_SIGMA = 2 + + DEFAULT_AXIS_ODF_WEIGHT = DEFAULT_ODF_WEIGHT + DEFAULT_AXIS_ODF_SIGMA = DEFAULT_ODF_SIGMA + + ODF_CUBIC_PRESETS = { + "cube": (0, 0, 0), + "goss": (0, 45, 0), + "brass": (35, 45, 0), + "copper": (90, 35, 45), + "s": (59, 37, 63), + "s1": (55, 30, 65), + "s2": (45, 35, 65), + "rc(rd1)": (0, 20, 0), + "rc(rd2)": (0, 35, 0), + "rc(nd1)": (20, 0, 0), + "rc(nd2)": (35, 0, 0), + "p": (70, 45, 0), + "q": (55, 20, 0), + "r": (55, 75, 25), + } + + vol_frac_sum = 0.0 + stats_JSON = [] + for phase_idx, phase_stats in enumerate(phase_statistics): + + # Validation: + + err_msg = f"Problem with `phase_statistics` index {phase_idx}: " + + phase_type = phase_stats["type"].lower() + if phase_type not in ALLOWED_PHASE_TYPES: + raise ValueError(err_msg + f'`type` "{phase_stats["type"]}" not allowed.') + + given_keys = set(phase_stats.keys()) + miss_keys = REQUIRED_PHASE_KEYS[phase_type] - given_keys + bad_keys = given_keys - ALLOWED_PHASE_KEYS[phase_type] + if miss_keys: + msg = err_msg + f'Missing keys: {", ".join([f"{i}" for i in miss_keys])}' + raise ValueError(msg) + if bad_keys: + msg = err_msg + f'Unknown keys: {", ".join([f"{i}" for i in bad_keys])}' + raise ValueError(msg) + + size_dist = phase_stats["size_distribution"] + given_size_keys = set(size_dist.keys()) + miss_size_keys = REQUIRED_PHASE_SIZE_DIST_KEYS - given_size_keys + bad_size_keys = given_size_keys - ALLOWED_PHASE_SIZE_DIST_KEYS + if miss_size_keys: + raise ValueError( + err_msg + f"Missing `size_distribution` keys: " + f'{", ".join([f"{i}" for i in miss_size_keys])}' + ) + if bad_size_keys: + raise ValueError( + err_msg + f"Unknown `size_distribution` keys: " + f'{", ".join([f"{i}" for i in bad_size_keys])}' + ) + num_bins = size_dist.get("num_bins") + bin_step_size = size_dist.get("bin_step_size") + if sum([i is None for i in (num_bins, bin_step_size)]) != 1: + raise ValueError( + err_msg + f'Specify exactly one of `num_bins` (given as "{num_bins}") ' + f'and `bin_step_size` (given as "{bin_step_size}").' + ) + + if phase_type == "precipitate": + RDF = phase_stats["radial_distribution_function"] + given_RDF_keys = set(RDF.keys()) + miss_RDF_keys = ALLOWED_PRECIP_RDF_KEYS - given_RDF_keys + bad_RDF_keys = given_RDF_keys - ALLOWED_PRECIP_RDF_KEYS + + if miss_RDF_keys: + raise ValueError( + err_msg + f"Missing `radial_distribution_function` keys: " + f'{", ".join([f"{i}" for i in miss_RDF_keys])}' + ) + if bad_RDF_keys: + raise ValueError( + err_msg + f"Unknown `radial_distribution_function` keys: " + f'{", ".join([f"{i}" for i in bad_RDF_keys])}' + ) + + phase_i_CS = phase_stats["crystal_structure"] + if phase_i_CS not in ALLOWED_CRYSTAL_STRUCTURES: + msg = err_msg + ( + f'`crystal_structure` value "{phase_i_CS}" unknown. Must be one of: ' + f'{", ".join([f"{i}" for i in ALLOWED_CRYSTAL_STRUCTURES])}' + ) + raise ValueError(msg) + + preset = phase_stats.get("preset_statistics_model") + if preset: + given_preset_keys = set(preset.keys()) + preset_type = preset.get("type") + if not preset_type: + raise ValueError( + err_msg + f"Missing `preset_statistics_model` key: " f'"type".' + ) + miss_preset_keys = PRESETS_TYPE_KEYS[preset_type] - given_preset_keys + bad_preset_keys = given_preset_keys - PRESETS_TYPE_KEYS[preset_type] + + if miss_preset_keys: + raise ValueError( + err_msg + f"Missing `preset_statistics_model` keys: " + f'{", ".join([f"{i}" for i in miss_preset_keys])}' + ) + if bad_preset_keys: + raise ValueError( + err_msg + f"Unknown `preset_statistics_model` keys: " + f'{", ".join([f"{i}" for i in bad_preset_keys])}' + ) + + if "rolled" in preset_type: + # check: A >= B >= C + if not ( + preset["A_axis_length"] + >= preset["B_axis_length"] + >= preset["C_axis_length"] + ): + raise ValueError( + err_msg + f"The following condition must be true: " + f"`A_axis_length >= B_axis_length >= C_axis_length`, but these " + f'are, respectively: {preset["A_axis_length"]}, ' + f'{preset["B_axis_length"]}, {preset["C_axis_length"]}.' + ) + + # Sum given volume fractions: + vol_frac_sum += phase_stats["volume_fraction"] + + log_mean = size_dist.get("ESD_log_mean") + mean = size_dist.get("ESD_mean") + if sum([i is None for i in (log_mean, mean)]) != 1: + raise ValueError( + err_msg + f"Specify exactly one of `ESD_log_mean` (given as " + f'"{log_mean}") and `ESD_mean` (given as "{mean}").' + ) + + sigma = size_dist["ESD_log_stddev"] + if log_mean is None: + # expected value (mean) of the variable's natural log + log_mean = np.log(mean) - (sigma**2 / 2) + + sigma_min = size_dist.get("ESD_log_stddev_min_cut_off", SIGMA_MIN_DEFAULT) + sigma_max = size_dist.get("ESD_log_stddev_max_cut_off", SIGMA_MAX_DEFAULT) + min_feat_ESD = np.exp(log_mean - (sigma_min * sigma)) + max_feat_ESD = np.exp(log_mean + (sigma_max * sigma)) + + if bin_step_size is not None: + bins = np.arange(min_feat_ESD, max_feat_ESD, bin_step_size) + num_bins = len(bins) + else: + bin_step_size = (max_feat_ESD - min_feat_ESD) / num_bins + bins = np.linspace(min_feat_ESD, max_feat_ESD, num_bins, endpoint=False) + + feat_diam_info = [bin_step_size, max_feat_ESD, min_feat_ESD] + + # Validate other distributions after sorting out number of bins: + all_dists = {} + for dist_key, dist_info in DISTRIBUTIONS_MAP.items(): + + dist = size_dist.get(dist_key) + if not dist: + if not preset: + dist = copy.deepcopy(dist_info["default_keys"]) + else: + continue + else: + if dist_key == "neighbours" and phase_type == "precipitate": + warnings.warn( + err_msg + f"`neighbours` distribution not allowed with " + f'"precipitate" phase type; ignoring.' + ) + continue + + required_dist_keys = set(dist_info["default_keys"].keys()) + given_dist_keys = set(dist.keys()) + + miss_dist_keys = required_dist_keys - given_dist_keys + bad_dist_keys = given_dist_keys - required_dist_keys + if miss_dist_keys: + raise ValueError( + err_msg + f"Missing `{dist_key}` keys: " + f'{", ".join([f"{i}" for i in miss_dist_keys])}' + ) + if bad_dist_keys: + raise ValueError( + err_msg + f"Unknown `{dist_key}` keys: " + f'{", ".join([f"{i}" for i in bad_dist_keys])}' + ) + + # Match number of distributions to number of bins: + for dist_param in required_dist_keys: # i.e. "alpha" and "beta" for beta dist + + dist_param_val = dist[dist_param] + + if isinstance(dist_param_val, np.ndarray): + dist_param_val = dist_param_val.tolist() + + if not isinstance(dist_param_val, list): + dist_param_val = [dist_param_val] + + if len(dist_param_val) == 1: + dist_param_val = dist_param_val * num_bins + + elif len(dist_param_val) != num_bins: + raise ValueError( + err_msg + f'Distribution `{dist_key}` key "{dist_param}" must ' + f"have length one, or length equal to the number of size " + f"distribution bins, which is {num_bins}, but in fact has " + f"length {len(dist_param_val)}." + ) + dist[dist_param] = dist_param_val + + all_dists.update({dist_key: dist}) + + # ODF: + ODF_weights = {} + axis_ODF_weights = {} + ODF = phase_stats.get("ODF") + axis_ODF = phase_stats.get("axis_ODF") + + if ODF or (phase_idx == 0 and orientations is not None): + if not ODF: + ODF = {} + given_ODF_keys = set(ODF.keys()) + miss_ODF_keys = REQUIRED_PHASE_ODF_KEYS - given_ODF_keys + bad_ODF_keys = given_ODF_keys - ALLOWED_PHASE_ODF_KEYS + if miss_ODF_keys: + raise ValueError( + err_msg + f"Missing `ODF` keys: " + f'{", ".join([f"{i}" for i in miss_ODF_keys])}' + ) + if bad_ODF_keys: + raise ValueError( + err_msg + f"Unknown `ODF` keys: " + f'{", ".join([f"{i}" for i in bad_ODF_keys])}' + ) + + ODF_presets = ODF.get("presets") + + if phase_idx == 0 and orientations is not None: + # ALlow importing orientations only for the first phase: + + if ODF_presets: + warnings.warn( + err_msg + f"Using locally defined ODF presets; not " + f"using `orientations` from a previous task." + ) + + elif "orientations" in ODF: + warnings.warn( + err_msg + f"Using locally defined `orientations`, not " + f"those from a previous task!" + ) + + else: + ODF["orientations"] = orientations + + if ODF_presets: + + if any([ODF.get(i) is not None for i in ALLOWED_PHASE_AXIS_ODF_KEYS]): + raise ValueError( + err_msg + f"Specify either `presets` or `orientations` (and " + f"`sigmas and `weights)." + ) + preset_eulers = [] + preset_weights = [] + preset_sigmas = [] + for ODF_preset_idx, ODF_preset in enumerate(ODF_presets): + if ( + "name" not in ODF_preset + or ODF_preset["name"].lower() not in ODF_CUBIC_PRESETS + ): + raise ValueError( + err_msg + f"Specify `name` for ODF preset index " + f"{ODF_preset_idx}; one of: " + f'{", ".join([f"{i}" for i in ODF_CUBIC_PRESETS.keys()])}' + ) + preset_eulers.append(ODF_CUBIC_PRESETS[ODF_preset["name"].lower()]) + preset_weights.append(ODF_preset.get("weight", DEFAULT_ODF_WEIGHT)) + preset_sigmas.append(ODF_preset.get("sigma", DEFAULT_ODF_SIGMA)) + + ODF["sigmas"] = preset_sigmas + ODF["weights"] = preset_weights + ODF["orientations"] = process_dream3D_euler_angles( + np.array(preset_eulers), + degrees=True, + ) + + oris = validate_orientations(ODF["orientations"]) # now as quaternions + + # Convert unit-cell alignment to x//a, as used by Dream.3D: + if phase_i_CS == "hexagonal": + if oris["unit_cell_alignment"].get("y") == "b": + hex_transform_quat = axang2quat( + oris["P"] * np.array([0, 0, 1]), np.pi / 6 + ) + for ori_idx, ori_i in enumerate(oris["quaternions"]): + oris["quaternions"][ori_idx] = multiply_quaternions( + q1=hex_transform_quat, + q2=ori_i, + P=oris["P"], + ) + elif oris["unit_cell_alignment"].get("x") != "a": + msg = ( + f"Cannot convert from the following specified unit cell " + f"alignment to Dream3D-compatible unit cell alignment (x//a): " + f'{oris["unit_cell_alignment"]}' + ) + NotImplementedError(msg) + + num_oris = oris["quaternions"].shape[0] + + # Add defaults: + if "weights" not in ODF: + ODF["weights"] = DEFAULT_ODF_WEIGHT + if "sigmas" not in ODF: + ODF["sigmas"] = DEFAULT_ODF_SIGMA + + for i in ("weights", "sigmas"): + + val = ODF[i] + + if isinstance(val, np.ndarray): + dist_param_val = val.tolist() + + if not isinstance(val, list): + val = [val] + + if len(val) == 1: + val = val * num_oris + + elif len(val) != num_oris: + raise ValueError( + err_msg + f'ODF key "{i}" must have length one, or length equal ' + f"to the number of ODF orientations, which is {num_oris}, but in " + f"fact has length {len(val)}." + ) + ODF[i] = val + + # Convert to Euler angles for Dream3D: + oris_euler = quat2euler(oris["quaternions"], degrees=False, P=oris["P"]) + + ODF_weights = { + "Euler 1": oris_euler[:, 0].tolist(), + "Euler 2": oris_euler[:, 1].tolist(), + "Euler 3": oris_euler[:, 2].tolist(), + "Sigma": ODF["sigmas"], + "Weight": ODF["weights"], + } + + if axis_ODF: + given_axis_ODF_keys = set(axis_ODF.keys()) + miss_axis_ODF_keys = REQUIRED_PHASE_AXIS_ODF_KEYS - given_axis_ODF_keys + bad_axis_ODF_keys = given_axis_ODF_keys - ALLOWED_PHASE_AXIS_ODF_KEYS + if miss_axis_ODF_keys: + raise ValueError( + err_msg + f"Missing `axis_ODF` keys: " + f'{", ".join([f"{i}" for i in miss_axis_ODF_keys])}' + ) + if bad_axis_ODF_keys: + raise ValueError( + err_msg + f"Unknown `axis_ODF` keys: " + f'{", ".join([f"{i}" for i in bad_axis_ODF_keys])}' + ) + + axis_oris = validate_orientations( + axis_ODF["orientations"] + ) # now as quaternions + + # Convert unit-cell alignment to x//a, as used by Dream.3D: + if phase_i_CS == "hexagonal": + if axis_oris["unit_cell_alignment"].get("y") == "b": + hex_transform_quat = axang2quat( + axis_oris["P"] * np.array([0, 0, 1]), np.pi / 6 + ) + for ori_idx, ori_i in enumerate(axis_oris["quaternions"]): + axis_oris["quaternions"][ori_idx] = multiply_quaternions( + q1=hex_transform_quat, + q2=ori_i, + P=axis_oris["P"], + ) + elif axis_oris["unit_cell_alignment"].get("x") != "a": + msg = ( + f"Cannot convert from the following specified unit cell " + f"alignment to Dream3D-compatible unit cell alignment (x//a): " + f'{axis_oris["unit_cell_alignment"]}' + ) + NotImplementedError(msg) + + num_oris = axis_oris["quaternions"].shape[0] + + # Add defaults: + if "weights" not in axis_ODF: + axis_ODF["weights"] = DEFAULT_AXIS_ODF_WEIGHT + if "sigmas" not in axis_ODF: + axis_ODF["sigmas"] = DEFAULT_AXIS_ODF_SIGMA + + for i in ("weights", "sigmas"): + + val = axis_ODF[i] + + if isinstance(val, np.ndarray): + dist_param_val = val.tolist() + + if not isinstance(val, list): + val = [val] + + if len(val) == 1: + val = val * num_oris + + elif len(val) != num_oris: + raise ValueError( + err_msg + + f'axis_ODF key "{i}" must have length one, or length equal ' + f"to the number of axis_ODF orientations, which is {num_oris}, but in " + f"fact has length {len(val)}." + ) + axis_ODF[i] = val + + # Convert to Euler angles for Dream3D: + axis_oris_euler = quat2euler( + axis_oris["quaternions"], degrees=False, P=axis_oris["P"] + ) + + axis_ODF_weights = { + "Euler 1": axis_oris_euler[:, 0].tolist(), + "Euler 2": axis_oris_euler[:, 1].tolist(), + "Euler 3": axis_oris_euler[:, 2].tolist(), + "Sigma": axis_ODF["sigmas"], + "Weight": axis_ODF["weights"], + } + + stats_JSON_i = { + "AxisODF-Weights": axis_ODF_weights, + "Bin Count": num_bins, + "BinNumber": bins.tolist(), + "BoundaryArea": 0, + "Crystal Symmetry": ALLOWED_CRYSTAL_STRUCTURES[phase_i_CS], + "FeatureSize Distribution": { + "Average": log_mean, + "Standard Deviation": sigma, + }, + "Feature_Diameter_Info": feat_diam_info, + "MDF-Weights": {}, + "ODF-Weights": ODF_weights, + "Name": phase_stats["name"], + "PhaseFraction": phase_stats["volume_fraction"], + "PhaseType": phase_stats["type"].title(), + } + + # Generate dists from `preset_statistics_model`: + if preset: + + if "omega3" not in all_dists: + omega3_dist = generate_omega3_dist_from_preset(num_bins) + all_dists.update({"omega3": omega3_dist}) + + if "c/a" not in all_dists: + c_a_aspect_ratio = preset["A_axis_length"] / preset["C_axis_length"] + c_a_dist = generate_shape_dist_from_preset( + num_bins, + c_a_aspect_ratio, + preset_type, + ) + all_dists.update({"c/a": c_a_dist}) + + if "b/a" not in all_dists: + b_a_aspect_ratio = preset["A_axis_length"] / preset["B_axis_length"] + b_a_dist = generate_shape_dist_from_preset( + num_bins, + b_a_aspect_ratio, + preset_type, + ) + all_dists.update({"b/a": b_a_dist}) + + if phase_type == "primary": + if "neighbours" not in all_dists: + neigh_dist = generate_neighbour_dist_from_preset( + num_bins, + preset_type, + ) + all_dists.update({"neighbours": neigh_dist}) + + # Coerce distributions into format expected in the JSON: + for dist_key, dist in all_dists.items(): + dist_info = DISTRIBUTIONS_MAP[dist_key] + stats_JSON_i.update( + { + dist_info["label"]: { + **{DISTRIBUTIONS_KEY_LABELS[k]: v for k, v in dist.items()}, + "Distribution Type": DISTRIBUTIONS_TYPE_LABELS[dist_info["type"]], + } + } + ) + + if phase_stats["type"] == "precipitate": + stats_JSON_i.update( + { + "Precipitate Boundary Fraction": phase_stats[ + "number_fraction_on_boundary" + ], + "Radial Distribution Function": { + "Bin Count": RDF["num_bins"], + "BoxDims": np.array(RDF["box_size"]).tolist(), + "BoxRes": [ # TODO: how is this calculated? + 0.1, + 0.1, + 0.1, + ], + "Max": RDF["max_distance"], + "Min": RDF["min_distance"], + }, + } + ) + + stats_JSON.append(stats_JSON_i) + + if not np.isclose(vol_frac_sum, 1.0): + raise ValueError( + f"Sum of `volume_fraction`s over all phases must sum to 1.0, " + f"but in fact sum to: {vol_frac_sum}" + ) + + stats_data_array = { + "Name": "Statistics", + "Phase Count": len(stats_JSON) + 1, # Don't know why this needs to be +1 + } + for idx, i in enumerate(stats_JSON, start=1): + stats_data_array.update({str(idx): i}) + + if precipitates: + precip_inp_file = str(Path.cwd().joinpath("precipitates.txt")) + else: + precip_inp_file = "" + + pipeline = { + "0": { + "CellEnsembleAttributeMatrixName": "CellEnsembleData", + "CrystalStructuresArrayName": "CrystalStructures", + "Filter_Enabled": True, + "Filter_Human_Label": "StatsGenerator", + "Filter_Name": "StatsGeneratorFilter", + "Filter_Uuid": "{f642e217-4722-5dd8-9df9-cee71e7b26ba}", + "PhaseNamesArrayName": "PhaseName", + "PhaseTypesArrayName": "PhaseTypes", + "StatsDataArray": stats_data_array, + "StatsDataArrayName": "Statistics", + "StatsGeneratorDataContainerName": "StatsGeneratorDataContainer", + }, + "1": { + # TODO: fix this + "BoxDimensions": "X Range: 0 to 2 (Delta: 2)\nY Range: 0 to 256 (Delta: 256)\nZ Range: 0 to 256 (Delta: 256)", + "CellAttributeMatrixName": "CellData", + "DataContainerName": "SyntheticVolumeDataContainer", + "Dimensions": {"x": grid_size[0], "y": grid_size[1], "z": grid_size[2]}, + "EnsembleAttributeMatrixName": "CellEnsembleData", + "EstimateNumberOfFeatures": 0, + "EstimatedPrimaryFeatures": "", + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Initialize Synthetic Volume", + "Filter_Name": "InitializeSyntheticVolume", + "Filter_Uuid": "{c2ae366b-251f-5dbd-9d70-d790376c0c0d}", + "InputPhaseTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "PhaseTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "InputStatsArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "Statistics", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "Origin": {"x": origin[0], "y": origin[1], "z": origin[2]}, + "Resolution": {"x": resolution[0], "y": resolution[1], "z": resolution[2]}, + }, + "2": { + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Establish Shape Types", + "Filter_Name": "EstablishShapeTypes", + "Filter_Uuid": "{4edbbd35-a96b-5ff1-984a-153d733e2abb}", + "InputPhaseTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "PhaseTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "ShapeTypeData": [999, 0, 0], + "ShapeTypesArrayName": "ShapeTypes", + }, + "3": { + "CellPhasesArrayName": "Phases", + "FeatureGeneration": 0, + "FeatureIdsArrayName": "FeatureIds", + "FeatureInputFile": "", + "FeaturePhasesArrayName": "Phases", + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Pack Primary Phases", + "Filter_Name": "PackPrimaryPhases", + "Filter_Uuid": "{84305312-0d10-50ca-b89a-fda17a353cc9}", + "InputPhaseNamesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "PhaseName", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "InputPhaseTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "PhaseTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "InputShapeTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "ShapeTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "InputStatsArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "Statistics", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "MaskArrayPath": { + "Attribute Matrix Name": "", + "Data Array Name": "", + "Data Container Name": "", + }, + "NewAttributeMatrixPath": { + "Attribute Matrix Name": "Synthetic Shape Parameters (Primary Phase)", + "Data Array Name": "", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "NumFeaturesArrayName": "NumFeatures", + "OutputCellAttributeMatrixPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "OutputCellEnsembleAttributeMatrixName": "CellEnsembleData", + "OutputCellFeatureAttributeMatrixName": "Grain Data", + "PeriodicBoundaries": int(periodic), + "SaveGeometricDescriptions": 0, + "SelectedAttributeMatrixPath": { + "Attribute Matrix Name": "", + "Data Array Name": "", + "Data Container Name": "", + }, + "UseMask": 0, + }, + "4": { + "BoundaryCellsArrayName": "BoundaryCells", + "FeatureIdsArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "FeatureIds", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Find Boundary Cells (Image)", + "Filter_Name": "FindBoundaryCells", + "Filter_Uuid": "{8a1106d4-c67f-5e09-a02a-b2e9b99d031e}", + "IgnoreFeatureZero": 1, + "IncludeVolumeBoundary": 0, + }, + "5": { + "BoundaryCellsArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "BoundaryCells", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "CellPhasesArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "Phases", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FeatureGeneration": 1 if precipitates else 0, # bug? should be opposite? + "FeatureIdsArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "FeatureIds", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FeaturePhasesArrayPath": { + "Attribute Matrix Name": "Grain Data", + "Data Array Name": "Phases", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Insert Precipitate Phases", + "Filter_Name": "InsertPrecipitatePhases", + "Filter_Uuid": "{1e552e0c-53bb-5ae1-bd1c-c7a6590f9328}", + "InputPhaseTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "PhaseTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "InputShapeTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "ShapeTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "InputStatsArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "Statistics", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "MaskArrayPath": { + "Attribute Matrix Name": "", + "Data Array Name": "", + "Data Container Name": "", + }, + "MatchRDF": 0, + "NewAttributeMatrixPath": { + "Attribute Matrix Name": "Synthetic Shape Parameters (Precipitate)", + "Data Array Name": "", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "NumFeaturesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "NumFeatures", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "PeriodicBoundaries": int(periodic), + "PrecipInputFile": precip_inp_file, + "SaveGeometricDescriptions": 0, + "SelectedAttributeMatrixPath": { + "Attribute Matrix Name": "", + "Data Array Name": "", + "Data Container Name": "", + }, + "UseMask": 0, + }, + "6": { + "BoundaryCellsArrayName": "BoundaryCells", + "CellFeatureAttributeMatrixPath": { + "Attribute Matrix Name": "Grain Data", + "Data Array Name": "", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FeatureIdsArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "FeatureIds", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Find Feature Neighbors", + "Filter_Name": "FindNeighbors", + "Filter_Uuid": "{97cf66f8-7a9b-5ec2-83eb-f8c4c8a17bac}", + "NeighborListArrayName": "NeighborList", + "NumNeighborsArrayName": "NumNeighbors", + "SharedSurfaceAreaListArrayName": "SharedSurfaceAreaList", + "StoreBoundaryCells": 0, + "StoreSurfaceFeatures": 1, + "SurfaceFeaturesArrayName": "SurfaceFeatures", + }, + "7": { + "AvgQuatsArrayName": "AvgQuats", + "CellEulerAnglesArrayName": "EulerAngles", + "CrystalStructuresArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "CrystalStructures", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "FeatureEulerAnglesArrayName": "EulerAngles", + "FeatureIdsArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "FeatureIds", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FeaturePhasesArrayPath": { + "Attribute Matrix Name": "Grain Data", + "Data Array Name": "Phases", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Match Crystallography", + "Filter_Name": "MatchCrystallography", + "Filter_Uuid": "{7bfb6e4a-6075-56da-8006-b262d99dff30}", + "InputStatsArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "Statistics", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "MaxIterations": 100000, + "NeighborListArrayPath": { + "Attribute Matrix Name": "Grain Data", + "Data Array Name": "NeighborList", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "NumFeaturesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "NumFeatures", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "PhaseTypesArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "PhaseTypes", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "SharedSurfaceAreaListArrayPath": { + "Attribute Matrix Name": "Grain Data", + "Data Array Name": "SharedSurfaceAreaList", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "SurfaceFeaturesArrayPath": { + "Attribute Matrix Name": "Grain Data", + "Data Array Name": "SurfaceFeatures", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "VolumesArrayName": "Volumes", + }, + "8": { + "CellEulerAnglesArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "EulerAngles", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "CellIPFColorsArrayName": "IPFColor", + "CellPhasesArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "Phases", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "CrystalStructuresArrayPath": { + "Attribute Matrix Name": "CellEnsembleData", + "Data Array Name": "CrystalStructures", + "Data Container Name": "StatsGeneratorDataContainer", + }, + "FilterVersion": "6.5.141", + "Filter_Enabled": True, + "Filter_Human_Label": "Generate IPF Colors", + "Filter_Name": "GenerateIPFColors", + "Filter_Uuid": "{a50e6532-8075-5de5-ab63-945feb0de7f7}", + "GoodVoxelsArrayPath": { + "Attribute Matrix Name": "CellData", + "Data Array Name": "", + "Data Container Name": "SyntheticVolumeDataContainer", + }, + "ReferenceDir": {"x": 0, "y": 0, "z": 1}, + "UseGoodVoxels": 0, + }, + "9": { + "FilterVersion": "1.2.675", + "Filter_Enabled": True, + "Filter_Human_Label": "Write DREAM.3D Data File", + "Filter_Name": "DataContainerWriter", + "Filter_Uuid": "{3fcd4c43-9d75-5b86-aad4-4441bc914f37}", + "OutputFile": f"{str(Path(path).absolute().parent.joinpath('pipeline.dream3d'))}", + "WriteTimeSeries": 0, + "WriteXdmfFile": 1, + }, + "PipelineBuilder": { + "Name": "RVE from precipitate statistics", + "Number_Filters": 10, + "Version": 6, + }, + } + + with Path(path).open("w") as fh: + json.dump(pipeline, fh, indent=4) + + +# These functions are copied from https://github.com/LightForm-group/matflow-dream3d/blob/master/matflow_dream3d/utilities.py +# and also exist in matflow/data/scripts/dream_3D/generate_volume_element_statistics.py + + +def process_dream3D_euler_angles(euler_angles, degrees=False): + orientations = { + "type": "euler", + "euler_degrees": degrees, + "euler_angles": euler_angles, + "unit_cell_alignment": {"x": "a"}, + } + return orientations + + +def quat2euler(quats, degrees=False, P=1): + """Convert quaternions to Bunge-convention Euler angles. + + Parameters + ---------- + quats : ndarray of shape (N, 4) of float + Array of N row four-vectors of unit quaternions. + degrees : bool, optional + If True, `euler_angles` are returned in degrees, rather than radians. + + P : int, optional + The "P" constant, either +1 or -1, as defined within [1]. + + Returns + ------- + euler_angles : ndarray of shape (N, 3) of float + Array of N row three-vectors of Euler angles, specified as proper Euler angles in + the Bunge convention (rotations are about Z, new X, new new Z). + + Notes + ----- + Conversion of quaternions to Bunge Euler angles due to Ref. [1]. + + References + ---------- + [1] Rowenhorst, D, A D Rollett, G S Rohrer, M Groeber, M Jackson, + P J Konijnenberg, and M De Graef. "Consistent Representations + of and Conversions between 3D Rotations". Modelling and Simulation + in Materials Science and Engineering 23, no. 8 (1 December 2015): + 083501. https://doi.org/10.1088/0965-0393/23/8/083501. + + """ + + num_oris = quats.shape[0] + euler_angles = np.zeros((num_oris, 3)) + + q0, q1, q2, q3 = quats.T + + q03 = q0**2 + q3**2 + q12 = q1**2 + q2**2 + chi = np.sqrt(q03 * q12) + + chi_zero_idx = np.isclose(chi, 0) + q12_zero_idx = np.isclose(q12, 0) + q03_zero_idx = np.isclose(q03, 0) + + # Three cases are distinguished: + idx_A = np.logical_and(chi_zero_idx, q12_zero_idx) + idx_B = np.logical_and(chi_zero_idx, q03_zero_idx) + idx_C = np.logical_not(chi_zero_idx) + + q0A, q3A = q0[idx_A], q3[idx_A] + q1B, q2B = q1[idx_B], q2[idx_B] + q0C, q1C, q2C, q3C, chiC = q0[idx_C], q1[idx_C], q2[idx_C], q3[idx_C], chi[idx_C] + + q03C = q03[idx_C] + q12C = q12[idx_C] + + # Case 1 + euler_angles[idx_A, 0] = np.arctan2(-2 * P * q0A * q3A, q0A**2 - q3A**2) + + # Case 2 + euler_angles[idx_B, 0] = np.arctan2(2 * q1B * q2B, q1B**2 - q2B**2) + euler_angles[idx_B, 1] = np.pi + + # Case 3 + euler_angles[idx_C, 0] = np.arctan2( + (q1C * q3C - P * q0C * q2C) / chiC, + (-P * q0C * q1C - q2C * q3C) / chiC, + ) + euler_angles[idx_C, 1] = np.arctan2(2 * chiC, q03C - q12C) + euler_angles[idx_C, 2] = np.arctan2( + (P * q0C * q2C + q1C * q3C) / chiC, + (q2C * q3C - P * q0C * q1C) / chiC, + ) + + euler_angles[euler_angles[:, 0] < 0, 0] += 2 * np.pi + euler_angles[euler_angles[:, 2] < 0, 2] += 2 * np.pi + + if degrees: + euler_angles = np.rad2deg(euler_angles) + + return euler_angles + + +# These are copied from https://github.com/LightForm-group/matflow-dream3d/blob/master/matflow_dream3d/preset_statistics.py, +# and are also reproduced in matflow/data/scripts/dream_3D/generate_volume_element_statistics.py +# As such there is duplication, so there is probably a better strategy for this code. + + +def generate_omega3_dist_from_preset(num_bins): + """Replicating: https://github.com/BlueQuartzSoftware/DREAM3D/blob/331c97215bb358321d9f92105a9c812a81fd1c79/Source/Plugins/SyntheticBuilding/SyntheticBuildingFilters/Presets/PrimaryRolledPreset.cpp#L62""" + + alphas = [] + betas = [] + for _ in range(num_bins): + alpha = 10.0 + np.random.random() + beta = 1.5 + (0.5 * np.random.random()) + alphas.append(alpha) + betas.append(beta) + + return {"alpha": alphas, "beta": betas} + + +def generate_shape_dist_from_preset(num_bins, aspect_ratio, preset_type): + """Replicating: https://github.com/BlueQuartzSoftware/DREAM3D/blob/331c97215bb358321d9f92105a9c812a81fd1c79/Source/Plugins/SyntheticBuilding/SyntheticBuildingFilters/Presets/PrimaryRolledPreset.cpp#L88""" + alphas = [] + betas = [] + for _ in range(num_bins): + + if preset_type in ["primary_rolled", "precipitate_rolled"]: + alpha = (1.1 + (28.9 * (1.0 / aspect_ratio))) + np.random.random() + beta = (30 - (28.9 * (1.0 / aspect_ratio))) + np.random.random() + + elif preset_type in ["primary_equiaxed", "precipitate_equiaxed"]: + alpha = 15.0 + np.random.random() + beta = 1.25 + (0.5 * np.random.random()) + + alphas.append(alpha) + betas.append(beta) + + return {"alpha": alphas, "beta": betas} + + +def generate_neighbour_dist_from_preset(num_bins, preset_type): + """Replicating: https://github.com/BlueQuartzSoftware/DREAM3D/blob/331c97215bb358321d9f92105a9c812a81fd1c79/Source/Plugins/SyntheticBuilding/SyntheticBuildingFilters/Presets/PrimaryRolledPreset.cpp#L140""" + mus = [] + sigmas = [] + middlebin = num_bins // 2 + for i in range(num_bins): + + if preset_type == "primary_equiaxed": + mu = np.log(14.0 + (2.0 * float(i - middlebin))) + sigma = 0.3 + (float(middlebin - i) / float(middlebin * 10)) + + elif preset_type == "primary_rolled": + mu = np.log(8.0 + (1.0 * float(i - middlebin))) + sigma = 0.3 + (float(middlebin - i) / float(middlebin * 10)) + + mus.append(mu) + sigmas.append(sigma) + + return {"average": mus, "stddev": sigmas} diff --git a/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi.py b/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi.py new file mode 100644 index 00000000..725e61f1 --- /dev/null +++ b/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi.py @@ -0,0 +1,50 @@ +from cipher_parse import ( + CIPHERInput, + MaterialDefinition, + InterfaceDefinition, + PhaseTypeDefinition, +) + + +def generate_phase_field_input_from_random_voronoi( + materials, + interfaces, + num_phases, + grid_size, + size, + components, + outputs, + solution_parameters, + random_seed, + is_periodic, + combine_phases, +): + # initialise `MaterialDefinition`, `InterfaceDefinition` and + # `PhaseTypeDefinition` objects: + mats = [] + for mat_i in materials: + if "phase_types" in mat_i: + mat_i["phase_types"] = [ + PhaseTypeDefinition(**j) for j in mat_i["phase_types"] + ] + mat_i = MaterialDefinition(**mat_i) + mats.append(mat_i) + + interfaces = [InterfaceDefinition(**int_i) for int_i in interfaces] + + inp = CIPHERInput.from_random_voronoi( + materials=mats, + interfaces=interfaces, + num_phases=num_phases, + grid_size=grid_size, + size=size, + components=components, + outputs=outputs, + solution_parameters=solution_parameters, + random_seed=random_seed, + is_periodic=is_periodic, + combine_phases=combine_phases, + ) + phase_field_input = inp.to_JSON(keep_arrays=True) + + return {"phase_field_input": phase_field_input} diff --git a/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations.py b/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations.py new file mode 100644 index 00000000..bb0bee4d --- /dev/null +++ b/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations.py @@ -0,0 +1,65 @@ +from cipher_parse import ( + CIPHERInput, + MaterialDefinition, + InterfaceDefinition, + PhaseTypeDefinition, +) + + +def generate_phase_field_input_from_random_voronoi_orientations( + materials, + interfaces, + num_phases, + grid_size, + size, + components, + outputs, + solution_parameters, + random_seed, + is_periodic, + orientations, + interface_binning, + combine_phases, +): + quats = orientations["quaternions"] + + # initialise `MaterialDefinition`, `InterfaceDefinition` and + # `PhaseTypeDefinition` objects: + mats = [] + for mat_idx, mat_i in enumerate(materials): + if "phase_types" in mat_i: + mat_i["phase_types"] = [ + PhaseTypeDefinition(**j) for j in mat_i["phase_types"] + ] + else: + mat_i["phase_types"] = [PhaseTypeDefinition()] + + if mat_idx == 0: + # add oris to the first defined phase type of the first material: + mat_i["phase_types"][0].orientations = quats + + mat_i = MaterialDefinition(**mat_i) + mats.append(mat_i) + + interfaces = [InterfaceDefinition(**int_i) for int_i in interfaces] + + inp = CIPHERInput.from_random_voronoi( + materials=mats, + interfaces=interfaces, + num_phases=num_phases, + grid_size=grid_size, + size=size, + components=components, + outputs=outputs, + solution_parameters=solution_parameters, + random_seed=random_seed, + is_periodic=is_periodic, + combine_phases=combine_phases, + ) + + if interface_binning: + inp.bin_interfaces_by_misorientation_angle(**interface_binning) + + phase_field_input = inp.to_JSON(keep_arrays=True) + + return {"phase_field_input": phase_field_input} diff --git a/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations_gradient.py b/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations_gradient.py new file mode 100644 index 00000000..e59d8755 --- /dev/null +++ b/matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations_gradient.py @@ -0,0 +1,97 @@ +import numpy as np + +from cipher_parse.utilities import read_shockley, sample_from_orientations_gradient +from cipher_parse import ( + CIPHERInput, + MaterialDefinition, + InterfaceDefinition, + PhaseTypeDefinition, +) + + +def generate_phase_field_input_from_random_voronoi_orientations_gradient( + materials, + interfaces, + num_phases, + grid_size, + size, + components, + outputs, + solution_parameters, + orientation_gradient, + random_seed, + is_periodic, + interface_binning, + combine_phases, +): + # initialise `MaterialDefinition`, `InterfaceDefinition` and + # `PhaseTypeDefinition` objects: + mats = [] + for mat_i in materials: + if "phase_types" in mat_i: + mat_i["phase_types"] = [ + PhaseTypeDefinition(**j) for j in mat_i["phase_types"] + ] + mat_i = MaterialDefinition(**mat_i) + mats.append(mat_i) + + interfaces = [InterfaceDefinition(**int_i) for int_i in interfaces] + + inp = CIPHERInput.from_random_voronoi( + materials=mats, + interfaces=interfaces, + num_phases=num_phases, + grid_size=grid_size, + size=size, + components=components, + outputs=outputs, + solution_parameters=solution_parameters, + random_seed=random_seed, + is_periodic=is_periodic, + combine_phases=combine_phases, + ) + + if orientation_gradient: + phase_centroids = inp.geometry.get_phase_voxel_centroids() + ori_range, ori_idx = sample_from_orientations_gradient( + phase_centroids=phase_centroids, + max_misorientation_deg=orientation_gradient["max_misorientation_deg"], + ) + oris = np.zeros((phase_centroids.shape[0], 4)) + oris[ori_idx] = ori_range + inp.geometry.phase_orientation = oris + + new_phase_ori = np.copy(inp.geometry.phase_orientation) + + if "add_highly_misoriented_grain" in orientation_gradient: + add_HMG = orientation_gradient["add_highly_misoriented_grain"] + if add_HMG is True: + phase_idx = np.argmin( + np.sum( + (phase_centroids - (inp.geometry.size * np.array([0.3, 0.5]))) + ** 2, + axis=1, + ) + ) + else: + phase_idx = add_HMG + + # consider a fraction to be misoriented by (1 == max_misorientation_deg) + if "highly_misoriented_grain_misorientation" in orientation_gradient: + HMG_misori = orientation_gradient[ + "highly_misoriented_grain_misorientation" + ] + else: + HMG_misori = 1.0 + + new_ori = ori_range[int(HMG_misori * (ori_range.shape[0] - 1))] + new_phase_ori[phase_idx] = new_ori + + inp.geometry.phase_orientation = new_phase_ori + + if interface_binning: + inp.bin_interfaces_by_misorientation_angle(**interface_binning) + + phase_field_input = inp.to_JSON(keep_arrays=True) + + return {"phase_field_input": phase_field_input} diff --git a/matflow/data/scripts/cipher/generate_phase_field_input_from_volume_element.py b/matflow/data/scripts/cipher/generate_phase_field_input_from_volume_element.py new file mode 100644 index 00000000..3190ce9f --- /dev/null +++ b/matflow/data/scripts/cipher/generate_phase_field_input_from_volume_element.py @@ -0,0 +1,126 @@ +import numpy as np + +from cipher_parse import ( + CIPHERInput, + MaterialDefinition, + InterfaceDefinition, + PhaseTypeDefinition, + CIPHERGeometry, +) + + +def generate_phase_field_input_from_volume_element( + volume_element, + materials, + interfaces, + phase_type_map, + size, + components, + outputs, + solution_parameters, + random_seed, + interface_binning, + keep_3D, + combine_phases=None, +): + mats = [] + for mat_i in materials: + if "phase_types" in mat_i: + mat_i["phase_types"] = [ + PhaseTypeDefinition(**j) for j in mat_i["phase_types"] + ] + mat_i = MaterialDefinition(**mat_i) + mats.append(mat_i) + + interfaces = [InterfaceDefinition(**int_i) for int_i in interfaces] + + geom = _volume_element_to_cipher_geometry( + volume_element=volume_element, + cipher_materials=mats, + cipher_interfaces=interfaces, + phase_type_map=phase_type_map, + size=size, + random_seed=random_seed, + keep_3D=keep_3D, + combine_phases=combine_phases, + ) + + inp = CIPHERInput( + geometry=geom, + components=components, + outputs=outputs, + solution_parameters=solution_parameters, + ) + + if interface_binning: + inp.bin_interfaces_by_misorientation_angle(**interface_binning) + + phase_field_input = inp.to_JSON(keep_arrays=True) + + return {"phase_field_input": phase_field_input} + + +def _volume_element_to_cipher_geometry( + volume_element, + cipher_materials, + cipher_interfaces, + combine_phases, + phase_type_map=None, + size=None, + random_seed=None, + keep_3D=False, +): + + uq, inv = np.unique(volume_element["constituent_phase_label"], return_inverse=True) + cipher_phases = {i: np.where(inv == idx)[0] for idx, i in enumerate(uq)} + orientations = volume_element["orientations"]["quaternions"] + orientations = np.array(orientations) + + # we need P=-1, because that's what DAMASK Rotation object assumes, which + # we use when/if finding the disorientations for the + # misorientation_matrix: + if volume_element["orientations"]["P"] == 1: + # multiple vector part by -1 to get P=-1: + if volume_element["orientations"]["quat_component_ordering"] == "scalar-vector": + quat_vec_idx = [1, 2, 3] + else: + quat_vec_idx = [0, 1, 2] + orientations[:, quat_vec_idx] *= -1 + + for mat_name_i in cipher_phases: + phases_set = False + if phase_type_map: + phase_type_name = phase_type_map[mat_name_i] + else: + phase_type_name = mat_name_i + for mat in cipher_materials: + for phase_type_i in mat.phase_types: + if phase_type_i.name == phase_type_name: + phase_i_idx = cipher_phases[mat_name_i] + phase_type_i.phases = phase_i_idx + phase_type_i.orientations = orientations[phase_i_idx] + phases_set = True + break + if phases_set: + break + + if not phases_set: + raise ValueError( + f"No defined material/phase-type for VE phase {mat_name_i!r}" + ) + + voxel_phase = volume_element["element_material_idx"] + size = volume_element["size"] if size is None else size + if voxel_phase.ndim == 3 and voxel_phase.shape[2] == 1 and not keep_3D: + voxel_phase = voxel_phase[..., 0] + size = size[:2] + + geom = CIPHERGeometry( + voxel_phase=voxel_phase, + size=size, + materials=cipher_materials, + interfaces=cipher_interfaces, + random_seed=random_seed, + combine_phases=combine_phases, + ) + return geom diff --git a/matflow/data/scripts/cipher/parse_cipher_outputs.py b/matflow/data/scripts/cipher/parse_cipher_outputs.py new file mode 100644 index 00000000..d5acd234 --- /dev/null +++ b/matflow/data/scripts/cipher/parse_cipher_outputs.py @@ -0,0 +1,35 @@ +from cipher_parse import CIPHEROutput + + +def parse_cipher_outputs( + cipher_VTU_files, + num_VTU_files, + VTU_files_time_interval, + derive_outputs, + save_outputs, + delete_VTIs, + delete_VTUs, +): + + out = CIPHEROutput.parse( + directory=".", + options={ + "num_VTU_files": num_VTU_files, + "VTU_files_time_interval": VTU_files_time_interval, + "derive_outputs": derive_outputs, + "save_outputs": save_outputs, + "delete_VTIs": delete_VTIs, + "delete_VTUs": delete_VTUs, + }, + ) + + # # GBs in initial geom: + # out.cipher_input.geometry.get_grain_boundaries() + + # # GBs in subsequent geoms: + # out.set_all_geometries() + # for geom in out.geometries: + # geom.get_grain_boundaries() + + phase_field_output = out.to_JSON(keep_arrays=True) + return {"phase_field_output": phase_field_output} diff --git a/matflow/data/scripts/cipher/parse_dream_3D_volume_element_from_stats.py b/matflow/data/scripts/cipher/parse_dream_3D_volume_element_from_stats.py new file mode 100644 index 00000000..f30e5307 --- /dev/null +++ b/matflow/data/scripts/cipher/parse_dream_3D_volume_element_from_stats.py @@ -0,0 +1,148 @@ +import h5py +import copy +import numpy as np + +from damask_parse.utils import validate_orientations, validate_volume_element +from matflow.param_classes.orientations import Orientations + + +def parse_dream_3D_volume_element_from_stats( + dream_3D_hdf5_file, + phase_statistics, + orientations, + RNG_seed, +): + orientations_phase_1 = orientations["phase_1"] + orientations_phase_2 = orientations["phase_2"] + + # TODO: make it work. + print(f"phase_statistics: {phase_statistics}") + print(f"ori phase 1: {orientations_phase_1}") + print(f"ori phase 2: {orientations_phase_2}") + + with h5py.File(dream_3D_hdf5_file, mode="r") as fh: + synth_vol = fh["DataContainers"]["SyntheticVolumeDataContainer"] + grid_size = synth_vol["_SIMPL_GEOMETRY"]["DIMENSIONS"][()] + resolution = synth_vol["_SIMPL_GEOMETRY"]["SPACING"][()] + size = [i * j for i, j in zip(resolution, grid_size)] + + # make zero-indexed: + # (not sure why FeatureIds is 4D?) + element_material_idx = synth_vol["CellData"]["FeatureIds"][()][..., 0] - 1 + element_material_idx = element_material_idx.transpose((2, 1, 0)) + + num_grains = element_material_idx.max() + 1 + phase_names = synth_vol["CellEnsembleData"]["PhaseName"][()][1:] + constituent_phase_idx = synth_vol["Grain Data"]["Phases"][()][1:] - 1 + constituent_phase_label = np.array( + [phase_names[i][0].decode() for i in constituent_phase_idx] + ) + old_format_orientations_phase_1 = _convert_orientations_to_old_matflow_format( + orientations_phase_1 + ) + old_format_orientations_phase_2 = _convert_orientations_to_old_matflow_format( + orientations_phase_2 + ) + + ori_1 = validate_orientations(old_format_orientations_phase_1) + ori_2 = validate_orientations(old_format_orientations_phase_2) + oris = copy.deepcopy(ori_1) # combined orientations + + phase_labels = [i["name"] for i in phase_statistics] + phase_labels_idx = np.ones(constituent_phase_label.size) * np.nan + for idx, i in enumerate(phase_labels): + phase_labels_idx[constituent_phase_label == i] = idx + assert not np.any(np.isnan(phase_labels_idx)) + phase_labels_idx = phase_labels_idx.astype(int) + + _, counts = np.unique(phase_labels_idx, return_counts=True) + + num_ori_1 = ori_1["quaternions"].shape[0] + num_ori_2 = ori_2["quaternions"].shape[0] + sampled_oris_1 = ori_1["quaternions"] + sampled_oris_2 = ori_2["quaternions"] + + rng = np.random.default_rng(seed=RNG_seed) + + # If there are more orientations than phase label assignments, choose a random subset: + if num_ori_1 != counts[0]: + try: + ori_1_idx = rng.choice(a=num_ori_1, size=counts[0], replace=False) + except ValueError as err: + raise ValueError( + f"Probably an insufficient number of `orientations_phase_1` " + f"({num_ori_1} given for phase {phase_labels[0]!r}, whereas {counts[0]} " + f"needed). Caught ValueError is: {err}" + ) + sampled_oris_1 = sampled_oris_1[ori_1_idx] + if num_ori_2 != counts[1]: + try: + ori_2_idx = rng.choice(a=num_ori_2, size=counts[1], replace=False) + except ValueError as err: + raise ValueError( + f"Probably an insufficient number of `orientations_phase_2` " + f"({num_ori_2} given for phase {phase_labels[1]!r}, whereas {counts[1]} " + f"needed). Caught ValueError is: {err}" + ) + sampled_oris_2 = sampled_oris_2[ori_2_idx] + + ori_idx = np.ones(num_grains) * np.nan + for idx, i in enumerate(counts): + ori_idx[phase_labels_idx == idx] = np.arange(i) + np.sum(counts[:idx]) + + if np.any(np.isnan(ori_idx)): + raise RuntimeError("Not all phases have an orientation assigned!") + ori_idx = ori_idx.astype(int) + + oris["quaternions"] = np.vstack([sampled_oris_1, sampled_oris_2]) + + volume_element = { + "size": size, + "grid_size": grid_size, + "orientations": oris, + "element_material_idx": element_material_idx, + "constituent_material_idx": np.arange(num_grains), + "constituent_material_fraction": np.ones(num_grains), + "constituent_phase_label": constituent_phase_label, + "constituent_orientation_idx": ori_idx, + "material_homog": np.full(num_grains, "SX"), + } + volume_element = validate_volume_element(volume_element) + return volume_element + + +# This code is copied from dream3D/generate_volume_element_statistics.py +def _convert_orientations_to_old_matflow_format(orientations: Orientations): + # see `LatticeDirection` enum: + align_lookup = { + "A": "a", + "B": "b", + "C": "c", + "A_STAR": "a*", + "B_STAR": "b*", + "C_STAR": "c*", + } + unit_cell_alignment = { + "x": align_lookup[orientations.unit_cell_alignment.x.name], + "y": align_lookup[orientations.unit_cell_alignment.y.name], + "z": align_lookup[orientations.unit_cell_alignment.z.name], + } + type_lookup = { + "QUATERNION": "quat", + "EULER": "euler", + } + type_ = type_lookup[orientations.representation.type.name] + oris = { + "type": type_, + "unit_cell_alignment": unit_cell_alignment, + } + + if type_ == "quat": + quat_order = orientations.representation.quat_order.name.lower().replace("_", "-") + oris["quaternions"] = np.array(orientations.data) + oris["quat_component_ordering"] = quat_order + elif type_ == "euler": + oris["euler_angles"] = np.array(orientations.data) + oris["euler_degrees"] = orientations.representation.euler_is_degrees + + return oris diff --git a/matflow/data/scripts/cipher/write_cipher_input.py b/matflow/data/scripts/cipher/write_cipher_input.py new file mode 100644 index 00000000..c9f5dd03 --- /dev/null +++ b/matflow/data/scripts/cipher/write_cipher_input.py @@ -0,0 +1,7 @@ +from cipher_parse import CIPHERInput + + +def write_cipher_input(path, phase_field_input, separate_mapping_files): + inp = CIPHERInput.from_JSON(phase_field_input) + inp.write_yaml(path, separate_mappings=separate_mapping_files) + inp.geometry.write_VTK("initial.vti") diff --git a/matflow/data/scripts/cipher/write_precipitates_file.py b/matflow/data/scripts/cipher/write_precipitates_file.py new file mode 100644 index 00000000..2275dd5a --- /dev/null +++ b/matflow/data/scripts/cipher/write_precipitates_file.py @@ -0,0 +1,19 @@ +from pathlib import Path + + +def write_precipitates_file(path, precipitates): + if precipitates: + with Path(path).open("wt") as fp: + fp.write(str(len(precipitates)) + "\n") + for i in precipitates: + fp.write( + f"{i['phase_number']} " + f"{i['position'][0]:.6f} {i['position'][1]:.6f} {i['position'][2]:.6f} " + f"{i['major_semi_axis_length']:.6f} " + f"{i['mid_semi_axis_length']:.6f} " + f"{i['minor_semi_axis_length']:.6f} " + f"{i.get('omega3', 1):.6f} " + f"{i['euler_angle'][0]:.6f} " + f"{i['euler_angle'][1]:.6f} " + f"{i['euler_angle'][2]:.6f}\n" + ) diff --git a/matflow/data/scripts/cluster_orientations.py b/matflow/data/scripts/cluster_orientations.py new file mode 100644 index 00000000..43fe8b2f --- /dev/null +++ b/matflow/data/scripts/cluster_orientations.py @@ -0,0 +1,75 @@ +import numpy as np +from damask_parse.utils import validate_volume_element +from subsurface import Shuffle +import matplotlib.pyplot as plt + + +def cluster_orientations( + volume_element, + alpha_file_path, + gamma_file_path, + n_iterations, + alpha_start_index, + alpha_stop_index, + gamma_start_index, + gamma_stop_index, +): + """Method to rearrange a list of orientations by minimising the misorientation between neighbouring orientations.""" + + # Convert zarr arrays to numpy arrays using existing code + volume_element = validate_volume_element(volume_element) + + quaternions = volume_element["orientations"]["quaternions"] + material_index = volume_element["element_material_idx"] + material_index_2d = material_index[:, :, 0] + material_index_3d = np.stack( + (material_index_2d, material_index_2d, material_index_2d), axis=-1 + ) + + # Replace subsets of quaternion values with those from files + alpha = np.load(alpha_file_path) + random_alpha_subset = alpha[ + np.random.choice( + alpha.shape[0], size=alpha_stop_index - alpha_start_index, replace=False + ) + ] + quaternions[alpha_start_index:alpha_stop_index] = np.array( + [list(x) for x in random_alpha_subset] + ) + gamma = np.load(gamma_file_path) + random_gamma_subset = gamma[ + np.random.choice( + gamma.shape[0], size=gamma_stop_index - gamma_start_index, replace=False + ) + ] + quaternions[gamma_start_index:gamma_stop_index] = np.array( + [list(x) for x in random_gamma_subset] + ) + np.random.shuffle(quaternions) + + # Shuffle orientations + orientations_shuffled_vol, misorientation_init = Shuffle( + material_index_3d, quaternions, 0, exclude=[], minimize=True, return_full=True + ) + orientations_shuffled_vol, misorientation = Shuffle( + material_index_3d, + quaternions, + n_iterations, + exclude=[], + minimize=True, + return_full=True, + ) + + # Replace quaternions in volume element + volume_element["orientations"]["quaternions"] = np.array( + [list(x) for x in orientations_shuffled_vol] + ) + + plt.hist(misorientation) + plt.hist(misorientation_init, color="r", alpha=0.5) + plt.legend(["Initial", "shuffled"], fontsize=5) + plt.xlabel("Misorientation") + plt.ylabel("Number of grains") + plt.savefig("misorientation.png") + + return {"volume_element": volume_element} diff --git a/matflow/data/template_components/command_files.yaml b/matflow/data/template_components/command_files.yaml index 9c181f30..4a8fdc27 100644 --- a/matflow/data/template_components/command_files.yaml +++ b/matflow/data/template_components/command_files.yaml @@ -69,3 +69,21 @@ name: name: pipeline.xdmf doc: DREAM.3D model data and metadata. + +- label: cipher_input_file + name: + name: cipher_input.yaml + doc: Main input file for CIPHER. + +- label: cipher_VTU_files + name: + is_regex: true + name: out_\d+\.vtu + +- label: precipitates_file + name: + name: precipitates.txt + +- label: misorientation_plot + name: + name: misorientation.png diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index 8d60cf52..7acb146b 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -1141,7 +1141,7 @@ # inputs: # - parameter: material # - parameter: load_case -# - paramater: RVE +# - parameter: RVE # outputs: # - parameter: RVE_response # actions: @@ -1208,6 +1208,61 @@ save_files: [dream_3D_hdf5_file, dream_3D_XDMF_file] script: <> +- objective: generate_volume_element + method: from_statistics_dual_phase_orientations + implementation: Dream3D + inputs: + - parameter: grid_size + - parameter: resolution # Define either resolution or size as task input + default_value: null + - parameter: size # Define either resolution or size as task input + default_value: null + - parameter: origin + default_value: null + - parameter: periodic + default_value: true + - parameter: orientations + multiple: true + labels: + phase_1: {} + phase_2: {} + - parameter: phase_statistics + - parameter: precipitates + default_value: null + - parameter: RNG_seed + outputs: + - parameter: volume_element + actions: + - environments: + - scope: any + environment: dream_3D_env + input_file_generators: + - input_file: precipitates_file + from_inputs: [precipitates] + script: <> + - input_file: dream_3D_pipeline + from_inputs: + - grid_size + - resolution + - size + - origin + - periodic + - phase_statistics + - precipitates + script: <> + commands: + - command: <> --pipeline ${PWD}/pipeline.json + output_file_parsers: + volume_element: + from_files: [dream_3D_hdf5_file] + script: <> + inputs: + - phase_statistics + - orientations + - RNG_seed + + + - objective: generate_volume_element doc: Generate a volume element by extrusion of a 2D model. method: extrusion @@ -1891,3 +1946,200 @@ - scope: type: any environment: python_env + +- objective: generate_phase_field_input + method: from_random_voronoi + inputs: + - parameter: num_phases + - parameter: grid_size + - parameter: size + - parameter: materials + - parameter: interfaces + - parameter: components + - parameter: outputs + - parameter: solution_parameters + - parameter: random_seed + default_value: null + - parameter: is_periodic + default_value: false + - parameter: combine_phases + default_value: null + outputs: + - parameter: phase_field_input + actions: + - script: <> + script_exe: python_script + script_data_in: direct + script_data_out: direct + environments: + - scope: { type: any } + environment: cipher_processing_env + +- objective: generate_phase_field_input + method: from_random_voronoi_with_orientations + inputs: + - parameter: materials + - parameter: interfaces + - parameter: num_phases + - parameter: grid_size + - parameter: size + - parameter: components + - parameter: outputs + - parameter: solution_parameters + - parameter: random_seed + default_value: null + - parameter: is_periodic + default_value: false + - parameter: orientations + - parameter: interface_binning + default_value: null + # A dict that, if not None, is passed as keyword arguments to + # `CIPHERInput.bin_interfaces_by_misorientation_angle` + - parameter: combine_phases + default_value: null + outputs: + - parameter: phase_field_input + actions: + - script: <> + script_exe: python_script + script_data_in: direct + script_data_out: direct + environments: + - scope: { type: any } + environment: cipher_processing_env + +- objective: generate_phase_field_input + method: from_volume_element + inputs: + - parameter: volume_element + - parameter: materials + - parameter: interfaces + - parameter: phase_type_map + default_value: null + - parameter: size + default_value: null + - parameter: components + - parameter: outputs + - parameter: solution_parameters + - parameter: random_seed + default_value: null + - parameter: interface_binning + default_value: null + # A dict that, if not None, is passed as keyword arguments to + # `CIPHERInput.bin_interfaces_by_misorientation_angle` + - parameter: keep_3D + default_value: false + - parameter: combine_phases + default_value: null + outputs: + - parameter: phase_field_input + actions: + - script: <> + script_exe: python_script + script_data_in: direct + script_data_out: direct + environments: + - scope: { type: any } + environment: cipher_processing_env + +- objective: generate_phase_field_input + method: from_random_voronoi_with_orientations_gradient + inputs: + - parameter: materials + - parameter: interfaces + - parameter: num_phases + - parameter: grid_size + - parameter: size + - parameter: components + - parameter: outputs + - parameter: solution_parameters + - parameter: orientation_gradient + - parameter: random_seed + - parameter: is_periodic + - parameter: interface_binning + default_value: null + # A dict that, if not None, is passed as keyword arguments to + # `CIPHERInput.bin_interfaces_by_misorientation_angle` + - parameter: combine_phases + default_value: null + outputs: + - parameter: phase_field_input + actions: + - script: <> + script_exe: python_script + script_data_in: direct + script_data_out: direct + environments: + - scope: { type: any } + environment: cipher_processing_env + +- objective: simulate_grain_growth + inputs: + - parameter: phase_field_input + - parameter: num_VTU_files # how many VTU files to archive; mutually exclusive with `VTU_files_time_interval` + default_value: 2 # i.e. initial and final + - parameter: VTU_files_time_interval # time interval of VTU files to keep; mutually exclusive with `num_VTU_files` + default_value: null + - parameter: derive_outputs # list of derived outputs to generate; available: ["num_voxels_per_phase"] + default_value: null + - parameter: save_outputs # list of outputs to save to MatFlow + default_value: null + - parameter: delete_VTIs # delete intermediate VTI files used to save data? + default_value: true + - parameter: delete_VTUs # delete original VTU outputs from CIPHER? + default_value: false + - parameter: separate_mapping_files + default_value: false + outputs: + - parameter: phase_field_output + actions: + - environments: + - scope: + type: main + environment: cipher_env + - scope: + type: processing + environment: cipher_processing_env + input_file_generators: + - input_file: cipher_input_file + from_inputs: [phase_field_input, separate_mapping_files] + script: <> + commands: + - command: <> + stdout: stdout.log + stderr: stderr.log + output_file_parsers: + phase_field_output: + from_files: [cipher_VTU_files] + inputs: + - num_VTU_files + - VTU_files_time_interval + - derive_outputs + - save_outputs + - delete_VTIs + - delete_VTUs + script: <> + +- objective: cluster_orientations + inputs: + - parameter: volume_element + - parameter: alpha_file_path + - parameter: gamma_file_path + - parameter: n_iterations + - parameter: alpha_start_index + - parameter: alpha_stop_index + - parameter: gamma_start_index + - parameter: gamma_stop_index + outputs: + - parameter: volume_element + actions: + - script: <> + script_exe: python_script + script_data_in: direct + script_data_out: direct + save_files: [misorientation_plot] + requires_dir: true + environments: + - scope: + type: any + environment: damask_parse_env diff --git a/matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml b/matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml new file mode 100644 index 00000000..27024e45 --- /dev/null +++ b/matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml @@ -0,0 +1,168 @@ +doc: + - > + Generates a nucleus within a uniform microstructure and + simulates grain coarsening based on misorientation dependant GB energy and mobility + +tasks: + # rotated cube texture for the sub-grain matrix: + - schema: sample_texture_from_model_ODF_mtex + inputs: + num_orientations: 1000 + crystal_symmetry: cubic + specimen_symmetry: orthorhombic + ODF_components: + - type: unimodal + component_fraction: 1.0 + modal_orientation_HKL: [0, 0, 1] + modal_orientation_UVW: [1, 1, 0] + halfwidth: 5 + - schema: visualise_orientations_pole_figure_mtex + inputs: + crystal_symmetry: cubic + pole_figure_directions: + - [0, 0, 1] + - [1, 0, 1] + - [1, 1, 1] + use_contours: false + + # within gamma-fibre for the nucleus: + - schema: sample_texture_from_model_ODF_mtex + inputs: + num_orientations: 100 + crystal_symmetry: cubic + specimen_symmetry: orthorhombic + ODF_components: + - type: unimodal + component_fraction: 1.0 + modal_orientation_HKL: [1, 1, 1] + modal_orientation_UVW: [1, 1, 2] + halfwidth: 1 + - schema: visualise_orientations_pole_figure_mtex + inputs: + crystal_symmetry: cubic + pole_figure_directions: + - [0, 0, 1] + - [1, 0, 1] + - [1, 1, 1] + use_contours: false + + - schema: generate_volume_element_from_statistics_dual_phase_orientations_Dream3D + input_sources: + orientations[phase_1]: task.sample_texture_from_model_ODF_mtex_1 + orientations[phase_2]: task.sample_texture_from_model_ODF_mtex_2 + inputs: + grid_size: [512, 512, 1] + resolution: [1, 1, 1] # side length is 512 + periodic: false + phase_statistics: + - type: primary + name: sub-grain-matrix + crystal_structure: cubic + volume_fraction: 1.0 + size_distribution: + ESD_mean: 10 + ESD_log_stddev: 0.03 + num_bins: 15 + - type: precipitate + name: nuclei + crystal_structure: cubic + volume_fraction: 0.0 + size_distribution: + ESD_mean: 2 + ESD_log_stddev: 0.1 + num_bins: 15 + number_fraction_on_boundary: 1 + radial_distribution_function: + min_distance: 10 + max_distance: 80 + num_bins: 50 + box_size: [100, 100, 100] + precipitates: + - phase_number: 2 + position: [256, 256, 0.5] + major_semi_axis_length: 45 + mid_semi_axis_length: 45 + minor_semi_axis_length: 45 + euler_angle: [0, 0, 0] + RNG_seed: 0 + + - schema: visualise_VE_VTK + + - schema: generate_phase_field_input_from_volume_element + inputs: + materials: + - name: sub-grain-matrix + properties: + chemicalenergy: none + molarvolume: 1e-5 + temperature0: 500.0 + - name: nuclei + properties: + chemicalenergy: none + molarvolume: 1e-5 + temperature0: 500.0 + interfaces: + - materials: [sub-grain-matrix, sub-grain-matrix] + properties: + width: 4.0 + energy: + e0: 1.0e+8 + mobility: + m0: 3.333e-11 + - materials: [nuclei, nuclei] + properties: + width: 4.0 + energy: + e0: 1.0e+8 + mobility: + m0: 10.0e-11 + - materials: [sub-grain-matrix, nuclei] + properties: + width: 4.0 + energy: + e0: 2.0e+8 + mobility: + m0: 10.0e-11 + components: [ti] + outputs: [phaseid, matid, interfaceid, 0_phi] + solution_parameters: + abstol: 0.0001 + amrinterval: 25 + initblocksize: [1, 1] + initcoarsen: 9 + initrefine: 9 + interpolation: cubic + maxnrefine: 9 + minnrefine: 0 + outfile: out + outputfreq: 100 + petscoptions: -ts_adapt_monitor -ts_rk_type 2a + random_seed: 1579993586 + reltol: 0.0001 + time: 40_000 + interface_binning: # specify one or both of energy_range/mobility_range: + base_interface_name: [sub-grain-matrix-sub-grain-matrix, sub-grain-matrix-nuclei] + theta_max: 50 + bin_width: 1 + energy_range: [0.1e+8, 2.0e+8] + # mobility_range: [1.0e-11, 10.0e-11] + # n: 1 # mobility parameter + # B: 5 # mobility parameter + + - schema: simulate_grain_growth + inputs: + num_VTU_files: 4 + derive_outputs: + - name: num_voxels_per_phase + save_outputs: + - name: phaseid + time_interval: 5_000 + - name: matid + number: 4 + - name: interfaceid + number: 4 + - name: 0_phi + number: 4 + - name: num_voxels_per_phase + time_interval: 5_000 + delete_VTUs: true diff --git a/matflow/data/workflows/grain_growth_random_voronoi.yaml b/matflow/data/workflows/grain_growth_random_voronoi.yaml new file mode 100644 index 00000000..410d256c --- /dev/null +++ b/matflow/data/workflows/grain_growth_random_voronoi.yaml @@ -0,0 +1,53 @@ +tasks: + - schema: generate_phase_field_input_from_random_voronoi + inputs: + num_phases: 2 + grid_size: [128, 128] + size: [128, 128] + materials: + - name: Al + properties: + chemicalenergy: none + molarvolume: 1e-5 + temperature0: 500.0 + interfaces: + - materials: [Al, Al] + properties: + width: 4 + energy: + e0: 5e+8 + mobility: + m0: 1e-11 + components: [al] + outputs: [phaseid, matid, interfaceid] + solution_parameters: + abstol: 0.0001 + amrinterval: 25 + initblocksize: [1, 1] + initcoarsen: 6 + initrefine: 7 + interpolation: cubic + maxnrefine: 7 + minnrefine: 0 + outfile: out + outputfreq: 10 + petscoptions: -ts_adapt_monitor -ts_rk_type 2a + random_seed: 1579993586 + reltol: 0.0001 + time: 1000 + + - schema: simulate_grain_growth + inputs: + derive_outputs: + - name: num_voxels_per_phase + save_outputs: + - name: phaseid + time_interval: 500 + - name: matid + number: 4 + - name: interfaceid + number: 4 + - name: 0_phi + number: 4 + - name: num_voxels_per_phase + time_interval: 500 diff --git a/matflow/data/workflows/grain_growth_single_phase_clustered.yaml b/matflow/data/workflows/grain_growth_single_phase_clustered.yaml new file mode 100644 index 00000000..80e61920 --- /dev/null +++ b/matflow/data/workflows/grain_growth_single_phase_clustered.yaml @@ -0,0 +1,114 @@ +doc: + - > + Generates a microstructure with a unimodal grain size distribution and simulates + grain coarsening based on misorientation dependant GB energy and mobility + +tasks: + # rotated cube texture for the sub-grain matrix: + - schema: sample_texture_from_model_ODF_mtex + inputs: + num_orientations: 2300 + crystal_symmetry: cubic + specimen_symmetry: orthorhombic + ODF_components: + - type: unimodal + component_fraction: 1.0 + modal_orientation_HKL: [0, 0, 1] + modal_orientation_UVW: [1, 1, 0] + halfwidth: 5 + + - schema: visualise_orientations_pole_figure_mtex + inputs: + crystal_symmetry: cubic + pole_figure_directions: + - [0, 0, 1] + - [1, 0, 1] + - [1, 1, 1] + use_contours: false + + - schema: generate_volume_element_from_statistics + inputs: + grid_size: [512, 512, 1] + resolution: [1, 1, 1] # side length is 512 + periodic: false + phase_statistics: + - type: primary + name: sub-grain-matrix + crystal_structure: cubic + volume_fraction: 1.0 + size_distribution: + ESD_mean: 7.03431 + ESD_log_stddev: 0.04 + num_bins: 15 + + - schema: visualise_VE_VTK + + - schema: cluster_orientations + inputs: + alpha_file_path: <>/alpha.npy + gamma_file_path: <>/gamma.npy + n_iterations: 60 + alpha_start_index: 150 + alpha_stop_index: 350 + gamma_start_index: 500 + gamma_stop_index: 700 + + - schema: generate_phase_field_input_from_volume_element + inputs: + materials: + - name: sub-grain-matrix + properties: + chemicalenergy: none + molarvolume: 1e-5 + temperature0: 500.0 + interfaces: + - materials: [sub-grain-matrix, sub-grain-matrix] + properties: + width: 6.0 + energy: + e0: 1.0e+8 + mobility: + m0: 3.333e-11 + components: [ti] + outputs: [phaseid, matid, interfaceid, 0_phi] + solution_parameters: + abstol: 0.0001 + amrinterval: 25 + initblocksize: [1, 1] + initcoarsen: 9 + initrefine: 9 + interpolation: cubic + maxnrefine: 9 + minnrefine: 0 + outfile: out + outputfreq: 100 + petscoptions: -ts_adapt_monitor -ts_rk_type 2a + random_seed: 1579993586 + reltol: 0.0001 + time: 1000_000 + interface_binning: # specify one or both of energy_range/mobility_range: + base_interface_name: sub-grain-matrix-sub-grain-matrix + theta_max: 50 + bin_width: 1 + energy_range: [0.1e+8, 0.555e+8] + mobility_range: [2.105e-11, 10.0e-11] + n: 7 # mobility parameter + B: 625 # mobility parameter + + - schema: simulate_grain_growth + inputs: + num_VTU_files: 22 + derive_outputs: + - name: num_voxels_per_phase + save_outputs: + - name: phaseid + time_interval: 5_000 + - name: matid + number: 4 + - name: interfaceid + number: 4 + - name: 0_phi + number: 4 + - name: num_voxels_per_phase + time_interval: 5_000 + delete_VTUs: true