From 8125cb1ed033288d290ce6688bd5c996c421f48c Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 10 Jul 2025 21:28:47 +0100 Subject: [PATCH 01/39] feat: initial support for CIPHER simulations --- ...e_phase_field_input_from_random_voronoi.py | 50 ++++++ .../scripts/cipher/parse_cipher_outputs.py | 35 +++++ .../data/scripts/cipher/write_cipher_input.py | 7 + matflow/data/workflows/cipher.yaml | 143 ++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi.py create mode 100644 matflow/data/scripts/cipher/parse_cipher_outputs.py create mode 100644 matflow/data/scripts/cipher/write_cipher_input.py create mode 100644 matflow/data/workflows/cipher.yaml 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..039453d2 --- /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=None, +): + # 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/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/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/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml new file mode 100644 index 00000000..30db85d4 --- /dev/null +++ b/matflow/data/workflows/cipher.yaml @@ -0,0 +1,143 @@ +template_components: + command_files: + - 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 + + task_schemas: + - 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 + 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: <> + +resources: + any: + shell_args: + executable: pwsh.exe # required to get stdout/stderr redirects from docker command in a sensible encoding + +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 From 61394cea5a311e0fe2232c5136ca61259c637800 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 10 Jul 2025 21:28:47 +0100 Subject: [PATCH 02/39] feat: initial support for CIPHER simulations --- ...e_phase_field_input_from_random_voronoi.py | 50 ++++++ .../scripts/cipher/parse_cipher_outputs.py | 35 +++++ .../data/scripts/cipher/write_cipher_input.py | 7 + matflow/data/workflows/cipher.yaml | 143 ++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi.py create mode 100644 matflow/data/scripts/cipher/parse_cipher_outputs.py create mode 100644 matflow/data/scripts/cipher/write_cipher_input.py create mode 100644 matflow/data/workflows/cipher.yaml 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..039453d2 --- /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=None, +): + # 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/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/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/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml new file mode 100644 index 00000000..30db85d4 --- /dev/null +++ b/matflow/data/workflows/cipher.yaml @@ -0,0 +1,143 @@ +template_components: + command_files: + - 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 + + task_schemas: + - 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 + 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: <> + +resources: + any: + shell_args: + executable: pwsh.exe # required to get stdout/stderr redirects from docker command in a sensible encoding + +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 From df8b593f7ff5d05b35b3cc2fa5a03f90ccbc06f1 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 13 Aug 2025 11:03:21 +0100 Subject: [PATCH 03/39] Remove resource request from cipher demo This is specific to running on Windows with Docker, which should be set by the user. --- matflow/data/workflows/cipher.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/matflow/data/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml index 30db85d4..61b9d516 100644 --- a/matflow/data/workflows/cipher.yaml +++ b/matflow/data/workflows/cipher.yaml @@ -83,11 +83,6 @@ template_components: - delete_VTUs script: <> -resources: - any: - shell_args: - executable: pwsh.exe # required to get stdout/stderr redirects from docker command in a sensible encoding - tasks: - schema: generate_phase_field_input_from_random_voronoi inputs: From 3b2206d7b566b5a7d62d255dc5670bdf72d6516a Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 14 Aug 2025 20:11:44 +0100 Subject: [PATCH 04/39] Add `combine_phases` to inputs in task schema - Set default value here instead of in the python script. --- .../cipher/generate_phase_field_input_from_random_voronoi.py | 2 +- matflow/data/workflows/cipher.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 index 039453d2..725e61f1 100644 --- 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 @@ -17,7 +17,7 @@ def generate_phase_field_input_from_random_voronoi( solution_parameters, random_seed, is_periodic, - combine_phases=None, + combine_phases, ): # initialise `MaterialDefinition`, `InterfaceDefinition` and # `PhaseTypeDefinition` objects: diff --git a/matflow/data/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml index 61b9d516..ccae1230 100644 --- a/matflow/data/workflows/cipher.yaml +++ b/matflow/data/workflows/cipher.yaml @@ -25,6 +25,8 @@ template_components: default_value: null - parameter: is_periodic default_value: false + - parameter: combine_phases + default_value: null outputs: - parameter: phase_field_input actions: From 54f10687bfd6fd6281d7cd62bf98465a5598201d Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 14 Aug 2025 20:17:08 +0100 Subject: [PATCH 05/39] Add schema for `from_random_voronoi_with_orientations` method --- ..._input_from_random_voronoi_orientations.py | 65 +++++++++++++++++++ matflow/data/workflows/cipher.yaml | 32 +++++++++ 2 files changed, 97 insertions(+) create mode 100644 matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations.py 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..c0863614 --- /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( + 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/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml index ccae1230..f095c447 100644 --- a/matflow/data/workflows/cipher.yaml +++ b/matflow/data/workflows/cipher.yaml @@ -38,6 +38,38 @@ template_components: - 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: simulate_grain_growth inputs: - parameter: phase_field_input From 0d75620241e97020175255205c314dd4c6f6e688 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 14 Aug 2025 20:35:49 +0100 Subject: [PATCH 06/39] Implement `from volume element` method --- ...e_phase_field_input_from_volume_element.py | 125 ++++++++++++++++++ matflow/data/workflows/cipher.yaml | 33 +++++ 2 files changed, 158 insertions(+) create mode 100644 matflow/data/scripts/cipher/generate_phase_field_input_from_volume_element.py 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..4c758f20 --- /dev/null +++ b/matflow/data/scripts/cipher/generate_phase_field_input_from_volume_element.py @@ -0,0 +1,125 @@ +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"] + + # 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/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml index f095c447..d2820af9 100644 --- a/matflow/data/workflows/cipher.yaml +++ b/matflow/data/workflows/cipher.yaml @@ -70,6 +70,39 @@ template_components: 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 + - parameter: size + - 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: simulate_grain_growth inputs: - parameter: phase_field_input From 6d9a27de1a0fbd20ee259bc4e97435ab772ee05d Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 3 Sep 2025 14:14:04 +0100 Subject: [PATCH 07/39] Fix function name to match file name --- ...nerate_phase_field_input_from_random_voronoi_orientations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c0863614..bb0bee4d 100644 --- 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 @@ -6,7 +6,7 @@ ) -def generate_phase_field_input_from_random_voronoi( +def generate_phase_field_input_from_random_voronoi_orientations( materials, interfaces, num_phases, From 71576e34c65cfb1a81e7d15db4f3170a6839e2c2 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 3 Sep 2025 14:46:59 +0100 Subject: [PATCH 08/39] Implement from_random_voronoi_orientations_gradient method --- ...om_random_voronoi_orientations_gradient.py | 97 +++++++++++++++++++ matflow/data/workflows/cipher.yaml | 33 ++++++- 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 matflow/data/scripts/cipher/generate_phase_field_input_from_random_voronoi_orientations_gradient.py 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/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml index d2820af9..71849123 100644 --- a/matflow/data/workflows/cipher.yaml +++ b/matflow/data/workflows/cipher.yaml @@ -26,7 +26,7 @@ template_components: - parameter: is_periodic default_value: false - parameter: combine_phases - default_value: null + default_value: null outputs: - parameter: phase_field_input actions: @@ -103,6 +103,37 @@ template_components: - 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 From a599deb84830828c6b7a605464c5fd40c0f815ff Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Fri, 5 Sep 2025 10:43:09 +0100 Subject: [PATCH 09/39] Add environment definitions for cipher --- docs/source/environments_template_linux.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/source/environments_template_linux.yaml b/docs/source/environments_template_linux.yaml index 3d7e1d30..e7815a5a 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 From 71cb666e400ddc0c2dd35453e932268b48d997b5 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 17 Sep 2025 09:57:00 +0100 Subject: [PATCH 10/39] Add Aiden's old workflow file This is clearly incompatible with the current Matflow. --- matflow/data/workflows/cipher-aiden.yaml | 193 +++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 matflow/data/workflows/cipher-aiden.yaml diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/cipher-aiden.yaml new file mode 100644 index 00000000..70adeff6 --- /dev/null +++ b/matflow/data/workflows/cipher-aiden.yaml @@ -0,0 +1,193 @@ +name: grain_growth_from_VE_dream3D_nucleus_with_MTEX_texture_binned +run_options: + l: short + +archive: dropbox + +tasks: + + # rotated cube texture for the sub-grain matrix: + - name: sample_texture + method: from_model_ODF + software: mtex + context: phase_1 + base: + 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: 3 + + # within gamma-fibre for the nucleus: + - name: sample_texture + method: from_model_ODF + software: mtex + context: phase_2 + base: + 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: 3 + + - name: visualise_orientations + method: pole_figure + software: mtex + context: phase_1 + base: + crystal_symmetry: cubic + pole_figure_directions: + - [0, 0, 1] + - [1, 0, 1] + - [1, 1, 1] + use_contours: false + + - name: visualise_orientations + method: pole_figure + software: mtex + context: phase_2 + base: + crystal_symmetry: cubic + pole_figure_directions: + - [0, 0, 1] + - [1, 0, 1] + - [1, 1, 1] + use_contours: false + + - name: generate_volume_element + method: from_statistics_dual_phase_orientations + software: Dream3D + base: + 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] + + - name: visualise_volume_element + method: VTK + software: damask + + - name: generate_phase_field_input + method: from_volume_element + software: cipher + run_options: + l: short + num_cores: 2 + base: + 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 + + - name: simulate_grain_growth + method: phase_field + software: cipher + base: + 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 + run_options: + num_cores: 8 From a9aaf3ba28c2e9860d40c7906bb290c833aa262c Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 17 Sep 2025 10:52:49 +0100 Subject: [PATCH 11/39] Start to implement tasks using new task schemas Started with the easier ones: Remove dropbox archiving Adapt task syntax for - sample texture from model ODF mtex for phase 1 - Visualise orientations and move straight after sampling tasks - visualise VE VTK --- matflow/data/workflows/cipher-aiden.yaml | 55 ++++++++---------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/cipher-aiden.yaml index 70adeff6..5f239558 100644 --- a/matflow/data/workflows/cipher-aiden.yaml +++ b/matflow/data/workflows/cipher-aiden.yaml @@ -1,17 +1,8 @@ -name: grain_growth_from_VE_dream3D_nucleus_with_MTEX_texture_binned -run_options: - l: short - -archive: dropbox - tasks: # rotated cube texture for the sub-grain matrix: - - name: sample_texture - method: from_model_ODF - software: mtex - context: phase_1 - base: + - schema: sample_texture_from_model_ODF_mtex + inputs: num_orientations: 1000 crystal_symmetry: cubic specimen_symmetry: orthorhombic @@ -21,13 +12,18 @@ tasks: modal_orientation_HKL: [0, 0, 1] modal_orientation_UVW: [1, 1, 0] halfwidth: 3 + - 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: - - name: sample_texture - method: from_model_ODF - software: mtex - context: phase_2 - base: + - schema: sample_texture_from_model_ODF_mtex + inputs: num_orientations: 100 crystal_symmetry: cubic specimen_symmetry: orthorhombic @@ -37,24 +33,8 @@ tasks: modal_orientation_HKL: [1, 1, 1] modal_orientation_UVW: [1, 1, 2] halfwidth: 3 - - - name: visualise_orientations - method: pole_figure - software: mtex - context: phase_1 - base: - crystal_symmetry: cubic - pole_figure_directions: - - [0, 0, 1] - - [1, 0, 1] - - [1, 1, 1] - use_contours: false - - - name: visualise_orientations - method: pole_figure - software: mtex - context: phase_2 - base: + - schema: visualise_orientations_pole_figure_mtex + inputs: crystal_symmetry: cubic pole_figure_directions: - [0, 0, 1] @@ -62,6 +42,7 @@ tasks: - [1, 1, 1] use_contours: false +# Not yet implemented in matflow-new! - name: generate_volume_element method: from_statistics_dual_phase_orientations software: Dream3D @@ -100,10 +81,9 @@ tasks: minor_semi_axis_length: 45 euler_angle: [0, 0, 0] - - name: visualise_volume_element - method: VTK - software: damask + - schema: visualise_VE_VTK +# TODO - name: generate_phase_field_input method: from_volume_element software: cipher @@ -170,6 +150,7 @@ tasks: # n: 1 # mobility parameter # B: 5 # mobility parameter +# TODO - name: simulate_grain_growth method: phase_field software: cipher From 4a5ce2e2bdb4c24a1882a0004bafab0852ac9451 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 17 Sep 2025 11:56:54 +0100 Subject: [PATCH 12/39] Move CIPHER definitions from workflow to template components --- .../template_components/command_files.yaml | 10 + .../template_components/task_schemas.yaml | 171 ++++++++++++++++ matflow/data/workflows/cipher.yaml | 183 ------------------ 3 files changed, 181 insertions(+), 183 deletions(-) diff --git a/matflow/data/template_components/command_files.yaml b/matflow/data/template_components/command_files.yaml index 9c181f30..dc73b8e3 100644 --- a/matflow/data/template_components/command_files.yaml +++ b/matflow/data/template_components/command_files.yaml @@ -69,3 +69,13 @@ 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 diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index 8d60cf52..d38c3884 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -1891,3 +1891,174 @@ - 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 + - parameter: size + - 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: <> diff --git a/matflow/data/workflows/cipher.yaml b/matflow/data/workflows/cipher.yaml index 71849123..410d256c 100644 --- a/matflow/data/workflows/cipher.yaml +++ b/matflow/data/workflows/cipher.yaml @@ -1,186 +1,3 @@ -template_components: - command_files: - - 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 - - task_schemas: - - 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 - - parameter: size - - 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: <> - tasks: - schema: generate_phase_field_input_from_random_voronoi inputs: From 0d82b1141dc9561927a6b714a307a106ad9f28aa Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 17 Sep 2025 19:14:50 +0100 Subject: [PATCH 13/39] Implement remaining CIPHER tasks --- matflow/data/workflows/cipher-aiden.yaml | 99 +++++++++++------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/cipher-aiden.yaml index 5f239558..140299a5 100644 --- a/matflow/data/workflows/cipher-aiden.yaml +++ b/matflow/data/workflows/cipher-aiden.yaml @@ -83,47 +83,41 @@ tasks: - schema: visualise_VE_VTK -# TODO - - name: generate_phase_field_input - method: from_volume_element - software: cipher - run_options: - l: short - num_cores: 2 - base: + - 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 + - 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 + - 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: @@ -150,25 +144,22 @@ tasks: # n: 1 # mobility parameter # B: 5 # mobility parameter -# TODO - name: simulate_grain_growth - method: phase_field - software: cipher - base: + inputs: num_VTU_files: 4 derive_outputs: - - name: num_voxels_per_phase + - 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 + - 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 run_options: num_cores: 8 From 91361c3db0811dc6439a7563e636d05d0648c13d Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 1 Oct 2025 09:55:22 +0100 Subject: [PATCH 14/39] Fix indentation to match other env definitions --- docs/source/environments_template_linux.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/environments_template_linux.yaml b/docs/source/environments_template_linux.yaml index e7815a5a..d072a1b2 100644 --- a/docs/source/environments_template_linux.yaml +++ b/docs/source/environments_template_linux.yaml @@ -108,18 +108,18 @@ - 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 + - 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 + - label: python_script + instances: + - command: python "<>" <> + num_cores: 1 + parallel_mode: null From d2b9bed1e49c7e36af047f7ece81809f0b95dca9 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 1 Oct 2025 10:27:13 +0100 Subject: [PATCH 15/39] Add simulate_grain_growth task to workflow --- matflow/data/workflows/cipher-aiden.yaml | 78 ++++++++++++------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/cipher-aiden.yaml index 140299a5..8374efe4 100644 --- a/matflow/data/workflows/cipher-aiden.yaml +++ b/matflow/data/workflows/cipher-aiden.yaml @@ -43,43 +43,43 @@ tasks: use_contours: false # Not yet implemented in matflow-new! - - name: generate_volume_element - method: from_statistics_dual_phase_orientations - software: Dream3D - base: - 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] + # - name: generate_volume_element + # method: from_statistics_dual_phase_orientations + # software: Dream3D + # base: + # 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] - schema: visualise_VE_VTK @@ -144,7 +144,7 @@ tasks: # n: 1 # mobility parameter # B: 5 # mobility parameter - - name: simulate_grain_growth + - schema: simulate_grain_growth inputs: num_VTU_files: 4 derive_outputs: @@ -161,5 +161,3 @@ tasks: - name: num_voxels_per_phase time_interval: 5_000 delete_VTUs: true - run_options: - num_cores: 8 From 9d70a2813ab0a284dac14f501a58ef61b089dab8 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 15 Oct 2025 20:23:19 +0100 Subject: [PATCH 16/39] fix: demo of how to implement `from_statistics_dual_phase_orientations` --- matflow/data/workflows/cipher-aiden.yaml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/cipher-aiden.yaml index 8374efe4..10f9e5e0 100644 --- a/matflow/data/workflows/cipher-aiden.yaml +++ b/matflow/data/workflows/cipher-aiden.yaml @@ -1,5 +1,18 @@ -tasks: +template_components: + task_schemas: + - objective: test_dual_oris + inputs: + - parameter: orientations + multiple: true + labels: + phase_1: {} + phase_2: {} + + # when running a script, the `orientations` input parameter will be a dict with two + # keys: `phase_1` and `phase_2`;the two orientations can be accessed + # see the scripts/formable/fit_yield_function.py for an example! +tasks: # rotated cube texture for the sub-grain matrix: - schema: sample_texture_from_model_ODF_mtex inputs: @@ -42,6 +55,11 @@ tasks: - [1, 1, 1] use_contours: false + - schema: test_dual_oris + input_sources: + orientations[phase_1]: task.sample_texture_from_model_ODF_mtex_1 + orientations[phase_2]: task.sample_texture_from_model_ODF_mtex_2 + # Not yet implemented in matflow-new! # - name: generate_volume_element # method: from_statistics_dual_phase_orientations From 3bfbfdabeb141f03f4d6c92ee35ebf4a396db534 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 21 Oct 2025 14:15:40 +0100 Subject: [PATCH 17/39] Implement dual phase orientations with Dream3D Remove explanation from previous commit. Note that this workflow gives an error: ``` ValueError: Action 0 input 'orientations' of schema 'generate_volume_element_from_statistics_dual_phase_orientations_Dream3D' is not a schema input, but nor is it an action output from a preceding action. ``` --- ...arse_dream_3D_volume_element_from_stats.py | 104 ++ .../scripts/cipher/write_dream_3D_pipeline.py | 1202 +++++++++++++++++ .../scripts/cipher/write_precipitates_file.py | 19 + .../template_components/command_files.yaml | 4 + .../template_components/task_schemas.yaml | 56 + matflow/data/workflows/cipher-aiden.yaml | 94 +- 6 files changed, 1423 insertions(+), 56 deletions(-) create mode 100644 matflow/data/scripts/cipher/parse_dream_3D_volume_element_from_stats.py create mode 100644 matflow/data/scripts/cipher/write_dream_3D_pipeline.py create mode 100644 matflow/data/scripts/cipher/write_precipitates_file.py 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..222dd13e --- /dev/null +++ b/matflow/data/scripts/cipher/parse_dream_3D_volume_element_from_stats.py @@ -0,0 +1,104 @@ +import h5py +import copy +import numpy as np + +from damask_parse.utils import validate_orientations, validate_volume_element + + +def parse_dream_3D_volume_element_from_stats( + path, + 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(path, 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] + ) + + ori_1 = validate_orientations(orientations_phase_1) + ori_2 = validate_orientations(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 diff --git a/matflow/data/scripts/cipher/write_dream_3D_pipeline.py b/matflow/data/scripts/cipher/write_dream_3D_pipeline.py new file mode 100644 index 00000000..ece2bec6 --- /dev/null +++ b/matflow/data/scripts/cipher/write_dream_3D_pipeline.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(path).parent.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).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/write_precipitates_file.py b/matflow/data/scripts/cipher/write_precipitates_file.py new file mode 100644 index 00000000..96e41581 --- /dev/null +++ b/matflow/data/scripts/cipher/write_precipitates_file.py @@ -0,0 +1,19 @@ +from pathlib import Path + + +def write_precipitate_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/template_components/command_files.yaml b/matflow/data/template_components/command_files.yaml index dc73b8e3..ad6c9543 100644 --- a/matflow/data/template_components/command_files.yaml +++ b/matflow/data/template_components/command_files.yaml @@ -79,3 +79,7 @@ name: is_regex: true name: out_\d+\.vtu + +- label: precipitates_file + name: + name: precipitates.txt diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index d38c3884..d9c2bfd0 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -1208,6 +1208,62 @@ 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: dream_3D_runner --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 diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/cipher-aiden.yaml index 10f9e5e0..f3655377 100644 --- a/matflow/data/workflows/cipher-aiden.yaml +++ b/matflow/data/workflows/cipher-aiden.yaml @@ -1,17 +1,3 @@ -template_components: - task_schemas: - - objective: test_dual_oris - inputs: - - parameter: orientations - multiple: true - labels: - phase_1: {} - phase_2: {} - - # when running a script, the `orientations` input parameter will be a dict with two - # keys: `phase_1` and `phase_2`;the two orientations can be accessed - # see the scripts/formable/fit_yield_function.py for an example! - tasks: # rotated cube texture for the sub-grain matrix: - schema: sample_texture_from_model_ODF_mtex @@ -24,7 +10,7 @@ tasks: component_fraction: 1.0 modal_orientation_HKL: [0, 0, 1] modal_orientation_UVW: [1, 1, 0] - halfwidth: 3 + halfwidth: 5 - schema: visualise_orientations_pole_figure_mtex inputs: crystal_symmetry: cubic @@ -45,7 +31,7 @@ tasks: component_fraction: 1.0 modal_orientation_HKL: [1, 1, 1] modal_orientation_UVW: [1, 1, 2] - halfwidth: 3 + halfwidth: 1 - schema: visualise_orientations_pole_figure_mtex inputs: crystal_symmetry: cubic @@ -55,49 +41,45 @@ tasks: - [1, 1, 1] use_contours: false - - schema: test_dual_oris + - 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 - -# Not yet implemented in matflow-new! - # - name: generate_volume_element - # method: from_statistics_dual_phase_orientations - # software: Dream3D - # base: - # 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] + 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 From 2dc7806109c7fa2ebfde589898aedc88c956901b Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 23 Oct 2025 14:24:33 +0100 Subject: [PATCH 18/39] Add original single-phase workflows These are in thd old matflow format. --- ...ipher-single-phase-microstructure-pt1.yaml | 79 +++++++++++ ...ipher-single-phase-microstructure-pt2.yaml | 132 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml create mode 100644 matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml diff --git a/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml b/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml new file mode 100644 index 00000000..be7cbd84 --- /dev/null +++ b/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml @@ -0,0 +1,79 @@ +name: grain_growth_from_VE_dream3D_nucleus_with_MTEX_texture_binned +run_options: + l: short + +#archive: dropbox + +tasks: + + # rotated cube texture for the sub-grain matrix: + - name: sample_texture + method: from_model_ODF + software: mtex +# context: phase_1 + base: + 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 + + + - name: visualise_orientations + method: pole_figure + software: mtex +# context: phase_1 + base: + crystal_symmetry: cubic + pole_figure_directions: + - [0, 0, 1] + - [1, 0, 1] + - [1, 1, 1] + use_contours: false + + + - name: generate_volume_element + method: from_statistics + software: Dream3D + base: + 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 +# - type: precipitate #81-101 +# 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] + + - name: visualise_volume_element + method: VTK + software: damask diff --git a/matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml b/matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml new file mode 100644 index 00000000..d0611afa --- /dev/null +++ b/matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml @@ -0,0 +1,132 @@ +name: grain_growth_from_VE_dream3D_nucleus_with_MTEX_texture_binned +run_options: + l: +import: + - parameter: volume_element + from: + workflow: "/scratch/j36293ah/nonuc/hw_7_with_seeds/runs_final/setup/2pcf/10000/workflow.hdf5" +#archive: dropbox + +tasks: + + # rotated cube texture for the sub-grain matrix: +# - name: sample_texture +# method: from_model_ODF +# software: mtex + # context: phase +# base: +# 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: 7 + + +# - name: visualise_orientations +# method: pole_figure +# software: mtex +# context: phase_1 +# base: +# crystal_symmetry: cubic +# pole_figure_directions: +# - [0, 0, 1] +# - [1, 0, 1] +# - [1, 1, 1] +# use_contours: false + + +# - name: generate_volume_element +# method: from_statistics +# software: Dream3D +# base: +# 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 + +# - name: visualise_volume_element +# method: VTK +# software: damask + + - name: generate_phase_field_input + method: from_volume_element + software: cipher + run_options: + l: short + num_cores: 2 + base: + 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 + + - name: simulate_grain_growth + method: phase_field + software: cipher + base: + 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 + run_options: + num_cores: 8 From 7ccfac10c6b536a98480e55f11388a1b065ffb88 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 28 Oct 2025 15:12:12 +0000 Subject: [PATCH 19/39] Fix various syntax errors Debugging the workflow turned up some incorrect syntax, incorrect filenames (scripts not matching the first function names), missing default values for input parameters. It also turns out that Dream3D needs the full path to the precipitates file. --- ...erate_RVE_from_statistics_dual_phase_pipeline_writer.py} | 2 +- .../cipher/parse_dream_3D_volume_element_from_stats.py | 4 ++-- matflow/data/scripts/cipher/write_precipitates_file.py | 2 +- matflow/data/template_components/task_schemas.yaml | 6 ++++-- 4 files changed, 8 insertions(+), 6 deletions(-) rename matflow/data/scripts/cipher/{write_dream_3D_pipeline.py => generate_RVE_from_statistics_dual_phase_pipeline_writer.py} (99%) diff --git a/matflow/data/scripts/cipher/write_dream_3D_pipeline.py b/matflow/data/scripts/cipher/generate_RVE_from_statistics_dual_phase_pipeline_writer.py similarity index 99% rename from matflow/data/scripts/cipher/write_dream_3D_pipeline.py rename to matflow/data/scripts/cipher/generate_RVE_from_statistics_dual_phase_pipeline_writer.py index ece2bec6..fd4316b7 100644 --- a/matflow/data/scripts/cipher/write_dream_3D_pipeline.py +++ b/matflow/data/scripts/cipher/generate_RVE_from_statistics_dual_phase_pipeline_writer.py @@ -711,7 +711,7 @@ def generate_RVE_from_statistics_pipeline_writer( stats_data_array.update({str(idx): i}) if precipitates: - precip_inp_file = str(Path(path).parent.joinpath("precipitates.txt")) + precip_inp_file = str(Path.cwd().joinpath("precipitates.txt")) else: precip_inp_file = "" 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 index 222dd13e..fe8f9aab 100644 --- 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 @@ -6,7 +6,7 @@ def parse_dream_3D_volume_element_from_stats( - path, + dream_3D_hdf5_file, phase_statistics, orientations, RNG_seed, @@ -19,7 +19,7 @@ def parse_dream_3D_volume_element_from_stats( print(f"ori phase 1: {orientations_phase_1}") print(f"ori phase 2: {orientations_phase_2}") - with h5py.File(path, mode="r") as fh: + 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"][()] diff --git a/matflow/data/scripts/cipher/write_precipitates_file.py b/matflow/data/scripts/cipher/write_precipitates_file.py index 96e41581..2275dd5a 100644 --- a/matflow/data/scripts/cipher/write_precipitates_file.py +++ b/matflow/data/scripts/cipher/write_precipitates_file.py @@ -1,7 +1,7 @@ from pathlib import Path -def write_precipitate_file(path, precipitates): +def write_precipitates_file(path, precipitates): if precipitates: with Path(path).open("wt") as fp: fp.write(str(len(precipitates)) + "\n") diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index d9c2bfd0..c29d0725 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -1249,9 +1249,9 @@ - periodic - phase_statistics - precipitates - script: <> + script: <> commands: - - command: dream_3D_runner --pipeline ${PWD}/pipeline.json + - command: <> --pipeline ${PWD}/pipeline.json output_file_parsers: volume_element: from_files: @@ -2016,7 +2016,9 @@ - parameter: materials - parameter: interfaces - parameter: phase_type_map + default_value: null - parameter: size + default_value: null - parameter: components - parameter: outputs - parameter: solution_parameters From f05ea10df21aaad56126075ac5b682e8ee46b3f1 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 29 Oct 2025 10:12:56 +0000 Subject: [PATCH 20/39] Save dream3d hdf5 file by using absolute path in json pipline file --- .../generate_RVE_from_statistics_dual_phase_pipeline_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index fd4316b7..0637030c 100644 --- 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 @@ -1028,7 +1028,7 @@ def generate_RVE_from_statistics_pipeline_writer( "Filter_Human_Label": "Write DREAM.3D Data File", "Filter_Name": "DataContainerWriter", "Filter_Uuid": "{3fcd4c43-9d75-5b86-aad4-4441bc914f37}", - "OutputFile": f"{str(Path(path).parent.joinpath('pipeline.dream3d'))}", + "OutputFile": f"{str(Path(path).absolute().parent.joinpath('pipeline.dream3d'))}", "WriteTimeSeries": 0, "WriteXdmfFile": 1, }, From 94b128dd26f76ec596303e9f165b08bea72d7234 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 29 Oct 2025 10:13:40 +0000 Subject: [PATCH 21/39] Use compact notation for output file parser --- matflow/data/template_components/task_schemas.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index c29d0725..9617db50 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -1254,8 +1254,7 @@ - command: <> --pipeline ${PWD}/pipeline.json output_file_parsers: volume_element: - from_files: - - dream_3D_hdf5_file + from_files: [dream_3D_hdf5_file] script: <> inputs: - phase_statistics From 52c24129ddb9cb27a91a6175c118cd2dd20bb066 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 29 Oct 2025 13:37:46 +0000 Subject: [PATCH 22/39] Convert orientations to old matflow format I still get this error that I got before converting to old matflow orientations format: ``` File "/home/mbexegc2/projects/lightform/cipher-workflows/cipher-aiden_2025-10-29_133031/artifacts/submissions/0/scripts/generate_volume_element_from_statistics_dual_phase_orientations_Dream3D_act_3.py", line 40, in parse_dream_3D_volume_element_from_stats ori_1 = validate_orientations(orientations_phase_1) File "/home/mbexegc2/projects/lightform/cipher-workflows/.venv/lib/python3.10/site-packages/damask_parse/utils.py", line 983, in validate_orientations ori_type = orientations.get('type') AttributeError: 'Orientations' object has no attribute 'get' ``` --- ...m_statistics_dual_phase_pipeline_writer.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) 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 index 0637030c..135f9d21 100644 --- 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 @@ -6,6 +6,7 @@ from pathlib import Path from damask_parse.utils import validate_orientations from damask_parse.quats import axang2quat, multiply_quaternions +from matflow.param_classes.orientations import Orientations def generate_RVE_from_statistics_dual_phase_pipeline_writer( @@ -467,8 +468,10 @@ def generate_RVE_from_statistics_pipeline_writer( np.array(preset_eulers), degrees=True, ) - - oris = validate_orientations(ODF["orientations"]) # now as quaternions + old_format_orientations = _convert_orientations_to_old_matflow_format( + ODF["orientations"] + ) + oris = validate_orientations(old_format_orientations) # now as quaternions # Convert unit-cell alignment to x//a, as used by Dream.3D: if phase_i_CS == "hexagonal": @@ -1200,3 +1203,40 @@ def generate_neighbour_dist_from_preset(num_bins, preset_type): sigmas.append(sigma) return {"average": mus, "stddev": sigmas} + + +# 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 From 3d6fac9b64f3d64a1984ec6b7aa836402dcf1931 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 6 Nov 2025 11:24:18 +0000 Subject: [PATCH 23/39] Convert orientations to old matflow format --- ...arse_dream_3D_volume_element_from_stats.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) 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 index fe8f9aab..f30e5307 100644 --- 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 @@ -3,6 +3,7 @@ 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( @@ -36,9 +37,15 @@ def parse_dream_3D_volume_element_from_stats( constituent_phase_label = np.array( [phase_names[i][0].decode() for i in constituent_phase_idx] ) - - ori_1 = validate_orientations(orientations_phase_1) - ori_2 = validate_orientations(orientations_phase_2) + 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] @@ -102,3 +109,40 @@ def parse_dream_3D_volume_element_from_stats( } 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 From 0b0f9935da2b5486885b91c72fd46e930a4bfa29 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 6 Nov 2025 11:24:40 +0000 Subject: [PATCH 24/39] Revert "Convert orientations to old matflow format" This reverts commit 52c24129ddb9cb27a91a6175c118cd2dd20bb066. --- ...m_statistics_dual_phase_pipeline_writer.py | 44 +------------------ 1 file changed, 2 insertions(+), 42 deletions(-) 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 index 135f9d21..0637030c 100644 --- 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 @@ -6,7 +6,6 @@ from pathlib import Path from damask_parse.utils import validate_orientations from damask_parse.quats import axang2quat, multiply_quaternions -from matflow.param_classes.orientations import Orientations def generate_RVE_from_statistics_dual_phase_pipeline_writer( @@ -468,10 +467,8 @@ def generate_RVE_from_statistics_pipeline_writer( np.array(preset_eulers), degrees=True, ) - old_format_orientations = _convert_orientations_to_old_matflow_format( - ODF["orientations"] - ) - oris = validate_orientations(old_format_orientations) # now as quaternions + + 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": @@ -1203,40 +1200,3 @@ def generate_neighbour_dist_from_preset(num_bins, preset_type): sigmas.append(sigma) return {"average": mus, "stddev": sigmas} - - -# 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 From 099d3d558d582a6096277a7ff91941f5ac357641 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 6 Nov 2025 13:15:40 +0000 Subject: [PATCH 25/39] Convert orientations to numpy so we can edit them --- .../cipher/generate_phase_field_input_from_volume_element.py | 1 + 1 file changed, 1 insertion(+) 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 index 4c758f20..3190ce9f 100644 --- 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 @@ -74,6 +74,7 @@ def _volume_element_to_cipher_geometry( 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 From 3645cc5d318cfb8b9d5cd15798c36f9b535ea0e0 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Thu, 6 Nov 2025 14:21:14 +0000 Subject: [PATCH 26/39] Rename CIPHER workflows more descriptively --- ...r-aiden.yaml => grain_growth_from_VE_nucleus_texture_ODF.yaml} | 0 .../workflows/{cipher.yaml => grain_growth_random_voronoi.yaml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename matflow/data/workflows/{cipher-aiden.yaml => grain_growth_from_VE_nucleus_texture_ODF.yaml} (100%) rename matflow/data/workflows/{cipher.yaml => grain_growth_random_voronoi.yaml} (100%) diff --git a/matflow/data/workflows/cipher-aiden.yaml b/matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml similarity index 100% rename from matflow/data/workflows/cipher-aiden.yaml rename to matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml diff --git a/matflow/data/workflows/cipher.yaml b/matflow/data/workflows/grain_growth_random_voronoi.yaml similarity index 100% rename from matflow/data/workflows/cipher.yaml rename to matflow/data/workflows/grain_growth_random_voronoi.yaml From f1dbc20cafe017f6f1b406eea1ea335c1d7a94f9 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Mon, 10 Nov 2025 13:28:06 +0000 Subject: [PATCH 27/39] Use new matflow syntax --- ...ipher-single-phase-microstructure-pt1.yaml | 54 +++---------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml b/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml index be7cbd84..e031e399 100644 --- a/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml +++ b/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml @@ -1,17 +1,7 @@ -name: grain_growth_from_VE_dream3D_nucleus_with_MTEX_texture_binned -run_options: - l: short - -#archive: dropbox - tasks: - # rotated cube texture for the sub-grain matrix: - - name: sample_texture - method: from_model_ODF - software: mtex -# context: phase_1 - base: + - schema: sample_texture_from_model_ODF_mtex + inputs: num_orientations: 2300 crystal_symmetry: cubic specimen_symmetry: orthorhombic @@ -22,12 +12,8 @@ tasks: modal_orientation_UVW: [1, 1, 0] halfwidth: 5 - - - name: visualise_orientations - method: pole_figure - software: mtex -# context: phase_1 - base: + - schema: visualise_orientations_pole_figure_mtex + inputs: crystal_symmetry: cubic pole_figure_directions: - [0, 0, 1] @@ -35,11 +21,8 @@ tasks: - [1, 1, 1] use_contours: false - - - name: generate_volume_element - method: from_statistics - software: Dream3D - base: + - schema: generate_volume_element_from_statistics + inputs: grid_size: [512, 512, 1] resolution: [1, 1, 1] # side length is 512 periodic: false @@ -52,28 +35,5 @@ tasks: ESD_mean: 7.03431 ESD_log_stddev: 0.04 num_bins: 15 -# - type: precipitate #81-101 -# 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] - - name: visualise_volume_element - method: VTK - software: damask + - schema: visualise_VE_VTK From ce0be31113ea610c5fbaeeceb1eee3716f48856d Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Mon, 24 Nov 2025 15:47:42 +0000 Subject: [PATCH 28/39] Add placeholder tasks to single phase microstructure workflow These are commented out because there's an error from the generate_phase_field_input_from_volume_element task. --- ...ipher-single-phase-microstructure-pt1.yaml | 39 ------ ...ipher-single-phase-microstructure-pt2.yaml | 132 ------------------ .../cipher-single-phase-microstructure.yaml | 101 ++++++++++++++ 3 files changed, 101 insertions(+), 171 deletions(-) delete mode 100644 matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml delete mode 100644 matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml create mode 100644 matflow/data/workflows/cipher-single-phase-microstructure.yaml diff --git a/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml b/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml deleted file mode 100644 index e031e399..00000000 --- a/matflow/data/workflows/cipher-single-phase-microstructure-pt1.yaml +++ /dev/null @@ -1,39 +0,0 @@ -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 diff --git a/matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml b/matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml deleted file mode 100644 index d0611afa..00000000 --- a/matflow/data/workflows/cipher-single-phase-microstructure-pt2.yaml +++ /dev/null @@ -1,132 +0,0 @@ -name: grain_growth_from_VE_dream3D_nucleus_with_MTEX_texture_binned -run_options: - l: -import: - - parameter: volume_element - from: - workflow: "/scratch/j36293ah/nonuc/hw_7_with_seeds/runs_final/setup/2pcf/10000/workflow.hdf5" -#archive: dropbox - -tasks: - - # rotated cube texture for the sub-grain matrix: -# - name: sample_texture -# method: from_model_ODF -# software: mtex - # context: phase -# base: -# 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: 7 - - -# - name: visualise_orientations -# method: pole_figure -# software: mtex -# context: phase_1 -# base: -# crystal_symmetry: cubic -# pole_figure_directions: -# - [0, 0, 1] -# - [1, 0, 1] -# - [1, 1, 1] -# use_contours: false - - -# - name: generate_volume_element -# method: from_statistics -# software: Dream3D -# base: -# 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 - -# - name: visualise_volume_element -# method: VTK -# software: damask - - - name: generate_phase_field_input - method: from_volume_element - software: cipher - run_options: - l: short - num_cores: 2 - base: - 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 - - - name: simulate_grain_growth - method: phase_field - software: cipher - base: - 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 - run_options: - num_cores: 8 diff --git a/matflow/data/workflows/cipher-single-phase-microstructure.yaml b/matflow/data/workflows/cipher-single-phase-microstructure.yaml new file mode 100644 index 00000000..27f27fa1 --- /dev/null +++ b/matflow/data/workflows/cipher-single-phase-microstructure.yaml @@ -0,0 +1,101 @@ +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 + + # - 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 From 96670cc4172679b045904db8abc1343bed9de5d8 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 2 Dec 2025 15:42:12 +0000 Subject: [PATCH 29/39] Add skeleton task schema to return a volume element This just returns the unaltered volume element. --- matflow/data/scripts/cluster_orientations.py | 10 ++++++++++ .../data/template_components/task_schemas.yaml | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 matflow/data/scripts/cluster_orientations.py diff --git a/matflow/data/scripts/cluster_orientations.py b/matflow/data/scripts/cluster_orientations.py new file mode 100644 index 00000000..f7cc6a7a --- /dev/null +++ b/matflow/data/scripts/cluster_orientations.py @@ -0,0 +1,10 @@ +import numpy as np +from damask_parse.utils import validate_volume_element, validate_orientations + + +def cluster_orientations(volume_element): + + # Convert zarr arrays to numpy arrays using existing code + new_volume_element = validate_volume_element(volume_element) + + return {"volume_element": new_volume_element} diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index 9617db50..ca75a24d 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -2119,3 +2119,19 @@ - delete_VTIs - delete_VTUs script: <> + +- objective: cluster_orientations + inputs: + - parameter: volume_element + outputs: + - parameter: volume_element + actions: + - script: <> + script_exe: python_script + script_data_in: direct + script_data_out: direct + environments: + - scope: + type: any + environment: damask_parse_env # or cipher_processing_env? + From 2e4494724649d0d56b2bb5166245e14aa19a4755 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Mon, 8 Dec 2025 13:54:59 +0000 Subject: [PATCH 30/39] Add completed task schema but with hard-coded values The demo workflow uses paths on my machine, and the clustering regions/values are hard-coded. I expect these both need updating. --- matflow/data/scripts/cluster_orientations.py | 37 +++++- .../template_components/task_schemas.yaml | 3 + .../cipher-single-phase-microstructure.yaml | 122 +++++++++--------- 3 files changed, 99 insertions(+), 63 deletions(-) diff --git a/matflow/data/scripts/cluster_orientations.py b/matflow/data/scripts/cluster_orientations.py index f7cc6a7a..837114d7 100644 --- a/matflow/data/scripts/cluster_orientations.py +++ b/matflow/data/scripts/cluster_orientations.py @@ -1,10 +1,39 @@ import numpy as np -from damask_parse.utils import validate_volume_element, validate_orientations +from damask_parse.utils import validate_volume_element +from subsurface import Shuffle -def cluster_orientations(volume_element): +def cluster_orientations(volume_element, alpha_file_path, gamma_file_path, n_iterations): # Convert zarr arrays to numpy arrays using existing code - new_volume_element = validate_volume_element(volume_element) + volume_element = validate_volume_element(volume_element) - return {"volume_element": new_volume_element} + quaternions = volume_element["orientations"]["quaternions"] + material_index = volume_element["element_material_idx"] + + # 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=200, replace=False)] + quaternions[150:350] = 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=200, replace=False)] + quaternions[500:700] = np.array([list(x) for x in random_gamma_subset]) + np.random.shuffle(quaternions) + + # Shuffle orientations + # orientations_shuffled_vol,misorientation_init = Shuffle(material_index, quaternions,0,exclude=[],minimize=True,return_full=True) + orientations_shuffled_vol, misorientation = Shuffle( + material_index, + 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] + ) + + return {"volume_element": volume_element} diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index ca75a24d..d38e05e7 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -2123,6 +2123,9 @@ - objective: cluster_orientations inputs: - parameter: volume_element + - parameter: alpha_file_path + - parameter: gamma_file_path + - parameter: n_iterations outputs: - parameter: volume_element actions: diff --git a/matflow/data/workflows/cipher-single-phase-microstructure.yaml b/matflow/data/workflows/cipher-single-phase-microstructure.yaml index 27f27fa1..0535556a 100644 --- a/matflow/data/workflows/cipher-single-phase-microstructure.yaml +++ b/matflow/data/workflows/cipher-single-phase-microstructure.yaml @@ -38,64 +38,68 @@ tasks: - schema: visualise_VE_VTK -# - schema: cluster_orientations + - schema: cluster_orientations + inputs: + alpha_file_path: /home/mbexegc2/projects/lightform/cipher-workflows/alpha_quat.npy + gamma_file_path: /home/mbexegc2/projects/lightform/cipher-workflows/gamma_mix.npy + n_iterations: 60 - # - 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: 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 + - 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 From 6b05a6aaec3a06a3f16f48c950a75df1055b586d Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Mon, 8 Dec 2025 14:02:38 +0000 Subject: [PATCH 31/39] Fix typo --- matflow/data/template_components/task_schemas.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index d38e05e7..bfb39fc6 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: @@ -2137,4 +2137,3 @@ - scope: type: any environment: damask_parse_env # or cipher_processing_env? - From c2b72aec3f5824eb1657e674bf199a617b1ed0a2 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Mon, 8 Dec 2025 16:22:41 +0000 Subject: [PATCH 32/39] Define quaternion replacement ranges through input parameters --- matflow/data/scripts/cluster_orientations.py | 32 +++++++++++++++---- .../template_components/task_schemas.yaml | 4 +++ .../cipher-single-phase-microstructure.yaml | 4 +++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/matflow/data/scripts/cluster_orientations.py b/matflow/data/scripts/cluster_orientations.py index 837114d7..1eb8304f 100644 --- a/matflow/data/scripts/cluster_orientations.py +++ b/matflow/data/scripts/cluster_orientations.py @@ -3,7 +3,16 @@ from subsurface import Shuffle -def cluster_orientations(volume_element, alpha_file_path, gamma_file_path, n_iterations): +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, +): # Convert zarr arrays to numpy arrays using existing code volume_element = validate_volume_element(volume_element) @@ -13,15 +22,26 @@ def cluster_orientations(volume_element, alpha_file_path, gamma_file_path, n_ite # 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=200, replace=False)] - quaternions[150:350] = np.array([list(x) for x in random_alpha_subset]) + 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=200, replace=False)] - quaternions[500:700] = np.array([list(x) for x in random_gamma_subset]) + 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, quaternions,0,exclude=[],minimize=True,return_full=True) orientations_shuffled_vol, misorientation = Shuffle( material_index, quaternions, diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index bfb39fc6..fb83ad54 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -2126,6 +2126,10 @@ - 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: diff --git a/matflow/data/workflows/cipher-single-phase-microstructure.yaml b/matflow/data/workflows/cipher-single-phase-microstructure.yaml index 0535556a..89877cb0 100644 --- a/matflow/data/workflows/cipher-single-phase-microstructure.yaml +++ b/matflow/data/workflows/cipher-single-phase-microstructure.yaml @@ -43,6 +43,10 @@ tasks: alpha_file_path: /home/mbexegc2/projects/lightform/cipher-workflows/alpha_quat.npy gamma_file_path: /home/mbexegc2/projects/lightform/cipher-workflows/gamma_mix.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: From 30691d6aca2bef48fc6e4dbcd907c1ed37e9754f Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 9 Dec 2025 11:41:27 +0000 Subject: [PATCH 33/39] Add quaternions as demo data file --- .../data/demo_data/clustering_quaternions.zip | Bin 0 -> 120552 bytes .../demo_data_manifest/demo_data_manifest.json | 3 +++ .../cipher-single-phase-microstructure.yaml | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 matflow/data/demo_data/clustering_quaternions.zip diff --git a/matflow/data/demo_data/clustering_quaternions.zip b/matflow/data/demo_data/clustering_quaternions.zip new file mode 100644 index 0000000000000000000000000000000000000000..7364957eb4f424079ef005e719fa7155b5a3d300 GIT binary patch literal 120552 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

o(UeGv12|PAz~@e`Qb>&8!F0v-K@doAP<5>` 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ΠkN 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Å?PAvvB*nc>HhRXo-;F5FHLYIc2M=%z?g8 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 literal 0 HcmV?d00001 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/workflows/cipher-single-phase-microstructure.yaml b/matflow/data/workflows/cipher-single-phase-microstructure.yaml index 89877cb0..2da915b0 100644 --- a/matflow/data/workflows/cipher-single-phase-microstructure.yaml +++ b/matflow/data/workflows/cipher-single-phase-microstructure.yaml @@ -40,8 +40,8 @@ tasks: - schema: cluster_orientations inputs: - alpha_file_path: /home/mbexegc2/projects/lightform/cipher-workflows/alpha_quat.npy - gamma_file_path: /home/mbexegc2/projects/lightform/cipher-workflows/gamma_mix.npy + alpha_file_path: <>/alpha.npy + gamma_file_path: <>/gamma.npy n_iterations: 60 alpha_start_index: 150 alpha_stop_index: 350 From 07b80503c2ccf4cd37fc057a5565ceea09377c8d Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 9 Dec 2025 15:10:56 +0000 Subject: [PATCH 34/39] Save misorientations plot --- matflow/data/scripts/cluster_orientations.py | 17 ++++++++++++++++- .../data/template_components/command_files.yaml | 4 ++++ .../data/template_components/task_schemas.yaml | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/matflow/data/scripts/cluster_orientations.py b/matflow/data/scripts/cluster_orientations.py index 1eb8304f..dd0a1a05 100644 --- a/matflow/data/scripts/cluster_orientations.py +++ b/matflow/data/scripts/cluster_orientations.py @@ -1,6 +1,7 @@ import numpy as np from damask_parse.utils import validate_volume_element from subsurface import Shuffle +import matplotlib.pyplot as plt def cluster_orientations( @@ -19,6 +20,10 @@ def cluster_orientations( 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) @@ -42,8 +47,11 @@ def cluster_orientations( 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, + material_index_3d, quaternions, n_iterations, exclude=[], @@ -56,4 +64,11 @@ def cluster_orientations( [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 ad6c9543..4a8fdc27 100644 --- a/matflow/data/template_components/command_files.yaml +++ b/matflow/data/template_components/command_files.yaml @@ -83,3 +83,7 @@ - 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 fb83ad54..5ec99c8a 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -2137,6 +2137,8 @@ script_exe: python_script script_data_in: direct script_data_out: direct + save_files: [misorientation_plot] + requires_dir: true environments: - scope: type: any From daab0a84f8d6532baabf6b7ff161df31a2177fb6 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 16 Dec 2025 09:44:56 +0000 Subject: [PATCH 35/39] Remove old comment --- matflow/data/template_components/task_schemas.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matflow/data/template_components/task_schemas.yaml b/matflow/data/template_components/task_schemas.yaml index 5ec99c8a..7acb146b 100644 --- a/matflow/data/template_components/task_schemas.yaml +++ b/matflow/data/template_components/task_schemas.yaml @@ -2142,4 +2142,4 @@ environments: - scope: type: any - environment: damask_parse_env # or cipher_processing_env? + environment: damask_parse_env From 58578f8463077947ba3f75a509cc38f5803d5093 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 16 Dec 2025 15:58:10 +0000 Subject: [PATCH 36/39] Add workflow doc attribute --- .../data/workflows/cipher-single-phase-microstructure.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/matflow/data/workflows/cipher-single-phase-microstructure.yaml b/matflow/data/workflows/cipher-single-phase-microstructure.yaml index 2da915b0..0d5b3ae1 100644 --- a/matflow/data/workflows/cipher-single-phase-microstructure.yaml +++ b/matflow/data/workflows/cipher-single-phase-microstructure.yaml @@ -1,3 +1,8 @@ +doc: + - > + Produces a uniform grain structure with alpha and gamma fibre clusters + via monte Carlo Markov chains and simulates grain coarsening + tasks: # rotated cube texture for the sub-grain matrix: - schema: sample_texture_from_model_ODF_mtex From df4c373aaf70a98855f44a1dfe63a87abbda267f Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 6 Jan 2026 11:16:25 +0000 Subject: [PATCH 37/39] Add docstring for clustering.py --- matflow/data/scripts/cluster_orientations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/matflow/data/scripts/cluster_orientations.py b/matflow/data/scripts/cluster_orientations.py index dd0a1a05..43fe8b2f 100644 --- a/matflow/data/scripts/cluster_orientations.py +++ b/matflow/data/scripts/cluster_orientations.py @@ -14,6 +14,7 @@ def cluster_orientations( 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) From 3a2874bf88e9364db6b4c0f58812be85fe798dc9 Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Tue, 6 Jan 2026 15:57:02 +0000 Subject: [PATCH 38/39] Rename single phase microstructure workflow --- ...crostructure.yaml => grain_growth_single_phase_clustered.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename matflow/data/workflows/{cipher-single-phase-microstructure.yaml => grain_growth_single_phase_clustered.yaml} (100%) diff --git a/matflow/data/workflows/cipher-single-phase-microstructure.yaml b/matflow/data/workflows/grain_growth_single_phase_clustered.yaml similarity index 100% rename from matflow/data/workflows/cipher-single-phase-microstructure.yaml rename to matflow/data/workflows/grain_growth_single_phase_clustered.yaml From 2b74ea218a48df4c80515a1540957642df4635ea Mon Sep 17 00:00:00 2001 From: Gerard Capes Date: Wed, 7 Jan 2026 15:19:23 +0000 Subject: [PATCH 39/39] Add doc attributes to cipher workflows --- .../workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml | 5 +++++ .../data/workflows/grain_growth_single_phase_clustered.yaml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) 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 index f3655377..27024e45 100644 --- a/matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml +++ b/matflow/data/workflows/grain_growth_from_VE_nucleus_texture_ODF.yaml @@ -1,3 +1,8 @@ +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 diff --git a/matflow/data/workflows/grain_growth_single_phase_clustered.yaml b/matflow/data/workflows/grain_growth_single_phase_clustered.yaml index 0d5b3ae1..80e61920 100644 --- a/matflow/data/workflows/grain_growth_single_phase_clustered.yaml +++ b/matflow/data/workflows/grain_growth_single_phase_clustered.yaml @@ -1,7 +1,7 @@ doc: - > - Produces a uniform grain structure with alpha and gamma fibre clusters - via monte Carlo Markov chains and simulates grain coarsening + 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: