From 3bb59d380355a1de7e3c528b81ba24db17993aa4 Mon Sep 17 00:00:00 2001 From: bocklund Date: Wed, 22 Sep 2021 16:32:58 -0400 Subject: [PATCH 1/2] Add ordering module --- scheil/ordering.py | 145 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 scheil/ordering.py diff --git a/scheil/ordering.py b/scheil/ordering.py new file mode 100644 index 0000000..5871d52 --- /dev/null +++ b/scheil/ordering.py @@ -0,0 +1,145 @@ +""" +Utilities for distinguishing and renaming ordered and disordered configurations of +multi-sublattice phases. + +`OrderingRecord` objects are able to be used for any phase. `OrderingRecords` can be +created automatically for phases modeled with a partitioned order/disorder model through +the `create_ordering_records` method, since the partitioned model contains all the +information about the ordered and disordered phase. +""" + +from dataclasses import dataclass +from typing import Sequence, List +import itertools +from collections import defaultdict +import numpy as np +import xarray as xr +from pycalphad.core.utils import unpack_components + +@dataclass +class OrderingRecord: + ordered_phase_name: str + disordered_phase_name: str + subl_dof: Sequence[int] # number of degrees of freedom in each sublattice of the ordered phase + symmetric_subl_idx: Sequence[Sequence[int]] # List of sublattices (of the ordered phase) that are symmetric + + def is_disordered(self, site_fractions): + # Short circuit if any site fraction is NaN (i.e. no phase or a different phase) + if np.any(np.isnan(site_fractions[:sum(self.subl_dof)])): + return False + + # For each sublattice, create a `slice` object for slicing the site + # fractions of that particular sublattice from the site fraction array + subl_slices = [] + for subl_idx in range(len(self.subl_dof)): + start_idx = np.sum(self.subl_dof[:subl_idx], dtype=np.int_) + end_idx = start_idx + self.subl_dof[subl_idx] + subl_slices.append(slice(start_idx, end_idx)) + + # For each set of symmetrically equivalent sublattices + for symm_subl in self.symmetric_subl_idx: + # Check whether the site fractions of each pair of symmetrically + # equivalent sublattices are ordered or disordered + for idx1, idx2 in itertools.combinations(symm_subl, 2): + # A phase is ordered if any pair of sublattices does not have + # equal (within numerical tolerance) site fractions + pair_is_ordered = np.any(~np.isclose(site_fractions[subl_slices[idx1]], site_fractions[subl_slices[idx2]])) + if pair_is_ordered: + return False + return True + + +def create_ordering_records(dbf, comps, phases): + """Return a dictionary with the sublattice degrees of freedom and equivalent + sublattices for order/disorder phases + + Parameters + ---------- + dbf : pycalphad.Database + comps : list[str] + List of active components to consider + phases : list[str] + List of active phases to consider + + Returns + ------- + List[OrderingRecord] + + Notes + ----- + Phases which should be checked for ordered/disordered configurations are + determined heuristically for this script. + + The heuristic for a phase satisfies the following: + 1. The phase is the ordered part of an order-disorder model + 2. The equivalent sublattices have all the same number of elements + """ + species = unpack_components(dbf, comps) + ordering_records = [] + for phase_name in phases: + phase_obj = dbf.phases[phase_name] + if phase_name == phase_obj.model_hints.get('ordered_phase', ''): + # This phase is active and modeled with an order/disorder model. + dof = [len(subl.intersection(species)) for subl in phase_obj.constituents] + # Define the symmetrically equivalent sublattices as any sublattices + # TODO: the heuristic here is simple and incorrect for cases like L1_2. + # that have the same site ratio. Create a {site_ratio: [subl idx]} dict + site_ratio_idxs = defaultdict(lambda: []) + for subl_idx, site_ratio in enumerate(phase_obj.sublattices): + site_ratio_idxs[site_ratio].append(subl_idx) + equiv_sublattices = list(site_ratio_idxs.values()) + ordering_records.append(OrderingRecord(phase_name, phase_obj.model_hints['disordered_phase'], dof, equiv_sublattices)) + return ordering_records + + +def rename_disordered_phases(eq_result, ordering_records): + """ + Modify an xarray Dataset to rename the ordered phase names to the disordered phase + names if the equilibrium configuration is disordered + + Parameters + ---------- + eq_result : xarray.Dataset + order_disorder_dict : OrderingRecord + Output from scheil.utils.order_disorder_dict + + Returns + ------- + xrray.Dataset + Dataset modified in-place + + Notes + ----- + This function does _not_ change the site fractions array of the disordered + configurations to match the site fractions matching the internal degrees of freedom + of the disordered phase's constituents (although that should be possible). + + Examples + -------- + >>> from pycalphad import Database, equilibrium, variables as v + >>> from pycalphad.tests.datasets import AL_C_FE_B2_TDB + >>> dbf = Database(AL_C_FE_B2_TDB) + >>> comps = ['AL', 'FE', 'VA'] + >>> phases = list(dbf.phases.keys()) + >>> eq_res = equilibrium(dbf, comps, ['B2_BCC'], {v.P: 101325, v.T: 1000, v.N: 1, v.X('AL'): [0.1, 0.4]}) + >>> ordering_records = create_ordering_records(dbf, comps, phases) + >>> eq_res.Phase.values.squeeze().tolist() + [['B2_BCC', '', ''], ['B2_BCC', '', '']] + >>> out_result = rename_disordered_phases(eq_res, ordering_records) + >>> eq_res.Phase.values.squeeze().tolist() + [['A2_BCC', '', ''], ['B2_BCC', '', '']] + """ + + for ord_rec in ordering_records: + # Array indices matching phase with ordered phase name + mask = eq_result.Phase == ord_rec.ordered_phase_name + # disordered_mask is a boolean mask that is True if the element listed as an + # ordered phase is a disordered configuration. We want to broadcast over all + # dimensions except for internal_dof (we need all internal dof to determine if + # the site fractions are disordered). The `OrderingRecord.is_disordered` method + # is not vectorized (operates on 1D site fractions), so we use `vectorize=True`. + disordered_mask = xr.apply_ufunc(ord_rec.is_disordered, eq_result.where(mask).Y, input_core_dims=[['internal_dof']], vectorize=True) + # Finally, use `xr.where` to set the value of the phase name to the disordered + # phase everywhere the mask is true and use the existing value otherwise + eq_result['Phase'] = xr.where(disordered_mask, ord_rec.disordered_phase_name, eq_result.Phase) + return eq_result \ No newline at end of file From 663a9cae9b8a84a7b6ebd1d31cbc50699441ac2e Mon Sep 17 00:00:00 2001 From: bocklund Date: Wed, 22 Sep 2021 16:59:00 -0400 Subject: [PATCH 2/2] Use new ordering code --- scheil/simulate.py | 18 +++--- scheil/utils.py | 135 --------------------------------------------- 2 files changed, 10 insertions(+), 143 deletions(-) diff --git a/scheil/simulate.py b/scheil/simulate.py index b342430..beba5d1 100644 --- a/scheil/simulate.py +++ b/scheil/simulate.py @@ -5,7 +5,8 @@ from pycalphad.core.calculate import _sample_phase_constitution from pycalphad.core.utils import instantiate_models, unpack_components, filter_phases, point_sample from .solidification_result import SolidificationResult -from .utils import order_disorder_dict, local_sample, order_disorder_eq_phases, get_phase_amounts +from .utils import local_sample, get_phase_amounts +from .ordering import create_ordering_records, rename_disordered_phases def is_converged(eq): @@ -95,14 +96,14 @@ def simulate_scheil_solidification(dbf, comps, phases, composition, MAXIMUM_STEP_SIZE_REDUCTION = 5.0 T_STEP_ORIG = step_temperature phases = filter_phases(dbf, unpack_components(dbf, comps), phases) - ord_disord_dict = order_disorder_dict(dbf, comps, phases) + ordering_records = create_ordering_records(dbf, comps, phases) models = instantiate_models(dbf, comps, phases) if verbose: print('building PhaseRecord objects... ', end='') phase_records = build_phase_records(dbf, comps, phases, [v.N, v.P, v.T], models) if verbose: print('done') - filtered_disordered_phases = {ord_ph_dict['disordered_phase'] for ord_ph_dict in ord_disord_dict.values()} + filtered_disordered_phases = {ord_rec.disordered_phase_name for ord_rec in ordering_records} solid_phases = sorted((set(phases) | filtered_disordered_phases) - {liquid_phase_name}) temp = start_temperature independent_comps = sorted([str(comp)[2:] for comp in composition.keys()]) @@ -140,8 +141,8 @@ def simulate_scheil_solidification(dbf, comps, phases, composition, if adaptive: _update_points(eq, eq_kwargs['calc_opts']['points'], dof_dict, verbose=verbose) eq = eq.get_dataset() # convert LightDataset to Dataset for fancy indexing - eq_phases = order_disorder_eq_phases(eq, ord_disord_dict) - num_eq_phases = np.nansum(np.array([str(ph) for ph in eq_phases]) != '') + eq = rename_disordered_phases(eq, ordering_records) + eq_phases = eq.Phase.values.squeeze().tolist() new_phases_seen = set(eq_phases).difference(phases_seen) if len(new_phases_seen) > 0: if verbose: @@ -259,8 +260,8 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition, """ eq_kwargs = eq_kwargs or dict() phases = filter_phases(dbf, unpack_components(dbf, comps), phases) - ord_disord_dict = order_disorder_dict(dbf, comps, phases) - filtered_disordered_phases = {ord_ph_dict['disordered_phase'] for ord_ph_dict in ord_disord_dict.values()} + ordering_records = create_ordering_records(dbf, comps, phases) + filtered_disordered_phases = {ord_rec.disordered_phase_name for ord_rec in ordering_records} solid_phases = sorted((set(phases) | filtered_disordered_phases) - {liquid_phase_name}) independent_comps = sorted([str(comp)[2:] for comp in composition.keys()]) models = instantiate_models(dbf, comps, phases) @@ -357,7 +358,8 @@ def simulate_equilibrium_solidification(dbf, comps, phases, composition, # Calculate fraction of solid and solid phase amounts current_fraction_solid = 0.0 - current_cum_phase_amnts = get_phase_amounts(order_disorder_eq_phases(eq.get_dataset(), ord_disord_dict), eq.NP.squeeze(), solid_phases) + eq = rename_disordered_phases(eq.get_dataset(), ordering_records) + current_cum_phase_amnts = get_phase_amounts(eq.Phase.values.squeeze(), eq.NP.squeeze(), solid_phases) for solid_phase, amount in current_cum_phase_amnts.items(): # Since the equilibrium calculations always give the "cumulative" phase amount, # we need to take the difference to get the instantaneous. diff --git a/scheil/utils.py b/scheil/utils.py index f0cc9dd..d3dde14 100644 --- a/scheil/utils.py +++ b/scheil/utils.py @@ -1,8 +1,5 @@ -import itertools -from collections import defaultdict import numpy as np from scipy.stats import norm -from pycalphad.core.utils import unpack_components, generate_dof def get_phase_amounts(eq_phases, phase_fractions, all_phases): @@ -29,138 +26,6 @@ def get_phase_amounts(eq_phases, phase_fractions, all_phases): return phase_amnts -def is_ordered(site_fracs, subl_dof, symmetric_subl_idx, **kwargs): - """Return True if the site fraction configuration is ordered - - Parameters - ---------- - site_fracs : numpy.ndarray[float,N] - Site fraction array for a phase of length sum(subl_dof) (can be padded - with arbitrary data, as returned by equilibrium calculations). - subl_dof : list[int] - List of the number of components active in each sublattice. Size should - be equivalent to the number of sublattices in the phase. - symmetric_subl_idx : list[list[int]] - List of sets of symmetrically equivalent sublattice indexes (as a list). - If sublattices at index 0 and index 1 are symmetric, as index 2 and - index 3, then the symmetric_subl_idx will be [[0, 1], [2, 3]]. - kwargs : - Additional keyword arguments passed to ``np.isclose`` to check if site - fractions in symmetric sublattices are distinct. - - Returns - ------- - bool - - Examples - -------- - >>> dbf = Database('Fe-Ni-Ti.tdb') # doctest: +SKIP - >>> subl_dof = [4, 4, 1] # sublattice model is (Fe,Ni,Ti,Va)(Fe,Ni,Ti,Va)(Va) # doctest: +SKIP - >>> symm = [[0, 1]] # sublattices 0 and 1 should be equivalent # doctest: +SKIP - >>> res = equilibrium(dbf, ['FE', 'NI', 'TI', 'VA'], ['BCC2'], {v.P: 101325, v.T: 1200, v.N: 1, v.X('FE'): 0.001, v.X('NI'): 0.001}) # doctest: +SKIP - >>> is_ordered(res.Y.isel(vertex=0).values.squeeze(), subl_dof, symm) # doctest: +SKIP - False - >>> res = equilibrium(dbf, ['FE', 'NI', 'TI', 'VA'], ['BCC2'], {v.P: 101325, v.T: 1200, v.N: 1, v.X('FE'): 0.25, v.X('NI'): 0.25}) # doctest: +SKIP - >>> is_ordered(res.Y.isel(vertex=0).values.squeeze(), subl_dof, symm) # doctest: +SKIP - True - - """ - # For each sublattice, create a ``slice`` object for slicing the site - # fractions of that particular sublattice from the site fraction array - subl_slices = [] - for subl_idx in range(len(subl_dof)): - start_idx = np.sum(subl_dof[:subl_idx], dtype=np.int_) - end_idx = start_idx + subl_dof[subl_idx] - subl_slices.append(slice(start_idx, end_idx)) - - # For each set of symmetrically equivalent sublattices - for symm_subl in symmetric_subl_idx: - # Check whether the site fractions of each pair of symmetrically - # equivalent sublattices are ordered or disordered - for idx1, idx2 in itertools.combinations(symm_subl, 2): - # A phase is ordered if any pair of sublattices does not have - # equal (within numerical tolerance) site fractions - pair_is_ordered = np.any(~np.isclose(site_fracs[subl_slices[idx1]], site_fracs[subl_slices[idx2]], **kwargs)) - if pair_is_ordered: - return True - return False - - -def order_disorder_dict(dbf, comps, phases): - """Return a dictionary with the sublattice degrees of freedom and equivalent - sublattices for order/disorder phases - - Parameters - ---------- - dbf : pycalphad.Database - comps : list[str] - List of active components to consider - phases : list[str] - List of active phases to consider - - Returns - ------- - dict - - Notes - ----- - Phases which should be checked for ordered/disordered configurations are - determined heuristically for this script. - - The heuristic for a phase satisfies the following: - 1. The phase is the ordered part of an order-disorder model - 2. The equivalent sublattices have all the same number of elements - """ - species = unpack_components(dbf, comps) - ord_disord_phases = {} - for phase_name in phases: - phase_obj = dbf.phases[phase_name] - if phase_name == phase_obj.model_hints.get('ordered_phase', ''): - # This phase is active and modeled with an order/disorder model. - dof = generate_dof(dbf.phases[phase_name], species)[1] - # Define the symmetrically equivalent sublattices as any sublattices - # that have the same site ratio. Create a {site_ratio: [subl idx]} dict - site_ratio_idxs = defaultdict(lambda: []) - for subl_idx, site_ratio in enumerate(phase_obj.sublattices): - site_ratio_idxs[site_ratio].append(subl_idx) - equiv_sublattices = list(site_ratio_idxs.values()) - ord_disord_phases[phase_name] = { - 'subl_dof': dof, - 'symmetric_subl_idx': equiv_sublattices, - 'disordered_phase': phase_obj.model_hints['disordered_phase'] - } - return ord_disord_phases - - -def order_disorder_eq_phases(eq_result, order_disorder_dict): - """Return a list corresponding to the eq_result.Phase with order/disorder - phases named correctly. - - Parameters - ---------- - eq_result : pycalphad.LightDataset - order_disorder_dict : Dict - - Returns - ------- - List - """ - eq_phases = [] - for vtx in eq_result.vertex.values: - eq_phase = str(eq_result["Phase"].isel(vertex=vtx).values.squeeze()) - site_fracs = eq_result["Y"].isel(vertex=vtx).values.squeeze() - if eq_phase in order_disorder_dict: - odd = order_disorder_dict[eq_phase] - is_ord = is_ordered(site_fracs, odd['subl_dof'], odd['symmetric_subl_idx']) - if is_ord: - eq_phases.append(eq_phase) - else: - eq_phases.append(odd['disordered_phase']) - else: - eq_phases.append(eq_phase) - return eq_phases - - def local_sample(sitefracs, comp_count, pdens=100, stddev=0.05): """Sample from a normal distribution around the optimal site fractions